@photivra/engine 2.21.0 → 2.22.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/CHANGELOG.md +118 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +11 -0
- package/dist/capture/c2pa-embed.d.ts +116 -0
- package/dist/capture/c2pa-embed.js +493 -0
- package/dist/capture/c2pa-embed.js.map +1 -0
- package/dist/capture/dng-import.d.ts +73 -8
- package/dist/capture/dng-import.js +315 -80
- package/dist/capture/dng-import.js.map +1 -1
- package/dist/capture/dng-opcodes.d.ts +96 -0
- package/dist/capture/dng-opcodes.js +391 -0
- package/dist/capture/dng-opcodes.js.map +1 -0
- package/dist/capture/dng-profile-maps.d.ts +99 -0
- package/dist/capture/dng-profile-maps.js +381 -0
- package/dist/capture/dng-profile-maps.js.map +1 -0
- package/dist/capture/dng-stage-image.d.ts +23 -0
- package/dist/capture/dng-stage-image.js +65 -0
- package/dist/capture/dng-stage-image.js.map +1 -0
- package/dist/capture/jpeg-xl-tiles.d.ts +22 -0
- package/dist/capture/jpeg-xl-tiles.js +151 -0
- package/dist/capture/jpeg-xl-tiles.js.map +1 -0
- package/dist/capture/lossy-jpeg.d.ts +17 -0
- package/dist/capture/lossy-jpeg.js +369 -0
- package/dist/capture/lossy-jpeg.js.map +1 -0
- package/dist/capture/native-raw-development.d.ts +2 -1
- package/dist/capture/native-raw-development.js.map +1 -1
- package/dist/color/neighbor-highlight-reconstruction.d.ts +66 -0
- package/dist/color/neighbor-highlight-reconstruction.js +234 -0
- package/dist/color/neighbor-highlight-reconstruction.js.map +1 -0
- package/dist/color/sensor-color-development.d.ts +5 -0
- package/dist/color/sensor-color-development.js +7 -1
- package/dist/color/sensor-color-development.js.map +1 -1
- package/dist/color/spectral-spikiness.d.ts +11 -0
- package/dist/color/spectral-spikiness.js +23 -0
- package/dist/color/spectral-spikiness.js.map +1 -0
- package/dist/core/execution-error.d.ts +21 -0
- package/dist/core/execution-error.js +12 -0
- package/dist/core/execution-error.js.map +1 -0
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/facade/catalogue-lens-signal.d.ts +26 -1
- package/dist/facade/catalogue-lens-signal.js +25 -7
- package/dist/facade/catalogue-lens-signal.js.map +1 -1
- package/dist/facade/diffraction-provider.d.ts +1 -1
- package/dist/facade/diffraction-provider.js +4 -4
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view-gpu-session.js +38 -6
- package/dist/facade/display-view-gpu-session.js.map +1 -1
- package/dist/facade/display-view-gpu.d.ts +13 -5
- package/dist/facade/display-view-gpu.js.map +1 -1
- package/dist/facade/display-view-normal.d.ts +15 -0
- package/dist/facade/display-view-normal.js +44 -0
- package/dist/facade/display-view-normal.js.map +1 -0
- package/dist/facade/display-view-rendering.d.ts +97 -0
- package/dist/facade/display-view-rendering.js +598 -0
- package/dist/facade/display-view-rendering.js.map +1 -0
- package/dist/facade/display-view.d.ts +37 -65
- package/dist/facade/display-view.js +147 -476
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.js +2 -2
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/fast-methods.d.ts +1 -1
- package/dist/facade/fast-methods.js +37 -8
- package/dist/facade/fast-methods.js.map +1 -1
- package/dist/facade/generic-lens-capture.d.ts +1 -1
- package/dist/facade/generic-lens-capture.js +1 -1
- package/dist/facade/generic-lens-capture.js.map +1 -1
- package/dist/facade/image-plane.js +41 -10
- package/dist/facade/image-plane.js.map +1 -1
- package/dist/facade/on-demand-kits.d.ts +14 -0
- package/dist/facade/on-demand-kits.js +36 -0
- package/dist/facade/on-demand-kits.js.map +1 -0
- package/dist/facade/opt-in-kit.d.ts +9 -0
- package/dist/facade/opt-in-kit.js +11 -0
- package/dist/facade/opt-in-kit.js.map +1 -0
- package/dist/facade/quality-kit.d.ts +21 -0
- package/dist/facade/quality-kit.js +23 -0
- package/dist/facade/quality-kit.js.map +1 -0
- package/dist/facade/quality-paths.d.ts +22 -0
- package/dist/facade/quality-paths.js +44 -0
- package/dist/facade/quality-paths.js.map +1 -0
- package/dist/facade/scene-radiance.d.ts +18 -2
- package/dist/facade/scene-radiance.js +6 -4
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +32 -13
- package/dist/facade/simulate-capture.js +162 -69
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +10 -12
- package/dist/facade/simulate-display-view.js +57 -48
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/display-view-gpu-renderer.d.ts +43 -0
- package/dist/gpu/display-view-gpu-renderer.js +134 -0
- package/dist/gpu/display-view-gpu-renderer.js.map +1 -0
- package/dist/gpu/display-view-image-material-module.d.ts +3 -2
- package/dist/gpu/display-view-image-material-module.js +25 -3
- package/dist/gpu/display-view-image-material-module.js.map +1 -1
- package/dist/gpu/display-view-image-quality.d.ts +43 -1
- package/dist/gpu/display-view-image-quality.js +256 -11
- package/dist/gpu/display-view-image-quality.js.map +1 -1
- package/dist/gpu/display-view-quality-gpu.d.ts +13 -64
- package/dist/gpu/display-view-quality-gpu.js +60 -249
- package/dist/gpu/display-view-quality-gpu.js.map +1 -1
- package/dist/gpu/display-view-speed-gpu.d.ts +16 -16
- package/dist/gpu/display-view-speed-gpu.js +50 -12
- package/dist/gpu/display-view-speed-gpu.js.map +1 -1
- package/dist/gpu/image-kernel-accelerator.js +3 -0
- package/dist/gpu/image-kernel-accelerator.js.map +1 -1
- package/dist/gpu/image-kernel-execution.d.ts +2 -13
- package/dist/gpu/image-kernel-execution.js +2 -10
- package/dist/gpu/image-kernel-execution.js.map +1 -1
- package/dist/gpu/wgsl-display-view-coupled-stub.d.ts +8 -0
- package/dist/gpu/wgsl-display-view-coupled-stub.js +21 -0
- package/dist/gpu/wgsl-display-view-coupled-stub.js.map +1 -0
- package/dist/gpu/wgsl-display-view-coupled.d.ts +88 -0
- package/dist/gpu/wgsl-display-view-coupled.js +353 -0
- package/dist/gpu/wgsl-display-view-coupled.js.map +1 -0
- package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +4 -9
- package/dist/gpu/wgsl-display-view-quality-kernel.js +109 -111
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +1 -41
- package/dist/gpu/wgsl-display-view-speed-kernel.js +71 -8
- package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/output/camera-channel-response.d.ts +50 -0
- package/dist/output/camera-channel-response.js +69 -0
- package/dist/output/camera-channel-response.js.map +1 -0
- package/dist/output/native-capture-sdr.js +5 -2
- package/dist/output/native-capture-sdr.js.map +1 -1
- package/dist/output/sdr-rendering.d.ts +18 -1
- package/dist/output/sdr-rendering.js +39 -2
- package/dist/output/sdr-rendering.js.map +1 -1
- package/dist/sensor/native-sensor-report.d.ts +10 -0
- package/dist/sensor/native-sensor-report.js +16 -0
- package/dist/sensor/native-sensor-report.js.map +1 -0
- package/dist/sensor/sensor-characteristics.d.ts +81 -13
- package/dist/sensor/sensor-characteristics.js +10 -6
- package/dist/sensor/sensor-characteristics.js.map +1 -1
- package/docs/API_REFERENCE.md +117 -4
- package/docs/QUICK_START.md +37 -8
- package/docs/USAGE.md +104 -22
- package/docs/api/capture-c2pa-embed.md +177 -0
- package/docs/api/capture-dng-import.md +86 -10
- package/docs/api/capture-dng-opcodes.md +93 -0
- package/docs/api/capture-dng-profile-maps.md +141 -0
- package/docs/api/capture-dng-stage-image.md +43 -0
- package/docs/api/capture-jpeg-xl-tiles.md +33 -0
- package/docs/api/capture-lossy-jpeg.md +41 -0
- package/docs/api/capture-native-raw-development.md +2 -1
- package/docs/api/core-execution-error.md +43 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +255 -5
- package/docs/api/facade-display-view-gpu.md +5 -2
- package/docs/api/facade-display-view.md +11 -15
- package/docs/api/facade-fast-methods.md +38 -9
- package/docs/api/facade-quality-paths.md +24 -0
- package/docs/api/facade-scene-radiance.md +1 -1
- package/docs/api/facade-simulate-capture.md +24 -7
- package/docs/api/facade-simulate-display-view.md +10 -12
- package/docs/api/gpu-image-kernel-execution.md +0 -36
- package/docs/api/output-camera-channel-response.md +55 -0
- package/docs/api/output-sdr-rendering.md +6 -1
- package/docs/api/sensor-sensor-characteristics.md +54 -2
- package/llms.txt +1 -0
- package/package.json +6 -5
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { type DngJpegXlDecoder } from "./jpeg-xl-tiles.js";
|
|
2
|
+
import { type DngOpcode } from "./dng-opcodes.js";
|
|
3
|
+
import { type DngProfileGainTableMap, type DngRgbTables } from "./dng-profile-maps.js";
|
|
4
|
+
/** 1.1.0 (2.22.0): lossy JPEG and JPEG XL raw data, opcode lists, profile maps, semantic masks, `scale`, `assumptions`. */
|
|
5
|
+
export declare const DNG_IMPORT_SCHEMA_VERSION: "1.1.0";
|
|
6
|
+
/**
|
|
7
|
+
* Upper bound for `maximumPixels`: 200 MP (megapixels) of 16-bit codes is 400 MB. `maximumBytes` bounds the estimated peak memory
|
|
8
|
+
* of the import (default 1 GiB, at most 4 GiB).
|
|
9
|
+
*/
|
|
3
10
|
export declare const DNG_IMPORT_LIMITS: Readonly<{
|
|
4
11
|
defaultMaximumPixels: 64000000;
|
|
5
12
|
maximumPixels: 200000000;
|
|
6
13
|
defaultMaximumIfds: 64;
|
|
7
14
|
maximumIfds: 1024;
|
|
15
|
+
defaultMaximumBytes: 1073741824;
|
|
16
|
+
maximumBytes: 4294967296;
|
|
8
17
|
}>;
|
|
9
18
|
export interface DngImportInput {
|
|
10
19
|
bytes: Uint8Array;
|
|
@@ -12,6 +21,32 @@ export interface DngImportInput {
|
|
|
12
21
|
maximumPixels?: number;
|
|
13
22
|
/** Total IFDs (main chain plus SubIFDs) visited. Default 64, at most 1024. */
|
|
14
23
|
maximumIfds?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Estimated peak memory of the import, bytes (the codes, the largest strip or tile's decode buffers and its decoder's working
|
|
26
|
+
* memory; see docs/IMPORT_AND_PNG.md, "Memory"), checked before any image allocation. Default 1 073 741 824 (1 GiB), at most 4 GiB.
|
|
27
|
+
*/
|
|
28
|
+
maximumBytes?: number;
|
|
29
|
+
/**
|
|
30
|
+
* "half": a half-size preview of a LinearRaw image, each output pixel the rounded mean of a 2 × 2 block of stored codes, decoded
|
|
31
|
+
* strip by strip (a quarter of the codes' memory). Areas, crops and black-level deltas are halved; CFA images are refused.
|
|
32
|
+
* Default "full".
|
|
33
|
+
*/
|
|
34
|
+
size?: "full" | "half";
|
|
35
|
+
}
|
|
36
|
+
/** {@link importDngRawImage}'s input: {@link DngImportInput} plus the JPEG XL decoder a 52546 file needs. */
|
|
37
|
+
export interface DngImportAsyncInput extends DngImportInput {
|
|
38
|
+
/** Decodes one JPEG XL strip or tile to PNG (`createJxlOxideDngDecoder`). Needed only for JPEG XL raw data or masks. */
|
|
39
|
+
jpegXlDecoder?: DngJpegXlDecoder;
|
|
40
|
+
}
|
|
41
|
+
/** A decoded semantic mask (DNG 1.6 SemanticName, NewSubFileType 0x10004, PhotometricInterpretation 52527). */
|
|
42
|
+
export interface ImportedDngSemanticMask {
|
|
43
|
+
name: string;
|
|
44
|
+
/** SemanticInstanceID, or null. */
|
|
45
|
+
instanceId: string | null;
|
|
46
|
+
width: number;
|
|
47
|
+
height: number;
|
|
48
|
+
/** Mask values 0..1 (code / (2^bits − 1)), row-major. */
|
|
49
|
+
data: Float32Array;
|
|
15
50
|
}
|
|
16
51
|
/** Repeating CFA pattern. Its origin is the top-left corner of `activeArea` (DNG 1.7.1.0, PhotometricInterpretation). */
|
|
17
52
|
export interface ImportedDngCfa {
|
|
@@ -54,7 +89,14 @@ export interface ImportedDngRaw {
|
|
|
54
89
|
height: number;
|
|
55
90
|
samplesPerPixel: number;
|
|
56
91
|
bitsPerSample: number;
|
|
57
|
-
compression: "uncompressed" | "lossless-jpeg";
|
|
92
|
+
compression: "uncompressed" | "lossless-jpeg" | "lossy-jpeg" | "jpeg-xl";
|
|
93
|
+
/**
|
|
94
|
+
* True when the raw data went through a lossy coder: lossy JPEG always; JPEG XL when JXLDistance (52553) is above 0 or absent
|
|
95
|
+
* (the file then does not say it is lossless).
|
|
96
|
+
*/
|
|
97
|
+
lossySource: boolean;
|
|
98
|
+
/** 1, or 0.5 for a `size: "half"` preview (every geometry field is already halved). */
|
|
99
|
+
scale: 1 | 0.5;
|
|
58
100
|
photometricInterpretation: "cfa" | "linear-raw";
|
|
59
101
|
/** Stored raw codes, row-major then sample, length width x height x samplesPerPixel. */
|
|
60
102
|
codes: Uint16Array;
|
|
@@ -78,14 +120,37 @@ export interface ImportedDngRaw {
|
|
|
78
120
|
asShotNeutral: number[] | null;
|
|
79
121
|
/** RawDataUniqueID as 32 lowercase hex digits, or null. */
|
|
80
122
|
rawDataUniqueId: string | null;
|
|
123
|
+
/** ForwardMatrix1/2 (white-balanced camera → XYZ D50), row-major 3 x colorPlanes, or null. */
|
|
124
|
+
forwardMatrix1: number[] | null;
|
|
125
|
+
forwardMatrix2: number[] | null;
|
|
126
|
+
/** BaselineExposure plus BaselineExposureOffset (stops), or null when neither is present. */
|
|
127
|
+
baselineExposure: number | null;
|
|
128
|
+
/** OpcodeList1/2/3 (raw IFD), parsed but not applied (`developDngStageImage` applies them), or null when absent. */
|
|
129
|
+
opcodeList1: DngOpcode[] | null;
|
|
130
|
+
opcodeList2: DngOpcode[] | null;
|
|
131
|
+
opcodeList3: DngOpcode[] | null;
|
|
132
|
+
/** ProfileGainTableMap2 (IFD 0), else ProfileGainTableMap (raw IFD), or null; applied by `applyDngProfileGainTableMap`. */
|
|
133
|
+
profileGainTableMap: DngProfileGainTableMap | null;
|
|
134
|
+
/** RGBTables (IFD 0), or null; applied by `applyDngRgbTables`. */
|
|
135
|
+
rgbTables: DngRgbTables | null;
|
|
136
|
+
/** Decoded semantic masks, for `applyDngRgbTables`. */
|
|
137
|
+
semanticMasks: ImportedDngSemanticMask[];
|
|
138
|
+
/** What the import did not do: tags present but not returned or applied, masks not decoded, lossy sources, inferred layouts. */
|
|
139
|
+
assumptions: string[];
|
|
81
140
|
}
|
|
82
141
|
/**
|
|
83
|
-
* Parse one DNG or TIFF-EP file and decode its primary CFA or LinearRaw image (the first IFD with
|
|
84
|
-
*
|
|
142
|
+
* Parse one DNG or TIFF-EP file and decode its primary CFA or LinearRaw image (the first IFD with NewSubFileType 0 and
|
|
143
|
+
* PhotometricInterpretation 32803 or 34892, in IFD-chain then SubIFD order), synchronously.
|
|
85
144
|
*
|
|
86
|
-
* Supported raw data: Compression 1 at 8..16 bits/sample (8 bits as bytes, 16 bits in the file byte
|
|
87
|
-
*
|
|
88
|
-
*
|
|
145
|
+
* Supported raw data: Compression 1 at 8..16 bits/sample (8 bits as bytes, 16 bits in the file byte order, other depths packed
|
|
146
|
+
* MSB-first with byte-aligned rows, per DNG BitsPerSample), Compression 7 lossless Huffman JPEG (T.81 Annex H) and Compression 34892
|
|
147
|
+
* 8-bit lossy JPEG (T.81 sequential DCT), in strips or tiles, chunky planar configuration only. JPEG XL (52546) needs
|
|
148
|
+
* {@link importDngRawImage}; a JPEG XL semantic mask is skipped here and named in `assumptions`.
|
|
89
149
|
* @returns Owned typed arrays and plain metadata; nothing is applied to the codes.
|
|
90
150
|
*/
|
|
91
151
|
export declare function parseDngRawImage(input: DngImportInput): ImportedDngRaw;
|
|
152
|
+
/**
|
|
153
|
+
* {@link parseDngRawImage} for every supported compression, JPEG XL included: each JPEG XL strip or tile goes to `jpegXlDecoder` in
|
|
154
|
+
* turn (one at a time, so only one tile's decoder memory is held), and its PNG is read at the DNG's bit depth (jpeg-xl-tiles.ts).
|
|
155
|
+
*/
|
|
156
|
+
export declare function importDngRawImage(input: DngImportAsyncInput): Promise<ImportedDngRaw>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// This product includes DNG technology under license by Adobe.
|
|
3
3
|
/**
|
|
4
|
-
* Browser-safe reader for DNG and TIFF-EP raw files with a CFA (colour filter array) mosaic.
|
|
4
|
+
* Browser-safe reader for DNG and TIFF-EP raw files with a CFA (colour filter array) mosaic or LinearRaw data.
|
|
5
5
|
*
|
|
6
6
|
* It was written independently from the published TIFF 6.0 tag definitions, the Adobe Digital
|
|
7
7
|
* Negative (DNG) Specification 1.7.1.0 and the Exif 2.32/3.0 CFAPattern definition (CIPA DC-008;
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
* It returns the stored raw codes plus the metadata the engine's RAW development needs: CFA
|
|
11
11
|
* pattern, black and white levels, active area, default crop, colour matrices, as-shot neutral,
|
|
12
12
|
* calibration illuminants and orientation. It never applies linearization, black subtraction,
|
|
13
|
-
* white balance, demosaicing or colour transforms.
|
|
13
|
+
* white balance, demosaicing or colour transforms (`developDngStageImage` does the DNG stages).
|
|
14
|
+
*
|
|
15
|
+
* 2.22.0 (#598, phone RAW files): lossy JPEG (Compression 34892, DNG 1.4.0.0) and JPEG XL (52546, DNG 1.7.0.0) raw data,
|
|
16
|
+
* Row/ColumnInterleaveFactor, the opcode lists, the profile gain table map, RGBTables and semantic masks, a `maximumBytes` memory
|
|
17
|
+
* pre-check and a half-size LinearRaw preview. Every DNG 1.5–1.7 tag that would change the rendering and is not returned or
|
|
18
|
+
* applied is named in `assumptions`; none of them fails an import.
|
|
14
19
|
*
|
|
15
20
|
* Input is untrusted. Every offset, count, IFD (image file directory) chain, strip or tile table
|
|
16
21
|
* and decoded size is checked against the buffer and explicit limits before allocation. IFD loops,
|
|
@@ -20,9 +25,18 @@
|
|
|
20
25
|
*/
|
|
21
26
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
22
27
|
import { parseLosslessJpeg } from "./lossless-jpeg.js";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
import { parseLossyJpeg } from "./lossy-jpeg.js";
|
|
29
|
+
import { readJpegXlTilePng } from "./jpeg-xl-tiles.js";
|
|
30
|
+
import { parseDngOpcodeList } from "./dng-opcodes.js";
|
|
31
|
+
import { parseDngProfileGainTableMap, parseDngRgbTables } from "./dng-profile-maps.js";
|
|
32
|
+
/** 1.1.0 (2.22.0): lossy JPEG and JPEG XL raw data, opcode lists, profile maps, semantic masks, `scale`, `assumptions`. */
|
|
33
|
+
export const DNG_IMPORT_SCHEMA_VERSION = "1.1.0";
|
|
34
|
+
/**
|
|
35
|
+
* Upper bound for `maximumPixels`: 200 MP (megapixels) of 16-bit codes is 400 MB. `maximumBytes` bounds the estimated peak memory
|
|
36
|
+
* of the import (default 1 GiB, at most 4 GiB).
|
|
37
|
+
*/
|
|
38
|
+
export const DNG_IMPORT_LIMITS = /* @__PURE__ */ Object.freeze({ defaultMaximumPixels: 64_000_000, maximumPixels: 200_000_000, defaultMaximumIfds: 64, maximumIfds: 1024,
|
|
39
|
+
defaultMaximumBytes: 1_073_741_824, maximumBytes: 4_294_967_296 });
|
|
26
40
|
const TYPE_SIZES = [0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 4];
|
|
27
41
|
function fail(message) { throw new InvalidConfigurationError("DNG import: " + message); }
|
|
28
42
|
function limit(value, fallback, max, name) {
|
|
@@ -32,26 +46,27 @@ function limit(value, fallback, max, name) {
|
|
|
32
46
|
fail(name + " must be an integer from 1 to " + max + ".");
|
|
33
47
|
return value;
|
|
34
48
|
}
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
* @returns Owned typed arrays and plain metadata; nothing is applied to the codes.
|
|
43
|
-
*/
|
|
44
|
-
export function parseDngRawImage(input) {
|
|
49
|
+
/** Rendering-relevant tags that are not returned or applied, by tag id (DNG 1.4–1.7 tag names). */
|
|
50
|
+
const UNAPPLIED_TAGS = [[50938, "ProfileHueSatMapData1"], [50939, "ProfileHueSatMapData2"], [50940, "ProfileToneCurve"], [50982, "ProfileLookTableData"],
|
|
51
|
+
[51110, "DefaultBlackRender"], [52529, "CalibrationIlluminant3"], [52531, "ColorMatrix3"], [52532, "ForwardMatrix3"], [52533, "IlluminantData1"], [52534, "IlluminantData2"],
|
|
52
|
+
[52535, "IlluminantData3"], [52551, "ProfileDynamicRange"], [51182, "EnhanceParams"], [52545, "JUMBF (C2PA)"], [52539, "RGBTables (draft)"]];
|
|
53
|
+
/** Estimated JPEG XL decoder working memory per sample, bytes (an estimate, not measured: 32-bit channels, a float render and the PNG). */
|
|
54
|
+
const JXL_WORKING_BYTES_PER_SAMPLE = 16;
|
|
55
|
+
function prepare(input, keys) {
|
|
45
56
|
if (input === null || typeof input !== "object")
|
|
46
57
|
fail("input must be an object.");
|
|
47
58
|
for (const key of Object.keys(input))
|
|
48
|
-
if (!
|
|
59
|
+
if (!keys.includes(key))
|
|
49
60
|
fail(`unknown input field. Unknown field ${JSON.stringify(key)}.`);
|
|
50
61
|
const bytes = input.bytes;
|
|
51
62
|
if (!(bytes instanceof Uint8Array))
|
|
52
63
|
fail("bytes must be a Uint8Array.");
|
|
53
64
|
const maximumPixels = limit(input.maximumPixels, DNG_IMPORT_LIMITS.defaultMaximumPixels, DNG_IMPORT_LIMITS.maximumPixels, "maximumPixels");
|
|
54
65
|
const maximumIfds = limit(input.maximumIfds, DNG_IMPORT_LIMITS.defaultMaximumIfds, DNG_IMPORT_LIMITS.maximumIfds, "maximumIfds");
|
|
66
|
+
const maximumBytes = limit(input.maximumBytes, DNG_IMPORT_LIMITS.defaultMaximumBytes, DNG_IMPORT_LIMITS.maximumBytes, "maximumBytes");
|
|
67
|
+
if (input.size !== undefined && input.size !== "full" && input.size !== "half")
|
|
68
|
+
fail('size must be "full" or "half".');
|
|
69
|
+
const half = input.size === "half";
|
|
55
70
|
const n = bytes.length, view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
56
71
|
if (n < 8)
|
|
57
72
|
fail("file is too short.");
|
|
@@ -163,7 +178,7 @@ export function parseDngRawImage(input) {
|
|
|
163
178
|
const ascii = (entry) => {
|
|
164
179
|
if (!entry)
|
|
165
180
|
return null;
|
|
166
|
-
if (entry.type !== 2 || entry.count > 4096)
|
|
181
|
+
if ((entry.type !== 2 && entry.type !== 1) || entry.count > 4096)
|
|
167
182
|
fail("invalid ASCII tag.");
|
|
168
183
|
let s = "";
|
|
169
184
|
for (let i = 0; i < entry.count && bytes[entry.at + i] !== 0; i++)
|
|
@@ -174,74 +189,103 @@ export function parseDngRawImage(input) {
|
|
|
174
189
|
const v = integers(ifd.get(id), 1, [3, 4]);
|
|
175
190
|
return v ? v[0] : fallback;
|
|
176
191
|
};
|
|
192
|
+
const blob = (entry) => entry ? bytes.subarray(entry.at, entry.at + entry.count * TYPE_SIZES[entry.type]) : null;
|
|
177
193
|
const raw = ifds.find((ifd) => one(ifd, 254, 0) === 0 && [32803, 34892].includes(one(ifd, 262, -1)));
|
|
178
194
|
if (!raw)
|
|
179
195
|
fail("no primary CFA or LinearRaw image IFD.");
|
|
180
196
|
const ifd0 = ifds[0];
|
|
181
197
|
// DNG places camera-level tags in IFD 0; TIFF-EP files may carry them only in the raw IFD.
|
|
182
198
|
const pick = (id) => ifd0.get(id) ?? raw.get(id);
|
|
183
|
-
const width = one(raw, 256, 0), height = one(raw, 257, 0), spp = one(raw, 277, 1), compressionCode = one(raw, 259, 1), photometric = one(raw, 262, 0);
|
|
184
|
-
if (width < 1 || height < 1 || width > 65535 * 4 || height > 65535 * 4)
|
|
185
|
-
fail("invalid image dimensions.");
|
|
186
|
-
if (width * height > maximumPixels)
|
|
187
|
-
fail("image exceeds the pixel limit.");
|
|
188
|
-
const cfaImage = photometric === 32803;
|
|
189
|
-
if (cfaImage ? spp !== 1 : spp < 1 || spp > 4)
|
|
190
|
-
fail("unsupported SamplesPerPixel.");
|
|
191
|
-
if (spp > 1 && one(raw, 284, 1) !== 1)
|
|
192
|
-
fail("only chunky PlanarConfiguration is supported.");
|
|
193
|
-
const bps = integers(raw.get(258), spp, [3]) ?? [1];
|
|
194
|
-
if (bps.length !== spp || bps.some((b) => b !== bps[0]))
|
|
195
|
-
fail("BitsPerSample must be equal for every sample.");
|
|
196
|
-
const bitsPerSample = bps[0];
|
|
197
|
-
if (bitsPerSample < 8 || bitsPerSample > 16)
|
|
198
|
-
fail("only 8 to 16 bits per sample are supported.");
|
|
199
|
-
if (one(raw, 339, 1) !== 1)
|
|
200
|
-
fail("only unsigned integer samples are supported.");
|
|
201
|
-
if (compressionCode !== 1 && compressionCode !== 7)
|
|
202
|
-
fail("only uncompressed and lossless JPEG raw data are supported.");
|
|
203
199
|
const dngVersionTag = integers(ifd0.get(50706), 4, [1]);
|
|
204
200
|
const dngVersion = dngVersionTag && dngVersionTag.length === 4 ? dngVersionTag : null;
|
|
205
201
|
if (dngVersionTag && !dngVersion)
|
|
206
202
|
fail("invalid DNGVersion.");
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (
|
|
218
|
-
fail("
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
203
|
+
const assumptions = [];
|
|
204
|
+
/** Plans one image IFD (the raw image or a mask): layout checks and the strip or tile table. */
|
|
205
|
+
const plan = (ifd, kind) => {
|
|
206
|
+
const width = one(ifd, 256, 0), height = one(ifd, 257, 0), spp = one(ifd, 277, 1), code = one(ifd, 259, 1), photometric = one(ifd, 262, 0);
|
|
207
|
+
if (width < 1 || height < 1 || width > 65535 * 4 || height > 65535 * 4)
|
|
208
|
+
fail("invalid image dimensions.");
|
|
209
|
+
if (width * height > maximumPixels)
|
|
210
|
+
fail("image exceeds the pixel limit.");
|
|
211
|
+
if (kind === "mask" ? spp !== 1 : photometric === 32803 ? spp !== 1 : spp < 1 || spp > 4)
|
|
212
|
+
fail("unsupported SamplesPerPixel.");
|
|
213
|
+
if (spp > 1 && one(ifd, 284, 1) !== 1)
|
|
214
|
+
fail("only chunky PlanarConfiguration is supported.");
|
|
215
|
+
const bps = integers(ifd.get(258), spp, [3]) ?? [1];
|
|
216
|
+
if (bps.length !== spp || bps.some((b) => b !== bps[0]))
|
|
217
|
+
fail("BitsPerSample must be equal for every sample.");
|
|
218
|
+
const bits = bps[0];
|
|
219
|
+
if (bits < 8 || bits > 16)
|
|
220
|
+
fail("only 8 to 16 bits per sample are supported.");
|
|
221
|
+
if (one(ifd, 339, 1) !== 1)
|
|
222
|
+
fail("only unsigned integer samples are supported.");
|
|
223
|
+
if (![1, 7, 34892, 52546].includes(code))
|
|
224
|
+
fail("Compression " + code + " is not supported (supported: 1 uncompressed, 7 lossless JPEG, 34892 lossy JPEG, 52546 JPEG XL).");
|
|
225
|
+
if (code === 34892 && (bits !== 8 || photometric === 32803))
|
|
226
|
+
fail("lossy JPEG (Compression 34892) raw data must be 8-bit LinearRaw or a mask (DNG 1.4.0.0).");
|
|
227
|
+
const tiled = ifd.has(322) || ifd.has(324);
|
|
228
|
+
const tileWidth = tiled ? one(ifd, 322, 0) : width, tileLength = tiled ? one(ifd, 323, 0) : Math.min(height, one(ifd, 278, 0xffffffff));
|
|
229
|
+
if (tileWidth < 1 || tileLength < 1 || tileWidth > 65535 * 4 || tileLength > 65535 * 4 || (tiled && tileWidth * tileLength > maximumPixels))
|
|
230
|
+
fail("invalid strip or tile size.");
|
|
231
|
+
const across = Math.ceil(width / tileWidth), down = Math.ceil(height / tileLength), count = across * down;
|
|
232
|
+
const offsets = integers(ifd.get(tiled ? 324 : 273), count, [3, 4]), counts = integers(ifd.get(tiled ? 325 : 279), count, [3, 4]);
|
|
233
|
+
if (!offsets || !counts || offsets.length !== count || counts.length !== count)
|
|
234
|
+
fail("strip or tile tables do not match the image layout.");
|
|
235
|
+
const chunks = [];
|
|
236
|
+
for (let i = 0; i < count; i++) {
|
|
237
|
+
if (offsets[i] + counts[i] > n)
|
|
238
|
+
fail("strip or tile data exceeds the file.");
|
|
239
|
+
const tx = i % across, ty = Math.floor(i / across);
|
|
223
240
|
// Strips cover whole rows (the last may be short); tiles are always full-size and clipped on copy.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
241
|
+
chunks.push({ data: bytes.subarray(offsets[i], offsets[i] + counts[i]), x0: tx * tileWidth, y0: ty * tileLength, width: tiled ? tileWidth : width, rows: tiled ? tileLength : Math.min(tileLength, height - ty * tileLength) });
|
|
242
|
+
}
|
|
243
|
+
return { width, height, spp, bits, code, chunks, jpegTables: blob(ifd.get(347)) ?? undefined, legacy: dngVersion !== null && (dngVersion[0] < 1 || (dngVersion[0] === 1 && dngVersion[1] < 1)) };
|
|
244
|
+
};
|
|
245
|
+
const image = plan(raw, "raw"), { width, height, spp, bits: bitsPerSample, code: compressionCode } = image;
|
|
246
|
+
const photometric = one(raw, 262, 0), cfaImage = photometric === 32803;
|
|
247
|
+
const rowInterleave = one(raw, 50975, 1), columnInterleave = one(raw, 52547, 1);
|
|
248
|
+
if (rowInterleave < 1 || rowInterleave > height || columnInterleave < 1 || columnInterleave > width)
|
|
249
|
+
fail("invalid RowInterleaveFactor or ColumnInterleaveFactor.");
|
|
250
|
+
const interleaved = rowInterleave > 1 || columnInterleave > 1;
|
|
251
|
+
if (half && cfaImage)
|
|
252
|
+
fail('size "half" needs LinearRaw data; this file is a CFA mosaic.');
|
|
253
|
+
if (half && interleaved)
|
|
254
|
+
fail('size "half" is not available for row- or column-interleaved data.');
|
|
255
|
+
if (interleaved)
|
|
256
|
+
assumptions.push("dngImport.interleave: RowInterleaveFactor " + rowInterleave + " and ColumnInterleaveFactor " + columnInterleave + " undone; the stored order (each field of rows or columns stored together, field by field) is inferred from the DNG SDK, not yet verified on a phone file.");
|
|
257
|
+
// Memory pre-check (estimates, docs/IMPORT_AND_PNG.md "Memory"): output codes (plus the half-size sums), one chunk's decoded
|
|
258
|
+
// samples twice (decode buffer and copy), and its decoder's working memory.
|
|
259
|
+
const outWidth = half ? Math.ceil(width / 2) : width, outHeight = half ? Math.ceil(height / 2) : height;
|
|
260
|
+
const largest = Math.max(...image.chunks.map((c) => c.width * c.rows)) * spp;
|
|
261
|
+
const decoderBytes = compressionCode === 52546 ? largest * JXL_WORKING_BYTES_PER_SAMPLE : compressionCode === 34892 ? largest * 2 + largest : largest * 2;
|
|
262
|
+
const estimate = outWidth * outHeight * spp * 2 + (half ? outWidth * outHeight * spp * 4 : 0) + (interleaved ? width * height * spp * 2 : 0) + largest * 4 + decoderBytes;
|
|
263
|
+
if (estimate > maximumBytes)
|
|
264
|
+
fail("the import needs an estimated " + Math.ceil(estimate / 1048576) + " MiB, above maximumBytes (" + Math.floor(maximumBytes / 1048576) + " MiB); raise maximumBytes or use size \"half\" for a LinearRaw preview.");
|
|
265
|
+
const maxCode = 2 ** bitsPerSample - 1, codes = new Uint16Array(outWidth * outHeight * spp), sums = half ? new Uint32Array(outWidth * outHeight * spp) : null;
|
|
266
|
+
const write = (chunk, samples) => {
|
|
267
|
+
for (let y = 0; y < chunk.rows && chunk.y0 + y < height; y++) {
|
|
268
|
+
const columns = Math.min(chunk.width, width - chunk.x0) * spp, from = y * chunk.width * spp;
|
|
269
|
+
if (sums) {
|
|
270
|
+
const row = ((chunk.y0 + y) >> 1) * outWidth;
|
|
237
271
|
for (let i = 0; i < columns; i++) {
|
|
238
272
|
const v = samples[from + i];
|
|
239
273
|
if (v > maxCode)
|
|
240
274
|
fail("raw code exceeds BitsPerSample.");
|
|
241
|
-
|
|
275
|
+
const x = chunk.x0 + Math.floor(i / spp), at = (row + (x >> 1)) * spp + (i % spp);
|
|
276
|
+
sums[at] = sums[at] + v;
|
|
242
277
|
}
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const to = ((chunk.y0 + y) * width + chunk.x0) * spp;
|
|
281
|
+
for (let i = 0; i < columns; i++) {
|
|
282
|
+
const v = samples[from + i];
|
|
283
|
+
if (v > maxCode)
|
|
284
|
+
fail("raw code exceeds BitsPerSample.");
|
|
285
|
+
codes[to + i] = v;
|
|
243
286
|
}
|
|
244
287
|
}
|
|
288
|
+
};
|
|
245
289
|
// CFA description.
|
|
246
290
|
let cfa = null, colorPlanes = spp;
|
|
247
291
|
if (cfaImage) {
|
|
@@ -280,10 +324,10 @@ export function parseDngRawImage(input) {
|
|
|
280
324
|
if (origin.length !== 2 || size.length !== 2 || size[0] <= 0 || size[1] <= 0 || origin[0] < 0 || origin[1] < 0)
|
|
281
325
|
fail("invalid default crop.");
|
|
282
326
|
const linear = integers(raw.get(50712), 65536, [3]);
|
|
283
|
-
const matrix = (id) => {
|
|
327
|
+
const matrix = (id, transpose = false) => {
|
|
284
328
|
const m = integers(pick(id), colorPlanes * 3, [10]);
|
|
285
329
|
if (m && m.length !== colorPlanes * 3)
|
|
286
|
-
fail("ColorMatrix count does not match the colour planes.");
|
|
330
|
+
fail((transpose ? "ForwardMatrix" : "ColorMatrix") + " count does not match the colour planes.");
|
|
287
331
|
return m;
|
|
288
332
|
};
|
|
289
333
|
const neutral = integers(pick(50728), colorPlanes, [3, 5]);
|
|
@@ -296,18 +340,209 @@ export function parseDngRawImage(input) {
|
|
|
296
340
|
const orientation = one(ifd0.has(274) ? ifd0 : raw, 274, 1);
|
|
297
341
|
if (orientation < 1 || orientation > 8)
|
|
298
342
|
fail("invalid Orientation.");
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
343
|
+
// Phone tags (#598): parsed when present; a malformed one is reported, never a failure.
|
|
344
|
+
const optional = (name, read) => {
|
|
345
|
+
try {
|
|
346
|
+
return read();
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
if (!(error instanceof InvalidConfigurationError))
|
|
350
|
+
throw error;
|
|
351
|
+
assumptions.push("dngImport.ignored: " + name + " is malformed and was ignored (" + error.message + ").");
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
310
354
|
};
|
|
355
|
+
const opcodes = (id, name) => { const b = blob(raw.get(id)); return b ? optional(name, () => parseDngOpcodeList(b)) : null; };
|
|
356
|
+
const opcodeList1 = opcodes(51008, "OpcodeList1"), opcodeList2 = opcodes(51009, "OpcodeList2"), opcodeList3 = opcodes(51022, "OpcodeList3");
|
|
357
|
+
const pgtm2 = blob(ifd0.get(52544)), pgtm1 = blob(raw.get(52525));
|
|
358
|
+
const profileGainTableMap = (pgtm2 ? optional("ProfileGainTableMap2", () => parseDngProfileGainTableMap(pgtm2, little, 2)) : null)
|
|
359
|
+
?? (pgtm1 ? optional("ProfileGainTableMap", () => parseDngProfileGainTableMap(pgtm1, little, 1)) : null);
|
|
360
|
+
const rgbBlob = blob(ifd0.get(52543)), rgbTables = rgbBlob ? optional("RGBTables", () => parseDngRgbTables(rgbBlob, little)) : null;
|
|
361
|
+
const exposure = integers(pick(50730), 1, [10]), exposureOffset = integers(pick(51109), 1, [10]);
|
|
362
|
+
const baselineExposure = exposure || exposureOffset ? (exposure?.[0] ?? 0) + (exposureOffset?.[0] ?? 0) : null;
|
|
363
|
+
for (const [id, name] of UNAPPLIED_TAGS)
|
|
364
|
+
if (ifd0.has(id) || raw.has(id))
|
|
365
|
+
assumptions.push("dngImport.notApplied: " + name + " (" + id + ") is present and not applied.");
|
|
366
|
+
for (const ifd of ifds) {
|
|
367
|
+
const type = one(ifd, 254, 0);
|
|
368
|
+
if (type === 8 || type === 9)
|
|
369
|
+
assumptions.push("dngImport.notApplied: a depth map IFD is present and not decoded.");
|
|
370
|
+
if (type === 16)
|
|
371
|
+
assumptions.push("dngImport.notApplied: an enhanced-image IFD (NewSubFileType 16) is present and not decoded; the raw image is used.");
|
|
372
|
+
if (type === 32 || one(ifd, 262, 0) === 52553)
|
|
373
|
+
assumptions.push("dngImport.notApplied: a gain-map image IFD (NewSubFileType 32) is present and not applied.");
|
|
374
|
+
}
|
|
375
|
+
if (compressionCode === 34892)
|
|
376
|
+
assumptions.push("dngImport.lossySource: the raw data is lossy JPEG (Compression 34892), 8-bit and gamma-coded; OpcodeList2 or the LinearizationTable makes it linear.");
|
|
377
|
+
const jxlDistance = integers(ifd0.get(52553) ?? raw.get(52553), 1, [11, 12]);
|
|
378
|
+
const lossyJxl = compressionCode === 52546 && !(jxlDistance && jxlDistance[0] <= 0);
|
|
379
|
+
if (lossyJxl)
|
|
380
|
+
assumptions.push("dngImport.lossySource: the JPEG XL raw data is " + (jxlDistance ? "lossy (JXLDistance " + jxlDistance[0] + ")." : "not marked lossless (no JXLDistance tag)."));
|
|
381
|
+
if (linear)
|
|
382
|
+
assumptions.push("dngImport.linearizationTable: returned, not applied to codes (developDngStageImage applies it).");
|
|
383
|
+
if (opcodeList1 || opcodeList2 || opcodeList3)
|
|
384
|
+
assumptions.push("dngImport.opcodes: the opcode lists are returned, not applied to codes (developDngStageImage applies them).");
|
|
385
|
+
if (profileGainTableMap)
|
|
386
|
+
assumptions.push("dngImport.profileGainTableMap: returned (version " + profileGainTableMap.version + "); applyDngProfileGainTableMap applies it in a development.");
|
|
387
|
+
if (rgbTables)
|
|
388
|
+
assumptions.push("dngImport.rgbTables: returned (" + rgbTables.tables.length + " tables); applyDngRgbTables applies them in a development.");
|
|
389
|
+
// Semantic masks (DNG 1.6): planned here, decoded with the image.
|
|
390
|
+
const masks = ifds.filter((ifd) => one(ifd, 254, 0) === 0x10004 && one(ifd, 262, 0) === 52527).map((ifd) => {
|
|
391
|
+
if (ifd.has(52536))
|
|
392
|
+
assumptions.push("dngImport.semanticMask: MaskSubArea is present; the mask is returned as stored and spans the whole image in applyDngRgbTables.");
|
|
393
|
+
return { name: ascii(ifd.get(52526)) ?? "", instanceId: ascii(ifd.get(52528)), ifd };
|
|
394
|
+
});
|
|
395
|
+
const halve = (r) => half ? { x: r.x / 2, y: r.y / 2, width: r.width / 2, height: r.height / 2 } : r;
|
|
396
|
+
const pairMean = (values) => values && half ? Array.from({ length: Math.ceil(values.length / 2) }, (_, i) => (values[2 * i] + (values[2 * i + 1] ?? values[2 * i])) / 2) : values;
|
|
397
|
+
const halfActive = half ? { x: Math.floor(activeArea.x / 2), y: Math.floor(activeArea.y / 2), width: Math.ceil((activeArea.x + activeArea.width) / 2) - Math.floor(activeArea.x / 2), height: Math.ceil((activeArea.y + activeArea.height) / 2) - Math.floor(activeArea.y / 2) } : activeArea;
|
|
398
|
+
let blackLevel = { repeatRows: repeat[0], repeatColumns: repeat[1], values: black, deltaH, deltaV };
|
|
399
|
+
if (half) {
|
|
400
|
+
// Half size: the black pattern is averaged over its repeat (a LinearRaw file normally has a 1 x 1 repeat), the deltas pairwise.
|
|
401
|
+
const mean = Array.from({ length: spp }, (_, s) => { let t = 0; for (let k = 0; k < repeat[0] * repeat[1]; k++)
|
|
402
|
+
t += black[k * spp + s]; return t / (repeat[0] * repeat[1]); });
|
|
403
|
+
blackLevel = { repeatRows: 1, repeatColumns: 1, values: mean, deltaH: pairMean(deltaH), deltaV: pairMean(deltaV) };
|
|
404
|
+
if (repeat[0] * repeat[1] > 1)
|
|
405
|
+
assumptions.push("dngImport.halfSize: the BlackLevel repeat pattern was averaged to one value per sample.");
|
|
406
|
+
assumptions.push("dngImport.halfSize: a half-size preview; each code is the rounded mean of a 2 x 2 block of stored codes.");
|
|
407
|
+
}
|
|
408
|
+
const finish = (semanticMasks) => {
|
|
409
|
+
if (sums)
|
|
410
|
+
for (let y = 0; y < outHeight; y++)
|
|
411
|
+
for (let x = 0; x < outWidth; x++) {
|
|
412
|
+
const count = (Math.min(2, height - 2 * y)) * (Math.min(2, width - 2 * x));
|
|
413
|
+
for (let s = 0; s < spp; s++) {
|
|
414
|
+
const at = (y * outWidth + x) * spp + s;
|
|
415
|
+
codes[at] = Math.round(sums[at] / count);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const out = interleaved ? deinterleave(codes, width, height, spp, rowInterleave, columnInterleave) : codes;
|
|
419
|
+
return {
|
|
420
|
+
schemaVersion: DNG_IMPORT_SCHEMA_VERSION, byteOrder: little ? "little-endian" : "big-endian", dngVersion,
|
|
421
|
+
make: ascii(pick(271)), model: ascii(pick(272)), uniqueCameraModel: ascii(pick(50708)), orientation,
|
|
422
|
+
width: outWidth, height: outHeight, samplesPerPixel: spp, bitsPerSample,
|
|
423
|
+
compression: compressionCode === 1 ? "uncompressed" : compressionCode === 7 ? "lossless-jpeg" : compressionCode === 34892 ? "lossy-jpeg" : "jpeg-xl",
|
|
424
|
+
lossySource: compressionCode === 34892 || lossyJxl, scale: half ? 0.5 : 1,
|
|
425
|
+
photometricInterpretation: cfaImage ? "cfa" : "linear-raw", codes: out, cfa,
|
|
426
|
+
linearizationTable: linear ? Uint16Array.from(linear) : null,
|
|
427
|
+
blackLevel, whiteLevel, activeArea: halfActive, defaultCrop: halve({ x: origin[0], y: origin[1], width: size[0], height: size[1] }),
|
|
428
|
+
colorMatrix1: matrix(50721), colorMatrix2: matrix(50722),
|
|
429
|
+
calibrationIlluminant1: illuminant(50778), calibrationIlluminant2: illuminant(50779), asShotNeutral: neutral,
|
|
430
|
+
rawDataUniqueId: uniqueId ? uniqueId.map((b) => b.toString(16).padStart(2, "0")).join("") : null,
|
|
431
|
+
forwardMatrix1: matrix(50964, true), forwardMatrix2: matrix(50965, true), baselineExposure,
|
|
432
|
+
opcodeList1, opcodeList2, opcodeList3, profileGainTableMap, rgbTables, semanticMasks, assumptions
|
|
433
|
+
};
|
|
434
|
+
};
|
|
435
|
+
return { image, write, finish, masks, plan, assumptions, little };
|
|
436
|
+
}
|
|
437
|
+
/** Decodes one chunk without a JPEG XL decoder; null for JPEG XL data. */
|
|
438
|
+
function decodeChunkSync(p, chunk, little) {
|
|
439
|
+
const samples = chunk.width * chunk.rows * p.spp;
|
|
440
|
+
if (p.code === 7) {
|
|
441
|
+
const image = parseLosslessJpeg(chunk.data, { maximumSamples: samples, legacySixteenBitDifference: p.legacy });
|
|
442
|
+
if (image.width * image.height * image.components !== samples)
|
|
443
|
+
fail("lossless JPEG sample count does not match the strip or tile.");
|
|
444
|
+
return image.samples;
|
|
445
|
+
}
|
|
446
|
+
if (p.code === 34892) {
|
|
447
|
+
const image = parseLossyJpeg(chunk.data, { maximumSamples: samples, ...(p.jpegTables ? { tables: p.jpegTables } : {}) });
|
|
448
|
+
if (image.width !== chunk.width || image.height !== chunk.rows || image.components !== p.spp)
|
|
449
|
+
fail("lossy JPEG size or components do not match the strip or tile.");
|
|
450
|
+
return Uint16Array.from(image.samples);
|
|
451
|
+
}
|
|
452
|
+
if (p.code === 52546)
|
|
453
|
+
return null;
|
|
454
|
+
return unpack(chunk.data, chunk.width * p.spp, chunk.rows, p.bits, little);
|
|
455
|
+
}
|
|
456
|
+
async function decodeChunk(p, chunk, little, decoder) {
|
|
457
|
+
const sync = decodeChunkSync(p, chunk, little);
|
|
458
|
+
if (sync)
|
|
459
|
+
return sync;
|
|
460
|
+
if (!decoder)
|
|
461
|
+
fail("JPEG XL (Compression 52546) data needs a jpegXlDecoder (importDngRawImage).");
|
|
462
|
+
return readJpegXlTilePng(await decoder(chunk.data), chunk.width, chunk.rows, p.spp, p.bits);
|
|
463
|
+
}
|
|
464
|
+
/** Copies decoded chunks into a mask's 0..1 values. */
|
|
465
|
+
function maskValues(p, decoded) {
|
|
466
|
+
const out = new Float32Array(p.width * p.height), scale = 1 / (2 ** p.bits - 1);
|
|
467
|
+
p.chunks.forEach((chunk, k) => {
|
|
468
|
+
for (let y = 0; y < chunk.rows && chunk.y0 + y < p.height; y++)
|
|
469
|
+
for (let x = 0; x < chunk.width && chunk.x0 + x < p.width; x++)
|
|
470
|
+
out[(chunk.y0 + y) * p.width + chunk.x0 + x] = Math.min(1, decoded[k][y * chunk.width + x] * scale);
|
|
471
|
+
});
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Undoes Row/ColumnInterleaveFactor (DNG 1.7.0.0): stored row s of a factor-R image holds image row (s mod ⌈H/R⌉)·R + ⌊s/⌈H/R⌉⌋
|
|
476
|
+
* (each field of every R-th row stored together, field by field), and likewise for columns. Inferred from the DNG SDK's
|
|
477
|
+
* `Interleave2D`; to be verified on a phone file.
|
|
478
|
+
*/
|
|
479
|
+
function deinterleave(codes, width, height, spp, rows, columns) {
|
|
480
|
+
const out = new Uint16Array(codes.length), fieldRows = Math.ceil(height / rows), fieldColumns = Math.ceil(width / columns);
|
|
481
|
+
for (let s = 0; s < height; s++) {
|
|
482
|
+
const y = (s % fieldRows) * rows + Math.floor(s / fieldRows);
|
|
483
|
+
if (y >= height)
|
|
484
|
+
fail("RowInterleaveFactor does not divide the stored rows into whole fields.");
|
|
485
|
+
for (let t = 0; t < width; t++) {
|
|
486
|
+
const x = (t % fieldColumns) * columns + Math.floor(t / fieldColumns);
|
|
487
|
+
if (x >= width)
|
|
488
|
+
fail("ColumnInterleaveFactor does not divide the stored columns into whole fields.");
|
|
489
|
+
for (let c = 0; c < spp; c++)
|
|
490
|
+
out[(y * width + x) * spp + c] = codes[(s * width + t) * spp + c];
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return out;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Parse one DNG or TIFF-EP file and decode its primary CFA or LinearRaw image (the first IFD with NewSubFileType 0 and
|
|
497
|
+
* PhotometricInterpretation 32803 or 34892, in IFD-chain then SubIFD order), synchronously.
|
|
498
|
+
*
|
|
499
|
+
* Supported raw data: Compression 1 at 8..16 bits/sample (8 bits as bytes, 16 bits in the file byte order, other depths packed
|
|
500
|
+
* MSB-first with byte-aligned rows, per DNG BitsPerSample), Compression 7 lossless Huffman JPEG (T.81 Annex H) and Compression 34892
|
|
501
|
+
* 8-bit lossy JPEG (T.81 sequential DCT), in strips or tiles, chunky planar configuration only. JPEG XL (52546) needs
|
|
502
|
+
* {@link importDngRawImage}; a JPEG XL semantic mask is skipped here and named in `assumptions`.
|
|
503
|
+
* @returns Owned typed arrays and plain metadata; nothing is applied to the codes.
|
|
504
|
+
*/
|
|
505
|
+
export function parseDngRawImage(input) {
|
|
506
|
+
const ctx = prepare(input, ["bytes", "maximumPixels", "maximumIfds", "maximumBytes", "size"]);
|
|
507
|
+
if (ctx.image.code === 52546)
|
|
508
|
+
fail("JPEG XL (Compression 52546) raw data needs importDngRawImage with a jpegXlDecoder.");
|
|
509
|
+
for (const chunk of ctx.image.chunks)
|
|
510
|
+
ctx.write(chunk, decodeChunkSync(ctx.image, chunk, ctx.little));
|
|
511
|
+
const masks = [];
|
|
512
|
+
for (const m of ctx.masks) {
|
|
513
|
+
const p = ctx.plan(m.ifd, "mask");
|
|
514
|
+
if (p.code === 52546) {
|
|
515
|
+
ctx.assumptions.push("dngImport.semanticMask: mask " + JSON.stringify(m.name) + " is JPEG XL and was not decoded (use importDngRawImage).");
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
masks.push({ name: m.name, instanceId: m.instanceId, width: p.width, height: p.height, data: maskValues(p, p.chunks.map((c) => decodeChunkSync(p, c, ctx.little))) });
|
|
519
|
+
}
|
|
520
|
+
return ctx.finish(masks);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* {@link parseDngRawImage} for every supported compression, JPEG XL included: each JPEG XL strip or tile goes to `jpegXlDecoder` in
|
|
524
|
+
* turn (one at a time, so only one tile's decoder memory is held), and its PNG is read at the DNG's bit depth (jpeg-xl-tiles.ts).
|
|
525
|
+
*/
|
|
526
|
+
export async function importDngRawImage(input) {
|
|
527
|
+
const ctx = prepare(input, ["bytes", "maximumPixels", "maximumIfds", "maximumBytes", "size", "jpegXlDecoder"]);
|
|
528
|
+
const decoder = input.jpegXlDecoder;
|
|
529
|
+
if (decoder !== undefined && typeof decoder !== "function")
|
|
530
|
+
fail("jpegXlDecoder must be a function.");
|
|
531
|
+
for (const chunk of ctx.image.chunks)
|
|
532
|
+
ctx.write(chunk, await decodeChunk(ctx.image, chunk, ctx.little, decoder));
|
|
533
|
+
const masks = [];
|
|
534
|
+
for (const m of ctx.masks) {
|
|
535
|
+
const p = ctx.plan(m.ifd, "mask");
|
|
536
|
+
if (p.code === 52546 && !decoder) {
|
|
537
|
+
ctx.assumptions.push("dngImport.semanticMask: mask " + JSON.stringify(m.name) + " is JPEG XL and no jpegXlDecoder was given.");
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const decoded = [];
|
|
541
|
+
for (const c of p.chunks)
|
|
542
|
+
decoded.push(await decodeChunk(p, c, ctx.little, decoder));
|
|
543
|
+
masks.push({ name: m.name, instanceId: m.instanceId, width: p.width, height: p.height, data: maskValues(p, decoded) });
|
|
544
|
+
}
|
|
545
|
+
return ctx.finish(masks);
|
|
311
546
|
}
|
|
312
547
|
/** Unpack uncompressed rows: 8-bit bytes, 16-bit words in file byte order, other depths MSB-first with byte-aligned rows. */
|
|
313
548
|
function unpack(data, rowSamples, rows, bits, little) {
|