UNPKG

@wordpress/upload-media

Version:
633 lines (632 loc) 17.9 kB
// 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