@wordpress/upload-media
Version:
Core media upload logic.
633 lines (632 loc) • 17.9 kB
JavaScript
// packages/upload-media/src/heic-parser.ts
var Reader = class {
view;
buffer;
pos;
constructor(buffer, offset = 0) {
this.buffer = buffer;
this.view = new DataView(buffer);
this.pos = offset;
}
u8() {
const v = this.view.getUint8(this.pos);
this.pos += 1;
return v;
}
u16() {
const v = this.view.getUint16(this.pos);
this.pos += 2;
return v;
}
u32() {
const v = this.view.getUint32(this.pos);
this.pos += 4;
return v;
}
u64() {
const hi = this.view.getUint32(this.pos);
const lo = this.view.getUint32(this.pos + 4);
this.pos += 8;
return hi * 4294967296 + lo;
}
/**
* Read a variable-width unsigned integer (0, 4 or 8 bytes).
*
* @param size Byte width to read (0, 4, or 8).
*/
uN(size) {
if (size === 0) {
return 0;
}
if (size === 4) {
return this.u32();
}
if (size === 8) {
return this.u64();
}
throw new Error(`Unsupported uint size: ${size}`);
}
str(len) {
let s = "";
for (let i = 0; i < len; i++) {
s += String.fromCharCode(this.view.getUint8(this.pos + i));
}
this.pos += len;
return s;
}
bytes(len) {
const b = new Uint8Array(this.buffer, this.pos, len);
this.pos += len;
return new Uint8Array(b);
}
};
function readBox(r) {
if (r.pos + 8 > r.view.byteLength) {
return null;
}
const offset = r.pos;
let size = r.u32();
const type = r.str(4);
let headerSize = 8;
if (size === 1) {
size = r.u64();
headerSize = 16;
} else if (size === 0) {
size = r.view.byteLength - offset;
}
return { type, offset, size, headerSize };
}
function findBoxes(r, start, end) {
const boxes = [];
r.pos = start;
while (r.pos < end) {
const box = readBox(r);
if (!box || box.size < 8) {
break;
}
boxes.push(box);
r.pos = box.offset + box.size;
}
return boxes;
}
function findBox(r, start, end, type) {
r.pos = start;
while (r.pos < end) {
const box = readBox(r);
if (!box || box.size < 8) {
break;
}
if (box.type === type) {
return box;
}
r.pos = box.offset + box.size;
}
return void 0;
}
function parsePitm(r, box) {
r.pos = box.offset + box.headerSize;
const version = r.u8();
r.pos += 3;
return version === 0 ? r.u16() : r.u32();
}
function parseIloc(r, box) {
r.pos = box.offset + box.headerSize;
const version = r.u8();
r.pos += 3;
const byte1 = r.u8();
const offsetSize = byte1 >> 4 & 15;
const lengthSize = byte1 & 15;
const byte2 = r.u8();
const baseOffsetSize = byte2 >> 4 & 15;
const indexSize = version >= 1 ? byte2 & 15 : 0;
const itemCount = version < 2 ? r.u16() : r.u32();
const items = /* @__PURE__ */ new Map();
for (let i = 0; i < itemCount; i++) {
const itemId = version < 2 ? r.u16() : r.u32();
let constructionMethod = 0;
if (version === 1 || version === 2) {
const cm = r.u16();
constructionMethod = cm & 15;
}
r.u16();
const baseOffset = r.uN(baseOffsetSize);
const extentCount = r.u16();
const extents = [];
for (let j = 0; j < extentCount; j++) {
if (version >= 1) {
r.uN(indexSize);
}
const extOffset = r.uN(offsetSize);
const extLength = r.uN(lengthSize);
extents.push({
offset: baseOffset + extOffset,
length: extLength
});
}
items.set(itemId, { constructionMethod, extents });
}
return items;
}
function parseIpma(r, box) {
r.pos = box.offset + box.headerSize;
const vf = r.u32();
const version = vf >>> 24;
const flags = vf & 16777215;
const largeIndex = (flags & 1) !== 0;
const entryCount = r.u32();
const associations = /* @__PURE__ */ new Map();
for (let i = 0; i < entryCount; i++) {
const itemId = version < 1 ? r.u16() : r.u32();
const assocCount = r.u8();
const indices = [];
for (let j = 0; j < assocCount; j++) {
if (largeIndex) {
indices.push(r.u16() & 32767);
} else {
indices.push(r.u8() & 127);
}
}
associations.set(itemId, indices);
}
return associations;
}
function parseIspe(r, box) {
r.pos = box.offset + box.headerSize + 4;
return { width: r.u32(), height: r.u32() };
}
function parseIrot(r, box) {
r.pos = box.offset + box.headerSize;
return (r.u8() & 3) * 90;
}
function parseIinf(r, box) {
r.pos = box.offset + box.headerSize;
const version = r.u8();
r.pos += 3;
const entryCount = version === 0 ? r.u16() : r.u32();
const itemTypes = /* @__PURE__ */ new Map();
const entriesStart = r.pos;
const boxEnd = box.offset + box.size;
const infeBoxes = findBoxes(r, entriesStart, boxEnd);
for (let i = 0; i < Math.min(entryCount, infeBoxes.length); i++) {
const infe = infeBoxes[i];
if (infe.type !== "infe") {
continue;
}
r.pos = infe.offset + infe.headerSize;
const infeVersion = r.u8();
r.pos += 3;
if (infeVersion >= 2) {
const itemId = infeVersion === 2 ? r.u16() : r.u32();
r.u16();
const itemType = r.str(4);
itemTypes.set(itemId, itemType);
}
}
return itemTypes;
}
function parseIref(r, box, refType) {
r.pos = box.offset + box.headerSize;
const version = r.u8();
r.pos += 3;
const refs = /* @__PURE__ */ new Map();
const boxEnd = box.offset + box.size;
while (r.pos < boxEnd) {
const refBox = readBox(r);
if (!refBox || refBox.size < 8) {
break;
}
r.pos = refBox.offset + refBox.headerSize;
const fromId = version === 0 ? r.u16() : r.u32();
const refCount = r.u16();
const toIds = [];
for (let i = 0; i < refCount; i++) {
toIds.push(version === 0 ? r.u16() : r.u32());
}
if (refBox.type === refType) {
refs.set(fromId, toIds);
}
r.pos = refBox.offset + refBox.size;
}
return refs;
}
function reverseBits32(n) {
n = n >>> 1 & 1431655765 | (n & 1431655765) << 1;
n = n >>> 2 & 858993459 | (n & 858993459) << 2;
n = n >>> 4 & 252645135 | (n & 252645135) << 4;
n = n >>> 8 & 16711935 | (n & 16711935) << 8;
n = n >>> 16 | n << 16;
return n >>> 0;
}
function buildCodecString(r, recordOffset) {
r.pos = recordOffset;
r.u8();
const byte1 = r.u8();
const profileSpace = byte1 >> 6 & 3;
const tierFlag = byte1 >> 5 & 1;
const profileIdc = byte1 & 31;
const compatFlags = r.u32();
const constraintBytes = r.bytes(6);
const levelIdc = r.u8();
const spacePrefix = profileSpace > 0 ? String.fromCharCode(64 + profileSpace) : "";
const compatHex = reverseBits32(compatFlags).toString(16).toUpperCase();
const tierChar = tierFlag ? "H" : "L";
let lastNonZero = -1;
for (let i = 5; i >= 0; i--) {
if (constraintBytes[i] !== 0) {
lastNonZero = i;
break;
}
}
let constraintStr = "";
if (lastNonZero >= 0) {
const parts = [];
for (let i = 0; i <= lastNonZero; i++) {
parts.push(constraintBytes[i].toString(16).toUpperCase());
}
constraintStr = "." + parts.join(".");
}
return `hvc1.${spacePrefix}${profileIdc}.${compatHex}.${tierChar}${levelIdc}${constraintStr}`;
}
function readItemData(buffer, loc, idatOffset) {
const baseOffset = loc.constructionMethod === 1 ? idatOffset : 0;
if (loc.extents.length === 1) {
const ext = loc.extents[0];
const start = baseOffset + ext.offset;
return new Uint8Array(buffer.slice(start, start + ext.length));
}
let totalLength = 0;
for (const ext of loc.extents) {
totalLength += ext.length;
}
const data = new Uint8Array(totalLength);
let pos = 0;
for (const ext of loc.extents) {
const start = baseOffset + ext.offset;
data.set(
new Uint8Array(buffer.slice(start, start + ext.length)),
pos
);
pos += ext.length;
}
return data;
}
function findHvcProperties(propIndices, properties) {
let hvcCBox;
let ispeBox;
let irotBox;
for (const idx of propIndices) {
if (idx < 1 || idx > properties.length) {
continue;
}
const prop = properties[idx - 1];
if (prop.type === "hvcC" && !hvcCBox) {
hvcCBox = prop;
}
if (prop.type === "ispe" && !ispeBox) {
ispeBox = prop;
}
if (prop.type === "irot" && !irotBox) {
irotBox = prop;
}
}
if (!hvcCBox) {
throw new Error("No HEVC configuration (hvcC) found");
}
if (!ispeBox) {
throw new Error("No image dimensions (ispe) found");
}
return { hvcCBox, ispeBox, irotBox };
}
function parseHeic(buffer) {
const r = new Reader(buffer);
const fileEnd = buffer.byteLength;
const metaBox = findBox(r, 0, fileEnd, "meta");
if (!metaBox) {
throw new Error("No meta box found in HEIC file");
}
const metaChildStart = metaBox.offset + metaBox.headerSize + 4;
const metaEnd = metaBox.offset + metaBox.size;
const children = findBoxes(r, metaChildStart, metaEnd);
const pitmBox = children.find((b) => b.type === "pitm");
const ilocBox = children.find((b) => b.type === "iloc");
const iprpBox = children.find((b) => b.type === "iprp");
const iinfBox = children.find((b) => b.type === "iinf");
const irefBox = children.find((b) => b.type === "iref");
const idatBox = children.find((b) => b.type === "idat");
const idatOffset = idatBox ? idatBox.offset + idatBox.headerSize : 0;
if (!pitmBox || !ilocBox || !iprpBox) {
throw new Error("Missing required boxes (pitm, iloc, iprp) in HEIC");
}
const primaryId = parsePitm(r, pitmBox);
const locations = parseIloc(r, ilocBox);
const iprpStart = iprpBox.offset + iprpBox.headerSize;
const iprpEnd = iprpBox.offset + iprpBox.size;
const iprpChildren = findBoxes(r, iprpStart, iprpEnd);
const ipcoBox = iprpChildren.find((b) => b.type === "ipco");
const ipmaBox = iprpChildren.find((b) => b.type === "ipma");
if (!ipcoBox || !ipmaBox) {
throw new Error("Missing ipco or ipma in HEIC properties");
}
const allAssoc = parseIpma(r, ipmaBox);
const ipcoStart = ipcoBox.offset + ipcoBox.headerSize;
const ipcoEnd = ipcoBox.offset + ipcoBox.size;
const properties = findBoxes(r, ipcoStart, ipcoEnd);
let primaryItemType = "hvc1";
if (iinfBox) {
const itemTypes = parseIinf(r, iinfBox);
const t = itemTypes.get(primaryId);
if (t) {
primaryItemType = t;
}
}
if (primaryItemType === "grid") {
return parseGridImage(
r,
buffer,
primaryId,
locations,
allAssoc,
properties,
irefBox,
idatOffset
);
}
const primaryLoc = locations.get(primaryId);
if (!primaryLoc || primaryLoc.extents.length === 0) {
throw new Error(`No location data for primary item ${primaryId}`);
}
const primaryPropIndices = allAssoc.get(primaryId);
if (!primaryPropIndices || primaryPropIndices.length === 0) {
throw new Error("No property associations for primary item");
}
const { hvcCBox, ispeBox, irotBox } = findHvcProperties(
primaryPropIndices,
properties
);
const hvcCDataStart = hvcCBox.offset + hvcCBox.headerSize;
const hvcCDataSize = hvcCBox.size - hvcCBox.headerSize;
const description = new Uint8Array(
buffer.slice(hvcCDataStart, hvcCDataStart + hvcCDataSize)
);
const codecString = buildCodecString(r, hvcCDataStart);
const { width, height } = parseIspe(r, ispeBox);
const rotation = irotBox ? parseIrot(r, irotBox) : 0;
return {
codecString,
description,
tiles: [
{
data: readItemData(buffer, primaryLoc, idatOffset),
x: 0,
y: 0
}
],
tileWidth: width,
tileHeight: height,
outputWidth: width,
outputHeight: height,
rotation,
exifOrientation: rotation === 0 ? getUnappliedExifOrientation(buffer) : 1
};
}
function parseGridImage(r, buffer, gridItemId, locations, allAssoc, properties, irefBox, idatOffset) {
const gridLoc = locations.get(gridItemId);
if (!gridLoc || gridLoc.extents.length === 0) {
throw new Error("No location data for grid item");
}
const gridData = readItemData(buffer, gridLoc, idatOffset);
const largeFields = gridData.length > 1 && (gridData[1] & 1) !== 0;
const minGridSize = largeFields ? 12 : 8;
if (gridData.length < minGridSize) {
throw new Error(
`Grid descriptor too short: ${gridData.length} bytes`
);
}
const rows = gridData[2] + 1;
const columns = gridData[3] + 1;
const gv = new DataView(gridData.buffer, gridData.byteOffset);
let outputWidth;
let outputHeight;
if (largeFields) {
outputWidth = gv.getUint32(4);
outputHeight = gv.getUint32(8);
} else {
outputWidth = gv.getUint16(4);
outputHeight = gv.getUint16(6);
}
if (!irefBox) {
throw new Error("Grid image requires iref box");
}
const dimgRefs = parseIref(r, irefBox, "dimg");
const tileItemIds = dimgRefs.get(gridItemId);
if (!tileItemIds || tileItemIds.length === 0) {
throw new Error("No tile references found for grid item");
}
const expectedTiles = rows * columns;
if (tileItemIds.length < expectedTiles) {
throw new Error(
`Grid expects ${expectedTiles} tiles but found ${tileItemIds.length}`
);
}
const firstTileProps = allAssoc.get(tileItemIds[0]);
if (!firstTileProps || firstTileProps.length === 0) {
throw new Error("No property associations for tile item");
}
const { hvcCBox, ispeBox } = findHvcProperties(
firstTileProps,
properties
);
const gridProps = allAssoc.get(gridItemId) || [];
let irotBox;
for (const idx of gridProps) {
if (idx >= 1 && idx <= properties.length) {
const prop = properties[idx - 1];
if (prop.type === "irot") {
irotBox = prop;
break;
}
}
}
const hvcCDataStart = hvcCBox.offset + hvcCBox.headerSize;
const hvcCDataSize = hvcCBox.size - hvcCBox.headerSize;
const description = new Uint8Array(
buffer.slice(hvcCDataStart, hvcCDataStart + hvcCDataSize)
);
const codecString = buildCodecString(r, hvcCDataStart);
const { width: tileWidth, height: tileHeight } = parseIspe(r, ispeBox);
const tiles = [];
for (let row = 0; row < rows; row++) {
for (let col = 0; col < columns; col++) {
const tileIdx = row * columns + col;
const tileId = tileItemIds[tileIdx];
const tileLoc = locations.get(tileId);
if (!tileLoc || tileLoc.extents.length === 0) {
throw new Error(`No location data for tile item ${tileId}`);
}
tiles.push({
data: readItemData(buffer, tileLoc, idatOffset),
x: col * tileWidth,
y: row * tileHeight
});
}
}
const rotation = irotBox ? parseIrot(r, irotBox) : 0;
return {
codecString,
description,
tiles,
tileWidth,
tileHeight,
outputWidth,
outputHeight,
rotation,
exifOrientation: rotation === 0 ? getUnappliedExifOrientation(buffer) : 1
};
}
function readTiffOrientation(payload) {
if (payload.length < 8) {
return 1;
}
const view = new DataView(
payload.buffer,
payload.byteOffset,
payload.byteLength
);
let tiffStart = 0;
const firstWord = view.getUint16(0);
if (firstWord !== 18761 && firstWord !== 19789) {
tiffStart = view.getUint32(0) + 4;
}
if (tiffStart + 8 > payload.length) {
return 1;
}
const byteOrder = view.getUint16(tiffStart);
let little;
if (byteOrder === 18761) {
little = true;
} else if (byteOrder === 19789) {
little = false;
} else {
return 1;
}
const ifd0 = tiffStart + view.getUint32(tiffStart + 4, little);
if (ifd0 + 2 > payload.length) {
return 1;
}
const entryCount = view.getUint16(ifd0, little);
for (let i = 0; i < entryCount; i++) {
const entry = ifd0 + 2 + i * 12;
if (entry + 12 > payload.length) {
break;
}
if (view.getUint16(entry, little) === 274) {
const value = view.getUint16(entry + 8, little);
return value >= 1 && value <= 8 ? value : 1;
}
}
return 1;
}
function findMeta(r) {
const metaBox = findBox(r, 0, r.view.byteLength, "meta");
if (!metaBox) {
return null;
}
const start = metaBox.offset + metaBox.headerSize + 4;
const end = metaBox.offset + metaBox.size;
return { box: metaBox, children: findBoxes(r, start, end) };
}
function parseExifOrientation(buffer) {
try {
const r = new Reader(buffer);
const meta = findMeta(r);
if (!meta) {
return 1;
}
const iinfBox = meta.children.find((b) => b.type === "iinf");
const ilocBox = meta.children.find((b) => b.type === "iloc");
if (!iinfBox || !ilocBox) {
return 1;
}
const itemTypes = parseIinf(r, iinfBox);
let exifItemId;
for (const [id, type] of itemTypes) {
if (type === "Exif") {
exifItemId = id;
break;
}
}
if (exifItemId === void 0) {
return 1;
}
const loc = parseIloc(r, ilocBox).get(exifItemId);
if (!loc || loc.extents.length === 0) {
return 1;
}
const idatBox = meta.children.find((b) => b.type === "idat");
const idatOffset = idatBox ? idatBox.offset + idatBox.headerSize : 0;
return readTiffOrientation(readItemData(buffer, loc, idatOffset));
} catch {
return 1;
}
}
function hasNativeTransform(r) {
const meta = findMeta(r);
if (!meta) {
return false;
}
const iprp = meta.children.find((b) => b.type === "iprp");
if (!iprp) {
return false;
}
const ipco = findBox(
r,
iprp.offset + iprp.headerSize,
iprp.offset + iprp.size,
"ipco"
);
if (!ipco) {
return false;
}
const start = ipco.offset + ipco.headerSize;
const end = ipco.offset + ipco.size;
return Boolean(
findBox(r, start, end, "irot") || findBox(r, start, end, "imir")
);
}
function getUnappliedExifOrientation(buffer) {
try {
if (hasNativeTransform(new Reader(buffer))) {
return 1;
}
} catch {
return 1;
}
return parseExifOrientation(buffer);
}
export {
getUnappliedExifOrientation,
parseExifOrientation,
parseHeic,
reverseBits32
};
//# sourceMappingURL=heic-parser.mjs.map