@photivra/engine 2.3.0 → 2.5.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 +38 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/dist/capture/native-raw-dng.d.ts +3 -0
- package/dist/capture/native-raw-dng.js +23 -3
- package/dist/capture/native-raw-dng.js.map +1 -1
- 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/simulate-capture.d.ts +12 -1
- package/dist/facade/simulate-capture.js +45 -5
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/subject-framing.d.ts +62 -0
- package/dist/facade/subject-framing.js +62 -19
- package/dist/facade/subject-framing.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/optics/diffraction-psf-profile.d.ts +51 -2
- package/dist/optics/diffraction-psf-profile.js +116 -3
- package/dist/optics/diffraction-psf-profile.js.map +1 -1
- package/dist/output/text-metadata.d.ts +32 -0
- package/dist/output/text-metadata.js +201 -0
- package/dist/output/text-metadata.js.map +1 -0
- package/docs/API_REFERENCE.md +12 -1
- package/docs/QUICK_START.md +151 -6
- package/docs/api/capture-native-raw-dng.md +2 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +22 -2
- package/docs/api/facade-simulate-capture.md +11 -1
- package/docs/api/facade-subject-framing.md +67 -0
- package/docs/api/optics-diffraction-psf-profile.md +8 -1
- package/docs/api/output-text-metadata.md +20 -0
- package/package.json +1 -1
|
@@ -26,6 +26,20 @@
|
|
|
26
26
|
* its many-blade (circular) limit. The sum of all cells is exactly 1 (Parseval), so the energy captured by
|
|
27
27
|
* the finite kernel is Σ K over its window and the truncated energy is 1 − Σ K.
|
|
28
28
|
*
|
|
29
|
+
* **Cells much larger than the pattern (2.5).** The frequency-domain node counts grow with β = p·f_c (the cell
|
|
30
|
+
* side over the cut-off period), so the work grows as β². From β ≥ `AIRY_REAL_SPACE_MINIMUM_BETA` (192) the cells
|
|
31
|
+
* are integrated in real space instead, with the same model and no frequency integral: the Airy encircled energy is
|
|
32
|
+
* EE(r) = 1 − J₀(x)² − J₁(x)², x = π·r·f_c (it follows from J₀′ = −J₁ and J₁′ = J₀ − J₁/x, DLMF §10.6(i), free), so
|
|
33
|
+
* the energy of the rectangle [0, X] × [0, Y] is (1/2π)∫ EE(r_edge(θ)) dθ over its quadrant, a one-dimensional
|
|
34
|
+
* integral in θ. Every cell is a signed sum of such rectangles. x ≥ π·β/2 ≥ 301 there, where J₀ and J₁ come from
|
|
35
|
+
* the Hankel asymptotic expansion (DLMF §10.17(i), eq. 10.17.3, free) with eight terms in each of P and Q; the
|
|
36
|
+
* remainder is below the first neglected term (DLMF §10.17(iii)), under 1e-17. The θ integral uses 16-point
|
|
37
|
+
* Gauss–Legendre panels on which the phase 2x changes by at most 2 radians, so the work grows only as β. The tests
|
|
38
|
+
* check the two methods against each other where both are practical (β from 192 to 400), the real-space cells
|
|
39
|
+
* against the asymptotic leakage 8√2/(π³·β) of the centre cell, and convergence under doubled panel counts.
|
|
40
|
+
* The β threshold lies above every β a `simulateCapture` pitch up to 100 µm can reach (at most 100/(0.3805·1.4) ≈ 188),
|
|
41
|
+
* so those captures keep the frequency-domain values unchanged.
|
|
42
|
+
*
|
|
29
43
|
* **Truncation and normalization.** The kernel is the (2R+1)² window of cells. The Airy tails fall only as
|
|
30
44
|
* r⁻³, so a finite window always misses some energy: it is reported per wavelength, and the window is then
|
|
31
45
|
* renormalized to unit sum because the sampled-profile contract requires unit-energy kernels. The missing
|
|
@@ -47,6 +61,11 @@ export const DIFFRACTION_LIMITED_PSF_PROFILE_MODEL = Object.freeze({
|
|
|
47
61
|
});
|
|
48
62
|
/** Input limits: at most 64 wavelengths, kernel radius 0–32 samples (the irradiance-convolution limit). */
|
|
49
63
|
export const DIFFRACTION_LIMITED_PSF_PROFILE_LIMITS = Object.freeze({ maximumWavelengths: 64, maximumKernelRadiusSamples: 32 });
|
|
64
|
+
/**
|
|
65
|
+
* Internal: β = p·f_c (cell side over the cut-off period) from which `calculateAiryCellEnergies` integrates the cells in
|
|
66
|
+
* real space (2.5). Above every β of a `simulateCapture` pitch up to 100 µm, whose captures keep the frequency-domain values.
|
|
67
|
+
*/
|
|
68
|
+
export const AIRY_REAL_SPACE_MINIMUM_BETA = 192;
|
|
50
69
|
/** Radius of the first dark ring of the Airy pattern in units of λ·N_w: j₁,₁/π, j₁,₁ = 3.8317059702 (first zero of J₁). */
|
|
51
70
|
const AIRY_FIRST_ZERO_PER_LAMBDA_N = 3.8317059702075125 / Math.PI;
|
|
52
71
|
const EVIDENCE = Object.freeze([Object.freeze({ sourceOrigin: "photivra",
|
|
@@ -111,9 +130,99 @@ function sinc(x) {
|
|
|
111
130
|
}
|
|
112
131
|
/**
|
|
113
132
|
* Cell energies K(i, j), 0 ≤ i, j ≤ R, of the Airy pattern for β = p·f_c (cell side in units of the cut-off
|
|
114
|
-
* period)
|
|
133
|
+
* period): in the frequency domain below `AIRY_REAL_SPACE_MINIMUM_BETA`, in real space from it (2.5). `scale`
|
|
134
|
+
* multiplies the node or panel counts (1 in production; the tests use 2 to check convergence).
|
|
115
135
|
*/
|
|
116
136
|
export function calculateAiryCellEnergies(beta, radius, scale = 1) {
|
|
137
|
+
return beta >= AIRY_REAL_SPACE_MINIMUM_BETA ? { ...calculateAiryCellEnergiesInRealSpace(beta, radius, scale), radialNodes: 0, realSpace: true } :
|
|
138
|
+
{ ...calculateAiryCellEnergiesInFrequencyDomain(beta, radius, scale), realSpace: false };
|
|
139
|
+
}
|
|
140
|
+
/** Hankel asymptotic coefficients a_k(ν) of DLMF 10.17.1 for ν = 0 and 1, k = 0..15. */
|
|
141
|
+
const HANKEL = (() => {
|
|
142
|
+
const a0 = new Float64Array(16), a1 = new Float64Array(16);
|
|
143
|
+
a0[0] = 1;
|
|
144
|
+
a1[0] = 1;
|
|
145
|
+
for (let k = 1; k < 16; k++) {
|
|
146
|
+
const odd = (2 * k - 1) * (2 * k - 1);
|
|
147
|
+
a0[k] = a0[k - 1] * (0 - odd) / (8 * k);
|
|
148
|
+
a1[k] = a1[k - 1] * (4 - odd) / (8 * k);
|
|
149
|
+
}
|
|
150
|
+
return { a0, a1 };
|
|
151
|
+
})();
|
|
152
|
+
/** Smallest argument of `besselSquareSum`: there the 16-term expansion's remainder is below 1e-17. */
|
|
153
|
+
const HANKEL_MINIMUM_ARGUMENT = 300;
|
|
154
|
+
/**
|
|
155
|
+
* J₀(x)² + J₁(x)² for x ≥ 300 from the Hankel expansion J_ν(x) = √(2/(πx))·(P_ν cos ω_ν − Q_ν sin ω_ν), ω_ν = x − νπ/2 − π/4,
|
|
156
|
+
* P_ν = Σ(−1)^k a_2k(ν)/x^2k, Q_ν = Σ(−1)^k a_2k+1(ν)/x^2k+1 (DLMF 10.17.3), eight terms each. With ω₁ = ω₀ − π/2,
|
|
157
|
+
* J₁ = √(2/(πx))·(P₁ sin ω₀ + Q₁ cos ω₀). Internal.
|
|
158
|
+
*/
|
|
159
|
+
export function besselSquareSum(x) {
|
|
160
|
+
if (!(x >= HANKEL_MINIMUM_ARGUMENT))
|
|
161
|
+
throw new InvalidConfigurationError("The Hankel expansion of J0 and J1 needs an argument of at least 300.");
|
|
162
|
+
const { a0, a1 } = HANKEL, inverse = 1 / x;
|
|
163
|
+
let p0 = 0, q0 = 0, p1 = 0, q1 = 0, power = 1;
|
|
164
|
+
for (let k = 0; k < 16; k++) {
|
|
165
|
+
const sign = (k >> 1) % 2 === 0 ? 1 : -1;
|
|
166
|
+
if (k % 2 === 0) {
|
|
167
|
+
p0 += sign * a0[k] * power;
|
|
168
|
+
p1 += sign * a1[k] * power;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
q0 += sign * a0[k] * power;
|
|
172
|
+
q1 += sign * a1[k] * power;
|
|
173
|
+
}
|
|
174
|
+
power *= inverse;
|
|
175
|
+
}
|
|
176
|
+
const omega = x - Math.PI / 4, c = Math.cos(omega), s = Math.sin(omega);
|
|
177
|
+
const j0 = p0 * c - q0 * s, j1 = p1 * s + q1 * c;
|
|
178
|
+
return 2 / (Math.PI * x) * (j0 * j0 + j1 * j1);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Real-space cell energies (2.5): see the module comment. G(Z, φ) = ∫₀^φ [J₀² + J₁²](π·β·Z/cos θ) dθ for cell boundaries at
|
|
182
|
+
* half-integer distances Z (in cells), accumulated along θ for each Z with every needed φ = atan(W/Z) as a panel boundary.
|
|
183
|
+
* The rectangle [0, X] × [0, Y] holds 1/4 − (G(X, atan(Y/X)) + G(Y, atan(X/Y)))/(2π) of the energy. Internal.
|
|
184
|
+
*/
|
|
185
|
+
export function calculateAiryCellEnergiesInRealSpace(beta, radius, scale = 1) {
|
|
186
|
+
const size = radius + 1, table = new Float64Array(size * size), { nodes, weights } = gaussLegendre(16);
|
|
187
|
+
let angularNodes = 0;
|
|
188
|
+
for (let z = 0; z < size; z++) {
|
|
189
|
+
const Z = z + 0.5, k = Math.PI * beta * Z;
|
|
190
|
+
let sum = 0, t0 = 1, theta0 = 0;
|
|
191
|
+
for (let w = 0; w < size; w++) {
|
|
192
|
+
// Panels equal in t = 1/cos θ, so the phase 2x = 2kt changes by at most 2 radians on each.
|
|
193
|
+
const t1 = Math.hypot(Z, w + 0.5) / Z, theta1 = Math.atan2(w + 0.5, Z), panels = Math.max(1, Math.ceil(scale * k * (t1 - t0)));
|
|
194
|
+
let a = theta0;
|
|
195
|
+
for (let n = 1; n <= panels; n++) {
|
|
196
|
+
const b = n === panels ? theta1 : Math.acos(1 / (t0 + (t1 - t0) * n / panels)), h = b - a;
|
|
197
|
+
let panel = 0;
|
|
198
|
+
for (let g = 0; g < 16; g++)
|
|
199
|
+
panel += weights[g] * besselSquareSum(k / Math.cos(a + h * nodes[g]));
|
|
200
|
+
sum += panel * h;
|
|
201
|
+
a = b;
|
|
202
|
+
}
|
|
203
|
+
angularNodes += 16 * panels;
|
|
204
|
+
table[z * size + w] = sum;
|
|
205
|
+
t0 = t1;
|
|
206
|
+
theta0 = theta1;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Boundaries of cell i along one axis as (half-integer index, weight): the centre cell is twice [0, 1/2].
|
|
210
|
+
const bounds = (i) => i === 0 ? [[0, 2]] : [[i, 1], [i - 1, -1]];
|
|
211
|
+
const cells = new Float64Array(size * size);
|
|
212
|
+
for (let j = 0; j < size; j++)
|
|
213
|
+
for (let i = 0; i < size; i++) {
|
|
214
|
+
let constant = 0, leak = 0;
|
|
215
|
+
for (const [a, wa] of bounds(i))
|
|
216
|
+
for (const [b, wb] of bounds(j)) {
|
|
217
|
+
constant += wa * wb;
|
|
218
|
+
leak += wa * wb * (table[a * size + b] + table[b * size + a]);
|
|
219
|
+
}
|
|
220
|
+
cells[j * size + i] = constant / 4 - leak / (2 * Math.PI);
|
|
221
|
+
}
|
|
222
|
+
return { cells, angularNodes };
|
|
223
|
+
}
|
|
224
|
+
/** Frequency-domain cell energies (2.1; see the module comment). Internal. */
|
|
225
|
+
export function calculateAiryCellEnergiesInFrequencyDomain(beta, radius, scale = 1) {
|
|
117
226
|
// Bandwidth of the angular and radial integrands: c = π·β·(2R + 1)·√2 (the sinc factors and the highest cosine).
|
|
118
227
|
const c = Math.PI * beta * (2 * radius + 1) * Math.SQRT2, margin = 10 * Math.cbrt(c) + 24;
|
|
119
228
|
const quarter = Math.ceil(scale * (c + margin) / 4), radialNodes = Math.ceil(scale * (c + margin));
|
|
@@ -203,10 +312,12 @@ export function createDiffractionLimitedPsfProfile(input) {
|
|
|
203
312
|
throw new InvalidConfigurationError("profileId must be a public opaque ID.");
|
|
204
313
|
const pupilDiameterMm = focalLengthMm / fNumber, workingFNumber = distanceMm / pupilDiameterMm, size = 2 * radius + 1;
|
|
205
314
|
let maximumRadialNodes = 0, maximumAngularNodes = 0, maximumTruncated = 0;
|
|
206
|
-
const records = [], kernels = [];
|
|
315
|
+
const records = [], kernels = [], realSpaceWavelengthsNm = [];
|
|
207
316
|
for (const nm of wavelengths) {
|
|
208
317
|
const lambdaN = nm / 1000 * workingFNumber, cutoff = 1 / lambdaN;
|
|
209
318
|
const airy = calculateAiryCellEnergies(pitch * cutoff, radius);
|
|
319
|
+
if (airy.realSpace)
|
|
320
|
+
realSpaceWavelengthsNm.push(nm);
|
|
210
321
|
maximumRadialNodes = Math.max(maximumRadialNodes, airy.radialNodes);
|
|
211
322
|
maximumAngularNodes = Math.max(maximumAngularNodes, airy.angularNodes);
|
|
212
323
|
const values = new Array(size * size);
|
|
@@ -247,6 +358,8 @@ export function createDiffractionLimitedPsfProfile(input) {
|
|
|
247
358
|
nodes, evidence: EVIDENCE, limitations: [...LIMITATIONS] });
|
|
248
359
|
return approximationResult({ profile, model: DIFFRACTION_LIMITED_PSF_PROFILE_MODEL, pupilDiameterMm, pupilToImageDistanceMm: distanceMm, workingFNumber,
|
|
249
360
|
kernelRadiusSamples: radius, samplePitchMicrometers: pitch, wavelengths: records, maximumTruncatedEnergyFraction: maximumTruncated,
|
|
250
|
-
quadrature: { method: "polar-frequency-domain-trapezoid-gauss-legendre", maximumRadialNodes, maximumAngularNodes }
|
|
361
|
+
quadrature: realSpaceWavelengthsNm.length === 0 ? { method: "polar-frequency-domain-trapezoid-gauss-legendre", maximumRadialNodes, maximumAngularNodes } :
|
|
362
|
+
{ method: realSpaceWavelengthsNm.length === wavelengths.length ? "real-space-encircled-energy-gauss-legendre" : "frequency-domain-and-real-space",
|
|
363
|
+
maximumRadialNodes, maximumAngularNodes, realSpaceWavelengthsNm } }, DIFFRACTION_LIMITED_PSF_PROFILE_MODEL.modelId, DIFFRACTION_LIMITED_PSF_PROFILE_MODEL.modelVersion, [...LIMITATIONS]);
|
|
251
364
|
}
|
|
252
365
|
//# sourceMappingURL=diffraction-psf-profile.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diffraction-psf-profile.js","sourceRoot":"","sources":["../../src/optics/diffraction-psf-profile.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Builds a `LensSampledPsfProfile` for an ideal, aberration-free, in-focus lens with a uniformly lit\n * circular pupil: the Airy pattern of scalar Fraunhofer diffraction, one unit-sum kernel per declared\n * wavelength on ONE shared sample grid (2.1.0).\n *\n * **Model.** The pupil diameter is D = f/N (entrance pupil = exit pupil, unity pupil magnification) and\n * the pupil-to-image distance z is the thin-lens image distance for the focus (f at infinity), so the\n * image-side (working) f-number is N_w = z/D. The incoherent optical transfer function (OTF) of a\n * uniform circular pupil is the normalized autocorrelation of the pupil,\n * H(ρ) = (2/π)·(acos ρ − ρ·√(1 − ρ²)), ρ = |f|/f_c ≤ 1, f_c = 1/(λ·N_w),\n * and the intensity PSF is its inverse Fourier transform (the Airy pattern, first zero at 1.21967·λ·N_w)\n * (MIT OpenCourseWare 2.71 Optics, Spring 2009, lectures 21–22, free: the intensity PSF as the transform of\n * the pupil autocorrelation and the circular-pupil MTF; also see: Goodman, Introduction to Fourier Optics,\n * 4th ed., 2017, §6.3).\n *\n * **Cell integration (exact, no Bessel functions).** Kernel sample (i, j) is the PSF energy inside the\n * square cell of side p centred on (i·p, j·p). The cell integral is the PSF convolved with the cell's\n * indicator, evaluated at the cell centre, so in the frequency domain\n * K(i, j) = p² ∫∫ H(|f|/f_c)·sinc(p·f_x)·sinc(p·f_y)·cos(2π·p·i·f_x)·cos(2π·p·j·f_y) d²f,\n * with sinc(x) = sin(πx)/(πx) (the sine terms vanish because H is even). It is evaluated in polar\n * coordinates: a trapezoid rule in angle (exponentially convergent for this smooth periodic integrand) and\n * Gauss–Legendre in t with ρ = 1 − t², which removes the (1 − ρ)^{3/2} edge behaviour of H. The node counts\n * follow from the integrand's bandwidth with a fixed margin; the tests check convergence by doubling them and\n * check the values against an independent real-space integration of `calculateIdealPolygonDiffractionPsf` in\n * its many-blade (circular) limit. The sum of all cells is exactly 1 (Parseval), so the energy captured by\n * the finite kernel is Σ K over its window and the truncated energy is 1 − Σ K.\n *\n * **Truncation and normalization.** The kernel is the (2R+1)² window of cells. The Airy tails fall only as\n * r⁻³, so a finite window always misses some energy: it is reported per wavelength, and the window is then\n * renormalized to unit sum because the sampled-profile contract requires unit-energy kernels. The missing\n * tail energy is thereby put back inside the window (an approximation that slightly raises low-frequency\n * contrast compared with the untruncated pattern).\n *\n * **Approximations (labelled):** scalar (no polarization), aberration-free, uniform pupil amplitude, exactly in\n * focus (no defocus), thin lens with unity pupil magnification, the on-axis PSF used across the declared field\n * (isoplanatic), PSF energy binned into the sample cells, finite window renormalized. Not a measured lens.\n * @see docs/PSF_FOUNDATION.md, \"Diffraction-limited sampled profile (2.1)\".\n */\n\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { requireAllowlistedRecord } from \"../core/record-validation.js\";\nimport type { EvidenceProvenance } from \"../core/evidence-provenance.js\";\nimport type { FocusPlane } from \"./focus-state.js\";\nimport { calculateThinLensImageDistance } from \"./thin-lens.js\";\nimport { parseLensSampledPsfProfile, type LensSampledPsfProfile } from \"./lens-psf-profile.js\";\n\nexport const DIFFRACTION_LIMITED_PSF_PROFILE_MODEL = Object.freeze({\n modelId: \"photivra-diffraction-limited-circular-pupil-psf\" as const, modelVersion: \"1.0.0\" as const });\n/** Input limits: at most 64 wavelengths, kernel radius 0–32 samples (the irradiance-convolution limit). */\nexport const DIFFRACTION_LIMITED_PSF_PROFILE_LIMITS = Object.freeze({ maximumWavelengths: 64, maximumKernelRadiusSamples: 32 });\n/** Radius of the first dark ring of the Airy pattern in units of λ·N_w: j₁,₁/π, j₁,₁ = 3.8317059702 (first zero of J₁). */\nconst AIRY_FIRST_ZERO_PER_LAMBDA_N = 3.8317059702075125 / Math.PI;\n\nexport interface CreateDiffractionLimitedPsfProfileInput {\n /** Only `circular` in 2.1.0 (polygon pupils: `calculateIdealPolygonDiffractionPsf` gives point densities). */\n aperture: \"circular\";\n focalLengthMm: number;\n apertureFNumber: number;\n focus: FocusPlane;\n /** Strictly increasing wavelengths, nm, in the medium of propagation (no air/vacuum conversion is applied). 1–64. */\n wavelengthsNm: readonly number[];\n /** Square sample pitch of every kernel, µm. */\n samplePitchMicrometers: number;\n /** Kernel half-width in samples; each kernel is (2R + 1)². 0–32. */\n kernelRadiusSamples: number;\n /** Half-extent of the image field over which the on-axis PSF is declared (profile field axes ±x, ±y), mm. */\n fieldHalfExtentMm: { x: number; y: number };\n profileId: string;\n}\n\nexport interface DiffractionLimitedPsfWavelength {\n wavelengthNm: number;\n /** f_c = 1/(λ·N_w), cycles per µm. */\n cutoffFrequencyCyclesPerMicrometer: number;\n /** 1.21967·λ·N_w, µm. */\n firstZeroRadiusMicrometers: number;\n /** Σ of the cell energies inside the window before renormalization (of a total of 1). */\n capturedEnergyFraction: number;\n /** 1 − capturedEnergyFraction: the Airy tail energy outside the window, put back by renormalization. */\n truncatedEnergyFraction: number;\n /** Fraction of the renormalized kernel in the centre sample. */\n centreSampleFraction: number;\n}\n\nexport interface DiffractionLimitedPsfProfile {\n profile: LensSampledPsfProfile;\n model: typeof DIFFRACTION_LIMITED_PSF_PROFILE_MODEL;\n pupilDiameterMm: number;\n pupilToImageDistanceMm: number;\n workingFNumber: number;\n kernelRadiusSamples: number;\n samplePitchMicrometers: number;\n wavelengths: readonly DiffractionLimitedPsfWavelength[];\n /** Largest truncated energy over the wavelengths. */\n maximumTruncatedEnergyFraction: number;\n quadrature: { method: \"polar-frequency-domain-trapezoid-gauss-legendre\"; maximumRadialNodes: number; maximumAngularNodes: number };\n}\n\nconst EVIDENCE: readonly EvidenceProvenance[] = Object.freeze([Object.freeze({ sourceOrigin: \"photivra\" as const,\n sourceReference: \"photivra:diffraction-limited-psf-profile\", reuseStatus: \"photivra-owned\" as const })]);\nconst LIMITATIONS = Object.freeze([\n \"Scalar Fraunhofer diffraction of an ideal, uniformly lit circular pupil (Airy pattern); no aberration, apodization, polarization or stray light.\",\n \"Exactly in focus: no defocus or depth of field. Thin lens with unity pupil magnification; the pupil-to-image distance is the thin-lens image distance.\",\n \"The on-axis PSF is declared unchanged across the field (isoplanatic); no field-dependent aberration or pupil clipping.\",\n \"PSF energy is binned into square sample cells; the finite window misses the Airy tail energy listed per wavelength and is renormalized to unit sum.\",\n \"Wavelengths are used as given, in the declared basis, without air/vacuum conversion.\"\n]);\n\nfunction positive(value: unknown, name: string): number {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0) throw new InvalidConfigurationError(name + \" must be a positive finite number.\");\n return value;\n}\n\n/** Gauss–Legendre nodes and weights on [0, 1] (Newton iteration on Pₙ; DLMF §3.5(v)). Cached per n. */\nconst GAUSS_LEGENDRE = new Map<number, { nodes: Float64Array; weights: Float64Array }>();\nfunction gaussLegendre(n: number): { nodes: Float64Array; weights: Float64Array } {\n const cached = GAUSS_LEGENDRE.get(n);\n if (cached) return cached;\n const nodes = new Float64Array(n), weights = new Float64Array(n);\n for (let k = 0; k < Math.ceil(n / 2); k++) {\n let x = Math.cos(Math.PI * (k + 0.75) / (n + 0.5));\n for (let iteration = 0; iteration < 100; iteration++) {\n let p0 = 1, p1 = x;\n for (let m = 2; m <= n; m++) { const p2 = ((2 * m - 1) * x * p1 - (m - 1) * p0) / m; p0 = p1; p1 = p2; }\n const step = p1 / (n * (x * p1 - p0) / (x * x - 1));\n x -= step;\n if (Math.abs(step) < 1e-16) break;\n }\n let p0 = 1, p1 = x;\n for (let m = 2; m <= n; m++) { const p2 = ((2 * m - 1) * x * p1 - (m - 1) * p0) / m; p0 = p1; p1 = p2; }\n const derivative = n * (x * p1 - p0) / (x * x - 1);\n const w = 1 / ((1 - x * x) * derivative * derivative);\n // Map [-1, 1] to [0, 1]: t = (1 ± x)/2, weight w (2/(1-x²)P'² halved).\n nodes[k] = (1 - x) / 2; weights[k] = w;\n nodes[n - 1 - k] = (1 + x) / 2; weights[n - 1 - k] = w;\n }\n const result = { nodes, weights };\n if (GAUSS_LEGENDRE.size < 64) GAUSS_LEGENDRE.set(n, result);\n return result;\n}\nfunction sinc(x: number): number {\n if (x === 0) return 1;\n const a = Math.PI * x;\n return Math.sin(a) / a;\n}\n\n/**\n * Cell energies K(i, j), 0 ≤ i, j ≤ R, of the Airy pattern for β = p·f_c (cell side in units of the cut-off\n * period). `scale` multiplies the node counts (1 in production; the tests use 2 to check convergence).\n */\nexport function calculateAiryCellEnergies(beta: number, radius: number, scale = 1): { cells: Float64Array; radialNodes: number; angularNodes: number } {\n // Bandwidth of the angular and radial integrands: c = π·β·(2R + 1)·√2 (the sinc factors and the highest cosine).\n const c = Math.PI * beta * (2 * radius + 1) * Math.SQRT2, margin = 10 * Math.cbrt(c) + 24;\n const quarter = Math.ceil(scale * (c + margin) / 4), radialNodes = Math.ceil(scale * (c + margin));\n const { nodes, weights } = gaussLegendre(radialNodes), size = radius + 1, cells = new Float64Array(size * size);\n const ca = new Float64Array(size), cb = new Float64Array(size), angularStep = (Math.PI / 2) / quarter;\n for (let r = 0; r < radialNodes; r++) {\n const t = nodes[r]!, rho = 1 - t * t;\n // H(1 − t²) without cancellation: acos(1 − t²) = 2·asin(t/√2), ρ·√(1 − ρ²) = ρ·t·√(2 − t²).\n const otf = (2 / Math.PI) * (2 * Math.asin(t / Math.SQRT2) - rho * t * Math.sqrt(2 - t * t));\n // ∫₀¹ g(ρ) dρ = ∫₀¹ g(1 − t²)·2t dt; the 4 is the four symmetric quadrants of the angular trapezoid.\n const radialWeight = weights[r]! * 2 * t * rho * otf * 4 * angularStep;\n for (let k = 0; k <= quarter; k++) {\n const theta = k * angularStep, x = beta * rho * Math.cos(theta), y = beta * rho * Math.sin(theta);\n const w = radialWeight * (k === 0 || k === quarter ? 0.5 : 1) * sinc(x) * sinc(y);\n // cos(2π·i·x) and cos(2π·j·y) by the Chebyshev recurrence.\n const cx = Math.cos(2 * Math.PI * x), cy = Math.cos(2 * Math.PI * y);\n ca[0] = 1; cb[0] = 1;\n if (size > 1) { ca[1] = cx; cb[1] = cy; }\n for (let i = 2; i < size; i++) { ca[i] = 2 * cx * ca[i - 1]! - ca[i - 2]!; cb[i] = 2 * cy * cb[i - 1]! - cb[i - 2]!; }\n for (let j = 0; j < size; j++) {\n const wj = w * cb[j]!;\n for (let i = 0; i < size; i++) { const o = j * size + i; cells[o] = cells[o]! + wj * ca[i]!; }\n }\n }\n }\n const area = beta * beta;\n for (let i = 0; i < cells.length; i++) cells[i] = cells[i]! * area;\n return { cells, radialNodes, angularNodes: 4 * quarter };\n}\n\n/**\n * Creates a sampled lens PSF profile for an ideal diffraction-limited lens with a circular pupil: one\n * unit-sum Airy kernel per wavelength on one shared (2R+1)² grid, with the truncated energy reported per\n * wavelength. The profile declares the given focal length, f-number and focus exactly, and the field axes\n * ±`fieldHalfExtentMm` with the same kernel at every field node.\n * @param input - See `CreateDiffractionLimitedPsfProfileInput`.\n * @returns The profile and its diffraction record. An approximation; not a measured lens.\n * @see docs/PSF_FOUNDATION.md, \"Diffraction-limited sampled profile (2.1)\".\n */\nexport function createDiffractionLimitedPsfProfile(input: CreateDiffractionLimitedPsfProfileInput): CalculationResult<DiffractionLimitedPsfProfile> {\n const r = requireAllowlistedRecord(input, [\"aperture\", \"focalLengthMm\", \"apertureFNumber\", \"focus\", \"wavelengthsNm\", \"samplePitchMicrometers\",\n \"kernelRadiusSamples\", \"fieldHalfExtentMm\", \"profileId\"], \"Invalid diffraction-limited PSF profile input.\");\n if (r.aperture !== \"circular\") throw new InvalidConfigurationError(\"aperture must be \\\"circular\\\".\");\n const focalLengthMm = positive(r.focalLengthMm, \"focalLengthMm\"), fNumber = positive(r.apertureFNumber, \"apertureFNumber\");\n const pitch = positive(r.samplePitchMicrometers, \"samplePitchMicrometers\");\n const L = DIFFRACTION_LIMITED_PSF_PROFILE_LIMITS;\n const radius = r.kernelRadiusSamples;\n if (typeof radius !== \"number\" || !Number.isSafeInteger(radius) || radius < 0 || radius > L.maximumKernelRadiusSamples) {\n throw new InvalidConfigurationError(\"kernelRadiusSamples must be an integer from 0 to \" + L.maximumKernelRadiusSamples + \".\");\n }\n const focus = requireAllowlistedRecord(r.focus, [\"kind\", \"distanceM\"], \"Invalid focus.\");\n let focusDiopters: number, distanceMm: number;\n if (focus.kind === \"infinity\") {\n if (focus.distanceM !== undefined) throw new InvalidConfigurationError(\"An infinity focus has no distanceM.\");\n focusDiopters = 0; distanceMm = focalLengthMm;\n } else if (focus.kind === \"finite\") {\n const distanceM = positive(focus.distanceM, \"focus.distanceM\");\n focusDiopters = 1 / distanceM;\n distanceMm = calculateThinLensImageDistance({ focalLengthMm, objectDistanceM: distanceM }).value.imageDistanceMm;\n } else throw new InvalidConfigurationError(\"focus.kind must be finite or infinity.\");\n const field = requireAllowlistedRecord(r.fieldHalfExtentMm, [\"x\", \"y\"], \"Invalid fieldHalfExtentMm.\");\n const fieldX = positive(field.x, \"fieldHalfExtentMm.x\"), fieldY = positive(field.y, \"fieldHalfExtentMm.y\");\n const list = r.wavelengthsNm;\n if (!Array.isArray(list) || list.length < 1 || list.length > L.maximumWavelengths) throw new InvalidConfigurationError(\"wavelengthsNm must list 1 to 64 wavelengths.\");\n const wavelengths = (list as unknown[]).map((value, index) => positive(value, \"wavelengthsNm[\" + index + \"]\"));\n for (let i = 1; i < wavelengths.length; i++) if (!(wavelengths[i]! > wavelengths[i - 1]!)) throw new InvalidConfigurationError(\"wavelengthsNm must be strictly increasing.\");\n if (typeof r.profileId !== \"string\" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(r.profileId)) throw new InvalidConfigurationError(\"profileId must be a public opaque ID.\");\n\n const pupilDiameterMm = focalLengthMm / fNumber, workingFNumber = distanceMm / pupilDiameterMm, size = 2 * radius + 1;\n let maximumRadialNodes = 0, maximumAngularNodes = 0, maximumTruncated = 0;\n const records: DiffractionLimitedPsfWavelength[] = [], kernels: number[][] = [];\n for (const nm of wavelengths) {\n const lambdaN = nm / 1000 * workingFNumber, cutoff = 1 / lambdaN;\n const airy = calculateAiryCellEnergies(pitch * cutoff, radius);\n maximumRadialNodes = Math.max(maximumRadialNodes, airy.radialNodes); maximumAngularNodes = Math.max(maximumAngularNodes, airy.angularNodes);\n const values: number[] = new Array(size * size);\n let captured = 0;\n for (let ky = 0; ky < size; ky++) for (let kx = 0; kx < size; kx++) {\n const value = airy.cells[Math.abs(ky - radius) * (radius + 1) + Math.abs(kx - radius)]!;\n // Every cell energy of a non-negative PSF is positive; a negative value would be a quadrature failure.\n if (!(value > -1e-13)) throw new InvalidConfigurationError(\"Diffraction cell integration failed to converge.\");\n values[ky * size + kx] = Math.max(0, value);\n captured += values[ky * size + kx]!;\n }\n if (!(captured > 0 && captured <= 1 + 1e-9)) throw new InvalidConfigurationError(\"Diffraction cell integration failed to converge.\");\n const truncated = Math.max(0, 1 - captured);\n maximumTruncated = Math.max(maximumTruncated, truncated);\n const kernel = values.map((v) => v / captured);\n kernels.push(kernel);\n records.push({ wavelengthNm: nm, cutoffFrequencyCyclesPerMicrometer: cutoff, firstZeroRadiusMicrometers: AIRY_FIRST_ZERO_PER_LAMBDA_N * lambdaN,\n capturedEnergyFraction: captured, truncatedEnergyFraction: truncated, centreSampleFraction: kernel[radius * size + radius]! });\n }\n const uncertainty = { kind: \"not-quantified\" as const, limitation: \"Ideal diffraction-limited model; no measured lens or quadrature error bound.\" };\n const nodes = [-fieldX, fieldX].flatMap((fx) => [-fieldY, fieldY].flatMap((fy) => wavelengths.map((nm, index) => ({\n nodeId: \"airy-\" + (fx < 0 ? \"xm\" : \"xp\") + \"-\" + (fy < 0 ? \"ym\" : \"yp\") + \"-\" + index,\n coordinate: { focalLengthMm, focusDiopters, apertureFNumber: fNumber, fieldXmm: fx, fieldYmm: fy, wavelengthNm: nm, signedDefocusImagePlaneMicrometers: 0 },\n kernel: { widthSamples: size, heightSamples: size, samplePitchMicrometersX: pitch, samplePitchMicrometersY: pitch, centerSampleX: radius, centerSampleY: radius,\n normalizedIntensity: kernels[index]! },\n bestFocusImagePlaneOffsetMicrometers: 0, relativePupilThroughputFactor: 1, evidence: EVIDENCE, uncertainty }))));\n const profile = parseLensSampledPsfProfile({ schemaVersion: \"0.1.0\", profileId: r.profileId, profileVersion: DIFFRACTION_LIMITED_PSF_PROFILE_MODEL.modelVersion,\n scientificStatus: \"approximation\", opticalDomain: \"lens-primary-optical-path-only\", coordinateSystem: \"image-plane-metric\", fieldAxes: \"+X right, +Y up\",\n kernelEnergyNormalization: \"unit-energy-shape\", interpolation: \"bounded-regular-grid-multilinear\", throughputOwnership: \"separate-relative-pupil-throughput-factor\",\n responseIncludes: { diffraction: true, aberration: false, defocus: false, pupilClippingShape: false },\n sensorOpticalStackIncluded: false, sensorSamplingIncluded: false, reconstructionIncluded: false, strayLightIncluded: false,\n axes: { focalLengthMm: [focalLengthMm], focusDiopters: [focusDiopters], apertureFNumber: [fNumber], fieldXmm: [-fieldX, fieldX], fieldYmm: [-fieldY, fieldY],\n wavelengthNm: wavelengths, signedDefocusImagePlaneMicrometers: [0] },\n nodes, evidence: EVIDENCE, limitations: [...LIMITATIONS] });\n return approximationResult({ profile, model: DIFFRACTION_LIMITED_PSF_PROFILE_MODEL, pupilDiameterMm, pupilToImageDistanceMm: distanceMm, workingFNumber,\n kernelRadiusSamples: radius, samplePitchMicrometers: pitch, wavelengths: records, maximumTruncatedEnergyFraction: maximumTruncated,\n quadrature: { method: \"polar-frequency-domain-trapezoid-gauss-legendre\" as const, maximumRadialNodes, maximumAngularNodes } },\n DIFFRACTION_LIMITED_PSF_PROFILE_MODEL.modelId, DIFFRACTION_LIMITED_PSF_PROFILE_MODEL.modelVersion, [...LIMITATIONS]);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"diffraction-psf-profile.js","sourceRoot":"","sources":["../../src/optics/diffraction-psf-profile.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Builds a `LensSampledPsfProfile` for an ideal, aberration-free, in-focus lens with a uniformly lit\n * circular pupil: the Airy pattern of scalar Fraunhofer diffraction, one unit-sum kernel per declared\n * wavelength on ONE shared sample grid (2.1.0).\n *\n * **Model.** The pupil diameter is D = f/N (entrance pupil = exit pupil, unity pupil magnification) and\n * the pupil-to-image distance z is the thin-lens image distance for the focus (f at infinity), so the\n * image-side (working) f-number is N_w = z/D. The incoherent optical transfer function (OTF) of a\n * uniform circular pupil is the normalized autocorrelation of the pupil,\n * H(ρ) = (2/π)·(acos ρ − ρ·√(1 − ρ²)), ρ = |f|/f_c ≤ 1, f_c = 1/(λ·N_w),\n * and the intensity PSF is its inverse Fourier transform (the Airy pattern, first zero at 1.21967·λ·N_w)\n * (MIT OpenCourseWare 2.71 Optics, Spring 2009, lectures 21–22, free: the intensity PSF as the transform of\n * the pupil autocorrelation and the circular-pupil MTF; also see: Goodman, Introduction to Fourier Optics,\n * 4th ed., 2017, §6.3).\n *\n * **Cell integration (exact, no Bessel functions).** Kernel sample (i, j) is the PSF energy inside the\n * square cell of side p centred on (i·p, j·p). The cell integral is the PSF convolved with the cell's\n * indicator, evaluated at the cell centre, so in the frequency domain\n * K(i, j) = p² ∫∫ H(|f|/f_c)·sinc(p·f_x)·sinc(p·f_y)·cos(2π·p·i·f_x)·cos(2π·p·j·f_y) d²f,\n * with sinc(x) = sin(πx)/(πx) (the sine terms vanish because H is even). It is evaluated in polar\n * coordinates: a trapezoid rule in angle (exponentially convergent for this smooth periodic integrand) and\n * Gauss–Legendre in t with ρ = 1 − t², which removes the (1 − ρ)^{3/2} edge behaviour of H. The node counts\n * follow from the integrand's bandwidth with a fixed margin; the tests check convergence by doubling them and\n * check the values against an independent real-space integration of `calculateIdealPolygonDiffractionPsf` in\n * its many-blade (circular) limit. The sum of all cells is exactly 1 (Parseval), so the energy captured by\n * the finite kernel is Σ K over its window and the truncated energy is 1 − Σ K.\n *\n * **Cells much larger than the pattern (2.5).** The frequency-domain node counts grow with β = p·f_c (the cell\n * side over the cut-off period), so the work grows as β². From β ≥ `AIRY_REAL_SPACE_MINIMUM_BETA` (192) the cells\n * are integrated in real space instead, with the same model and no frequency integral: the Airy encircled energy is\n * EE(r) = 1 − J₀(x)² − J₁(x)², x = π·r·f_c (it follows from J₀′ = −J₁ and J₁′ = J₀ − J₁/x, DLMF §10.6(i), free), so\n * the energy of the rectangle [0, X] × [0, Y] is (1/2π)∫ EE(r_edge(θ)) dθ over its quadrant, a one-dimensional\n * integral in θ. Every cell is a signed sum of such rectangles. x ≥ π·β/2 ≥ 301 there, where J₀ and J₁ come from\n * the Hankel asymptotic expansion (DLMF §10.17(i), eq. 10.17.3, free) with eight terms in each of P and Q; the\n * remainder is below the first neglected term (DLMF §10.17(iii)), under 1e-17. The θ integral uses 16-point\n * Gauss–Legendre panels on which the phase 2x changes by at most 2 radians, so the work grows only as β. The tests\n * check the two methods against each other where both are practical (β from 192 to 400), the real-space cells\n * against the asymptotic leakage 8√2/(π³·β) of the centre cell, and convergence under doubled panel counts.\n * The β threshold lies above every β a `simulateCapture` pitch up to 100 µm can reach (at most 100/(0.3805·1.4) ≈ 188),\n * so those captures keep the frequency-domain values unchanged.\n *\n * **Truncation and normalization.** The kernel is the (2R+1)² window of cells. The Airy tails fall only as\n * r⁻³, so a finite window always misses some energy: it is reported per wavelength, and the window is then\n * renormalized to unit sum because the sampled-profile contract requires unit-energy kernels. The missing\n * tail energy is thereby put back inside the window (an approximation that slightly raises low-frequency\n * contrast compared with the untruncated pattern).\n *\n * **Approximations (labelled):** scalar (no polarization), aberration-free, uniform pupil amplitude, exactly in\n * focus (no defocus), thin lens with unity pupil magnification, the on-axis PSF used across the declared field\n * (isoplanatic), PSF energy binned into the sample cells, finite window renormalized. Not a measured lens.\n * @see docs/PSF_FOUNDATION.md, \"Diffraction-limited sampled profile (2.1)\".\n */\n\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { requireAllowlistedRecord } from \"../core/record-validation.js\";\nimport type { EvidenceProvenance } from \"../core/evidence-provenance.js\";\nimport type { FocusPlane } from \"./focus-state.js\";\nimport { calculateThinLensImageDistance } from \"./thin-lens.js\";\nimport { parseLensSampledPsfProfile, type LensSampledPsfProfile } from \"./lens-psf-profile.js\";\n\nexport const DIFFRACTION_LIMITED_PSF_PROFILE_MODEL = Object.freeze({\n modelId: \"photivra-diffraction-limited-circular-pupil-psf\" as const, modelVersion: \"1.0.0\" as const });\n/** Input limits: at most 64 wavelengths, kernel radius 0–32 samples (the irradiance-convolution limit). */\nexport const DIFFRACTION_LIMITED_PSF_PROFILE_LIMITS = Object.freeze({ maximumWavelengths: 64, maximumKernelRadiusSamples: 32 });\n/**\n * Internal: β = p·f_c (cell side over the cut-off period) from which `calculateAiryCellEnergies` integrates the cells in\n * real space (2.5). Above every β of a `simulateCapture` pitch up to 100 µm, whose captures keep the frequency-domain values.\n */\nexport const AIRY_REAL_SPACE_MINIMUM_BETA = 192;\n/** Radius of the first dark ring of the Airy pattern in units of λ·N_w: j₁,₁/π, j₁,₁ = 3.8317059702 (first zero of J₁). */\nconst AIRY_FIRST_ZERO_PER_LAMBDA_N = 3.8317059702075125 / Math.PI;\n\nexport interface CreateDiffractionLimitedPsfProfileInput {\n /** Only `circular` in 2.1.0 (polygon pupils: `calculateIdealPolygonDiffractionPsf` gives point densities). */\n aperture: \"circular\";\n focalLengthMm: number;\n apertureFNumber: number;\n focus: FocusPlane;\n /** Strictly increasing wavelengths, nm, in the medium of propagation (no air/vacuum conversion is applied). 1–64. */\n wavelengthsNm: readonly number[];\n /** Square sample pitch of every kernel, µm. */\n samplePitchMicrometers: number;\n /** Kernel half-width in samples; each kernel is (2R + 1)². 0–32. */\n kernelRadiusSamples: number;\n /** Half-extent of the image field over which the on-axis PSF is declared (profile field axes ±x, ±y), mm. */\n fieldHalfExtentMm: { x: number; y: number };\n profileId: string;\n}\n\nexport interface DiffractionLimitedPsfWavelength {\n wavelengthNm: number;\n /** f_c = 1/(λ·N_w), cycles per µm. */\n cutoffFrequencyCyclesPerMicrometer: number;\n /** 1.21967·λ·N_w, µm. */\n firstZeroRadiusMicrometers: number;\n /** Σ of the cell energies inside the window before renormalization (of a total of 1). */\n capturedEnergyFraction: number;\n /** 1 − capturedEnergyFraction: the Airy tail energy outside the window, put back by renormalization. */\n truncatedEnergyFraction: number;\n /** Fraction of the renormalized kernel in the centre sample. */\n centreSampleFraction: number;\n}\n\nexport interface DiffractionLimitedPsfProfile {\n profile: LensSampledPsfProfile;\n model: typeof DIFFRACTION_LIMITED_PSF_PROFILE_MODEL;\n pupilDiameterMm: number;\n pupilToImageDistanceMm: number;\n workingFNumber: number;\n kernelRadiusSamples: number;\n samplePitchMicrometers: number;\n wavelengths: readonly DiffractionLimitedPsfWavelength[];\n /** Largest truncated energy over the wavelengths. */\n maximumTruncatedEnergyFraction: number;\n /**\n * How the cells were integrated. 2.5: wavelengths with β = p·f_c ≥ 192 (cells far larger than the pattern) use the real-space\n * encircled-energy method; `realSpaceWavelengthsNm` lists them, and `method` is `\"frequency-domain-and-real-space\"` when both ran.\n * `maximumRadialNodes` counts frequency-domain radial nodes (0 when only the real-space method ran); `maximumAngularNodes` the\n * angular nodes of either method (for the real-space method, all θ nodes of one wavelength).\n */\n quadrature: { method: \"polar-frequency-domain-trapezoid-gauss-legendre\" | \"real-space-encircled-energy-gauss-legendre\" | \"frequency-domain-and-real-space\";\n maximumRadialNodes: number; maximumAngularNodes: number; realSpaceWavelengthsNm?: readonly number[] };\n}\n\nconst EVIDENCE: readonly EvidenceProvenance[] = Object.freeze([Object.freeze({ sourceOrigin: \"photivra\" as const,\n sourceReference: \"photivra:diffraction-limited-psf-profile\", reuseStatus: \"photivra-owned\" as const })]);\nconst LIMITATIONS = Object.freeze([\n \"Scalar Fraunhofer diffraction of an ideal, uniformly lit circular pupil (Airy pattern); no aberration, apodization, polarization or stray light.\",\n \"Exactly in focus: no defocus or depth of field. Thin lens with unity pupil magnification; the pupil-to-image distance is the thin-lens image distance.\",\n \"The on-axis PSF is declared unchanged across the field (isoplanatic); no field-dependent aberration or pupil clipping.\",\n \"PSF energy is binned into square sample cells; the finite window misses the Airy tail energy listed per wavelength and is renormalized to unit sum.\",\n \"Wavelengths are used as given, in the declared basis, without air/vacuum conversion.\"\n]);\n\nfunction positive(value: unknown, name: string): number {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0) throw new InvalidConfigurationError(name + \" must be a positive finite number.\");\n return value;\n}\n\n/** Gauss–Legendre nodes and weights on [0, 1] (Newton iteration on Pₙ; DLMF §3.5(v)). Cached per n. */\nconst GAUSS_LEGENDRE = new Map<number, { nodes: Float64Array; weights: Float64Array }>();\nfunction gaussLegendre(n: number): { nodes: Float64Array; weights: Float64Array } {\n const cached = GAUSS_LEGENDRE.get(n);\n if (cached) return cached;\n const nodes = new Float64Array(n), weights = new Float64Array(n);\n for (let k = 0; k < Math.ceil(n / 2); k++) {\n let x = Math.cos(Math.PI * (k + 0.75) / (n + 0.5));\n for (let iteration = 0; iteration < 100; iteration++) {\n let p0 = 1, p1 = x;\n for (let m = 2; m <= n; m++) { const p2 = ((2 * m - 1) * x * p1 - (m - 1) * p0) / m; p0 = p1; p1 = p2; }\n const step = p1 / (n * (x * p1 - p0) / (x * x - 1));\n x -= step;\n if (Math.abs(step) < 1e-16) break;\n }\n let p0 = 1, p1 = x;\n for (let m = 2; m <= n; m++) { const p2 = ((2 * m - 1) * x * p1 - (m - 1) * p0) / m; p0 = p1; p1 = p2; }\n const derivative = n * (x * p1 - p0) / (x * x - 1);\n const w = 1 / ((1 - x * x) * derivative * derivative);\n // Map [-1, 1] to [0, 1]: t = (1 ± x)/2, weight w (2/(1-x²)P'² halved).\n nodes[k] = (1 - x) / 2; weights[k] = w;\n nodes[n - 1 - k] = (1 + x) / 2; weights[n - 1 - k] = w;\n }\n const result = { nodes, weights };\n if (GAUSS_LEGENDRE.size < 64) GAUSS_LEGENDRE.set(n, result);\n return result;\n}\nfunction sinc(x: number): number {\n if (x === 0) return 1;\n const a = Math.PI * x;\n return Math.sin(a) / a;\n}\n\n/**\n * Cell energies K(i, j), 0 ≤ i, j ≤ R, of the Airy pattern for β = p·f_c (cell side in units of the cut-off\n * period): in the frequency domain below `AIRY_REAL_SPACE_MINIMUM_BETA`, in real space from it (2.5). `scale`\n * multiplies the node or panel counts (1 in production; the tests use 2 to check convergence).\n */\nexport function calculateAiryCellEnergies(beta: number, radius: number, scale = 1): { cells: Float64Array; radialNodes: number; angularNodes: number; realSpace: boolean } {\n return beta >= AIRY_REAL_SPACE_MINIMUM_BETA ? { ...calculateAiryCellEnergiesInRealSpace(beta, radius, scale), radialNodes: 0, realSpace: true } :\n { ...calculateAiryCellEnergiesInFrequencyDomain(beta, radius, scale), realSpace: false };\n}\n\n/** Hankel asymptotic coefficients a_k(ν) of DLMF 10.17.1 for ν = 0 and 1, k = 0..15. */\nconst HANKEL = ((): { a0: Float64Array; a1: Float64Array } => {\n const a0 = new Float64Array(16), a1 = new Float64Array(16);\n a0[0] = 1; a1[0] = 1;\n for (let k = 1; k < 16; k++) {\n const odd = (2 * k - 1) * (2 * k - 1);\n a0[k] = a0[k - 1]! * (0 - odd) / (8 * k);\n a1[k] = a1[k - 1]! * (4 - odd) / (8 * k);\n }\n return { a0, a1 };\n})();\n/** Smallest argument of `besselSquareSum`: there the 16-term expansion's remainder is below 1e-17. */\nconst HANKEL_MINIMUM_ARGUMENT = 300;\n\n/**\n * J₀(x)² + J₁(x)² for x ≥ 300 from the Hankel expansion J_ν(x) = √(2/(πx))·(P_ν cos ω_ν − Q_ν sin ω_ν), ω_ν = x − νπ/2 − π/4,\n * P_ν = Σ(−1)^k a_2k(ν)/x^2k, Q_ν = Σ(−1)^k a_2k+1(ν)/x^2k+1 (DLMF 10.17.3), eight terms each. With ω₁ = ω₀ − π/2,\n * J₁ = √(2/(πx))·(P₁ sin ω₀ + Q₁ cos ω₀). Internal.\n */\nexport function besselSquareSum(x: number): number {\n if (!(x >= HANKEL_MINIMUM_ARGUMENT)) throw new InvalidConfigurationError(\"The Hankel expansion of J0 and J1 needs an argument of at least 300.\");\n const { a0, a1 } = HANKEL, inverse = 1 / x;\n let p0 = 0, q0 = 0, p1 = 0, q1 = 0, power = 1;\n for (let k = 0; k < 16; k++) {\n const sign = (k >> 1) % 2 === 0 ? 1 : -1;\n if (k % 2 === 0) { p0 += sign * a0[k]! * power; p1 += sign * a1[k]! * power; } else { q0 += sign * a0[k]! * power; q1 += sign * a1[k]! * power; }\n power *= inverse;\n }\n const omega = x - Math.PI / 4, c = Math.cos(omega), s = Math.sin(omega);\n const j0 = p0 * c - q0 * s, j1 = p1 * s + q1 * c;\n return 2 / (Math.PI * x) * (j0 * j0 + j1 * j1);\n}\n\n/**\n * Real-space cell energies (2.5): see the module comment. G(Z, φ) = ∫₀^φ [J₀² + J₁²](π·β·Z/cos θ) dθ for cell boundaries at\n * half-integer distances Z (in cells), accumulated along θ for each Z with every needed φ = atan(W/Z) as a panel boundary.\n * The rectangle [0, X] × [0, Y] holds 1/4 − (G(X, atan(Y/X)) + G(Y, atan(X/Y)))/(2π) of the energy. Internal.\n */\nexport function calculateAiryCellEnergiesInRealSpace(beta: number, radius: number, scale = 1): { cells: Float64Array; angularNodes: number } {\n const size = radius + 1, table = new Float64Array(size * size), { nodes, weights } = gaussLegendre(16);\n let angularNodes = 0;\n for (let z = 0; z < size; z++) {\n const Z = z + 0.5, k = Math.PI * beta * Z;\n let sum = 0, t0 = 1, theta0 = 0;\n for (let w = 0; w < size; w++) {\n // Panels equal in t = 1/cos θ, so the phase 2x = 2kt changes by at most 2 radians on each.\n const t1 = Math.hypot(Z, w + 0.5) / Z, theta1 = Math.atan2(w + 0.5, Z), panels = Math.max(1, Math.ceil(scale * k * (t1 - t0)));\n let a = theta0;\n for (let n = 1; n <= panels; n++) {\n const b = n === panels ? theta1 : Math.acos(1 / (t0 + (t1 - t0) * n / panels)), h = b - a;\n let panel = 0;\n for (let g = 0; g < 16; g++) panel += weights[g]! * besselSquareSum(k / Math.cos(a + h * nodes[g]!));\n sum += panel * h;\n a = b;\n }\n angularNodes += 16 * panels;\n table[z * size + w] = sum;\n t0 = t1; theta0 = theta1;\n }\n }\n // Boundaries of cell i along one axis as (half-integer index, weight): the centre cell is twice [0, 1/2].\n const bounds = (i: number): [number, number][] => i === 0 ? [[0, 2]] : [[i, 1], [i - 1, -1]];\n const cells = new Float64Array(size * size);\n for (let j = 0; j < size; j++) for (let i = 0; i < size; i++) {\n let constant = 0, leak = 0;\n for (const [a, wa] of bounds(i)) for (const [b, wb] of bounds(j)) {\n constant += wa * wb;\n leak += wa * wb * (table[a * size + b]! + table[b * size + a]!);\n }\n cells[j * size + i] = constant / 4 - leak / (2 * Math.PI);\n }\n return { cells, angularNodes };\n}\n\n/** Frequency-domain cell energies (2.1; see the module comment). Internal. */\nexport function calculateAiryCellEnergiesInFrequencyDomain(beta: number, radius: number, scale = 1): { cells: Float64Array; radialNodes: number; angularNodes: number } {\n // Bandwidth of the angular and radial integrands: c = π·β·(2R + 1)·√2 (the sinc factors and the highest cosine).\n const c = Math.PI * beta * (2 * radius + 1) * Math.SQRT2, margin = 10 * Math.cbrt(c) + 24;\n const quarter = Math.ceil(scale * (c + margin) / 4), radialNodes = Math.ceil(scale * (c + margin));\n const { nodes, weights } = gaussLegendre(radialNodes), size = radius + 1, cells = new Float64Array(size * size);\n const ca = new Float64Array(size), cb = new Float64Array(size), angularStep = (Math.PI / 2) / quarter;\n for (let r = 0; r < radialNodes; r++) {\n const t = nodes[r]!, rho = 1 - t * t;\n // H(1 − t²) without cancellation: acos(1 − t²) = 2·asin(t/√2), ρ·√(1 − ρ²) = ρ·t·√(2 − t²).\n const otf = (2 / Math.PI) * (2 * Math.asin(t / Math.SQRT2) - rho * t * Math.sqrt(2 - t * t));\n // ∫₀¹ g(ρ) dρ = ∫₀¹ g(1 − t²)·2t dt; the 4 is the four symmetric quadrants of the angular trapezoid.\n const radialWeight = weights[r]! * 2 * t * rho * otf * 4 * angularStep;\n for (let k = 0; k <= quarter; k++) {\n const theta = k * angularStep, x = beta * rho * Math.cos(theta), y = beta * rho * Math.sin(theta);\n const w = radialWeight * (k === 0 || k === quarter ? 0.5 : 1) * sinc(x) * sinc(y);\n // cos(2π·i·x) and cos(2π·j·y) by the Chebyshev recurrence.\n const cx = Math.cos(2 * Math.PI * x), cy = Math.cos(2 * Math.PI * y);\n ca[0] = 1; cb[0] = 1;\n if (size > 1) { ca[1] = cx; cb[1] = cy; }\n for (let i = 2; i < size; i++) { ca[i] = 2 * cx * ca[i - 1]! - ca[i - 2]!; cb[i] = 2 * cy * cb[i - 1]! - cb[i - 2]!; }\n for (let j = 0; j < size; j++) {\n const wj = w * cb[j]!;\n for (let i = 0; i < size; i++) { const o = j * size + i; cells[o] = cells[o]! + wj * ca[i]!; }\n }\n }\n }\n const area = beta * beta;\n for (let i = 0; i < cells.length; i++) cells[i] = cells[i]! * area;\n return { cells, radialNodes, angularNodes: 4 * quarter };\n}\n\n/**\n * Creates a sampled lens PSF profile for an ideal diffraction-limited lens with a circular pupil: one\n * unit-sum Airy kernel per wavelength on one shared (2R+1)² grid, with the truncated energy reported per\n * wavelength. The profile declares the given focal length, f-number and focus exactly, and the field axes\n * ±`fieldHalfExtentMm` with the same kernel at every field node.\n * @param input - See `CreateDiffractionLimitedPsfProfileInput`.\n * @returns The profile and its diffraction record. An approximation; not a measured lens.\n * @see docs/PSF_FOUNDATION.md, \"Diffraction-limited sampled profile (2.1)\".\n */\nexport function createDiffractionLimitedPsfProfile(input: CreateDiffractionLimitedPsfProfileInput): CalculationResult<DiffractionLimitedPsfProfile> {\n const r = requireAllowlistedRecord(input, [\"aperture\", \"focalLengthMm\", \"apertureFNumber\", \"focus\", \"wavelengthsNm\", \"samplePitchMicrometers\",\n \"kernelRadiusSamples\", \"fieldHalfExtentMm\", \"profileId\"], \"Invalid diffraction-limited PSF profile input.\");\n if (r.aperture !== \"circular\") throw new InvalidConfigurationError(\"aperture must be \\\"circular\\\".\");\n const focalLengthMm = positive(r.focalLengthMm, \"focalLengthMm\"), fNumber = positive(r.apertureFNumber, \"apertureFNumber\");\n const pitch = positive(r.samplePitchMicrometers, \"samplePitchMicrometers\");\n const L = DIFFRACTION_LIMITED_PSF_PROFILE_LIMITS;\n const radius = r.kernelRadiusSamples;\n if (typeof radius !== \"number\" || !Number.isSafeInteger(radius) || radius < 0 || radius > L.maximumKernelRadiusSamples) {\n throw new InvalidConfigurationError(\"kernelRadiusSamples must be an integer from 0 to \" + L.maximumKernelRadiusSamples + \".\");\n }\n const focus = requireAllowlistedRecord(r.focus, [\"kind\", \"distanceM\"], \"Invalid focus.\");\n let focusDiopters: number, distanceMm: number;\n if (focus.kind === \"infinity\") {\n if (focus.distanceM !== undefined) throw new InvalidConfigurationError(\"An infinity focus has no distanceM.\");\n focusDiopters = 0; distanceMm = focalLengthMm;\n } else if (focus.kind === \"finite\") {\n const distanceM = positive(focus.distanceM, \"focus.distanceM\");\n focusDiopters = 1 / distanceM;\n distanceMm = calculateThinLensImageDistance({ focalLengthMm, objectDistanceM: distanceM }).value.imageDistanceMm;\n } else throw new InvalidConfigurationError(\"focus.kind must be finite or infinity.\");\n const field = requireAllowlistedRecord(r.fieldHalfExtentMm, [\"x\", \"y\"], \"Invalid fieldHalfExtentMm.\");\n const fieldX = positive(field.x, \"fieldHalfExtentMm.x\"), fieldY = positive(field.y, \"fieldHalfExtentMm.y\");\n const list = r.wavelengthsNm;\n if (!Array.isArray(list) || list.length < 1 || list.length > L.maximumWavelengths) throw new InvalidConfigurationError(\"wavelengthsNm must list 1 to 64 wavelengths.\");\n const wavelengths = (list as unknown[]).map((value, index) => positive(value, \"wavelengthsNm[\" + index + \"]\"));\n for (let i = 1; i < wavelengths.length; i++) if (!(wavelengths[i]! > wavelengths[i - 1]!)) throw new InvalidConfigurationError(\"wavelengthsNm must be strictly increasing.\");\n if (typeof r.profileId !== \"string\" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(r.profileId)) throw new InvalidConfigurationError(\"profileId must be a public opaque ID.\");\n\n const pupilDiameterMm = focalLengthMm / fNumber, workingFNumber = distanceMm / pupilDiameterMm, size = 2 * radius + 1;\n let maximumRadialNodes = 0, maximumAngularNodes = 0, maximumTruncated = 0;\n const records: DiffractionLimitedPsfWavelength[] = [], kernels: number[][] = [], realSpaceWavelengthsNm: number[] = [];\n for (const nm of wavelengths) {\n const lambdaN = nm / 1000 * workingFNumber, cutoff = 1 / lambdaN;\n const airy = calculateAiryCellEnergies(pitch * cutoff, radius);\n if (airy.realSpace) realSpaceWavelengthsNm.push(nm);\n maximumRadialNodes = Math.max(maximumRadialNodes, airy.radialNodes); maximumAngularNodes = Math.max(maximumAngularNodes, airy.angularNodes);\n const values: number[] = new Array(size * size);\n let captured = 0;\n for (let ky = 0; ky < size; ky++) for (let kx = 0; kx < size; kx++) {\n const value = airy.cells[Math.abs(ky - radius) * (radius + 1) + Math.abs(kx - radius)]!;\n // Every cell energy of a non-negative PSF is positive; a negative value would be a quadrature failure.\n if (!(value > -1e-13)) throw new InvalidConfigurationError(\"Diffraction cell integration failed to converge.\");\n values[ky * size + kx] = Math.max(0, value);\n captured += values[ky * size + kx]!;\n }\n if (!(captured > 0 && captured <= 1 + 1e-9)) throw new InvalidConfigurationError(\"Diffraction cell integration failed to converge.\");\n const truncated = Math.max(0, 1 - captured);\n maximumTruncated = Math.max(maximumTruncated, truncated);\n const kernel = values.map((v) => v / captured);\n kernels.push(kernel);\n records.push({ wavelengthNm: nm, cutoffFrequencyCyclesPerMicrometer: cutoff, firstZeroRadiusMicrometers: AIRY_FIRST_ZERO_PER_LAMBDA_N * lambdaN,\n capturedEnergyFraction: captured, truncatedEnergyFraction: truncated, centreSampleFraction: kernel[radius * size + radius]! });\n }\n const uncertainty = { kind: \"not-quantified\" as const, limitation: \"Ideal diffraction-limited model; no measured lens or quadrature error bound.\" };\n const nodes = [-fieldX, fieldX].flatMap((fx) => [-fieldY, fieldY].flatMap((fy) => wavelengths.map((nm, index) => ({\n nodeId: \"airy-\" + (fx < 0 ? \"xm\" : \"xp\") + \"-\" + (fy < 0 ? \"ym\" : \"yp\") + \"-\" + index,\n coordinate: { focalLengthMm, focusDiopters, apertureFNumber: fNumber, fieldXmm: fx, fieldYmm: fy, wavelengthNm: nm, signedDefocusImagePlaneMicrometers: 0 },\n kernel: { widthSamples: size, heightSamples: size, samplePitchMicrometersX: pitch, samplePitchMicrometersY: pitch, centerSampleX: radius, centerSampleY: radius,\n normalizedIntensity: kernels[index]! },\n bestFocusImagePlaneOffsetMicrometers: 0, relativePupilThroughputFactor: 1, evidence: EVIDENCE, uncertainty }))));\n const profile = parseLensSampledPsfProfile({ schemaVersion: \"0.1.0\", profileId: r.profileId, profileVersion: DIFFRACTION_LIMITED_PSF_PROFILE_MODEL.modelVersion,\n scientificStatus: \"approximation\", opticalDomain: \"lens-primary-optical-path-only\", coordinateSystem: \"image-plane-metric\", fieldAxes: \"+X right, +Y up\",\n kernelEnergyNormalization: \"unit-energy-shape\", interpolation: \"bounded-regular-grid-multilinear\", throughputOwnership: \"separate-relative-pupil-throughput-factor\",\n responseIncludes: { diffraction: true, aberration: false, defocus: false, pupilClippingShape: false },\n sensorOpticalStackIncluded: false, sensorSamplingIncluded: false, reconstructionIncluded: false, strayLightIncluded: false,\n axes: { focalLengthMm: [focalLengthMm], focusDiopters: [focusDiopters], apertureFNumber: [fNumber], fieldXmm: [-fieldX, fieldX], fieldYmm: [-fieldY, fieldY],\n wavelengthNm: wavelengths, signedDefocusImagePlaneMicrometers: [0] },\n nodes, evidence: EVIDENCE, limitations: [...LIMITATIONS] });\n return approximationResult({ profile, model: DIFFRACTION_LIMITED_PSF_PROFILE_MODEL, pupilDiameterMm, pupilToImageDistanceMm: distanceMm, workingFNumber,\n kernelRadiusSamples: radius, samplePitchMicrometers: pitch, wavelengths: records, maximumTruncatedEnergyFraction: maximumTruncated,\n quadrature: realSpaceWavelengthsNm.length === 0 ? { method: \"polar-frequency-domain-trapezoid-gauss-legendre\" as const, maximumRadialNodes, maximumAngularNodes } :\n { method: realSpaceWavelengthsNm.length === wavelengths.length ? \"real-space-encircled-energy-gauss-legendre\" as const : \"frequency-domain-and-real-space\" as const,\n maximumRadialNodes, maximumAngularNodes, realSpaceWavelengthsNm } },\n DIFFRACTION_LIMITED_PSF_PROFILE_MODEL.modelId, DIFFRACTION_LIMITED_PSF_PROFILE_MODEL.modelVersion, [...LIMITATIONS]);\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Caller-supplied text written into JPEG, PNG and DNG outputs. Every field is optional; at least one must be set. */
|
|
2
|
+
export interface OutputTextMetadata {
|
|
3
|
+
/** What the image shows. JPEG/DNG `dc:description`, PNG Description, DNG ImageDescription (appended when ASCII). */
|
|
4
|
+
description?: string;
|
|
5
|
+
/** Name and version of the caller's software. JPEG/DNG `xmp:CreatorTool`, PNG Software. */
|
|
6
|
+
software?: string;
|
|
7
|
+
/** Rights statement. JPEG/DNG `dc:rights`; the PNG keyword and DNG tag 33432 (when ASCII) of the same name. */
|
|
8
|
+
copyright?: string;
|
|
9
|
+
/** Free-text comment. JPEG COM segment and `exif:UserComment`, PNG Comment, DNG `exif:UserComment`. */
|
|
10
|
+
comment?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Largest UTF-8 length of one metadata field, in bytes. Four escaped fields still fit one JPEG APP1 segment. */
|
|
13
|
+
export declare const OUTPUT_TEXT_METADATA_MAXIMUM_BYTES = 2048;
|
|
14
|
+
/**
|
|
15
|
+
* Checks caller text metadata. Returns undefined when `value` is undefined, otherwise a frozen copy with only
|
|
16
|
+
* the given fields. `path` prefixes the error messages, for example "output.metadata".
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseOutputTextMetadata(value: unknown, path: string): Readonly<OutputTextMetadata> | undefined;
|
|
19
|
+
/** One `rdf:Description` of the current resource with the given fields, declaring only the namespaces it uses. */
|
|
20
|
+
export declare function xmpTextMetadataDescription(metadata: Readonly<OutputTextMetadata>): string;
|
|
21
|
+
/** A complete XMP packet (`x:xmpmeta` wrapper, no `xpacket` processing instructions) holding only the text fields. */
|
|
22
|
+
export declare function xmpTextMetadataPacket(metadata: Readonly<OutputTextMetadata>): Uint8Array;
|
|
23
|
+
/**
|
|
24
|
+
* Returns a copy of a baseline JPEG with the text fields added: merged into its XMP APP1 packet (or a new packet
|
|
25
|
+
* after the last leading APPn segment when it has none), plus a COM segment for `comment`. Every other byte,
|
|
26
|
+
* including the entropy-coded data, is unchanged. Reads marker segments only up to SOS.
|
|
27
|
+
*/
|
|
28
|
+
export declare function addJpegTextMetadata(jpeg: Uint8Array, metadata: Readonly<OutputTextMetadata>): Uint8Array;
|
|
29
|
+
/** The text chunks for the given fields, in the order description, software, copyright, comment. */
|
|
30
|
+
export declare function pngTextMetadataChunks(metadata: Readonly<OutputTextMetadata>): Uint8Array[];
|
|
31
|
+
/** Returns a copy of a PNG with one text chunk per field inserted before the first IDAT chunk. Other chunks are unchanged. */
|
|
32
|
+
export declare function addPngTextMetadata(png: Uint8Array, metadata: Readonly<OutputTextMetadata>): Uint8Array;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Optional caller-supplied text metadata for encoded images (2.4): a description, the creating software, a
|
|
4
|
+
* copyright notice and a comment. The engine writes the caller's text unchanged and adds no text of its own
|
|
5
|
+
* (in a DNG the description follows the existing simulated-RAW statement).
|
|
6
|
+
* Independently written from these free specifications; no third-party source was used:
|
|
7
|
+
*
|
|
8
|
+
* - JPEG: the existing XMP packet in APP1 (Adobe XMP Specification Part 3, 2020, §1.1.3: identifier
|
|
9
|
+
* "http://ns.adobe.com/xap/1.0/" and a UTF-8 packet of at most 65 502 bytes, §1.1.3.1). The fields are added
|
|
10
|
+
* as one more top-level `rdf:Description` of the same resource (`rdf:about=""`): XMP Part 1 (2012, identical
|
|
11
|
+
* to ISO 16684-1:2011) §7.4 lets properties be "arbitrarily apportioned among the rdf:Description elements"
|
|
12
|
+
* of the single `rdf:RDF`. Mapping: `dc:description` and `dc:rights` as language alternatives with
|
|
13
|
+
* `x-default` (Part 1 §8.3), `xmp:CreatorTool` as text (Part 1 §8.4) and `exif:UserComment` as a language
|
|
14
|
+
* alternative (Part 2, 2022, §3.4, the Exif namespace). The comment is also a COM marker segment (ITU-T T.81
|
|
15
|
+
* §B.2.4.5, "the interpretation is left to the application"; written as UTF-8) after the APPn segments.
|
|
16
|
+
* - PNG: one text chunk per field with the predefined keywords Description, Software, Copyright and Comment
|
|
17
|
+
* (W3C PNG Specification, Third Edition, §11.3.3.1, Table 21). `tEXt` (§11.3.3.2; printable Latin-1 plus line
|
|
18
|
+
* feed) when every character fits, otherwise an uncompressed `iTXt` (§11.3.3.4; UTF-8, empty language tag and
|
|
19
|
+
* translated keyword). The chunks go before the first IDAT, as §12.9 recommends for small text.
|
|
20
|
+
* - DNG: `serializeNativeRawDng` writes the same XMP fields as a packet in TIFF tag 700 (XMP Part 3 §1.1.6,
|
|
21
|
+
* Table 13) plus the TIFF ASCII tags that XMP Part 3 §3.2.3 maps to them.
|
|
22
|
+
*
|
|
23
|
+
* Validation: each given field is a non-empty string of at most `OUTPUT_TEXT_METADATA_MAXIMUM_BYTES` UTF-8
|
|
24
|
+
* bytes, with no unpaired UTF-16 surrogate (so it encodes to valid UTF-8 without replacement), no NUL or other
|
|
25
|
+
* C0/C1 control character except line feed, and no U+FFFE/U+FFFF (not XML 1.0 characters). Line breaks are a
|
|
26
|
+
* single line feed, as the PNG text chunks require.
|
|
27
|
+
* @see docs/QUICK_START.md "Text metadata (2.4)".
|
|
28
|
+
*/
|
|
29
|
+
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
30
|
+
import { pngCrc32 } from "./png-encoder.js";
|
|
31
|
+
/** Largest UTF-8 length of one metadata field, in bytes. Four escaped fields still fit one JPEG APP1 segment. */
|
|
32
|
+
export const OUTPUT_TEXT_METADATA_MAXIMUM_BYTES = 2048;
|
|
33
|
+
const FIELDS = ["description", "software", "copyright", "comment"];
|
|
34
|
+
/** PNG Third Edition §11.3.3.1 Table 21 keyword for each field: the field name with an initial capital. */
|
|
35
|
+
const pngKeyword = (field) => field[0].toUpperCase() + field.slice(1);
|
|
36
|
+
const XMP_IDENTIFIER = "http://ns.adobe.com/xap/1.0/\0";
|
|
37
|
+
/** Largest XMP packet in one JPEG APP1 segment (XMP Part 3 §1.1.3.1). */
|
|
38
|
+
const JPEG_XMP_MAXIMUM_PACKET_BYTES = 65502;
|
|
39
|
+
const encoder = new TextEncoder();
|
|
40
|
+
/**
|
|
41
|
+
* Checks caller text metadata. Returns undefined when `value` is undefined, otherwise a frozen copy with only
|
|
42
|
+
* the given fields. `path` prefixes the error messages, for example "output.metadata".
|
|
43
|
+
*/
|
|
44
|
+
export function parseOutputTextMetadata(value, path) {
|
|
45
|
+
if (value === undefined)
|
|
46
|
+
return undefined;
|
|
47
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
48
|
+
throw new InvalidConfigurationError(path + " must be an object.");
|
|
49
|
+
const out = {};
|
|
50
|
+
for (const key of Object.keys(value))
|
|
51
|
+
if (!FIELDS.includes(key))
|
|
52
|
+
throw new InvalidConfigurationError("Unknown " + path + " field: " + key + ".");
|
|
53
|
+
for (const field of FIELDS) {
|
|
54
|
+
const text = value[field];
|
|
55
|
+
if (text === undefined)
|
|
56
|
+
continue;
|
|
57
|
+
const name = path + "." + field;
|
|
58
|
+
if (typeof text !== "string" || text.length === 0)
|
|
59
|
+
throw new InvalidConfigurationError(name + " must be a non-empty string.");
|
|
60
|
+
if (/\p{Cs}/u.test(text))
|
|
61
|
+
throw new InvalidConfigurationError(name + " must be valid Unicode text (it has an unpaired UTF-16 surrogate).");
|
|
62
|
+
if (text.includes("\0"))
|
|
63
|
+
throw new InvalidConfigurationError(name + " must not contain NUL characters.");
|
|
64
|
+
// eslint-disable-next-line no-control-regex
|
|
65
|
+
if (/[\u0001-\u0009\u000b-\u001f\u007f-\u009f]/u.test(text))
|
|
66
|
+
throw new InvalidConfigurationError(name + " must not contain control characters other than line feed (\\n), or U+FFFE/U+FFFF.");
|
|
67
|
+
const bytes = encoder.encode(text).length;
|
|
68
|
+
if (bytes > OUTPUT_TEXT_METADATA_MAXIMUM_BYTES)
|
|
69
|
+
throw new InvalidConfigurationError(name + " must be at most " + OUTPUT_TEXT_METADATA_MAXIMUM_BYTES + " UTF-8 bytes (it has " + bytes + ").");
|
|
70
|
+
out[field] = text;
|
|
71
|
+
}
|
|
72
|
+
if (Object.keys(out).length === 0)
|
|
73
|
+
throw new InvalidConfigurationError(path + " must set at least one of description, software, copyright or comment.");
|
|
74
|
+
return Object.freeze(out);
|
|
75
|
+
}
|
|
76
|
+
function xmlText(text) {
|
|
77
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
78
|
+
}
|
|
79
|
+
const alt = (text) => '<rdf:Alt><rdf:li xml:lang="x-default">' + xmlText(text) + "</rdf:li></rdf:Alt>";
|
|
80
|
+
/** One `rdf:Description` of the current resource with the given fields, declaring only the namespaces it uses. */
|
|
81
|
+
export function xmpTextMetadataDescription(metadata) {
|
|
82
|
+
const dc = metadata.description !== undefined || metadata.copyright !== undefined;
|
|
83
|
+
return '<rdf:Description rdf:about=""' + (dc ? ' xmlns:dc="http://purl.org/dc/elements/1.1/"' : "") +
|
|
84
|
+
(metadata.software !== undefined ? ' xmlns:xmp="http://ns.adobe.com/xap/1.0/"' : "") +
|
|
85
|
+
(metadata.comment !== undefined ? ' xmlns:exif="http://ns.adobe.com/exif/1.0/"' : "") + ">" +
|
|
86
|
+
(metadata.description !== undefined ? "<dc:description>" + alt(metadata.description) + "</dc:description>" : "") +
|
|
87
|
+
(metadata.copyright !== undefined ? "<dc:rights>" + alt(metadata.copyright) + "</dc:rights>" : "") +
|
|
88
|
+
(metadata.software !== undefined ? "<xmp:CreatorTool>" + xmlText(metadata.software) + "</xmp:CreatorTool>" : "") +
|
|
89
|
+
(metadata.comment !== undefined ? "<exif:UserComment>" + alt(metadata.comment) + "</exif:UserComment>" : "") + "</rdf:Description>";
|
|
90
|
+
}
|
|
91
|
+
/** A complete XMP packet (`x:xmpmeta` wrapper, no `xpacket` processing instructions) holding only the text fields. */
|
|
92
|
+
export function xmpTextMetadataPacket(metadata) {
|
|
93
|
+
return encoder.encode('<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">' +
|
|
94
|
+
xmpTextMetadataDescription(metadata) + "</rdf:RDF></x:xmpmeta>");
|
|
95
|
+
}
|
|
96
|
+
function concat(parts) {
|
|
97
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
98
|
+
let offset = 0;
|
|
99
|
+
for (const part of parts) {
|
|
100
|
+
out.set(part, offset);
|
|
101
|
+
offset += part.length;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
function segment(marker, payload) {
|
|
106
|
+
return concat([Uint8Array.of(0xff, marker, (payload.length + 2) >> 8, (payload.length + 2) & 255), payload]);
|
|
107
|
+
}
|
|
108
|
+
function startsWith(bytes, offset, prefix) {
|
|
109
|
+
if (offset + prefix.length > bytes.length)
|
|
110
|
+
return false;
|
|
111
|
+
for (let i = 0; i < prefix.length; i++)
|
|
112
|
+
if (bytes[offset + i] !== prefix[i])
|
|
113
|
+
return false;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Returns a copy of a baseline JPEG with the text fields added: merged into its XMP APP1 packet (or a new packet
|
|
118
|
+
* after the last leading APPn segment when it has none), plus a COM segment for `comment`. Every other byte,
|
|
119
|
+
* including the entropy-coded data, is unchanged. Reads marker segments only up to SOS.
|
|
120
|
+
*/
|
|
121
|
+
export function addJpegTextMetadata(jpeg, metadata) {
|
|
122
|
+
if (jpeg.length < 4 || jpeg[0] !== 0xff || jpeg[1] !== 0xd8)
|
|
123
|
+
throw new InvalidConfigurationError("JPEG text metadata needs a JPEG stream starting with SOI.");
|
|
124
|
+
const identifier = encoder.encode(XMP_IDENTIFIER);
|
|
125
|
+
let offset = 2, xmp, appEnd = 2, inLeadingApps = true;
|
|
126
|
+
for (;;) {
|
|
127
|
+
if (offset + 4 > jpeg.length || jpeg[offset] !== 0xff)
|
|
128
|
+
throw new InvalidConfigurationError("JPEG text metadata found a malformed marker segment.");
|
|
129
|
+
const marker = jpeg[offset + 1];
|
|
130
|
+
if (marker === 0xda)
|
|
131
|
+
break;
|
|
132
|
+
const end = offset + 2 + ((jpeg[offset + 2] << 8) | jpeg[offset + 3]);
|
|
133
|
+
if (end > jpeg.length)
|
|
134
|
+
throw new InvalidConfigurationError("JPEG text metadata found a truncated marker segment.");
|
|
135
|
+
if (marker === 0xe1 && !xmp && startsWith(jpeg, offset + 4, identifier))
|
|
136
|
+
xmp = { start: offset, end };
|
|
137
|
+
if (inLeadingApps && marker >= 0xe0 && marker <= 0xef)
|
|
138
|
+
appEnd = end;
|
|
139
|
+
else
|
|
140
|
+
inLeadingApps = false;
|
|
141
|
+
offset = end;
|
|
142
|
+
}
|
|
143
|
+
const description = xmpTextMetadataDescription(metadata);
|
|
144
|
+
let xmpPacket;
|
|
145
|
+
if (xmp) {
|
|
146
|
+
const packet = new TextDecoder("utf-8", { fatal: true }).decode(jpeg.subarray(xmp.start + 4 + identifier.length, xmp.end));
|
|
147
|
+
const close = packet.lastIndexOf("</rdf:RDF>");
|
|
148
|
+
if (close < 0)
|
|
149
|
+
throw new InvalidConfigurationError("JPEG text metadata found an XMP packet without rdf:RDF.");
|
|
150
|
+
xmpPacket = encoder.encode(packet.slice(0, close) + description + packet.slice(close));
|
|
151
|
+
}
|
|
152
|
+
else
|
|
153
|
+
xmpPacket = xmpTextMetadataPacket(metadata);
|
|
154
|
+
if (xmpPacket.length > JPEG_XMP_MAXIMUM_PACKET_BYTES)
|
|
155
|
+
throw new InvalidConfigurationError("The text metadata makes the JPEG XMP packet " + xmpPacket.length + " bytes; one APP1 segment holds at most " + JPEG_XMP_MAXIMUM_PACKET_BYTES + ".");
|
|
156
|
+
const xmpSegment = segment(0xe1, concat([identifier, xmpPacket]));
|
|
157
|
+
const com = metadata.comment === undefined ? new Uint8Array(0) : segment(0xfe, encoder.encode(metadata.comment));
|
|
158
|
+
if (xmp)
|
|
159
|
+
return concat([jpeg.subarray(0, xmp.start), xmpSegment, jpeg.subarray(xmp.end, appEnd), com, jpeg.subarray(appEnd)]);
|
|
160
|
+
return concat([jpeg.subarray(0, appEnd), xmpSegment, com, jpeg.subarray(appEnd)]);
|
|
161
|
+
}
|
|
162
|
+
function latin1Text(text) {
|
|
163
|
+
for (const ch of text) {
|
|
164
|
+
const c = ch.codePointAt(0);
|
|
165
|
+
if (!(c === 10 || (c >= 0x20 && c <= 0x7e) || (c >= 0xa1 && c <= 0xff)))
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
function pngChunk(type, data) {
|
|
171
|
+
const out = new Uint8Array(12 + data.length), view = new DataView(out.buffer), typeBytes = Uint8Array.from(type, (c) => c.charCodeAt(0));
|
|
172
|
+
view.setUint32(0, data.length);
|
|
173
|
+
out.set(typeBytes, 4);
|
|
174
|
+
out.set(data, 8);
|
|
175
|
+
view.setUint32(8 + data.length, pngCrc32(data, pngCrc32(typeBytes)));
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
/** The text chunks for the given fields, in the order description, software, copyright, comment. */
|
|
179
|
+
export function pngTextMetadataChunks(metadata) {
|
|
180
|
+
return FIELDS.filter((field) => metadata[field] !== undefined).map((field) => {
|
|
181
|
+
const keyword = Uint8Array.from(pngKeyword(field), (c) => c.charCodeAt(0)), text = metadata[field];
|
|
182
|
+
// tEXt: keyword, NUL, Latin-1 text. iTXt: keyword, NUL, compression flag 0, method 0, empty language tag, NUL, empty translated keyword, NUL, UTF-8 text.
|
|
183
|
+
return latin1Text(text) ? pngChunk("tEXt", concat([keyword, Uint8Array.of(0), Uint8Array.from(text, (c) => c.charCodeAt(0))]))
|
|
184
|
+
: pngChunk("iTXt", concat([keyword, Uint8Array.of(0, 0, 0, 0, 0), encoder.encode(text)]));
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/** Returns a copy of a PNG with one text chunk per field inserted before the first IDAT chunk. Other chunks are unchanged. */
|
|
188
|
+
export function addPngTextMetadata(png, metadata) {
|
|
189
|
+
const signature = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
190
|
+
if (png.length < 8 || signature.some((b, i) => png[i] !== b))
|
|
191
|
+
throw new InvalidConfigurationError("PNG text metadata needs a PNG signature.");
|
|
192
|
+
const view = new DataView(png.buffer, png.byteOffset, png.byteLength);
|
|
193
|
+
for (let offset = 8; offset + 12 <= png.length;) {
|
|
194
|
+
const length = view.getUint32(offset), type = String.fromCharCode(...png.subarray(offset + 4, offset + 8));
|
|
195
|
+
if (type === "IDAT")
|
|
196
|
+
return concat([png.subarray(0, offset), ...pngTextMetadataChunks(metadata), png.subarray(offset)]);
|
|
197
|
+
offset += 12 + length;
|
|
198
|
+
}
|
|
199
|
+
throw new InvalidConfigurationError("PNG text metadata found no IDAT chunk.");
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=text-metadata.js.map
|