Skip to content

Commit 17b3850

Browse files
committed
PngParser: Add support for pHYs chunk
1 parent 1869cd7 commit 17b3850

File tree

3 files changed

+67
-5
lines changed

3 files changed

+67
-5
lines changed

image/parsers/png.js

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { ByteStream } from '../../io/bytestream.js';
1414
// https://www.w3.org/TR/png-3/
1515
// https://en.wikipedia.org/wiki/PNG#File_format
1616

17-
// TODO: Ancillary chunks eXIf, hIST, pHYs, sPLT.
17+
// TODO: Ancillary chunks eXIf, hIST, sPLT.
1818

1919
// let DEBUG = true;
2020
let DEBUG = false;
@@ -32,6 +32,7 @@ export const PngParseEventType = {
3232
cHRM: 'chromaticities_white_point',
3333
gAMA: 'image_gamma',
3434
iTXt: 'intl_text_data',
35+
pHYs: 'physical_pixel_dims',
3536
sBIT: 'significant_bits',
3637
tEXt: 'textual_data',
3738
tIME: 'last_mod_time',
@@ -262,6 +263,27 @@ export class PngLastModTimeEvent extends Event {
262263
}
263264
}
264265

266+
export const PngUnitSpecifier = {
267+
UNKNOWN: 0,
268+
METRE: 1,
269+
};
270+
271+
/**
272+
* @typedef PngPhysicalPixelDimensions
273+
* @property {number} pixelPerUnitX
274+
* @property {number} pixelPerUnitY
275+
* @property {PngUnitSpecifier} unitSpecifier
276+
*/
277+
278+
export class PngPhysicalPixelDimensionsEvent extends Event {
279+
/** @param {PngPhysicalPixelDimensions} physicalPixelDimensions */
280+
constructor(physicalPixelDimensions) {
281+
super(PngParseEventType.pHYs);
282+
/** @type {PngPhysicalPixelDimensions} */
283+
this.physicalPixelDimensions = physicalPixelDimensions;
284+
}
285+
}
286+
265287
/**
266288
* @typedef PngChunk Internal use only.
267289
* @property {number} length
@@ -368,8 +390,8 @@ export class PngParser extends EventTarget {
368390
}
369391

370392
/**
371-
* Type-safe way to bind a listener for a PngLastModTime.
372-
* @param {function(PngLastModTime): void} listener
393+
* Type-safe way to bind a listener for a PngLastModTimeEvent.
394+
* @param {function(PngLastModTimeEvent): void} listener
373395
* @returns {PngParser} for chaining
374396
*/
375397
onLastModTime(listener) {
@@ -387,6 +409,16 @@ export class PngParser extends EventTarget {
387409
return this;
388410
}
389411

412+
/**
413+
* Type-safe way to bind a listener for a PngPhysicalPixelDimensionsEvent.
414+
* @param {function(PngPhysicalPixelDimensionsEvent): void} listener
415+
* @returns {PngParser} for chaining
416+
*/
417+
onPhysicalPixelDimensions(listener) {
418+
super.addEventListener(PngParseEventType.pHYs, listener);
419+
return this;
420+
}
421+
390422
/**
391423
* Type-safe way to bind a listener for a PngSignificantBitsEvent.
392424
* @param {function(PngSignificantBitsEvent): void} listener
@@ -571,6 +603,21 @@ export class PngParser extends EventTarget {
571603
this.dispatchEvent(new PngPaletteEvent(this.palette));
572604
break;
573605

606+
// https://www.w3.org/TR/png-3/#11pHYs
607+
case 'pHYs':
608+
/** @type {physicalPixelDimensions} */
609+
const pixelDims = {
610+
pixelPerUnitX: chStream.readNumber(4),
611+
pixelPerUnitY: chStream.readNumber(4),
612+
unitSpecifier: chStream.readNumber(1),
613+
};
614+
if (!Object.values(PngUnitSpecifier).includes(pixelDims.unitSpecifier)) {
615+
throw `Bad pHYs unit specifier: ${pixelDims.unitSpecifier}`;
616+
}
617+
618+
this.dispatchEvent(new PngPhysicalPixelDimensionsEvent(pixelDims));
619+
break;
620+
574621
// https://www.w3.org/TR/png-3/#11tEXt
575622
case 'tEXt':
576623
const byteArr = chStream.peekBytes(length);
@@ -750,7 +797,10 @@ async function main() {
750797
// console.dir(evt.backgroundColor);
751798
});
752799
parser.onLastModTime(evt => {
753-
console.dir(evt.lastModTime);
800+
// console.dir(evt.lastModTime);
801+
});
802+
parser.onPhysicalPixelDimensions(evt => {
803+
// console.dir(evt.physicalPixelDimensions);
754804
});
755805

756806
try {

tests/image-parsers-png.spec.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as fs from 'node:fs';
22
import 'mocha';
33
import { expect } from 'chai';
4-
import { PngColorType, PngInterlaceMethod, PngParser } from '../image/parsers/png.js';
4+
import { PngColorType, PngInterlaceMethod, PngUnitSpecifier, PngParser } from '../image/parsers/png.js';
55

66
/** @typedef {import('../image/parsers/png.js').PngBackgroundColor} PngBackgroundColor */
77
/** @typedef {import('../image/parsers/png.js').PngChromaticies} PngChromaticies */
@@ -12,6 +12,7 @@ import { PngColorType, PngInterlaceMethod, PngParser } from '../image/parsers/pn
1212
/** @typedef {import('../image/parsers/png.js').PngIntlTextualData} PngIntlTextualData */
1313
/** @typedef {import('../image/parsers/png.js').PngLastModTime} PngLastModTime */
1414
/** @typedef {import('../image/parsers/png.js').PngPalette} PngPalette */
15+
/** @typedef {import('../image/parsers/png.js').PngPhysicalPixelDimensions} PngPhysicalPixelDimensions */
1516
/** @typedef {import('../image/parsers/png.js').PngSignificantBits} PngSignificantBits */
1617
/** @typedef {import('../image/parsers/png.js').PngTextualData} PngTextualData */
1718
/** @typedef {import('../image/parsers/png.js').PngTransparency} PngTransparency */
@@ -248,4 +249,15 @@ describe('bitjs.image.parsers.PngParser', () => {
248249
expect(lastModTime.minute).equals(59);
249250
expect(lastModTime.second).equals(59);
250251
});
252+
253+
it('extracts pHYs', async () => {
254+
/** @type {PngPhysicalPixelDimensions} */
255+
let pixelDims;
256+
await getPngParser('tests/image-testfiles/cdun2c08.png')
257+
.onPhysicalPixelDimensions(evt => { pixelDims = evt.physicalPixelDimensions })
258+
.start();
259+
expect(pixelDims.pixelPerUnitX).equals(1000);
260+
expect(pixelDims.pixelPerUnitY).equals(1000);
261+
expect(pixelDims.unitSpecifier).equals(PngUnitSpecifier.METRE);
262+
});
251263
});

tests/image-testfiles/cdun2c08.png

724 Bytes
Loading

0 commit comments

Comments
 (0)