@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.
Files changed (169) hide show
  1. package/CHANGELOG.md +118 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +6 -6
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +11 -0
  6. package/dist/capture/c2pa-embed.d.ts +116 -0
  7. package/dist/capture/c2pa-embed.js +493 -0
  8. package/dist/capture/c2pa-embed.js.map +1 -0
  9. package/dist/capture/dng-import.d.ts +73 -8
  10. package/dist/capture/dng-import.js +315 -80
  11. package/dist/capture/dng-import.js.map +1 -1
  12. package/dist/capture/dng-opcodes.d.ts +96 -0
  13. package/dist/capture/dng-opcodes.js +391 -0
  14. package/dist/capture/dng-opcodes.js.map +1 -0
  15. package/dist/capture/dng-profile-maps.d.ts +99 -0
  16. package/dist/capture/dng-profile-maps.js +381 -0
  17. package/dist/capture/dng-profile-maps.js.map +1 -0
  18. package/dist/capture/dng-stage-image.d.ts +23 -0
  19. package/dist/capture/dng-stage-image.js +65 -0
  20. package/dist/capture/dng-stage-image.js.map +1 -0
  21. package/dist/capture/jpeg-xl-tiles.d.ts +22 -0
  22. package/dist/capture/jpeg-xl-tiles.js +151 -0
  23. package/dist/capture/jpeg-xl-tiles.js.map +1 -0
  24. package/dist/capture/lossy-jpeg.d.ts +17 -0
  25. package/dist/capture/lossy-jpeg.js +369 -0
  26. package/dist/capture/lossy-jpeg.js.map +1 -0
  27. package/dist/capture/native-raw-development.d.ts +2 -1
  28. package/dist/capture/native-raw-development.js.map +1 -1
  29. package/dist/color/neighbor-highlight-reconstruction.d.ts +66 -0
  30. package/dist/color/neighbor-highlight-reconstruction.js +234 -0
  31. package/dist/color/neighbor-highlight-reconstruction.js.map +1 -0
  32. package/dist/color/sensor-color-development.d.ts +5 -0
  33. package/dist/color/sensor-color-development.js +7 -1
  34. package/dist/color/sensor-color-development.js.map +1 -1
  35. package/dist/color/spectral-spikiness.d.ts +11 -0
  36. package/dist/color/spectral-spikiness.js +23 -0
  37. package/dist/color/spectral-spikiness.js.map +1 -0
  38. package/dist/core/execution-error.d.ts +21 -0
  39. package/dist/core/execution-error.js +12 -0
  40. package/dist/core/execution-error.js.map +1 -0
  41. package/dist/core/version.d.ts +1 -1
  42. package/dist/core/version.js +1 -1
  43. package/dist/core/version.js.map +1 -1
  44. package/dist/facade/catalogue-lens-signal.d.ts +26 -1
  45. package/dist/facade/catalogue-lens-signal.js +25 -7
  46. package/dist/facade/catalogue-lens-signal.js.map +1 -1
  47. package/dist/facade/diffraction-provider.d.ts +1 -1
  48. package/dist/facade/diffraction-provider.js +4 -4
  49. package/dist/facade/diffraction-provider.js.map +1 -1
  50. package/dist/facade/display-view-gpu-session.js +38 -6
  51. package/dist/facade/display-view-gpu-session.js.map +1 -1
  52. package/dist/facade/display-view-gpu.d.ts +13 -5
  53. package/dist/facade/display-view-gpu.js.map +1 -1
  54. package/dist/facade/display-view-normal.d.ts +15 -0
  55. package/dist/facade/display-view-normal.js +44 -0
  56. package/dist/facade/display-view-normal.js.map +1 -0
  57. package/dist/facade/display-view-rendering.d.ts +97 -0
  58. package/dist/facade/display-view-rendering.js +598 -0
  59. package/dist/facade/display-view-rendering.js.map +1 -0
  60. package/dist/facade/display-view.d.ts +37 -65
  61. package/dist/facade/display-view.js +147 -476
  62. package/dist/facade/display-view.js.map +1 -1
  63. package/dist/facade/expected-signal-setup.js +2 -2
  64. package/dist/facade/expected-signal-setup.js.map +1 -1
  65. package/dist/facade/fast-methods.d.ts +1 -1
  66. package/dist/facade/fast-methods.js +37 -8
  67. package/dist/facade/fast-methods.js.map +1 -1
  68. package/dist/facade/generic-lens-capture.d.ts +1 -1
  69. package/dist/facade/generic-lens-capture.js +1 -1
  70. package/dist/facade/generic-lens-capture.js.map +1 -1
  71. package/dist/facade/image-plane.js +41 -10
  72. package/dist/facade/image-plane.js.map +1 -1
  73. package/dist/facade/on-demand-kits.d.ts +14 -0
  74. package/dist/facade/on-demand-kits.js +36 -0
  75. package/dist/facade/on-demand-kits.js.map +1 -0
  76. package/dist/facade/opt-in-kit.d.ts +9 -0
  77. package/dist/facade/opt-in-kit.js +11 -0
  78. package/dist/facade/opt-in-kit.js.map +1 -0
  79. package/dist/facade/quality-kit.d.ts +21 -0
  80. package/dist/facade/quality-kit.js +23 -0
  81. package/dist/facade/quality-kit.js.map +1 -0
  82. package/dist/facade/quality-paths.d.ts +22 -0
  83. package/dist/facade/quality-paths.js +44 -0
  84. package/dist/facade/quality-paths.js.map +1 -0
  85. package/dist/facade/scene-radiance.d.ts +18 -2
  86. package/dist/facade/scene-radiance.js +6 -4
  87. package/dist/facade/scene-radiance.js.map +1 -1
  88. package/dist/facade/simulate-capture.d.ts +32 -13
  89. package/dist/facade/simulate-capture.js +162 -69
  90. package/dist/facade/simulate-capture.js.map +1 -1
  91. package/dist/facade/simulate-display-view.d.ts +10 -12
  92. package/dist/facade/simulate-display-view.js +57 -48
  93. package/dist/facade/simulate-display-view.js.map +1 -1
  94. package/dist/gpu/display-view-gpu-renderer.d.ts +43 -0
  95. package/dist/gpu/display-view-gpu-renderer.js +134 -0
  96. package/dist/gpu/display-view-gpu-renderer.js.map +1 -0
  97. package/dist/gpu/display-view-image-material-module.d.ts +3 -2
  98. package/dist/gpu/display-view-image-material-module.js +25 -3
  99. package/dist/gpu/display-view-image-material-module.js.map +1 -1
  100. package/dist/gpu/display-view-image-quality.d.ts +43 -1
  101. package/dist/gpu/display-view-image-quality.js +256 -11
  102. package/dist/gpu/display-view-image-quality.js.map +1 -1
  103. package/dist/gpu/display-view-quality-gpu.d.ts +13 -64
  104. package/dist/gpu/display-view-quality-gpu.js +60 -249
  105. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  106. package/dist/gpu/display-view-speed-gpu.d.ts +16 -16
  107. package/dist/gpu/display-view-speed-gpu.js +50 -12
  108. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  109. package/dist/gpu/image-kernel-accelerator.js +3 -0
  110. package/dist/gpu/image-kernel-accelerator.js.map +1 -1
  111. package/dist/gpu/image-kernel-execution.d.ts +2 -13
  112. package/dist/gpu/image-kernel-execution.js +2 -10
  113. package/dist/gpu/image-kernel-execution.js.map +1 -1
  114. package/dist/gpu/wgsl-display-view-coupled-stub.d.ts +8 -0
  115. package/dist/gpu/wgsl-display-view-coupled-stub.js +21 -0
  116. package/dist/gpu/wgsl-display-view-coupled-stub.js.map +1 -0
  117. package/dist/gpu/wgsl-display-view-coupled.d.ts +88 -0
  118. package/dist/gpu/wgsl-display-view-coupled.js +353 -0
  119. package/dist/gpu/wgsl-display-view-coupled.js.map +1 -0
  120. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +4 -9
  121. package/dist/gpu/wgsl-display-view-quality-kernel.js +109 -111
  122. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  123. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +1 -41
  124. package/dist/gpu/wgsl-display-view-speed-kernel.js +71 -8
  125. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  126. package/dist/index.d.ts +11 -3
  127. package/dist/index.js +9 -1
  128. package/dist/index.js.map +1 -1
  129. package/dist/output/camera-channel-response.d.ts +50 -0
  130. package/dist/output/camera-channel-response.js +69 -0
  131. package/dist/output/camera-channel-response.js.map +1 -0
  132. package/dist/output/native-capture-sdr.js +5 -2
  133. package/dist/output/native-capture-sdr.js.map +1 -1
  134. package/dist/output/sdr-rendering.d.ts +18 -1
  135. package/dist/output/sdr-rendering.js +39 -2
  136. package/dist/output/sdr-rendering.js.map +1 -1
  137. package/dist/sensor/native-sensor-report.d.ts +10 -0
  138. package/dist/sensor/native-sensor-report.js +16 -0
  139. package/dist/sensor/native-sensor-report.js.map +1 -0
  140. package/dist/sensor/sensor-characteristics.d.ts +81 -13
  141. package/dist/sensor/sensor-characteristics.js +10 -6
  142. package/dist/sensor/sensor-characteristics.js.map +1 -1
  143. package/docs/API_REFERENCE.md +117 -4
  144. package/docs/QUICK_START.md +37 -8
  145. package/docs/USAGE.md +104 -22
  146. package/docs/api/capture-c2pa-embed.md +177 -0
  147. package/docs/api/capture-dng-import.md +86 -10
  148. package/docs/api/capture-dng-opcodes.md +93 -0
  149. package/docs/api/capture-dng-profile-maps.md +141 -0
  150. package/docs/api/capture-dng-stage-image.md +43 -0
  151. package/docs/api/capture-jpeg-xl-tiles.md +33 -0
  152. package/docs/api/capture-lossy-jpeg.md +41 -0
  153. package/docs/api/capture-native-raw-development.md +2 -1
  154. package/docs/api/core-execution-error.md +43 -0
  155. package/docs/api/core-version.md +1 -1
  156. package/docs/api/exports.json +255 -5
  157. package/docs/api/facade-display-view-gpu.md +5 -2
  158. package/docs/api/facade-display-view.md +11 -15
  159. package/docs/api/facade-fast-methods.md +38 -9
  160. package/docs/api/facade-quality-paths.md +24 -0
  161. package/docs/api/facade-scene-radiance.md +1 -1
  162. package/docs/api/facade-simulate-capture.md +24 -7
  163. package/docs/api/facade-simulate-display-view.md +10 -12
  164. package/docs/api/gpu-image-kernel-execution.md +0 -36
  165. package/docs/api/output-camera-channel-response.md +55 -0
  166. package/docs/api/output-sdr-rendering.md +6 -1
  167. package/docs/api/sensor-sensor-characteristics.md +54 -2
  168. package/llms.txt +1 -0
  169. package/package.json +6 -5
