diff --git a/image/parsers/png.js b/image/parsers/png.js index 3b4380a..f5c3399 100644 --- a/image/parsers/png.js +++ b/image/parsers/png.js @@ -10,11 +10,14 @@ import * as fs from 'node:fs'; // TODO: Remove. import { ByteStream } from '../../io/bytestream.js'; +import { getExifProfile } from './exif.js'; + +/** @typedef {import('./exif.js').ExifValue} ExifValue */ // https://www.w3.org/TR/png-3/ // https://en.wikipedia.org/wiki/PNG#File_format -// TODO: Ancillary chunks eXIf, hIST, sPLT. +// TODO: Ancillary chunks: hIST, sPLT. // let DEBUG = true; let DEBUG = false; @@ -30,6 +33,7 @@ export const PngParseEventType = { // Ancillary chunks. bKGD: 'background_color', cHRM: 'chromaticities_white_point', + eXIf: 'exif_profile', gAMA: 'image_gamma', iTXt: 'intl_text_data', pHYs: 'physical_pixel_dims', @@ -284,6 +288,17 @@ export class PngPhysicalPixelDimensionsEvent extends Event { } } +/** @typedef {Map} PngExifProfile */ + +export class PngExifProfileEvent extends Event { + /** @param {PngExifProfile} exifProfile */ + constructor(exifProfile) { + super(PngParseEventType.eXIf); + /** @type {PngExifProfile} */ + this.exifProfile = exifProfile; + } +} + /** * @typedef PngChunk Internal use only. * @property {number} length @@ -349,6 +364,16 @@ export class PngParser extends EventTarget { return this; } + /** + * Type-safe way to bind a listener for a PngExifProfileEvent. + * @param {function(PngExifProfileEvent): void} listener + * @returns {PngParser} for chaining + */ + onExifProfile(listener) { + super.addEventListener(PngParseEventType.eXIf, listener); + return this; + } + /** * Type-safe way to bind a listener for a PngImageGammaEvent. * @param {function(PngImageGammaEvent): void} listener @@ -715,6 +740,12 @@ export class PngParser extends EventTarget { this.dispatchEvent(new PngIntlTextualDataEvent(intlTextData)); break; + // https://www.w3.org/TR/png-3/#eXIf + case 'eXIf': + const exifValueMap = getExifProfile(chStream); + this.dispatchEvent(new PngExifProfileEvent(exifValueMap)); + break; + // https://www.w3.org/TR/png-3/#11IDAT case 'IDAT': /** @type {PngImageData} */ @@ -802,6 +833,9 @@ async function main() { parser.onPhysicalPixelDimensions(evt => { // console.dir(evt.physicalPixelDimensions); }); + parser.onExifProfile(evt => { + // console.dir(evt.exifProfile); + }); try { await parser.start(); diff --git a/tests/image-parsers-png.spec.js b/tests/image-parsers-png.spec.js index 0da7b94..815cacf 100644 --- a/tests/image-parsers-png.spec.js +++ b/tests/image-parsers-png.spec.js @@ -2,6 +2,9 @@ import * as fs from 'node:fs'; import 'mocha'; import { expect } from 'chai'; import { PngColorType, PngInterlaceMethod, PngUnitSpecifier, PngParser } from '../image/parsers/png.js'; +import { ExifDataFormat, ExifTagNumber } from '../image/parsers/exif.js'; + +/** @typedef {import('../image/parsers/exif.js').ExifValue} ExifValue */ /** @typedef {import('../image/parsers/png.js').PngBackgroundColor} PngBackgroundColor */ /** @typedef {import('../image/parsers/png.js').PngChromaticies} PngChromaticies */ @@ -260,4 +263,16 @@ describe('bitjs.image.parsers.PngParser', () => { expect(pixelDims.pixelPerUnitY).equals(1000); expect(pixelDims.unitSpecifier).equals(PngUnitSpecifier.METRE); }); + + it('extracts eXIf', async () => { + /** @type {PngPhysicalPixelDimensions} */ + let exif; + await getPngParser('tests/image-testfiles/exif2c08.png') + .onExifProfile(evt => { exif = evt.exifProfile }) + .start(); + + const descVal = exif.get(ExifTagNumber.COPYRIGHT); + expect(descVal.dataFormat).equals(ExifDataFormat.ASCII_STRING); + expect(descVal.stringValue).equals('2017 Willem van Schaik'); + }); }); diff --git a/tests/image-testfiles/exif2c08.png b/tests/image-testfiles/exif2c08.png new file mode 100644 index 0000000..56eb734 Binary files /dev/null and b/tests/image-testfiles/exif2c08.png differ