release: v0.20.0
This commit is contained in:
492
lib/__tests__/fixtures/location.ts
Normal file
492
lib/__tests__/fixtures/location.ts
Normal file
@@ -0,0 +1,492 @@
|
||||
/**
|
||||
* Builders for real-world-shaped metadata fixtures: TIFF/EXIF with a GPS IFD
|
||||
* in either byte order, XMP packets, JPEGs and HEIF files. Shared by
|
||||
* lib/__tests__/locationScrub.test.ts and the download route tests.
|
||||
*/
|
||||
|
||||
export const cat = (...parts: Uint8Array[]) => {
|
||||
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
||||
let o = 0;
|
||||
for (const p of parts) {
|
||||
out.set(p, o);
|
||||
o += p.length;
|
||||
}
|
||||
return out;
|
||||
};
|
||||
export const bytes = (...n: number[]) => new Uint8Array(n);
|
||||
export const ascii = (s: string) => new Uint8Array([...s].map((c) => c.charCodeAt(0)));
|
||||
export const utf8 = (s: string) => new TextEncoder().encode(s);
|
||||
export const be16 = (n: number) => bytes((n >> 8) & 0xff, n & 0xff);
|
||||
export const be32 = (n: number) =>
|
||||
bytes((n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff);
|
||||
|
||||
/** Index of `needle` in `hay`, or -1. */
|
||||
export function indexOf(hay: Uint8Array, needle: Uint8Array, from = 0): number {
|
||||
outer: for (let i = from; i + needle.length <= hay.length; i++) {
|
||||
for (let k = 0; k < needle.length; k++) if (hay[i + k] !== needle[k]) continue outer;
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
export const contains = (hay: Uint8Array, needle: Uint8Array) => indexOf(hay, needle) >= 0;
|
||||
export const latin1 = (b: Uint8Array) => Buffer.from(b).toString('latin1');
|
||||
|
||||
// ── TIFF ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const TYPE = { BYTE: 1, ASCII: 2, SHORT: 3, LONG: 4, RATIONAL: 5, UNDEFINED: 7 } as const;
|
||||
const TYPE_SIZE: Record<number, number> = { 1: 1, 2: 1, 3: 2, 4: 4, 5: 8, 7: 1 };
|
||||
|
||||
export type TiffEntry =
|
||||
| { tag: number; type: number; count: number; value: (le: boolean) => Uint8Array }
|
||||
| { tag: number; ifd: string };
|
||||
|
||||
export interface TiffIfd {
|
||||
name: string;
|
||||
entries: TiffEntry[];
|
||||
next?: string;
|
||||
}
|
||||
|
||||
const u16 = (le: boolean, n: number) => (le ? bytes(n & 0xff, (n >> 8) & 0xff) : be16(n));
|
||||
const u32 = (le: boolean, n: number) =>
|
||||
le ? bytes(n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, (n >>> 24) & 0xff) : be32(n);
|
||||
|
||||
export const asciiValue = (s: string) => ({
|
||||
type: TYPE.ASCII,
|
||||
count: s.length + 1,
|
||||
value: () => ascii(`${s}\0`),
|
||||
});
|
||||
export const shortValue = (...n: number[]) => ({
|
||||
type: TYPE.SHORT,
|
||||
count: n.length,
|
||||
value: (le: boolean) => cat(...n.map((v) => u16(le, v))),
|
||||
});
|
||||
export const byteValue = (...n: number[]) => ({
|
||||
type: TYPE.BYTE,
|
||||
count: n.length,
|
||||
value: () => bytes(...n),
|
||||
});
|
||||
export const rationalValue = (...pairs: Array<[number, number]>) => ({
|
||||
type: TYPE.RATIONAL,
|
||||
count: pairs.length,
|
||||
value: (le: boolean) => cat(...pairs.flatMap(([a, b]) => [u32(le, a), u32(le, b)])),
|
||||
});
|
||||
export const undefinedValue = (data: Uint8Array) => ({
|
||||
type: TYPE.UNDEFINED,
|
||||
count: data.length,
|
||||
value: () => data,
|
||||
});
|
||||
|
||||
/** Lay out IFDs after the header, out-of-line values after the IFDs. */
|
||||
export function buildTiff(le: boolean, ifds: TiffIfd[]): Uint8Array {
|
||||
const offsets = new Map<string, number>();
|
||||
let pos = 8;
|
||||
for (const ifd of ifds) {
|
||||
offsets.set(ifd.name, pos);
|
||||
pos += 2 + ifd.entries.length * 12 + 4;
|
||||
}
|
||||
const data: Uint8Array[] = [];
|
||||
let dataPos = pos;
|
||||
const tables: Uint8Array[] = [];
|
||||
for (const ifd of ifds) {
|
||||
const parts: Uint8Array[] = [u16(le, ifd.entries.length)];
|
||||
for (const e of ifd.entries) {
|
||||
if ('ifd' in e) {
|
||||
parts.push(u16(le, e.tag), u16(le, TYPE.LONG), u32(le, 1), u32(le, offsets.get(e.ifd)!));
|
||||
continue;
|
||||
}
|
||||
const value = e.value(le);
|
||||
expectLength(value.length, TYPE_SIZE[e.type] * e.count);
|
||||
parts.push(u16(le, e.tag), u16(le, e.type), u32(le, e.count));
|
||||
if (value.length <= 4) {
|
||||
const inline = new Uint8Array(4);
|
||||
inline.set(value);
|
||||
parts.push(inline);
|
||||
} else {
|
||||
parts.push(u32(le, dataPos));
|
||||
data.push(value);
|
||||
dataPos += value.length;
|
||||
if (value.length % 2) {
|
||||
data.push(bytes(0));
|
||||
dataPos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
parts.push(u32(le, ifd.next ? offsets.get(ifd.next)! : 0));
|
||||
tables.push(cat(...parts));
|
||||
}
|
||||
const header = le
|
||||
? cat(ascii('II'), bytes(42, 0), u32(le, 8))
|
||||
: cat(ascii('MM'), bytes(0, 42), u32(le, 8));
|
||||
return cat(header, ...tables, ...data);
|
||||
}
|
||||
|
||||
function expectLength(actual: number, expected: number) {
|
||||
if (actual !== expected)
|
||||
throw new Error(`fixture value is ${actual} bytes, expected ${expected}`);
|
||||
}
|
||||
|
||||
export const LAT = rationalValue([52, 1], [31, 1], [1234, 100]);
|
||||
export const LON = rationalValue([13, 1], [24, 1], [5678, 100]);
|
||||
export const ALT = rationalValue([3456, 100]);
|
||||
|
||||
/** A camera EXIF block: IFD0, EXIF IFD, GPS IFD and an IFD1. */
|
||||
export function cameraTiff(le: boolean, opts: { withGps?: boolean; xmp?: Uint8Array } = {}) {
|
||||
const withGps = opts.withGps ?? true;
|
||||
const ifd0: TiffEntry[] = [
|
||||
{ tag: 0x010f, ...asciiValue('Canon') },
|
||||
{ tag: 0x0110, ...asciiValue('Canon EOS R5') },
|
||||
{ tag: 0x0112, ...shortValue(1) },
|
||||
];
|
||||
if (opts.xmp) ifd0.push({ tag: 0x02bc, ...undefinedValue(opts.xmp) });
|
||||
ifd0.push({ tag: 0x8298, ...asciiValue('(c) Jane Doe') });
|
||||
ifd0.push({ tag: 0x8769, ifd: 'exif' });
|
||||
if (withGps) ifd0.push({ tag: 0x8825, ifd: 'gps' });
|
||||
|
||||
const ifds: TiffIfd[] = [
|
||||
{ name: 'ifd0', entries: ifd0, next: 'ifd1' },
|
||||
{
|
||||
name: 'exif',
|
||||
entries: [
|
||||
{ tag: 0x829a, ...rationalValue([1, 250]) },
|
||||
{ tag: 0x829d, ...rationalValue([28, 10]) },
|
||||
{ tag: 0x8827, ...shortValue(100) },
|
||||
{ tag: 0xa434, ...asciiValue('RF24-70mm F2.8 L IS USM') },
|
||||
],
|
||||
},
|
||||
];
|
||||
if (withGps) {
|
||||
ifds.push({
|
||||
name: 'gps',
|
||||
entries: [
|
||||
{ tag: 0x0000, ...byteValue(2, 3, 0, 0) },
|
||||
{ tag: 0x0001, ...asciiValue('N') },
|
||||
{ tag: 0x0002, ...LAT },
|
||||
{ tag: 0x0003, ...asciiValue('E') },
|
||||
{ tag: 0x0004, ...LON },
|
||||
{ tag: 0x0005, ...byteValue(0) },
|
||||
{ tag: 0x0006, ...ALT },
|
||||
],
|
||||
});
|
||||
}
|
||||
ifds.push({ name: 'ifd1', entries: [{ tag: 0x0103, ...shortValue(6) }] });
|
||||
return buildTiff(le, ifds);
|
||||
}
|
||||
|
||||
export interface ParsedEntry {
|
||||
type: number;
|
||||
count: number;
|
||||
value: Uint8Array;
|
||||
}
|
||||
export interface ParsedTiff {
|
||||
ifd0: Map<number, ParsedEntry>;
|
||||
exif: Map<number, ParsedEntry> | null;
|
||||
gps: Map<number, ParsedEntry> | null;
|
||||
ifd1: Map<number, ParsedEntry> | null;
|
||||
}
|
||||
|
||||
/** A small, strict TIFF reader standing in for exiftool. */
|
||||
export function parseTiff(t: Uint8Array): ParsedTiff {
|
||||
const le = t[0] === 0x49;
|
||||
const r16 = (o: number) => (le ? t[o] | (t[o + 1] << 8) : (t[o] << 8) | t[o + 1]);
|
||||
const r32 = (o: number) =>
|
||||
le
|
||||
? (t[o] | (t[o + 1] << 8) | (t[o + 2] << 16) | (t[o + 3] << 24)) >>> 0
|
||||
: ((t[o] << 24) | (t[o + 1] << 16) | (t[o + 2] << 8) | t[o + 3]) >>> 0;
|
||||
if (r16(2) !== 42) throw new Error('not TIFF');
|
||||
const readIfd = (off: number) => {
|
||||
const map = new Map<number, ParsedEntry>();
|
||||
const count = r16(off);
|
||||
if (off + 2 + count * 12 + 4 > t.length) throw new Error('IFD out of bounds');
|
||||
let lastTag = -1;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const e = off + 2 + i * 12;
|
||||
const tag = r16(e);
|
||||
if (tag <= lastTag) throw new Error('IFD entries out of order');
|
||||
lastTag = tag;
|
||||
const type = r16(e + 2);
|
||||
const n = r32(e + 4);
|
||||
const size = TYPE_SIZE[type] * n;
|
||||
const at = size <= 4 ? e + 8 : r32(e + 8);
|
||||
if (at + size > t.length) throw new Error('value out of bounds');
|
||||
map.set(tag, { type, count: n, value: t.slice(at, at + size) });
|
||||
}
|
||||
return { map, next: r32(off + 2 + count * 12) };
|
||||
};
|
||||
const ifd0 = readIfd(r32(4));
|
||||
const ptr = (m: Map<number, ParsedEntry>, tag: number) => {
|
||||
const e = m.get(tag);
|
||||
return e
|
||||
? (le
|
||||
? e.value[0] | (e.value[1] << 8) | (e.value[2] << 16) | (e.value[3] << 24)
|
||||
: (e.value[0] << 24) | (e.value[1] << 16) | (e.value[2] << 8) | e.value[3]) >>> 0
|
||||
: 0;
|
||||
};
|
||||
const exifOff = ptr(ifd0.map, 0x8769);
|
||||
const gpsOff = ptr(ifd0.map, 0x8825);
|
||||
return {
|
||||
ifd0: ifd0.map,
|
||||
exif: exifOff ? readIfd(exifOff).map : null,
|
||||
gps: gpsOff ? readIfd(gpsOff).map : null,
|
||||
ifd1: ifd0.next ? readIfd(ifd0.next).map : null,
|
||||
};
|
||||
}
|
||||
|
||||
export const asciiOf = (e: ParsedEntry | undefined) =>
|
||||
e ? latin1(e.value).replace(/\0+$/, '') : undefined;
|
||||
|
||||
// ── XMP ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const XMP_PACKET = `<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
||||
<x:xmpmeta xmlns:x="adobe:ns:meta/">
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:exif="http://ns.adobe.com/exif/1.0/"
|
||||
xmlns:tiff="http://ns.adobe.com/tiff/1.0/"
|
||||
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:Iptc4xmpExt="http://iptc.org/std/Iptc4xmpExt/2008-02-29/"
|
||||
xmlns:drone-dji="http://www.dji.com/drone-dji/1.0/"
|
||||
tiff:Make="Canon"
|
||||
exif:GPSLatitude="52,31.2057N"
|
||||
exif:GPSLongitude='13,24.0946E'
|
||||
drone-dji:GpsLatitude="52.520095"
|
||||
photoshop:City="Berlin"
|
||||
photoshop:Country="Germany">
|
||||
<exif:GPSAltitude>3456/100</exif:GPSAltitude>
|
||||
<exif:GPSAltitudeRef>0</exif:GPSAltitudeRef>
|
||||
<exif:GPSTimeStamp>2024-05-01T10:00:00Z</exif:GPSTimeStamp>
|
||||
<exif:GPSVersionID/>
|
||||
<dc:rights><rdf:Alt><rdf:li xml:lang="x-default">© Jane Doe</rdf:li></rdf:Alt></dc:rights>
|
||||
<Iptc4xmpExt:LocationCreated>
|
||||
<rdf:Bag>
|
||||
<rdf:li rdf:parseType="Resource">
|
||||
<Iptc4xmpExt:City>Berlin</Iptc4xmpExt:City>
|
||||
<Iptc4xmpExt:GPSLatitude>52.520095</Iptc4xmpExt:GPSLatitude>
|
||||
<Iptc4xmpExt:GPSLongitude>13.401577</Iptc4xmpExt:GPSLongitude>
|
||||
</rdf:li>
|
||||
</rdf:Bag>
|
||||
</Iptc4xmpExt:LocationCreated>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
<?xpacket end="w"?>`;
|
||||
|
||||
/** Strings from the packet above that must not survive. */
|
||||
export const XMP_LOCATION_VALUES = [
|
||||
'52,31.2057N',
|
||||
'13,24.0946E',
|
||||
'52.520095',
|
||||
'13.401577',
|
||||
'3456/100',
|
||||
];
|
||||
|
||||
// ── JPEG ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** JPEG segment: FFxx <length incl. itself> <payload>. */
|
||||
export const seg = (marker: number, payload: Uint8Array) =>
|
||||
cat(bytes(0xff, marker), be16(payload.length + 2), payload);
|
||||
|
||||
export const exifSegment = (tiff: Uint8Array) => seg(0xe1, cat(ascii('Exif\0\0'), tiff));
|
||||
export const xmpSegment = (packet: string) =>
|
||||
seg(0xe1, cat(ascii('http://ns.adobe.com/xap/1.0/\0'), utf8(packet)));
|
||||
|
||||
export const SOS = cat(bytes(0xff, 0xda), be16(8), bytes(1, 1, 0, 0, 0x3f, 0));
|
||||
/** Entropy-coded data with byte stuffing and a restart marker. */
|
||||
export const SCAN = bytes(0x12, 0xff, 0x00, 0x34, 0xff, 0xd0, 0x56, 0x78, 0xff, 0x00, 0x9a);
|
||||
export const EOI = bytes(0xff, 0xd9);
|
||||
|
||||
export const ICC = cat(ascii('ICC_PROFILE\0'), bytes(1, 1), ascii('colour profile bytes'));
|
||||
export const JFIF = cat(ascii('JFIF\0'), bytes(1, 2, 0, 0, 1, 0, 1, 0, 0));
|
||||
|
||||
/** Photoshop APP13 with an IPTC record and an EXIF resource. */
|
||||
export function photoshopSegment(exif: Uint8Array) {
|
||||
const iptc = cat(bytes(0x1c, 0x02, 0x74), be16(18), ascii('Copyright Jane Doe'));
|
||||
const resource = (id: number, data: Uint8Array) =>
|
||||
cat(
|
||||
ascii('8BIM'),
|
||||
be16(id),
|
||||
bytes(0, 0),
|
||||
be32(data.length),
|
||||
data,
|
||||
data.length % 2 ? bytes(0) : bytes(),
|
||||
);
|
||||
return seg(0xed, cat(ascii('Photoshop 3.0\0'), resource(0x0404, iptc), resource(0x0422, exif)));
|
||||
}
|
||||
|
||||
/** A JPEG as a camera or phone writes it, with a second image appended (MPF). */
|
||||
export function cameraJpeg(le: boolean) {
|
||||
const primaryHead = cat(
|
||||
bytes(0xff, 0xd8),
|
||||
seg(0xe0, JFIF),
|
||||
exifSegment(cameraTiff(le)),
|
||||
xmpSegment(XMP_PACKET),
|
||||
seg(
|
||||
0xe1,
|
||||
cat(
|
||||
ascii('http://ns.adobe.com/xmp/extension/\0'),
|
||||
ascii('GUID'),
|
||||
ascii('exif:GPSLatitude="1"'),
|
||||
),
|
||||
),
|
||||
seg(0xe2, ICC),
|
||||
seg(
|
||||
0xeb,
|
||||
cat(
|
||||
ascii('JP'),
|
||||
bytes(0, 1),
|
||||
be32(1),
|
||||
be32(40),
|
||||
ascii('jumbjumd'),
|
||||
ascii('c2pa GPS 52.520095'),
|
||||
),
|
||||
),
|
||||
photoshopSegment(cameraTiff(!le)),
|
||||
seg(0xdb, bytes(0, ...new Array(64).fill(1))),
|
||||
);
|
||||
const primary = cat(primaryHead, SOS, SCAN, EOI);
|
||||
const secondary = cat(bytes(0xff, 0xd8), exifSegment(cameraTiff(le)), SOS, SCAN, EOI);
|
||||
return {
|
||||
file: cat(primary, secondary),
|
||||
primaryHeadLength: primaryHead.length,
|
||||
primaryLength: primary.length,
|
||||
};
|
||||
}
|
||||
|
||||
/** Every APPn segment of the JPEG at `soi`, up to SOS. */
|
||||
export function jpegSegments(b: Uint8Array, soi = 0) {
|
||||
const out: Array<{ marker: number; payload: Uint8Array; start: number }> = [];
|
||||
let pos = soi + 2;
|
||||
while (pos + 4 <= b.length && b[pos] === 0xff && b[pos + 1] !== 0xda) {
|
||||
const len = (b[pos + 2] << 8) | b[pos + 3];
|
||||
out.push({ marker: b[pos + 1], payload: b.slice(pos + 4, pos + 2 + len), start: pos + 4 });
|
||||
pos += 2 + len;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The TIFF block of the first EXIF APP1 in the JPEG at `soi`. */
|
||||
export function exifOf(b: Uint8Array, soi = 0): Uint8Array | null {
|
||||
const s = jpegSegments(b, soi).find(
|
||||
(x) => x.marker === 0xe1 && latin1(x.payload.slice(0, 6)) === 'Exif\0\0',
|
||||
);
|
||||
return s ? s.payload.slice(6) : null;
|
||||
}
|
||||
|
||||
// ── HEIF ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const box = (type: string, ...content: Uint8Array[]) => {
|
||||
const body = cat(...content);
|
||||
return cat(be32(body.length + 8), ascii(type), body);
|
||||
};
|
||||
|
||||
const infe = (id: number, type: string, extra: Uint8Array = bytes(0)) =>
|
||||
box('infe', bytes(2, 0, 0, 0), be16(id), be16(0), ascii(type), extra);
|
||||
|
||||
export const IMAGE_DATA = ascii('HEVC-IMAGE-DATA-THAT-MUST-NOT-CHANGE');
|
||||
|
||||
/**
|
||||
* A HEIC with an image item, an `Exif` item and an XMP `mime` item. Item data
|
||||
* goes to `mdat`, or for `idatExif` the EXIF goes into `idat` (method 1).
|
||||
* `metaLast` puts `meta` after `mdat`; `c2pa` appends a manifest `uuid` box.
|
||||
*/
|
||||
export function cameraHeic(
|
||||
opts: {
|
||||
le?: boolean;
|
||||
idatExif?: boolean;
|
||||
metaLast?: boolean;
|
||||
c2pa?: boolean;
|
||||
exifPayload?: Uint8Array;
|
||||
} = {},
|
||||
) {
|
||||
const le = opts.le ?? false;
|
||||
const exifItem = opts.exifPayload ?? cat(be32(6), ascii('Exif\0\0'), cameraTiff(le));
|
||||
const xmpItem = utf8(XMP_PACKET);
|
||||
|
||||
const ftyp = box('ftyp', ascii('heic'), be32(0), ascii('mif1heic'));
|
||||
const iinf = box(
|
||||
'iinf',
|
||||
bytes(0, 0, 0, 0),
|
||||
be16(3),
|
||||
infe(1, 'hvc1'),
|
||||
infe(2, 'Exif'),
|
||||
infe(3, 'mime', cat(bytes(0), ascii('application/rdf+xml\0'))),
|
||||
);
|
||||
// iloc v1: offset_size 4, length_size 4, base_offset_size 0, index_size 0.
|
||||
const entry = (id: number, method: number, offset: number, length: number) =>
|
||||
cat(be16(id), be16(method), be16(0), be16(1), be32(offset), be32(length));
|
||||
const buildMeta = (o: { image: number; exif: number; xmp: number }) =>
|
||||
box(
|
||||
'meta',
|
||||
bytes(0, 0, 0, 0),
|
||||
box('hdlr', bytes(0, 0, 0, 0), be32(0), ascii('pict'), be32(0), be32(0), be32(0), bytes(0)),
|
||||
iinf,
|
||||
box(
|
||||
'iloc',
|
||||
bytes(1, 0, 0, 0),
|
||||
bytes(0x44, 0x00),
|
||||
be16(3),
|
||||
entry(1, 0, o.image, IMAGE_DATA.length),
|
||||
entry(2, opts.idatExif ? 1 : 0, o.exif, exifItem.length),
|
||||
entry(3, 0, o.xmp, xmpItem.length),
|
||||
),
|
||||
...(opts.idatExif ? [box('idat', exifItem)] : []),
|
||||
);
|
||||
|
||||
const mdatBody = opts.idatExif ? cat(IMAGE_DATA, xmpItem) : cat(IMAGE_DATA, exifItem, xmpItem);
|
||||
const metaLength = buildMeta({ image: 0, exif: 0, xmp: 0 }).length;
|
||||
const mdatStart = ftyp.length + (opts.metaLast ? 0 : metaLength) + 8;
|
||||
const offsets = {
|
||||
image: mdatStart,
|
||||
exif: opts.idatExif ? 0 : mdatStart + IMAGE_DATA.length,
|
||||
xmp: mdatStart + IMAGE_DATA.length + (opts.idatExif ? 0 : exifItem.length),
|
||||
};
|
||||
const meta = buildMeta(offsets);
|
||||
const mdat = box('mdat', mdatBody);
|
||||
const c2pa = opts.c2pa
|
||||
? box(
|
||||
'uuid',
|
||||
bytes(
|
||||
0xd8,
|
||||
0xfe,
|
||||
0xc3,
|
||||
0xd6,
|
||||
0x1b,
|
||||
0x0e,
|
||||
0x48,
|
||||
0x3c,
|
||||
0x92,
|
||||
0x97,
|
||||
0x58,
|
||||
0x28,
|
||||
0x87,
|
||||
0x7e,
|
||||
0xc4,
|
||||
0x81,
|
||||
),
|
||||
ascii('c2pa manifest GPS 52.520095'),
|
||||
)
|
||||
: bytes();
|
||||
const file = opts.metaLast ? cat(ftyp, mdat, meta, c2pa) : cat(ftyp, meta, mdat, c2pa);
|
||||
return { file, imageAt: offsets.image };
|
||||
}
|
||||
|
||||
/** The TIFF block of the EXIF item, read back by its known offset prefix. */
|
||||
export function heicExif(file: Uint8Array): Uint8Array | null {
|
||||
const at = indexOf(file, cat(be32(6), ascii('Exif\0\0')));
|
||||
return at < 0 ? null : file.slice(at + 10);
|
||||
}
|
||||
|
||||
/** Feed `data` as a stream cut into `size`-byte chunks. */
|
||||
export function chunked(data: Uint8Array, size: number): ReadableStream<Uint8Array> {
|
||||
let pos = 0;
|
||||
return new ReadableStream<Uint8Array>({
|
||||
pull(controller) {
|
||||
if (pos >= data.length) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
controller.enqueue(data.slice(pos, pos + size));
|
||||
pos += size;
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user