@@ -1,10 +1,19 @@
1
- export declare const DNG_IMPORT_SCHEMA_VERSION: "1.0.0";
2
- /** Upper bound for `maximumPixels`. 200 MP (megapixels) of 16-bit codes is 400 MB. */
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
- * NewSubFileType 0 and PhotometricInterpretation 32803 or 34892, in IFD-chain then SubIFD order).
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
- * order, other depths packed MSB-first with byte-aligned rows, per DNG BitsPerSample) and Compression 7
88
- * lossless Huffman JPEG (T.81 Annex H) in strips or tiles, chunky planar configuration only.
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
- export const DNG_IMPORT_SCHEMA_VERSION = "1.0.0";
24
- /** Upper bound for `maximumPixels`. 200 MP (megapixels) of 16-bit codes is 400 MB. */
25
- export const DNG_IMPORT_LIMITS = /* @__PURE__ */ Object.freeze({ defaultMaximumPixels: 64_000_000, maximumPixels: 200_000_000, defaultMaximumIfds: 64, maximumIfds: 1024 });
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
- * Parse one DNG or TIFF-EP file and decode its primary CFA or LinearRaw image (the first IFD with
37
- * NewSubFileType 0 and PhotometricInterpretation 32803 or 34892, in IFD-chain then SubIFD order).
38
- *
39
- * Supported raw data: Compression 1 at 8..16 bits/sample (8 bits as bytes, 16 bits in the file byte
40
- * order, other depths packed MSB-first with byte-aligned rows, per DNG BitsPerSample) and Compression 7
41
- * lossless Huffman JPEG (T.81 Annex H) in strips or tiles, chunky planar configuration only.
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 (!["bytes", "maximumPixels", "maximumIfds"].includes(key))
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
- // Strip or tile layout.
208
- const tiled = raw.has(322) || raw.has(324);
209
- const tileWidth = tiled ? one(raw, 322, 0) : width, tileLength = tiled ? one(raw, 323, 0) : Math.min(height, one(raw, 278, 0xffffffff));
210
- if (tileWidth < 1 || tileLength < 1 || tileWidth > 65535 * 4 || tileLength > 65535 * 4 || (tiled && tileWidth * tileLength > maximumPixels))
211
- fail("invalid strip or tile size.");
212
- const across = Math.ceil(width / tileWidth), down = Math.ceil(height / tileLength), chunks = across * down;
213
- const offsets = integers(raw.get(tiled ? 324 : 273), chunks, [3, 4]), counts = integers(raw.get(tiled ? 325 : 279), chunks, [3, 4]);
214
- if (!offsets || !counts || offsets.length !== chunks || counts.length !== chunks)
215
- fail("strip or tile tables do not match the image layout.");
216
- for (let i = 0; i < chunks; i++)
217
- if (offsets[i] + counts[i] > n)
218
- fail("strip or tile data exceeds the file.");
219
- const codes = new Uint16Array(width * height * spp), maxCode = 2 ** bitsPerSample - 1;
220
- for (let ty = 0; ty < down; ty++)
221
- for (let tx = 0; tx < across; tx++) {
222
- const index = ty * across + tx, start = offsets[index], data = bytes.subarray(start, start + counts[index]);
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
- const chunkWidth = tiled ? tileWidth : width, chunkRows = tiled ? tileLength : Math.min(tileLength, height - ty * tileLength);
225
- const chunkSamples = chunkWidth * chunkRows * spp, x0 = tx * tileWidth, y0 = ty * tileLength;
226
- let samples;
227
- if (compressionCode === 7) {
228
- const image = parseLosslessJpeg(data, { maximumSamples: chunkSamples, legacySixteenBitDifference: dngVersion !== null && (dngVersion[0] < 1 || (dngVersion[0] === 1 && dngVersion[1] < 1)) });
229
- if (image.width * image.height * image.components !== chunkSamples)
230
- fail("lossless JPEG sample count does not match the strip or tile.");
231
- samples = image.samples;
232
- }
233
- else
234
- samples = unpack(data, chunkWidth * spp, chunkRows, bitsPerSample, little);
235
- for (let y = 0; y < chunkRows && y0 + y < height; y++) {
236
- const columns = Math.min(chunkWidth, width - x0) * spp, from = y * chunkWidth * spp, to = ((y0 + y) * width + x0) * spp;
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
- codes[to + i] = v;
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
- return {
300
- schemaVersion: DNG_IMPORT_SCHEMA_VERSION, byteOrder: little ? "little-endian" : "big-endian", dngVersion,
301
- make: ascii(pick(271)), model: ascii(pick(272)), uniqueCameraModel: ascii(pick(50708)), orientation,
302
- width, height, samplesPerPixel: spp, bitsPerSample, compression: compressionCode === 1 ? "uncompressed" : "lossless-jpeg",
303
- photometricInterpretation: cfaImage ? "cfa" : "linear-raw", codes, cfa,
304
- linearizationTable: linear ? Uint16Array.from(linear) : null,
305
- blackLevel: { repeatRows: repeat[0], repeatColumns: repeat[1], values: black, deltaH, deltaV },
306
- whiteLevel, activeArea, defaultCrop: { x: origin[0], y: origin[1], width: size[0], height: size[1] },
307
- colorMatrix1: matrix(50721), colorMatrix2: matrix(50722),
308
- calibrationIlluminant1: illuminant(50778), calibrationIlluminant2: illuminant(50779), asShotNeutral: neutral,
309
- rawDataUniqueId: uniqueId ? uniqueId.map((b) => b.toString(16).padStart(2, "0")).join("") : null
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) {