diff --git a/image/parsers/png.js b/image/parsers/png.js index f378af8..3b4380a 100644 --- a/image/parsers/png.js +++ b/image/parsers/png.js @@ -14,7 +14,7 @@ import { ByteStream } from '../../io/bytestream.js'; // https://www.w3.org/TR/png-3/ // https://en.wikipedia.org/wiki/PNG#File_format -// TODO: Ancillary chunks eXIf, hIST, pHYs, sPLT. +// TODO: Ancillary chunks eXIf, hIST, sPLT. // let DEBUG = true; let DEBUG = false; @@ -32,6 +32,7 @@ export const PngParseEventType = { cHRM: 'chromaticities_white_point', gAMA: 'image_gamma', iTXt: 'intl_text_data', + pHYs: 'physical_pixel_dims', sBIT: 'significant_bits', tEXt: 'textual_data', tIME: 'last_mod_time', @@ -262,6 +263,27 @@ export class PngLastModTimeEvent extends Event { } } +export const PngUnitSpecifier = { + UNKNOWN: 0, + METRE: 1, +}; + +/** + * @typedef PngPhysicalPixelDimensions + * @property {number} pixelPerUnitX + * @property {number} pixelPerUnitY + * @property {PngUnitSpecifier} unitSpecifier + */ + +export class PngPhysicalPixelDimensionsEvent extends Event { + /** @param {PngPhysicalPixelDimensions} physicalPixelDimensions */ + constructor(physicalPixelDimensions) { + super(PngParseEventType.pHYs); + /** @type {PngPhysicalPixelDimensions} */ + this.physicalPixelDimensions = physicalPixelDimensions; + } +} + /** * @typedef PngChunk Internal use only. * @property {number} length @@ -368,8 +390,8 @@ export class PngParser extends EventTarget { } /** - * Type-safe way to bind a listener for a PngLastModTime. - * @param {function(PngLastModTime): void} listener + * Type-safe way to bind a listener for a PngLastModTimeEvent. + * @param {function(PngLastModTimeEvent): void} listener * @returns {PngParser} for chaining */ onLastModTime(listener) { @@ -387,6 +409,16 @@ export class PngParser extends EventTarget { return this; } + /** + * Type-safe way to bind a listener for a PngPhysicalPixelDimensionsEvent. + * @param {function(PngPhysicalPixelDimensionsEvent): void} listener + * @returns {PngParser} for chaining + */ + onPhysicalPixelDimensions(listener) { + super.addEventListener(PngParseEventType.pHYs, listener); + return this; + } + /** * Type-safe way to bind a listener for a PngSignificantBitsEvent. * @param {function(PngSignificantBitsEvent): void} listener @@ -571,6 +603,21 @@ export class PngParser extends EventTarget { this.dispatchEvent(new PngPaletteEvent(this.palette)); break; + // https://www.w3.org/TR/png-3/#11pHYs + case 'pHYs': + /** @type {physicalPixelDimensions} */ + const pixelDims = { + pixelPerUnitX: chStream.readNumber(4), + pixelPerUnitY: chStream.readNumber(4), + unitSpecifier: chStream.readNumber(1), + }; + if (!Object.values(PngUnitSpecifier).includes(pixelDims.unitSpecifier)) { + throw `Bad pHYs unit specifier: ${pixelDims.unitSpecifier}`; + } + + this.dispatchEvent(new PngPhysicalPixelDimensionsEvent(pixelDims)); + break; + // https://www.w3.org/TR/png-3/#11tEXt case 'tEXt': const byteArr = chStream.peekBytes(length); @@ -750,7 +797,10 @@ async function main() { // console.dir(evt.backgroundColor); }); parser.onLastModTime(evt => { - console.dir(evt.lastModTime); + // console.dir(evt.lastModTime); + }); + parser.onPhysicalPixelDimensions(evt => { + // console.dir(evt.physicalPixelDimensions); }); try { diff --git a/tests/image-parsers-png.spec.js b/tests/image-parsers-png.spec.js index c20def3..0da7b94 100644 --- a/tests/image-parsers-png.spec.js +++ b/tests/image-parsers-png.spec.js @@ -1,7 +1,7 @@ import * as fs from 'node:fs'; import 'mocha'; import { expect } from 'chai'; -import { PngColorType, PngInterlaceMethod, PngParser } from '../image/parsers/png.js'; +import { PngColorType, PngInterlaceMethod, PngUnitSpecifier, PngParser } from '../image/parsers/png.js'; /** @typedef {import('../image/parsers/png.js').PngBackgroundColor} PngBackgroundColor */ /** @typedef {import('../image/parsers/png.js').PngChromaticies} PngChromaticies */ @@ -12,6 +12,7 @@ import { PngColorType, PngInterlaceMethod, PngParser } from '../image/parsers/pn /** @typedef {import('../image/parsers/png.js').PngIntlTextualData} PngIntlTextualData */ /** @typedef {import('../image/parsers/png.js').PngLastModTime} PngLastModTime */ /** @typedef {import('../image/parsers/png.js').PngPalette} PngPalette */ +/** @typedef {import('../image/parsers/png.js').PngPhysicalPixelDimensions} PngPhysicalPixelDimensions */ /** @typedef {import('../image/parsers/png.js').PngSignificantBits} PngSignificantBits */ /** @typedef {import('../image/parsers/png.js').PngTextualData} PngTextualData */ /** @typedef {import('../image/parsers/png.js').PngTransparency} PngTransparency */ @@ -248,4 +249,15 @@ describe('bitjs.image.parsers.PngParser', () => { expect(lastModTime.minute).equals(59); expect(lastModTime.second).equals(59); }); + + it('extracts pHYs', async () => { + /** @type {PngPhysicalPixelDimensions} */ + let pixelDims; + await getPngParser('tests/image-testfiles/cdun2c08.png') + .onPhysicalPixelDimensions(evt => { pixelDims = evt.physicalPixelDimensions }) + .start(); + expect(pixelDims.pixelPerUnitX).equals(1000); + expect(pixelDims.pixelPerUnitY).equals(1000); + expect(pixelDims.unitSpecifier).equals(PngUnitSpecifier.METRE); + }); }); diff --git a/tests/image-testfiles/cdun2c08.png b/tests/image-testfiles/cdun2c08.png new file mode 100644 index 0000000..846033b Binary files /dev/null and b/tests/image-testfiles/cdun2c08.png differ