@intactfile/engines 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/README.md +68 -0
- package/dist/archive/diagnose.d.ts +37 -0
- package/dist/archive/diagnose.d.ts.map +1 -0
- package/dist/archive/diagnose.js +165 -0
- package/dist/archive/diagnose.js.map +1 -0
- package/dist/archive/format.d.ts +52 -0
- package/dist/archive/format.d.ts.map +1 -0
- package/dist/archive/format.js +87 -0
- package/dist/archive/format.js.map +1 -0
- package/dist/archive/host.d.ts +83 -0
- package/dist/archive/host.d.ts.map +1 -0
- package/dist/archive/host.js +92 -0
- package/dist/archive/host.js.map +1 -0
- package/dist/archive/index.d.ts +22 -0
- package/dist/archive/index.d.ts.map +1 -0
- package/dist/archive/index.js +19 -0
- package/dist/archive/index.js.map +1 -0
- package/dist/archive/salvage.d.ts +73 -0
- package/dist/archive/salvage.d.ts.map +1 -0
- package/dist/archive/salvage.js +210 -0
- package/dist/archive/salvage.js.map +1 -0
- package/dist/byte-source.d.ts +19 -0
- package/dist/byte-source.d.ts.map +1 -0
- package/dist/byte-source.js +16 -0
- package/dist/byte-source.js.map +1 -0
- package/dist/bytesource/blob.d.ts +14 -0
- package/dist/bytesource/blob.d.ts.map +1 -0
- package/dist/bytesource/blob.js +23 -0
- package/dist/bytesource/blob.js.map +1 -0
- package/dist/crc32.d.ts +2 -0
- package/dist/crc32.d.ts.map +1 -0
- package/dist/crc32.js +18 -0
- package/dist/crc32.js.map +1 -0
- package/dist/detect/index.d.ts +16 -0
- package/dist/detect/index.d.ts.map +1 -0
- package/dist/detect/index.js +159 -0
- package/dist/detect/index.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/jpeg/browser-loader.d.ts +21 -0
- package/dist/jpeg/browser-loader.d.ts.map +1 -0
- package/dist/jpeg/browser-loader.js +28 -0
- package/dist/jpeg/browser-loader.js.map +1 -0
- package/dist/jpeg/diagnose.d.ts +50 -0
- package/dist/jpeg/diagnose.d.ts.map +1 -0
- package/dist/jpeg/diagnose.js +124 -0
- package/dist/jpeg/diagnose.js.map +1 -0
- package/dist/jpeg/exif.d.ts +11 -0
- package/dist/jpeg/exif.d.ts.map +1 -0
- package/dist/jpeg/exif.js +50 -0
- package/dist/jpeg/exif.js.map +1 -0
- package/dist/jpeg/format.d.ts +106 -0
- package/dist/jpeg/format.d.ts.map +1 -0
- package/dist/jpeg/format.js +248 -0
- package/dist/jpeg/format.js.map +1 -0
- package/dist/jpeg/index.d.ts +18 -0
- package/dist/jpeg/index.d.ts.map +1 -0
- package/dist/jpeg/index.js +14 -0
- package/dist/jpeg/index.js.map +1 -0
- package/dist/jpeg/mozjpeg-host.d.ts +60 -0
- package/dist/jpeg/mozjpeg-host.d.ts.map +1 -0
- package/dist/jpeg/mozjpeg-host.js +138 -0
- package/dist/jpeg/mozjpeg-host.js.map +1 -0
- package/dist/jpeg/repair.d.ts +89 -0
- package/dist/jpeg/repair.d.ts.map +1 -0
- package/dist/jpeg/repair.js +216 -0
- package/dist/jpeg/repair.js.map +1 -0
- package/dist/node.d.ts +11 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +29 -0
- package/dist/node.js.map +1 -0
- package/dist/pdf/browser-loader.d.ts +30 -0
- package/dist/pdf/browser-loader.d.ts.map +1 -0
- package/dist/pdf/browser-loader.js +21 -0
- package/dist/pdf/browser-loader.js.map +1 -0
- package/dist/pdf/diagnose.d.ts +23 -0
- package/dist/pdf/diagnose.d.ts.map +1 -0
- package/dist/pdf/diagnose.js +113 -0
- package/dist/pdf/diagnose.js.map +1 -0
- package/dist/pdf/format.d.ts +61 -0
- package/dist/pdf/format.d.ts.map +1 -0
- package/dist/pdf/format.js +150 -0
- package/dist/pdf/format.js.map +1 -0
- package/dist/pdf/index.d.ts +17 -0
- package/dist/pdf/index.d.ts.map +1 -0
- package/dist/pdf/index.js +12 -0
- package/dist/pdf/index.js.map +1 -0
- package/dist/pdf/qpdf-host.d.ts +66 -0
- package/dist/pdf/qpdf-host.d.ts.map +1 -0
- package/dist/pdf/qpdf-host.js +149 -0
- package/dist/pdf/qpdf-host.js.map +1 -0
- package/dist/pdf/rebuild.d.ts +32 -0
- package/dist/pdf/rebuild.d.ts.map +1 -0
- package/dist/pdf/rebuild.js +90 -0
- package/dist/pdf/rebuild.js.map +1 -0
- package/dist/pdf/repair.d.ts +61 -0
- package/dist/pdf/repair.d.ts.map +1 -0
- package/dist/pdf/repair.js +167 -0
- package/dist/pdf/repair.js.map +1 -0
- package/dist/png/diagnose.d.ts +50 -0
- package/dist/png/diagnose.d.ts.map +1 -0
- package/dist/png/diagnose.js +261 -0
- package/dist/png/diagnose.js.map +1 -0
- package/dist/png/format.d.ts +133 -0
- package/dist/png/format.d.ts.map +1 -0
- package/dist/png/format.js +350 -0
- package/dist/png/format.js.map +1 -0
- package/dist/png/index.d.ts +13 -0
- package/dist/png/index.d.ts.map +1 -0
- package/dist/png/index.js +10 -0
- package/dist/png/index.js.map +1 -0
- package/dist/png/repair.d.ts +31 -0
- package/dist/png/repair.d.ts.map +1 -0
- package/dist/png/repair.js +335 -0
- package/dist/png/repair.js.map +1 -0
- package/dist/sqlite/diagnose.d.ts +31 -0
- package/dist/sqlite/diagnose.d.ts.map +1 -0
- package/dist/sqlite/diagnose.js +169 -0
- package/dist/sqlite/diagnose.js.map +1 -0
- package/dist/sqlite/format.d.ts +103 -0
- package/dist/sqlite/format.d.ts.map +1 -0
- package/dist/sqlite/format.js +237 -0
- package/dist/sqlite/format.js.map +1 -0
- package/dist/sqlite/index.d.ts +24 -0
- package/dist/sqlite/index.d.ts.map +1 -0
- package/dist/sqlite/index.js +19 -0
- package/dist/sqlite/index.js.map +1 -0
- package/dist/sqlite/recover.d.ts +66 -0
- package/dist/sqlite/recover.d.ts.map +1 -0
- package/dist/sqlite/recover.js +305 -0
- package/dist/sqlite/recover.js.map +1 -0
- package/dist/sqlite/scan.d.ts +94 -0
- package/dist/sqlite/scan.d.ts.map +1 -0
- package/dist/sqlite/scan.js +356 -0
- package/dist/sqlite/scan.js.map +1 -0
- package/dist/sqlite/sqlite-host.d.ts +37 -0
- package/dist/sqlite/sqlite-host.d.ts.map +1 -0
- package/dist/sqlite/sqlite-host.js +80 -0
- package/dist/sqlite/sqlite-host.js.map +1 -0
- package/dist/sqlite/wal.d.ts +14 -0
- package/dist/sqlite/wal.d.ts.map +1 -0
- package/dist/sqlite/wal.js +79 -0
- package/dist/sqlite/wal.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/video/diagnose.d.ts +41 -0
- package/dist/video/diagnose.d.ts.map +1 -0
- package/dist/video/diagnose.js +188 -0
- package/dist/video/diagnose.js.map +1 -0
- package/dist/video/format.d.ts +87 -0
- package/dist/video/format.d.ts.map +1 -0
- package/dist/video/format.js +273 -0
- package/dist/video/format.js.map +1 -0
- package/dist/video/index.d.ts +21 -0
- package/dist/video/index.d.ts.map +1 -0
- package/dist/video/index.js +17 -0
- package/dist/video/index.js.map +1 -0
- package/dist/video/libav-host.d.ts +88 -0
- package/dist/video/libav-host.d.ts.map +1 -0
- package/dist/video/libav-host.js +66 -0
- package/dist/video/libav-host.js.map +1 -0
- package/dist/video/mp4-rebuild.d.ts +167 -0
- package/dist/video/mp4-rebuild.d.ts.map +1 -0
- package/dist/video/mp4-rebuild.js +270 -0
- package/dist/video/mp4-rebuild.js.map +1 -0
- package/dist/video/mp4box-host.d.ts +62 -0
- package/dist/video/mp4box-host.d.ts.map +1 -0
- package/dist/video/mp4box-host.js +209 -0
- package/dist/video/mp4box-host.js.map +1 -0
- package/dist/video/reconstruct.d.ts +97 -0
- package/dist/video/reconstruct.d.ts.map +1 -0
- package/dist/video/reconstruct.js +271 -0
- package/dist/video/reconstruct.js.map +1 -0
- package/dist/video/remux.d.ts +85 -0
- package/dist/video/remux.d.ts.map +1 -0
- package/dist/video/remux.js +350 -0
- package/dist/video/remux.js.map +1 -0
- package/dist/zip/diagnose.d.ts +22 -0
- package/dist/zip/diagnose.d.ts.map +1 -0
- package/dist/zip/diagnose.js +126 -0
- package/dist/zip/diagnose.js.map +1 -0
- package/dist/zip/format.d.ts +79 -0
- package/dist/zip/format.d.ts.map +1 -0
- package/dist/zip/format.js +129 -0
- package/dist/zip/format.js.map +1 -0
- package/dist/zip/repair.d.ts +32 -0
- package/dist/zip/repair.d.ts.map +1 -0
- package/dist/zip/repair.js +82 -0
- package/dist/zip/repair.js.map +1 -0
- package/dist/zip/salvage.d.ts +23 -0
- package/dist/zip/salvage.d.ts.map +1 -0
- package/dist/zip/salvage.js +140 -0
- package/dist/zip/salvage.js.map +1 -0
- package/dist/zip/scan.d.ts +64 -0
- package/dist/zip/scan.d.ts.map +1 -0
- package/dist/zip/scan.js +386 -0
- package/dist/zip/scan.js.map +1 -0
- package/dist/zip/writer.d.ts +14 -0
- package/dist/zip/writer.d.ts.map +1 -0
- package/dist/zip/writer.js +131 -0
- package/dist/zip/writer.js.map +1 -0
- package/package.json +71 -0
- package/src/archive/LICENSE-unrar.txt +53 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scoped, honest JPEG *salvage* (docs/03 §4: "we repair the file's structure;
|
|
3
|
+
* data physically overwritten is recoverable by no software"). This is not
|
|
4
|
+
* pixel reconstruction and never invents data.
|
|
5
|
+
*
|
|
6
|
+
* Ladder of strategies, tried until one decodes:
|
|
7
|
+
* 1. strip a garbage prefix (SOI not at offset 0);
|
|
8
|
+
* 2. append a missing EOI to a truncated scan and partial-decode the rows
|
|
9
|
+
* that survived (libjpeg fills the missing bottom grey — we say so);
|
|
10
|
+
* 3. decode as-is;
|
|
11
|
+
* 4. donor transplant — when the header tables (DQT/DHT/SOF) are destroyed
|
|
12
|
+
* but the scan survived, splice a healthy same-camera reference's header
|
|
13
|
+
* onto the broken scan (what commercial JPEG-Repair does; it still cannot
|
|
14
|
+
* invent data, only reunite a valid header with real entropy data).
|
|
15
|
+
* Guaranteed fallback: if nothing decodes, extract the embedded EXIF thumbnail
|
|
16
|
+
* as an honest preview ("thumbnail only", never the full image).
|
|
17
|
+
*
|
|
18
|
+
* Whatever decodes is re-emitted as a fresh valid baseline JPEG. Outcome is
|
|
19
|
+
* success only for a clean decode; a partial/truncated salvage is reported as
|
|
20
|
+
* such.
|
|
21
|
+
*/
|
|
22
|
+
import type { ByteSource } from '../byte-source.js';
|
|
23
|
+
import { type JpegDamageClass, type JpegDiagnosis } from './diagnose.js';
|
|
24
|
+
import { type JpegEncodeOptions } from './mozjpeg-host.js';
|
|
25
|
+
export type JpegRepairOutcome = 'success' | 'partial' | 'failed';
|
|
26
|
+
export interface JpegRepairOptions {
|
|
27
|
+
/**
|
|
28
|
+
* A healthy JPEG from the SAME camera/model whose header (DQT/DHT/SOF) can
|
|
29
|
+
* be transplanted when the broken file's header is destroyed but its scan
|
|
30
|
+
* data survives. Dimensions and sampling must match for a correct result.
|
|
31
|
+
*/
|
|
32
|
+
reference?: ByteSource;
|
|
33
|
+
/** Encoder options for the re-emitted salvage (defaults to baseline q85). */
|
|
34
|
+
encodeOptions?: JpegEncodeOptions;
|
|
35
|
+
}
|
|
36
|
+
export type JpegRepairStrategy = 'original' | 'strip-prefix' | 'append-eoi' | 'donor-transplant' | 'exif-thumbnail';
|
|
37
|
+
export interface JpegRepairAttempt {
|
|
38
|
+
strategy: JpegRepairStrategy;
|
|
39
|
+
decoded: boolean;
|
|
40
|
+
warnings: string[];
|
|
41
|
+
error?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface JpegRepairReport {
|
|
44
|
+
outcome: JpegRepairOutcome;
|
|
45
|
+
damageClass: JpegDamageClass;
|
|
46
|
+
/** Winning strategy, or null when nothing worked. */
|
|
47
|
+
strategy: JpegRepairStrategy | null;
|
|
48
|
+
/** Human-readable list of what was actually recovered. */
|
|
49
|
+
salvaged: string[];
|
|
50
|
+
notes: string[];
|
|
51
|
+
warnings: string[];
|
|
52
|
+
bytesIn: number;
|
|
53
|
+
bytesOut: number;
|
|
54
|
+
dimensions: {
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
} | null;
|
|
58
|
+
previewSource: 'salvage' | 'exif-thumbnail' | null;
|
|
59
|
+
usedReference: boolean;
|
|
60
|
+
attempts: JpegRepairAttempt[];
|
|
61
|
+
}
|
|
62
|
+
export interface JpegRepairResult {
|
|
63
|
+
/** Re-emitted valid JPEG of the salvaged raster (absent if none decoded). */
|
|
64
|
+
image?: Uint8Array;
|
|
65
|
+
/** A preview to show the user — the salvage itself, or the EXIF thumbnail. */
|
|
66
|
+
preview?: Uint8Array;
|
|
67
|
+
report: JpegRepairReport;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build "donor head (SOI..before SOS) + broken tail (SOS..end)". Returns null
|
|
71
|
+
* when either side lacks the marker it must contribute.
|
|
72
|
+
*/
|
|
73
|
+
export declare function transplantHeader(reference: Uint8Array, broken: Uint8Array): Uint8Array | null;
|
|
74
|
+
interface Candidate {
|
|
75
|
+
strategy: JpegRepairStrategy;
|
|
76
|
+
bytes: Uint8Array;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The candidate ladder, pure given the bytes, diagnosis and optional donor.
|
|
80
|
+
* Exposed for unit testing the strategy selection.
|
|
81
|
+
*/
|
|
82
|
+
export declare function buildRepairCandidates(bytes: Uint8Array, diagnosis: JpegDiagnosis, reference?: Uint8Array): Candidate[];
|
|
83
|
+
/**
|
|
84
|
+
* Salvage a JPEG. Returns the re-emitted image (when anything decoded), a
|
|
85
|
+
* preview (the salvage or the EXIF thumbnail), and an honest report.
|
|
86
|
+
*/
|
|
87
|
+
export declare function repairJpeg(src: ByteSource, opts?: JpegRepairOptions): Promise<JpegRepairResult>;
|
|
88
|
+
export {};
|
|
89
|
+
//# sourceMappingURL=repair.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repair.d.ts","sourceRoot":"","sources":["../../src/jpeg/repair.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAIL,KAAK,eAAe,EACpB,KAAK,aAAa,EACnB,MAAM,eAAe,CAAC;AAGvB,OAAO,EAA6C,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtG,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEjE,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,SAAS,CAAC,EAAE,UAAU,CAAC;IACvB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,iBAAiB,CAAC;CACnC;AAED,MAAM,MAAM,kBAAkB,GAC1B,UAAU,GACV,cAAc,GACd,YAAY,GACZ,kBAAkB,GAClB,gBAAgB,CAAC;AAErB,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,WAAW,EAAE,eAAe,CAAC;IAC7B,qDAAqD;IACrD,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACpC,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACrD,aAAa,EAAE,SAAS,GAAG,gBAAgB,GAAG,IAAI,CAAC;IACnD,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAoBD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,GAAG,UAAU,GAAG,IAAI,CAiB7F;AAED,UAAU,SAAS;IACjB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,KAAK,EAAE,UAAU,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,UAAU,EACjB,SAAS,EAAE,aAAa,EACxB,SAAS,CAAC,EAAE,UAAU,GACrB,SAAS,EAAE,CAoBb;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,GAAE,iBAAsB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAoKzG"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { LOW_SCAN_ENTROPY, SOI_SCAN_WINDOW, diagnoseJpeg, } from './diagnose.js';
|
|
2
|
+
import { extractExifThumbnail } from './exif.js';
|
|
3
|
+
import { M_EOI, findSoi, scanMarkerPresence, scanMarkers } from './format.js';
|
|
4
|
+
import { decodeJpeg, encodeJpeg } from './mozjpeg-host.js';
|
|
5
|
+
/** True when libjpeg's stderr signals real missing/corrupt data (partial salvage). */
|
|
6
|
+
function warningsIndicatePartial(warnings) {
|
|
7
|
+
return warnings.some((w) => /premature|corrupt|extraneous|bogus|out of|damaged/i.test(w));
|
|
8
|
+
}
|
|
9
|
+
function endsWithEoi(b) {
|
|
10
|
+
return b.length >= 2 && b[b.length - 2] === 0xff && b[b.length - 1] === M_EOI;
|
|
11
|
+
}
|
|
12
|
+
function ensureEoi(b) {
|
|
13
|
+
if (endsWithEoi(b))
|
|
14
|
+
return b;
|
|
15
|
+
const out = new Uint8Array(b.length + 2);
|
|
16
|
+
out.set(b);
|
|
17
|
+
out[b.length] = 0xff;
|
|
18
|
+
out[b.length + 1] = M_EOI;
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Build "donor head (SOI..before SOS) + broken tail (SOS..end)". Returns null
|
|
23
|
+
* when either side lacks the marker it must contribute.
|
|
24
|
+
*/
|
|
25
|
+
export function transplantHeader(reference, broken) {
|
|
26
|
+
const refSoi = findSoi(reference, SOI_SCAN_WINDOW);
|
|
27
|
+
if (refSoi === -1)
|
|
28
|
+
return null;
|
|
29
|
+
const refScan = scanMarkers(reference, refSoi);
|
|
30
|
+
if (refScan.sosOffset === -1)
|
|
31
|
+
return null;
|
|
32
|
+
const donorHead = reference.subarray(refSoi, refScan.sosOffset);
|
|
33
|
+
const brSoi = findSoi(broken, SOI_SCAN_WINDOW);
|
|
34
|
+
const brStart = brSoi === -1 ? 0 : brSoi;
|
|
35
|
+
const brPres = scanMarkerPresence(broken, brStart + 2);
|
|
36
|
+
if (brPres.sosOffset === -1)
|
|
37
|
+
return null;
|
|
38
|
+
const brokenTail = broken.subarray(brPres.sosOffset);
|
|
39
|
+
const out = new Uint8Array(donorHead.length + brokenTail.length);
|
|
40
|
+
out.set(donorHead, 0);
|
|
41
|
+
out.set(brokenTail, donorHead.length);
|
|
42
|
+
return ensureEoi(out);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The candidate ladder, pure given the bytes, diagnosis and optional donor.
|
|
46
|
+
* Exposed for unit testing the strategy selection.
|
|
47
|
+
*/
|
|
48
|
+
export function buildRepairCandidates(bytes, diagnosis, reference) {
|
|
49
|
+
const candidates = [];
|
|
50
|
+
if (diagnosis.soiOffset === null)
|
|
51
|
+
return candidates;
|
|
52
|
+
const base = diagnosis.soiOffset > 0 ? bytes.subarray(diagnosis.soiOffset) : bytes;
|
|
53
|
+
const baseLabel = diagnosis.soiOffset > 0 ? 'strip-prefix' : 'original';
|
|
54
|
+
// Truncated / missing-EOI: appending EOI is strictly better, try it first.
|
|
55
|
+
if (diagnosis.hasSos && !endsWithEoi(base)) {
|
|
56
|
+
candidates.push({ strategy: 'append-eoi', bytes: ensureEoi(base) });
|
|
57
|
+
}
|
|
58
|
+
candidates.push({ strategy: baseLabel, bytes: base });
|
|
59
|
+
// Donor transplant: only meaningful when we have a reference. Put it last so
|
|
60
|
+
// an unaided decode wins when the header is actually fine.
|
|
61
|
+
if (reference) {
|
|
62
|
+
const t = transplantHeader(reference, bytes);
|
|
63
|
+
if (t)
|
|
64
|
+
candidates.push({ strategy: 'donor-transplant', bytes: t });
|
|
65
|
+
}
|
|
66
|
+
return candidates;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Salvage a JPEG. Returns the re-emitted image (when anything decoded), a
|
|
70
|
+
* preview (the salvage or the EXIF thumbnail), and an honest report.
|
|
71
|
+
*/
|
|
72
|
+
export async function repairJpeg(src, opts = {}) {
|
|
73
|
+
const diagnosis = await diagnoseJpeg(src);
|
|
74
|
+
const bytes = await src.read(0, src.size);
|
|
75
|
+
const referenceBytes = opts.reference ? await opts.reference.read(0, opts.reference.size) : undefined;
|
|
76
|
+
const notes = [...diagnosis.notes];
|
|
77
|
+
const warnings = [];
|
|
78
|
+
const salvaged = [];
|
|
79
|
+
const attempts = [];
|
|
80
|
+
const finish = (outcome, strategy, result) => ({
|
|
81
|
+
image: result.image,
|
|
82
|
+
preview: result.preview,
|
|
83
|
+
report: {
|
|
84
|
+
outcome,
|
|
85
|
+
damageClass: diagnosis.damageClass,
|
|
86
|
+
strategy,
|
|
87
|
+
salvaged,
|
|
88
|
+
notes,
|
|
89
|
+
warnings,
|
|
90
|
+
bytesIn: src.size,
|
|
91
|
+
bytesOut: result.image?.length ?? result.preview?.length ?? 0,
|
|
92
|
+
dimensions: result.dimensions,
|
|
93
|
+
previewSource: result.previewSource,
|
|
94
|
+
usedReference: strategy === 'donor-transplant',
|
|
95
|
+
attempts,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
// Always try the EXIF thumbnail up front: it is the guaranteed fallback.
|
|
99
|
+
const exifThumb = await extractExifThumbnail(bytes);
|
|
100
|
+
if (diagnosis.damageClass === 'not-jpeg') {
|
|
101
|
+
notes.push('input is not recognizable as a JPEG; nothing to salvage');
|
|
102
|
+
if (exifThumb) {
|
|
103
|
+
salvaged.push(`recovered an embedded EXIF thumbnail (${exifThumb.length} bytes)`);
|
|
104
|
+
return finish('partial', 'exif-thumbnail', {
|
|
105
|
+
preview: exifThumb,
|
|
106
|
+
previewSource: 'exif-thumbnail',
|
|
107
|
+
dimensions: null,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return finish('failed', null, { previewSource: null, dimensions: null });
|
|
111
|
+
}
|
|
112
|
+
// Overwritten scan: libjpeg would happily "decode" a zeroed scan into a flat
|
|
113
|
+
// image with no warning — presenting that as a repair would be a lie (docs/03
|
|
114
|
+
// §4). Detect it and refuse to decode; the EXIF thumbnail is the only honest
|
|
115
|
+
// recovery left. A donor cannot help — the entropy data itself is gone.
|
|
116
|
+
const scanOverwritten = diagnosis.damageClass === 'marker-corruption' &&
|
|
117
|
+
diagnosis.scanEntropy !== null &&
|
|
118
|
+
diagnosis.scanEntropy < LOW_SCAN_ENTROPY;
|
|
119
|
+
if (scanOverwritten) {
|
|
120
|
+
notes.push('the scan region is zeroed/overwritten (near-zero entropy); decoding it would produce a meaningless ' +
|
|
121
|
+
'flat image, so it is not attempted — those pixels are unrecoverable by any tool');
|
|
122
|
+
if (exifThumb) {
|
|
123
|
+
salvaged.push(`recovered an embedded EXIF thumbnail (${exifThumb.length} bytes)`);
|
|
124
|
+
return finish('partial', 'exif-thumbnail', {
|
|
125
|
+
preview: exifThumb,
|
|
126
|
+
previewSource: 'exif-thumbnail',
|
|
127
|
+
dimensions: diagnosis.width && diagnosis.height ? { width: diagnosis.width, height: diagnosis.height } : null,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return finish('failed', null, {
|
|
131
|
+
previewSource: null,
|
|
132
|
+
dimensions: diagnosis.width && diagnosis.height ? { width: diagnosis.width, height: diagnosis.height } : null,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const candidates = buildRepairCandidates(bytes, diagnosis, referenceBytes);
|
|
136
|
+
let winner = null;
|
|
137
|
+
for (const candidate of candidates) {
|
|
138
|
+
const r = await decodeJpeg(candidate.bytes);
|
|
139
|
+
if (r.ok) {
|
|
140
|
+
attempts.push({ strategy: candidate.strategy, decoded: true, warnings: r.warnings });
|
|
141
|
+
winner = { strategy: candidate.strategy, image: r.image, warnings: r.warnings };
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
attempts.push({ strategy: candidate.strategy, decoded: false, warnings: r.warnings, error: r.error });
|
|
145
|
+
}
|
|
146
|
+
if (winner) {
|
|
147
|
+
const w = winner;
|
|
148
|
+
let image;
|
|
149
|
+
try {
|
|
150
|
+
image = await encodeJpeg(w.image, opts.encodeOptions);
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
notes.push(`decoded the raster but re-encoding failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
154
|
+
// Fall through to the EXIF thumbnail / failure path below.
|
|
155
|
+
if (exifThumb) {
|
|
156
|
+
salvaged.push(`recovered an embedded EXIF thumbnail (${exifThumb.length} bytes)`);
|
|
157
|
+
return finish('partial', 'exif-thumbnail', {
|
|
158
|
+
preview: exifThumb,
|
|
159
|
+
previewSource: 'exif-thumbnail',
|
|
160
|
+
dimensions: { width: w.image.width, height: w.image.height },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return finish('failed', null, { previewSource: null, dimensions: { width: w.image.width, height: w.image.height } });
|
|
164
|
+
}
|
|
165
|
+
warnings.push(...w.warnings);
|
|
166
|
+
const dimensions = { width: w.image.width, height: w.image.height };
|
|
167
|
+
const partial = warningsIndicatePartial(w.warnings);
|
|
168
|
+
switch (w.strategy) {
|
|
169
|
+
case 'strip-prefix':
|
|
170
|
+
salvaged.push(`stripped ${diagnosis.soiOffset} garbage byte(s) before SOI and re-emitted a valid JPEG`);
|
|
171
|
+
break;
|
|
172
|
+
case 'append-eoi':
|
|
173
|
+
salvaged.push('restored the missing EOI marker and re-emitted a valid JPEG');
|
|
174
|
+
break;
|
|
175
|
+
case 'donor-transplant':
|
|
176
|
+
salvaged.push('transplanted the DQT/DHT/SOF header from the reference photo onto the surviving scan');
|
|
177
|
+
break;
|
|
178
|
+
default:
|
|
179
|
+
salvaged.push('re-emitted a valid baseline JPEG from the decoded raster');
|
|
180
|
+
}
|
|
181
|
+
let outcome;
|
|
182
|
+
if (partial) {
|
|
183
|
+
outcome = 'partial';
|
|
184
|
+
notes.push('partial salvage: libjpeg reported missing/corrupt scan data — recovered rows are real, ' +
|
|
185
|
+
'the remainder is rendered grey because that image data is physically gone');
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
outcome = 'success';
|
|
189
|
+
}
|
|
190
|
+
return finish(outcome, w.strategy, {
|
|
191
|
+
image,
|
|
192
|
+
preview: image,
|
|
193
|
+
previewSource: 'salvage',
|
|
194
|
+
dimensions,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
// Nothing decoded. The EXIF thumbnail is the last honest thing we can offer.
|
|
198
|
+
if (exifThumb) {
|
|
199
|
+
notes.push('the main image could not be decoded; only the embedded EXIF thumbnail survived');
|
|
200
|
+
salvaged.push(`recovered an embedded EXIF thumbnail (${exifThumb.length} bytes)`);
|
|
201
|
+
return finish('partial', 'exif-thumbnail', {
|
|
202
|
+
preview: exifThumb,
|
|
203
|
+
previewSource: 'exif-thumbnail',
|
|
204
|
+
dimensions: null,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (diagnosis.damageClass === 'header-damage' && !referenceBytes) {
|
|
208
|
+
notes.push('the header (DQT/DHT/SOF) is destroyed and no reference photo was provided; supply a same-camera JPEG to attempt a donor transplant');
|
|
209
|
+
}
|
|
210
|
+
if (diagnosis.damageClass === 'marker-corruption' && diagnosis.scanEntropy !== null && diagnosis.scanEntropy < 1.5) {
|
|
211
|
+
notes.push('the scan region appears zeroed/overwritten; the pixels are not recoverable by any tool');
|
|
212
|
+
}
|
|
213
|
+
notes.push('no strategy produced a decodable image');
|
|
214
|
+
return finish('failed', null, { previewSource: null, dimensions: null });
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=repair.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repair.js","sourceRoot":"","sources":["../../src/jpeg/repair.ts"],"names":[],"mappings":"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"}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ByteSource } from './byte-source.js';
|
|
2
|
+
/** Node-only ByteSource over an fs file handle. Kept out of the main entry so the browser bundle never touches node:fs. */
|
|
3
|
+
export declare class NodeFileSource implements ByteSource {
|
|
4
|
+
private readonly handle;
|
|
5
|
+
readonly size: number;
|
|
6
|
+
private constructor();
|
|
7
|
+
static open(path: string): Promise<NodeFileSource>;
|
|
8
|
+
read(offset: number, length: number): Promise<Uint8Array>;
|
|
9
|
+
close(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,2HAA2H;AAC3H,qBAAa,cAAe,YAAW,UAAU;IAE7C,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM;IAFvB,OAAO;WAKM,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAMlD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IASzD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { open } from 'node:fs/promises';
|
|
2
|
+
/** Node-only ByteSource over an fs file handle. Kept out of the main entry so the browser bundle never touches node:fs. */
|
|
3
|
+
export class NodeFileSource {
|
|
4
|
+
handle;
|
|
5
|
+
size;
|
|
6
|
+
constructor(handle, size) {
|
|
7
|
+
this.handle = handle;
|
|
8
|
+
this.size = size;
|
|
9
|
+
}
|
|
10
|
+
static async open(path) {
|
|
11
|
+
const handle = await open(path, 'r');
|
|
12
|
+
const stat = await handle.stat();
|
|
13
|
+
return new NodeFileSource(handle, stat.size);
|
|
14
|
+
}
|
|
15
|
+
async read(offset, length) {
|
|
16
|
+
if (offset < 0 || length < 0)
|
|
17
|
+
throw new RangeError('negative offset/length');
|
|
18
|
+
if (offset >= this.size)
|
|
19
|
+
return new Uint8Array(0);
|
|
20
|
+
const len = Math.min(length, this.size - offset);
|
|
21
|
+
const buf = new Uint8Array(len);
|
|
22
|
+
const { bytesRead } = await this.handle.read(buf, 0, len, offset);
|
|
23
|
+
return bytesRead === len ? buf : buf.subarray(0, bytesRead);
|
|
24
|
+
}
|
|
25
|
+
async close() {
|
|
26
|
+
await this.handle.close();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=node.js.map
|
package/dist/node.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.js","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,kBAAkB,CAAC;AAGzD,2HAA2H;AAC3H,MAAM,OAAO,cAAc;IAEN;IACR;IAFX,YACmB,MAAkB,EAC1B,IAAY;QADJ,WAAM,GAAN,MAAM,CAAY;QAC1B,SAAI,GAAJ,IAAI,CAAQ;IACpB,CAAC;IAEJ,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAY;QAC5B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACrC,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACjC,OAAO,IAAI,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/C,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,MAAc,EAAE,MAAc;QACvC,IAAI,MAAM,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC;YAAE,MAAM,IAAI,UAAU,CAAC,wBAAwB,CAAC,CAAC;QAC7E,IAAI,MAAM,IAAI,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;QAClD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;QACjD,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC;QAChC,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QAClE,OAAO,SAAS,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IAC9D,CAAC;IAED,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IAC5B,CAAC;CACF"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side qpdf loader plumbing. Hand the result of
|
|
3
|
+
* `createBrowserQpdfLoader()` to `setQpdfLoader()` before calling
|
|
4
|
+
* `repairPdf()` in a browser.
|
|
5
|
+
*
|
|
6
|
+
* Two moving parts, split on purpose:
|
|
7
|
+
* - The JS host (`@neslinesli93/qpdf-wasm`, dist/qpdf.js) is a CommonJS/UMD
|
|
8
|
+
* Emscripten artifact. We pull it in with a dynamic `import()` so bundlers
|
|
9
|
+
* put it in its own lazy chunk, loaded only when a repair actually starts.
|
|
10
|
+
* Vite handles the CJS entry without a shim (esbuild interop in dev,
|
|
11
|
+
* Rollup's commonjs plugin in build), but the two disagree on where the
|
|
12
|
+
* callable lands (`module.exports` vs a synthetic `default`, and the build
|
|
13
|
+
* also sets `module.exports.default`), so the factory is unwrapped through
|
|
14
|
+
* `.default` hops until something callable appears.
|
|
15
|
+
* - The wasm binary is NOT bundled. It must be served as a same-origin
|
|
16
|
+
* static asset (apps/www copies it to /engines/qpdf/qpdf.wasm at build
|
|
17
|
+
* time); `locateFile` points Emscripten at `baseUrl` + "qpdf.wasm".
|
|
18
|
+
* Same-origin is a product requirement: "your file never leaves your
|
|
19
|
+
* device" stays verifiable in the Network tab only if no third-party
|
|
20
|
+
* host is involved.
|
|
21
|
+
*/
|
|
22
|
+
import type { QpdfLoader } from './qpdf-host.js';
|
|
23
|
+
/** Where apps/www serves the wasm from (see apps/www/scripts/copy-engine-assets.mjs). */
|
|
24
|
+
export declare const DEFAULT_QPDF_BASE_URL = "/engines/qpdf/";
|
|
25
|
+
/**
|
|
26
|
+
* Build a loader for `setQpdfLoader()`. `baseUrl` is the same-origin
|
|
27
|
+
* directory the wasm is served from; trailing slash optional.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createBrowserQpdfLoader(baseUrl?: string): () => Promise<QpdfLoader>;
|
|
30
|
+
//# sourceMappingURL=browser-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-loader.d.ts","sourceRoot":"","sources":["../../src/pdf/browser-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAe,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE9D,yFAAyF;AACzF,eAAO,MAAM,qBAAqB,mBAAmB,CAAC;AAEtD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,GAAE,MAA8B,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,CAa1G"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Where apps/www serves the wasm from (see apps/www/scripts/copy-engine-assets.mjs). */
|
|
2
|
+
export const DEFAULT_QPDF_BASE_URL = '/engines/qpdf/';
|
|
3
|
+
/**
|
|
4
|
+
* Build a loader for `setQpdfLoader()`. `baseUrl` is the same-origin
|
|
5
|
+
* directory the wasm is served from; trailing slash optional.
|
|
6
|
+
*/
|
|
7
|
+
export function createBrowserQpdfLoader(baseUrl = DEFAULT_QPDF_BASE_URL) {
|
|
8
|
+
const wasmUrl = `${baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`}qpdf.wasm`;
|
|
9
|
+
return async () => {
|
|
10
|
+
const mod = await import('@neslinesli93/qpdf-wasm');
|
|
11
|
+
let factory = mod;
|
|
12
|
+
for (let hops = 0; hops < 3 && factory !== null && typeof factory !== 'function'; hops++) {
|
|
13
|
+
factory = factory.default ?? null;
|
|
14
|
+
}
|
|
15
|
+
if (typeof factory !== 'function') {
|
|
16
|
+
throw new Error('qpdf browser loader: @neslinesli93/qpdf-wasm did not resolve to an Emscripten factory');
|
|
17
|
+
}
|
|
18
|
+
return { factory: factory, locateWasm: () => wasmUrl };
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=browser-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-loader.js","sourceRoot":"","sources":["../../src/pdf/browser-loader.ts"],"names":[],"mappings":"AAuBA,yFAAyF;AACzF,MAAM,CAAC,MAAM,qBAAqB,GAAG,gBAAgB,CAAC;AAEtD;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,UAAkB,qBAAqB;IAC7E,MAAM,OAAO,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,GAAG,WAAW,CAAC;IAC9E,OAAO,KAAK,IAAI,EAAE;QAChB,MAAM,GAAG,GAAY,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC;QAC7D,IAAI,OAAO,GAAY,GAAG,CAAC;QAC3B,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,IAAI,EAAE,EAAE,CAAC;YACzF,OAAO,GAAI,OAAiC,CAAC,OAAO,IAAI,IAAI,CAAC;QAC/D,CAAC;QACD,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CAAC,uFAAuF,CAAC,CAAC;QAC3G,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,OAAsB,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;IACxE,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ByteSource } from '../byte-source.js';
|
|
2
|
+
import type { Confidence } from '../types.js';
|
|
3
|
+
export type PdfDamageClass = 'none' | 'garbage-prefix' | 'broken-xref' | 'truncated' | 'not-pdf' | 'encrypted' | 'unknown';
|
|
4
|
+
export interface PdfDiagnosis {
|
|
5
|
+
damageClass: PdfDamageClass;
|
|
6
|
+
confidence: Confidence;
|
|
7
|
+
notes: string[];
|
|
8
|
+
/** Offset of the %PDF- header, or null when absent. */
|
|
9
|
+
headerOffset: number | null;
|
|
10
|
+
/** Declared PDF version ("1.7"), when readable. */
|
|
11
|
+
version: string | null;
|
|
12
|
+
hasEof: boolean;
|
|
13
|
+
hasStartxref: boolean;
|
|
14
|
+
/** null = not evaluated (e.g. startxref missing). */
|
|
15
|
+
xrefValid: boolean | null;
|
|
16
|
+
encrypted: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Structural PDF diagnosis via light own parsing: header scan, tail scan for
|
|
20
|
+
* startxref/trailer/%%EOF, and an xref sanity spot-check. Never runs qpdf.
|
|
21
|
+
*/
|
|
22
|
+
export declare function diagnosePdf(src: ByteSource): Promise<PdfDiagnosis>;
|
|
23
|
+
//# sourceMappingURL=diagnose.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnose.d.ts","sourceRoot":"","sources":["../../src/pdf/diagnose.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAY9C,MAAM,MAAM,cAAc,GACtB,MAAM,GACN,gBAAgB,GAChB,aAAa,GACb,WAAW,GACX,SAAS,GACT,WAAW,GACX,SAAS,CAAC;AAEd,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,cAAc,CAAC;IAC5B,UAAU,EAAE,UAAU,CAAC;IACvB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,uDAAuD;IACvD,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,mDAAmD;IACnD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,OAAO,CAAC;IACtB,qDAAqD;IACrD,SAAS,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;CACpB;AAID;;;GAGG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAsFxE"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { PDF_HEADER_SCAN_WINDOW, PDF_TAIL_SCAN_WINDOW, findPdfHeader, firstInUseXrefEntry, looksLikeXrefStart, parsePdfVersion, scanPdfTail, toLatin1, } from './format.js';
|
|
2
|
+
const MIN_STRUCTURAL_SIZE = 32;
|
|
3
|
+
/**
|
|
4
|
+
* Structural PDF diagnosis via light own parsing: header scan, tail scan for
|
|
5
|
+
* startxref/trailer/%%EOF, and an xref sanity spot-check. Never runs qpdf.
|
|
6
|
+
*/
|
|
7
|
+
export async function diagnosePdf(src) {
|
|
8
|
+
const notes = [];
|
|
9
|
+
const base = {
|
|
10
|
+
notes,
|
|
11
|
+
headerOffset: null,
|
|
12
|
+
version: null,
|
|
13
|
+
hasEof: false,
|
|
14
|
+
hasStartxref: false,
|
|
15
|
+
xrefValid: null,
|
|
16
|
+
encrypted: false,
|
|
17
|
+
};
|
|
18
|
+
const done = (damageClass, confidence) => ({
|
|
19
|
+
damageClass,
|
|
20
|
+
confidence,
|
|
21
|
+
...base,
|
|
22
|
+
});
|
|
23
|
+
if (src.size === 0) {
|
|
24
|
+
notes.push('file is empty');
|
|
25
|
+
return done('not-pdf', 'high');
|
|
26
|
+
}
|
|
27
|
+
const head = await src.read(0, Math.min(src.size, PDF_HEADER_SCAN_WINDOW));
|
|
28
|
+
const headerOffset = findPdfHeader(head);
|
|
29
|
+
if (headerOffset === -1) {
|
|
30
|
+
notes.push(`no %PDF- header in the first ${head.length} bytes`);
|
|
31
|
+
return done('not-pdf', src.size > PDF_HEADER_SCAN_WINDOW ? 'medium' : 'high');
|
|
32
|
+
}
|
|
33
|
+
base.headerOffset = headerOffset;
|
|
34
|
+
base.version = parsePdfVersion(head, headerOffset);
|
|
35
|
+
if (base.version === null)
|
|
36
|
+
notes.push('header version unreadable');
|
|
37
|
+
if (src.size - headerOffset < MIN_STRUCTURAL_SIZE) {
|
|
38
|
+
notes.push('file too small to contain any PDF structure');
|
|
39
|
+
return done('unknown', 'low');
|
|
40
|
+
}
|
|
41
|
+
const tailBase = Math.max(headerOffset, src.size - PDF_TAIL_SCAN_WINDOW);
|
|
42
|
+
const tail = await src.read(tailBase, src.size - tailBase);
|
|
43
|
+
const t = scanPdfTail(tail, tailBase);
|
|
44
|
+
base.hasEof = t.eofOffset !== -1;
|
|
45
|
+
base.hasStartxref = t.startxrefOffset !== -1 && t.startxrefTarget !== null;
|
|
46
|
+
base.encrypted = t.hasEncrypt;
|
|
47
|
+
// Xref sanity: follow startxref. Offsets are relative to the original file
|
|
48
|
+
// start, so with a garbage prefix the real position is shifted by it.
|
|
49
|
+
if (base.hasStartxref) {
|
|
50
|
+
base.xrefValid = await checkXrefAt(src, headerOffset + t.startxrefTarget, headerOffset, notes);
|
|
51
|
+
if (!base.xrefValid && headerOffset > 0) {
|
|
52
|
+
// Some writers would have produced offsets already including the prefix.
|
|
53
|
+
base.xrefValid = await checkXrefAt(src, t.startxrefTarget, 0, notes);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (headerOffset > 0) {
|
|
57
|
+
notes.push(`%PDF- header at offset ${headerOffset} (garbage prefix)`);
|
|
58
|
+
if (!base.hasEof)
|
|
59
|
+
notes.push('tail structures also missing');
|
|
60
|
+
else if (base.xrefValid === false)
|
|
61
|
+
notes.push('xref also looks damaged');
|
|
62
|
+
return done('garbage-prefix', base.hasEof ? 'high' : 'medium');
|
|
63
|
+
}
|
|
64
|
+
if (!base.hasEof && !base.hasStartxref && t.trailerOffset === -1) {
|
|
65
|
+
notes.push('no %%EOF, startxref or trailer: file tail is missing');
|
|
66
|
+
return done('truncated', 'medium');
|
|
67
|
+
}
|
|
68
|
+
if (!base.hasEof) {
|
|
69
|
+
notes.push('%%EOF marker missing (file cut near the very end)');
|
|
70
|
+
return done('truncated', 'medium');
|
|
71
|
+
}
|
|
72
|
+
if (!base.hasStartxref) {
|
|
73
|
+
notes.push('%%EOF present but startxref missing or unreadable');
|
|
74
|
+
return done('broken-xref', 'medium');
|
|
75
|
+
}
|
|
76
|
+
if (base.xrefValid === false) {
|
|
77
|
+
notes.push('startxref does not lead to a readable xref table or xref stream');
|
|
78
|
+
if (base.encrypted)
|
|
79
|
+
notes.push('an /Encrypt key is present; contents may also be encrypted');
|
|
80
|
+
return done('broken-xref', 'high');
|
|
81
|
+
}
|
|
82
|
+
if (base.encrypted) {
|
|
83
|
+
notes.push('document is encrypted (/Encrypt in trailer); repair requires the password unless the user password is empty');
|
|
84
|
+
return done('encrypted', 'high');
|
|
85
|
+
}
|
|
86
|
+
return done('none', 'high');
|
|
87
|
+
}
|
|
88
|
+
async function checkXrefAt(src, target, headerOffset, notes) {
|
|
89
|
+
if (target < 0 || target >= src.size) {
|
|
90
|
+
notes.push(`startxref points outside the file (${target})`);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const probe = await src.read(target, Math.min(2048, src.size - target));
|
|
94
|
+
if (!looksLikeXrefStart(probe))
|
|
95
|
+
return false;
|
|
96
|
+
// For classic tables, spot-check that the first in-use entry points at an
|
|
97
|
+
// object header; a zeroed table parses but points nowhere.
|
|
98
|
+
const first = firstInUseXrefEntry(probe);
|
|
99
|
+
if (first) {
|
|
100
|
+
const at = headerOffset + first.objectOffset;
|
|
101
|
+
if (at >= src.size) {
|
|
102
|
+
notes.push('first xref entry points outside the file');
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
const objProbe = await src.read(at, Math.min(48, src.size - at));
|
|
106
|
+
if (!/^\s*\d{1,9}\s+\d{1,5}\s+obj\b/.test(toLatin1(objProbe))) {
|
|
107
|
+
notes.push('first in-use xref entry does not point at an object');
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=diagnose.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnose.js","sourceRoot":"","sources":["../../src/pdf/diagnose.ts"],"names":[],"mappings":"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"}
|