safe-upload 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +134 -2
- package/dist/api.d.ts +25 -0
- package/dist/api.js +172 -0
- package/dist/browser/canvas.d.ts +20 -0
- package/dist/browser/canvas.js +53 -0
- package/dist/browser/faces.d.ts +26 -0
- package/dist/browser/faces.js +68 -0
- package/dist/browser/index.d.ts +9 -0
- package/dist/browser/index.js +5 -0
- package/dist/browser/mediapipe.d.ts +28 -0
- package/dist/browser/mediapipe.js +69 -0
- package/dist/browser/objects.d.ts +29 -0
- package/dist/browser/objects.js +69 -0
- package/dist/browser/redact.d.ts +29 -0
- package/dist/browser/redact.js +147 -0
- package/dist/bytes.d.ts +22 -0
- package/dist/bytes.js +109 -0
- package/dist/categories.d.ts +13 -0
- package/dist/categories.js +113 -0
- package/dist/collect.d.ts +16 -0
- package/dist/collect.js +108 -0
- package/dist/exif-tags.d.ts +23 -0
- package/dist/exif-tags.js +200 -0
- package/dist/exif.d.ts +64 -0
- package/dist/exif.js +507 -0
- package/dist/filename.d.ts +19 -0
- package/dist/filename.js +64 -0
- package/dist/heif.d.ts +6 -0
- package/dist/heif.js +299 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/iptc.d.ts +11 -0
- package/dist/iptc.js +118 -0
- package/dist/jpeg.d.ts +51 -0
- package/dist/jpeg.js +374 -0
- package/dist/png.d.ts +9 -0
- package/dist/png.js +245 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.js +1 -0
- package/dist/webp.d.ts +6 -0
- package/dist/webp.js +123 -0
- package/dist/xmp.d.ts +25 -0
- package/dist/xmp.js +364 -0
- package/package.json +78 -4
package/dist/heif.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { latin1, u16be, u32be, u64be } from './bytes.js';
|
|
2
|
+
import { Collector } from './collect.js';
|
|
3
|
+
import { buildTiff, parseTiff, tiffCategories } from './exif.js';
|
|
4
|
+
import { analyzeXmp, emptyXmpPacket } from './xmp.js';
|
|
5
|
+
function boxes(b, from, to) {
|
|
6
|
+
const out = [];
|
|
7
|
+
let p = from;
|
|
8
|
+
while (p + 8 <= to) {
|
|
9
|
+
let size = u32be(b, p);
|
|
10
|
+
const type = latin1(b, p + 4, p + 8);
|
|
11
|
+
let body = p + 8;
|
|
12
|
+
if (size === 1) {
|
|
13
|
+
if (p + 16 > to)
|
|
14
|
+
break;
|
|
15
|
+
size = u64be(b, p + 8);
|
|
16
|
+
body = p + 16;
|
|
17
|
+
}
|
|
18
|
+
else if (size === 0)
|
|
19
|
+
size = to - p;
|
|
20
|
+
if (size < 8 || p + size > to)
|
|
21
|
+
break;
|
|
22
|
+
out.push({ type, start: p, body, end: p + size });
|
|
23
|
+
p += size;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
const HEIC_BRANDS = new Set(['heic', 'heix', 'heim', 'heis', 'hevc', 'hevx', 'mif1', 'msf1', 'mif2', 'MiHE', 'MiHB']);
|
|
28
|
+
export function heifFormat(b) {
|
|
29
|
+
if (b.length < 16 || latin1(b, 4, 8) !== 'ftyp')
|
|
30
|
+
return null;
|
|
31
|
+
const size = Math.min(u32be(b, 0), b.length);
|
|
32
|
+
const brands = [latin1(b, 8, 12)];
|
|
33
|
+
for (let p = 16; p + 4 <= size; p += 4)
|
|
34
|
+
brands.push(latin1(b, p, p + 4));
|
|
35
|
+
if (brands.some((x) => x === 'avif' || x === 'avis'))
|
|
36
|
+
return 'avif';
|
|
37
|
+
if (brands.some((x) => HEIC_BRANDS.has(x)))
|
|
38
|
+
return 'heic';
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function parseHeif(b) {
|
|
42
|
+
const top = boxes(b, 0, b.length);
|
|
43
|
+
const meta = top.find((x) => x.type === 'meta');
|
|
44
|
+
if (!meta)
|
|
45
|
+
throw new Error('HEIF file has no meta box');
|
|
46
|
+
const kids = boxes(b, meta.body + 4, meta.end);
|
|
47
|
+
const h = { items: new Map(), refs: [], props: [], assoc: new Map() };
|
|
48
|
+
let idat;
|
|
49
|
+
for (const k of kids) {
|
|
50
|
+
if (k.type === 'pitm') {
|
|
51
|
+
const v = b[k.body];
|
|
52
|
+
h.primary = v === 0 ? u16be(b, k.body + 4) : u32be(b, k.body + 4);
|
|
53
|
+
}
|
|
54
|
+
else if (k.type === 'idat')
|
|
55
|
+
idat = k;
|
|
56
|
+
else if (k.type === 'iinf') {
|
|
57
|
+
const v = b[k.body];
|
|
58
|
+
const start = k.body + 4 + (v === 0 ? 2 : 4);
|
|
59
|
+
for (const infe of boxes(b, start, k.end)) {
|
|
60
|
+
if (infe.type !== 'infe')
|
|
61
|
+
continue;
|
|
62
|
+
const iv = b[infe.body];
|
|
63
|
+
if (iv < 2)
|
|
64
|
+
continue;
|
|
65
|
+
let p = infe.body + 4;
|
|
66
|
+
const id = iv === 2 ? u16be(b, p) : u32be(b, p);
|
|
67
|
+
p += iv === 2 ? 2 : 4;
|
|
68
|
+
p += 2; // protection index
|
|
69
|
+
const type = latin1(b, p, p + 4);
|
|
70
|
+
p += 4;
|
|
71
|
+
const nameEnd = b.indexOf(0, p);
|
|
72
|
+
const name = latin1(b, p, nameEnd < 0 || nameEnd > infe.end ? infe.end : nameEnd);
|
|
73
|
+
p = nameEnd + 1;
|
|
74
|
+
let contentType;
|
|
75
|
+
if (type === 'mime' && p < infe.end) {
|
|
76
|
+
const ce = b.indexOf(0, p);
|
|
77
|
+
contentType = latin1(b, p, ce < 0 || ce > infe.end ? infe.end : ce);
|
|
78
|
+
}
|
|
79
|
+
h.items.set(id, { id, type, name, contentType, extents: [] });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (k.type === 'iref') {
|
|
83
|
+
const v = b[k.body];
|
|
84
|
+
const idSize = v === 0 ? 2 : 4;
|
|
85
|
+
for (const r of boxes(b, k.body + 4, k.end)) {
|
|
86
|
+
let p = r.body;
|
|
87
|
+
const rd = (q) => (idSize === 2 ? u16be(b, q) : u32be(b, q));
|
|
88
|
+
const from = rd(p);
|
|
89
|
+
p += idSize;
|
|
90
|
+
const n = u16be(b, p);
|
|
91
|
+
p += 2;
|
|
92
|
+
const to = [];
|
|
93
|
+
for (let i = 0; i < n && p + idSize <= r.end; i++, p += idSize)
|
|
94
|
+
to.push(rd(p));
|
|
95
|
+
h.refs.push({ type: r.type, from, to });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (k.type === 'iprp') {
|
|
99
|
+
for (const c of boxes(b, k.body, k.end)) {
|
|
100
|
+
if (c.type === 'ipco')
|
|
101
|
+
for (const prop of boxes(b, c.body, c.end))
|
|
102
|
+
h.props.push({ type: prop.type, data: b.subarray(prop.body, prop.end) });
|
|
103
|
+
else if (c.type === 'ipma') {
|
|
104
|
+
const v = b[c.body];
|
|
105
|
+
const flags = (b[c.body + 1] << 16) | (b[c.body + 2] << 8) | b[c.body + 3];
|
|
106
|
+
let p = c.body + 4;
|
|
107
|
+
const count = u32be(b, p);
|
|
108
|
+
p += 4;
|
|
109
|
+
for (let i = 0; i < count && p < c.end; i++) {
|
|
110
|
+
const id = v < 1 ? u16be(b, p) : u32be(b, p);
|
|
111
|
+
p += v < 1 ? 2 : 4;
|
|
112
|
+
const n = b[p++];
|
|
113
|
+
const list = [];
|
|
114
|
+
for (let j = 0; j < n; j++) {
|
|
115
|
+
if (flags & 1) {
|
|
116
|
+
list.push(u16be(b, p) & 0x7fff);
|
|
117
|
+
p += 2;
|
|
118
|
+
}
|
|
119
|
+
else
|
|
120
|
+
list.push(b[p++] & 0x7f);
|
|
121
|
+
}
|
|
122
|
+
h.assoc.set(id, list);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const iloc = kids.find((x) => x.type === 'iloc');
|
|
129
|
+
if (iloc) {
|
|
130
|
+
const v = b[iloc.body];
|
|
131
|
+
let p = iloc.body + 4;
|
|
132
|
+
const offsetSize = b[p] >> 4;
|
|
133
|
+
const lengthSize = b[p] & 15;
|
|
134
|
+
const baseSize = b[p + 1] >> 4;
|
|
135
|
+
const indexSize = v === 1 || v === 2 ? b[p + 1] & 15 : 0;
|
|
136
|
+
p += 2;
|
|
137
|
+
const rdN = (n) => {
|
|
138
|
+
const val = n === 0 ? 0 : n === 4 ? u32be(b, p) : n === 8 ? u64be(b, p) : n === 2 ? u16be(b, p) : 0;
|
|
139
|
+
p += n;
|
|
140
|
+
return val;
|
|
141
|
+
};
|
|
142
|
+
const count = v < 2 ? u16be(b, p) : u32be(b, p);
|
|
143
|
+
p += v < 2 ? 2 : 4;
|
|
144
|
+
for (let i = 0; i < count && p < iloc.end; i++) {
|
|
145
|
+
const id = v < 2 ? u16be(b, p) : u32be(b, p);
|
|
146
|
+
p += v < 2 ? 2 : 4;
|
|
147
|
+
let method = 0;
|
|
148
|
+
if (v === 1 || v === 2) {
|
|
149
|
+
method = u16be(b, p) & 15;
|
|
150
|
+
p += 2;
|
|
151
|
+
}
|
|
152
|
+
p += 2; // data reference index
|
|
153
|
+
const base = rdN(baseSize);
|
|
154
|
+
const ext = u16be(b, p);
|
|
155
|
+
p += 2;
|
|
156
|
+
const extents = [];
|
|
157
|
+
for (let e = 0; e < ext; e++) {
|
|
158
|
+
rdN(indexSize);
|
|
159
|
+
const off = rdN(offsetSize);
|
|
160
|
+
const len = rdN(lengthSize);
|
|
161
|
+
const fileBase = method === 1 && idat ? idat.body : 0;
|
|
162
|
+
if (method === 0 || method === 1)
|
|
163
|
+
extents.push({ offset: fileBase + base + off, length: len });
|
|
164
|
+
}
|
|
165
|
+
const item = h.items.get(id);
|
|
166
|
+
if (item)
|
|
167
|
+
item.extents = extents;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return h;
|
|
171
|
+
}
|
|
172
|
+
function itemBytes(b, item) {
|
|
173
|
+
if (item.extents.length === 1) {
|
|
174
|
+
const e = item.extents[0];
|
|
175
|
+
return b.subarray(e.offset, Math.min(b.length, e.offset + e.length));
|
|
176
|
+
}
|
|
177
|
+
const total = item.extents.reduce((s, e) => s + e.length, 0);
|
|
178
|
+
const out = new Uint8Array(total);
|
|
179
|
+
let o = 0;
|
|
180
|
+
for (const e of item.extents) {
|
|
181
|
+
out.set(b.subarray(e.offset, e.offset + e.length), o);
|
|
182
|
+
o += e.length;
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
function writeItem(out, item, data) {
|
|
187
|
+
let o = 0;
|
|
188
|
+
for (const e of item.extents) {
|
|
189
|
+
out.set(data.subarray(o, o + e.length), e.offset);
|
|
190
|
+
o += e.length;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function auxType(h, id) {
|
|
194
|
+
for (const idx of h.assoc.get(id) ?? []) {
|
|
195
|
+
const prop = h.props[idx - 1];
|
|
196
|
+
if (prop?.type === 'auxC') {
|
|
197
|
+
const d = prop.data;
|
|
198
|
+
const end = d.indexOf(0, 4);
|
|
199
|
+
return latin1(d, 4, end < 0 ? d.length : end);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
function isXmpItem(i) {
|
|
205
|
+
return i.type === 'mime' && /rdf\+xml|xmp/i.test(i.contentType ?? '');
|
|
206
|
+
}
|
|
207
|
+
function extras(h) {
|
|
208
|
+
const out = [];
|
|
209
|
+
const thumbs = h.refs.filter((r) => r.type === 'thmb');
|
|
210
|
+
if (thumbs.length)
|
|
211
|
+
out.push({ finding: { category: 'thumbnail', key: 'heif.thumbnail', label: 'Embedded thumbnail image', value: `${thumbs.length} preview image${thumbs.length > 1 ? 's' : ''}` } });
|
|
212
|
+
for (const r of h.refs.filter((x) => x.type === 'auxl')) {
|
|
213
|
+
const t = auxType(h, r.from) ?? '';
|
|
214
|
+
if (/alpha/i.test(t))
|
|
215
|
+
continue;
|
|
216
|
+
const label = /depth/i.test(t)
|
|
217
|
+
? 'Depth map'
|
|
218
|
+
: /gainmap|gain-map|hdrgainmap/i.test(t)
|
|
219
|
+
? 'HDR gain map'
|
|
220
|
+
: /matte/i.test(t)
|
|
221
|
+
? 'Portrait matte (person or skin mask)'
|
|
222
|
+
: 'Auxiliary image';
|
|
223
|
+
out.push({ finding: { category: 'hidden', key: `heif.aux.${r.from}`, label, value: t || 'auxiliary image' } });
|
|
224
|
+
}
|
|
225
|
+
for (const i of h.items.values())
|
|
226
|
+
if (i.type === 'tmap')
|
|
227
|
+
out.push({ finding: { category: 'hidden', key: `heif.tmap.${i.id}`, label: 'HDR tone map image', value: 'ISO 21496 gain map' } });
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
export function inspectHeif(b, c) {
|
|
231
|
+
const h = parseHeif(b);
|
|
232
|
+
// Primary image size from its ispe property
|
|
233
|
+
if (h.primary !== undefined) {
|
|
234
|
+
for (const idx of h.assoc.get(h.primary) ?? []) {
|
|
235
|
+
const prop = h.props[idx - 1];
|
|
236
|
+
if (prop?.type === 'ispe' && prop.data.length >= 12) {
|
|
237
|
+
c.report.width = u32be(prop.data, 4);
|
|
238
|
+
c.report.height = u32be(prop.data, 8);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (const item of h.items.values()) {
|
|
243
|
+
if (item.type === 'Exif' && item.extents.length) {
|
|
244
|
+
const d = itemBytes(b, item);
|
|
245
|
+
const skip = u32be(d, 0);
|
|
246
|
+
c.tiff(d.subarray(4 + skip), 'exif');
|
|
247
|
+
}
|
|
248
|
+
else if (isXmpItem(item) && item.extents.length)
|
|
249
|
+
c.xmp(itemBytes(b, item), 'xmp');
|
|
250
|
+
}
|
|
251
|
+
for (const e of extras(h))
|
|
252
|
+
c.add({ ...e.finding, source: 'heif', removable: false });
|
|
253
|
+
if (extras(h).length)
|
|
254
|
+
c.note('HEIC/AVIF embedded thumbnails and auxiliary images cannot be removed without converting the photo.');
|
|
255
|
+
}
|
|
256
|
+
const EMPTY_TIFF = new Uint8Array([0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
|
|
257
|
+
export function stripHeif(b, remove) {
|
|
258
|
+
const h = parseHeif(b);
|
|
259
|
+
const out = b.slice();
|
|
260
|
+
const removed = new Set();
|
|
261
|
+
const notes = [];
|
|
262
|
+
for (const item of h.items.values()) {
|
|
263
|
+
if (!item.extents.length)
|
|
264
|
+
continue;
|
|
265
|
+
const d = itemBytes(b, item);
|
|
266
|
+
if (item.type === 'Exif') {
|
|
267
|
+
const skip = u32be(d, 0);
|
|
268
|
+
const tiffStart = 4 + skip;
|
|
269
|
+
const tiff = parseTiff(d.subarray(tiffStart));
|
|
270
|
+
if (!tiff || ![...tiffCategories(tiff)].some((c) => remove.has(c)))
|
|
271
|
+
continue;
|
|
272
|
+
const r = buildTiff(tiff, { remove });
|
|
273
|
+
r.removed.forEach((c) => removed.add(c));
|
|
274
|
+
notes.push(...r.notes);
|
|
275
|
+
const newTiff = r.bytes ?? EMPTY_TIFF;
|
|
276
|
+
const replacement = new Uint8Array(d.length);
|
|
277
|
+
replacement.set(d.subarray(0, tiffStart), 0);
|
|
278
|
+
if (tiffStart + newTiff.length > d.length)
|
|
279
|
+
throw new Error('Rewritten EXIF does not fit in place');
|
|
280
|
+
replacement.set(newTiff, tiffStart);
|
|
281
|
+
writeItem(out, item, replacement);
|
|
282
|
+
}
|
|
283
|
+
else if (isXmpItem(item)) {
|
|
284
|
+
const hits = [...analyzeXmp(new TextDecoder().decode(d)).categories].filter((c) => remove.has(c));
|
|
285
|
+
if (!hits.length)
|
|
286
|
+
continue;
|
|
287
|
+
hits.forEach((c) => removed.add(c));
|
|
288
|
+
writeItem(out, item, emptyXmpPacket(d.length));
|
|
289
|
+
notes.push('The XMP metadata block was emptied as a whole.');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const retained = [];
|
|
293
|
+
for (const e of extras(h))
|
|
294
|
+
if (remove.has(e.finding.category))
|
|
295
|
+
retained.push({ ...e.finding, source: 'heif', removable: false });
|
|
296
|
+
if (retained.length)
|
|
297
|
+
notes.push('Embedded previews and auxiliary images in HEIC/AVIF stay in place; convert the photo to remove them.');
|
|
298
|
+
return { bytes: out, removed, retained, notes };
|
|
299
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { inspect, strip, stripToBlob, carryExif, detectFormat, toBytes, UnsupportedFormatError, describeCategory } from './api.js';
|
|
2
|
+
export { inspectFileName, neutralFileName } from './filename.js';
|
|
3
|
+
export type { FileNameFinding, FileNameReport } from './filename.js';
|
|
4
|
+
export { CATEGORY_INFO, CATEGORY_ORDER, DEFAULT_REMOVE } from './categories.js';
|
|
5
|
+
export type { CategoryInfo } from './categories.js';
|
|
6
|
+
export type { Category, Finding, Format, GpsPoint, HiddenMedia, Input, Report, Risk, Source, StripOptions, StripResult } from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { inspect, strip, stripToBlob, carryExif, detectFormat, toBytes, UnsupportedFormatError, describeCategory } from './api.js';
|
|
2
|
+
export { inspectFileName, neutralFileName } from './filename.js';
|
|
3
|
+
export { CATEGORY_INFO, CATEGORY_ORDER, DEFAULT_REMOVE } from './categories.js';
|
package/dist/iptc.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Category, Finding, Source } from './types.js';
|
|
2
|
+
export interface IrbAnalysis {
|
|
3
|
+
findings: Finding[];
|
|
4
|
+
categories: Set<Category>;
|
|
5
|
+
names: string[];
|
|
6
|
+
thumbnail?: Uint8Array;
|
|
7
|
+
/** XMP stored inside the Photoshop resources (resource 0x0424). */
|
|
8
|
+
xmp?: Uint8Array;
|
|
9
|
+
}
|
|
10
|
+
/** Parse Photoshop image resources ("8BIM" blocks), optionally after a "Photoshop 3.0\0" header. */
|
|
11
|
+
export declare function analyzeIrb(b: Uint8Array, source: Source, removable?: boolean): IrbAnalysis;
|
package/dist/iptc.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { latin1, startsWith, u16be, u32be, utf8, clean } from './bytes.js';
|
|
2
|
+
/** IIM record 2 datasets we classify. Unlisted datasets count as 'descriptions'. */
|
|
3
|
+
const DATASETS = {
|
|
4
|
+
0: ['Record version', 'technical'],
|
|
5
|
+
5: ['Object name', 'descriptions'],
|
|
6
|
+
7: ['Edit status', 'descriptions'],
|
|
7
|
+
10: ['Urgency', 'descriptions'],
|
|
8
|
+
15: ['Category', 'descriptions'],
|
|
9
|
+
20: ['Supplemental category', 'descriptions'],
|
|
10
|
+
25: ['Keywords', 'descriptions'],
|
|
11
|
+
40: ['Special instructions', 'descriptions'],
|
|
12
|
+
55: ['Date created', 'time'],
|
|
13
|
+
60: ['Time created', 'time'],
|
|
14
|
+
62: ['Digital creation date', 'time'],
|
|
15
|
+
63: ['Digital creation time', 'time'],
|
|
16
|
+
65: ['Originating program', 'software'],
|
|
17
|
+
70: ['Program version', 'software'],
|
|
18
|
+
80: ['By-line', 'author'],
|
|
19
|
+
85: ['By-line title', 'author'],
|
|
20
|
+
90: ['City', 'location'],
|
|
21
|
+
92: ['Sub-location', 'location'],
|
|
22
|
+
95: ['Province/State', 'location'],
|
|
23
|
+
100: ['Country code', 'location'],
|
|
24
|
+
101: ['Country', 'location'],
|
|
25
|
+
103: ['Original transmission reference', 'descriptions'],
|
|
26
|
+
105: ['Headline', 'descriptions'],
|
|
27
|
+
110: ['Credit', 'author'],
|
|
28
|
+
115: ['Source', 'author'],
|
|
29
|
+
116: ['Copyright notice', 'author'],
|
|
30
|
+
118: ['Contact', 'author'],
|
|
31
|
+
120: ['Caption', 'descriptions'],
|
|
32
|
+
122: ['Caption writer', 'author'],
|
|
33
|
+
};
|
|
34
|
+
/** Parse Photoshop image resources ("8BIM" blocks), optionally after a "Photoshop 3.0\0" header. */
|
|
35
|
+
export function analyzeIrb(b, source, removable = true) {
|
|
36
|
+
const out = { findings: [], categories: new Set(), names: [] };
|
|
37
|
+
let p = startsWith(b, 0, 'Photoshop 3.0\0') ? 14 : 0;
|
|
38
|
+
while (p + 12 <= b.length && startsWith(b, p, '8BIM')) {
|
|
39
|
+
const id = u16be(b, p + 4);
|
|
40
|
+
const nameLen = b[p + 6];
|
|
41
|
+
let q = p + 7 + nameLen;
|
|
42
|
+
if ((nameLen + 1) & 1)
|
|
43
|
+
q++;
|
|
44
|
+
if (q + 4 > b.length)
|
|
45
|
+
break;
|
|
46
|
+
const size = u32be(b, q);
|
|
47
|
+
const start = q + 4;
|
|
48
|
+
const end = Math.min(b.length, start + size);
|
|
49
|
+
const data = b.subarray(start, end);
|
|
50
|
+
if (id === 0x0404)
|
|
51
|
+
parseIim(data, source, removable, out);
|
|
52
|
+
else if (id === 0x040c || id === 0x0409) {
|
|
53
|
+
out.categories.add('thumbnail');
|
|
54
|
+
out.findings.push({ category: 'thumbnail', source, key: 'iptc.PhotoshopThumbnail', label: 'Photoshop thumbnail', value: `${Math.max(0, data.length - 28).toLocaleString('en')} byte preview`, removable });
|
|
55
|
+
if (data.length > 28 && data[28] === 0xff && data[29] === 0xd8)
|
|
56
|
+
out.thumbnail = data.subarray(28);
|
|
57
|
+
}
|
|
58
|
+
else if (id === 0x0424)
|
|
59
|
+
out.xmp = data;
|
|
60
|
+
else if (id === 0x0425) {
|
|
61
|
+
// IPTC digest: harmless checksum
|
|
62
|
+
}
|
|
63
|
+
else if (id === 0x040f || id === 0x0422 || id === 0x0423) {
|
|
64
|
+
// ICC profile / EXIF copies: EXIF copy is handled as 'other'
|
|
65
|
+
if (id !== 0x040f) {
|
|
66
|
+
out.categories.add('other');
|
|
67
|
+
out.findings.push({ category: 'other', source, key: `iptc.irb${id.toString(16)}`, label: 'Photoshop copy of EXIF data', value: `${data.length.toLocaleString('en')} bytes`, removable });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (id >= 0x07d0 && id <= 0x0bb6) {
|
|
71
|
+
out.categories.add('history');
|
|
72
|
+
out.findings.push({ category: 'history', source, key: 'iptc.paths', label: 'Photoshop paths', value: 'Clipping or work paths', removable });
|
|
73
|
+
}
|
|
74
|
+
p = end + (size & 1);
|
|
75
|
+
}
|
|
76
|
+
// Deduplicate path findings
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
out.findings = out.findings.filter((f) => (seen.has(f.key + f.value) ? false : (seen.add(f.key + f.value), true)));
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
function parseIim(b, source, removable, out) {
|
|
82
|
+
let p = 0;
|
|
83
|
+
let isUtf8 = false;
|
|
84
|
+
const values = new Map();
|
|
85
|
+
while (p + 5 <= b.length && b[p] === 0x1c) {
|
|
86
|
+
const record = b[p + 1];
|
|
87
|
+
const dataset = b[p + 2];
|
|
88
|
+
let len = u16be(b, p + 3);
|
|
89
|
+
let start = p + 5;
|
|
90
|
+
if (len & 0x8000) {
|
|
91
|
+
const n = len & 0x7fff;
|
|
92
|
+
len = 0;
|
|
93
|
+
for (let i = 0; i < n; i++)
|
|
94
|
+
len = len * 256 + b[start + i];
|
|
95
|
+
start += n;
|
|
96
|
+
}
|
|
97
|
+
const data = b.subarray(start, Math.min(b.length, start + len));
|
|
98
|
+
if (record === 1 && dataset === 90 && latin1(data).includes('%G'))
|
|
99
|
+
isUtf8 = true;
|
|
100
|
+
if (record === 2) {
|
|
101
|
+
const s = clean(isUtf8 ? utf8(data) : latin1(data));
|
|
102
|
+
if (!values.has(dataset))
|
|
103
|
+
values.set(dataset, []);
|
|
104
|
+
values.get(dataset).push(s);
|
|
105
|
+
}
|
|
106
|
+
p = start + len;
|
|
107
|
+
}
|
|
108
|
+
for (const [ds, vals] of values) {
|
|
109
|
+
const [label, cls] = DATASETS[ds] ?? [`IPTC dataset 2:${ds}`, 'descriptions'];
|
|
110
|
+
if (cls === 'technical')
|
|
111
|
+
continue;
|
|
112
|
+
out.categories.add(cls);
|
|
113
|
+
const value = vals.join(', ');
|
|
114
|
+
out.findings.push({ category: cls, source, key: `iptc.2:${ds}`, label, value: value.length > 300 ? value.slice(0, 300) + '…' : value, removable });
|
|
115
|
+
if (cls === 'author' && (ds === 80 || ds === 122))
|
|
116
|
+
out.names.push(...vals);
|
|
117
|
+
}
|
|
118
|
+
}
|
package/dist/jpeg.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Collector } from './collect.js';
|
|
2
|
+
import type { Category, Finding, HiddenMedia } from './types.js';
|
|
3
|
+
type Kind = 'exif' | 'xmp' | 'xmpext' | 'iptc' | 'icc' | 'mpf' | 'jfif' | 'jfxx' | 'adobe' | 'jumbf' | 'com' | 'app' | 'image';
|
|
4
|
+
export interface JpegSegment {
|
|
5
|
+
marker: number;
|
|
6
|
+
/** Offset of the 0xFF marker byte. */
|
|
7
|
+
start: number;
|
|
8
|
+
/** Offset after the segment. */
|
|
9
|
+
end: number;
|
|
10
|
+
/** Payload after the 2-byte length. */
|
|
11
|
+
payload: Uint8Array;
|
|
12
|
+
kind: Kind;
|
|
13
|
+
}
|
|
14
|
+
export interface ParsedJpeg {
|
|
15
|
+
/** Segments between SOI and the first SOS, in file order. */
|
|
16
|
+
head: JpegSegment[];
|
|
17
|
+
/** Offset of the first SOS marker. */
|
|
18
|
+
scanStart: number;
|
|
19
|
+
/** Offset just after the main image's EOI. */
|
|
20
|
+
imageEnd: number;
|
|
21
|
+
trailer: Uint8Array;
|
|
22
|
+
width?: number;
|
|
23
|
+
height?: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function isJpeg(b: Uint8Array): boolean;
|
|
26
|
+
/** Find the end (offset after EOI) of a JPEG starting at `from`, scanning through entropy-coded data. */
|
|
27
|
+
export declare function findJpegEnd(b: Uint8Array, from: number): number;
|
|
28
|
+
export declare function parseJpeg(b: Uint8Array): ParsedJpeg;
|
|
29
|
+
/**
|
|
30
|
+
* Read the MPF index in an APP2 segment and return a label for each extra image, keyed by its
|
|
31
|
+
* absolute offset in the file. Offsets in the index count from the MPF TIFF header.
|
|
32
|
+
*/
|
|
33
|
+
export declare function mpfLabels(seg: JpegSegment): Map<number, string>;
|
|
34
|
+
/** Split bytes after the main image into recognisable items (extra JPEGs, MP4/MOV video, vendor data). */
|
|
35
|
+
export declare function splitTrailer(trailer: Uint8Array, motionVideoLength?: number, labelAt?: (relativeOffset: number) => string | undefined): HiddenMedia[];
|
|
36
|
+
export declare function inspectJpeg(b: Uint8Array, c: Collector): void;
|
|
37
|
+
export interface FormatStrip {
|
|
38
|
+
bytes: Uint8Array;
|
|
39
|
+
removed: Set<Category>;
|
|
40
|
+
retained: Finding[];
|
|
41
|
+
notes: string[];
|
|
42
|
+
}
|
|
43
|
+
/** Remove metadata categories from a JPEG without touching the compressed picture. */
|
|
44
|
+
export declare function stripJpeg(b: Uint8Array, remove: Set<Category>, exifOverride?: {
|
|
45
|
+
orientation?: number;
|
|
46
|
+
dimensions?: {
|
|
47
|
+
width: number;
|
|
48
|
+
height: number;
|
|
49
|
+
};
|
|
50
|
+
}): FormatStrip;
|
|
51
|
+
export {};
|