88 * Copyright(c) 2024 Google Inc.
99 */
1010
11- import * as fs from 'node:fs' ; // TODO: Remove.
1211import { ByteStream } from '../../io/bytestream.js' ;
1312import { getExifProfile } from './exif.js' ;
1413
@@ -17,10 +16,6 @@ import { getExifProfile } from './exif.js';
1716// https://www.w3.org/TR/png-3/
1817// https://en.wikipedia.org/wiki/PNG#File_format
1918
20- // TODO: Ancillary chunks: sPLT.
21-
22- // let DEBUG = true;
23- let DEBUG = false ;
2419const SIG = new Uint8Array ( [ 0x89 , 0x50 , 0x4E , 0x47 , 0x0D , 0x0A , 0x1A , 0x0A ] ) ;
2520
2621/** @enum {string} */
@@ -39,6 +34,7 @@ export const PngParseEventType = {
3934 iTXt : 'intl_text_data' ,
4035 pHYs : 'physical_pixel_dims' ,
4136 sBIT : 'significant_bits' ,
37+ sPLT : 'suggested_palette' ,
4238 tEXt : 'textual_data' ,
4339 tIME : 'last_mod_time' ,
4440 tRNS : 'transparency' ,
@@ -314,6 +310,31 @@ export class PngHistogramEvent extends Event {
314310 }
315311}
316312
313+ /**
314+ * @typedef PngSuggestedPaletteEntry
315+ * @property {number } red
316+ * @property {number } green
317+ * @property {number } blue
318+ * @property {number } alpha
319+ * @property {number } frequency
320+ */
321+
322+ /**
323+ * @typedef PngSuggestedPalette
324+ * @property {string } paletteName
325+ * @property {number } sampleDepth Either 8 or 16.
326+ * @property {PngSuggestedPaletteEntry[] } entries
327+ */
328+
329+ export class PngSuggestedPaletteEvent extends Event {
330+ /** @param {PngSuggestedPalette } suggestedPalette */
331+ constructor ( suggestedPalette ) {
332+ super ( PngParseEventType . sPLT ) ;
333+ /** @type {PngSuggestedPalette } */
334+ this . suggestedPalette = suggestedPalette ;
335+ }
336+ }
337+
317338/**
318339 * @typedef PngChunk Internal use only.
319340 * @property {number } length
@@ -478,6 +499,16 @@ export class PngParser extends EventTarget {
478499 return this ;
479500 }
480501
502+ /**
503+ * Type-safe way to bind a listener for a PngSuggestedPaletteEvent.
504+ * @param {function(PngSuggestedPaletteEvent): void } listener
505+ * @returns {PngParser } for chaining
506+ */
507+ onSuggestedPalette ( listener ) {
508+ super . addEventListener ( PngParseEventType . sPLT , listener ) ;
509+ return this ;
510+ }
511+
481512 /**
482513 * Type-safe way to bind a listener for a PngTextualDataEvent.
483514 * @param {function(PngTextualDataEvent): void } listener
@@ -784,6 +815,42 @@ export class PngParser extends EventTarget {
784815 this . dispatchEvent ( new PngHistogramEvent ( hist ) ) ;
785816 break ;
786817
818+ // https://www.w3.org/TR/png-3/#11sPLT
819+ case 'sPLT' :
820+ const spByteArr = chStream . peekBytes ( length ) ;
821+ const spNameNullIndex = spByteArr . indexOf ( 0 ) ;
822+
823+ /** @type {PngSuggestedPalette } */
824+ const sPalette = {
825+ paletteName : chStream . readString ( spNameNullIndex ) ,
826+ sampleDepth : chStream . skip ( 1 ) . readNumber ( 1 ) ,
827+ entries : [ ] ,
828+ } ;
829+
830+ const sampleDepth = sPalette . sampleDepth ;
831+ if ( ! [ 8 , 16 ] . includes ( sampleDepth ) ) throw `Invalid sPLT sample depth: ${ sampleDepth } ` ;
832+
833+ const remainingByteLength = length - spNameNullIndex - 1 - 1 ;
834+ const compByteLength = sPalette . sampleDepth === 8 ? 1 : 2 ;
835+ const entryByteLength = 4 * compByteLength + 2 ;
836+ if ( remainingByteLength % entryByteLength !== 0 ) {
837+ throw `Invalid # of bytes left in sPLT: ${ remainingByteLength } ` ;
838+ }
839+
840+ const numEntries = remainingByteLength / entryByteLength ;
841+ for ( let e = 0 ; e < numEntries ; ++ e ) {
842+ sPalette . entries . push ( {
843+ red : chStream . readNumber ( compByteLength ) ,
844+ green : chStream . readNumber ( compByteLength ) ,
845+ blue : chStream . readNumber ( compByteLength ) ,
846+ alpha : chStream . readNumber ( compByteLength ) ,
847+ frequency : chStream . readNumber ( 2 ) ,
848+ } ) ;
849+ }
850+
851+ this . dispatchEvent ( new PngSuggestedPaletteEvent ( sPalette ) ) ;
852+ break ;
853+
787854 // https://www.w3.org/TR/png-3/#11IDAT
788855 case 'IDAT' :
789856 /** @type {PngImageData } */
@@ -803,87 +870,3 @@ export class PngParser extends EventTarget {
803870 } while ( chunk . chunkType !== 'IEND' ) ;
804871 }
805872}
806-
807- const FILES = `PngSuite.png basn0g04.png bggn4a16.png cs8n2c08.png f03n2c08.png g10n3p04.png s01i3p01.png s32i3p04.png tbbn0g04.png xd0n2c08.png
808- basi0g01.png basn0g08.png bgwn6a08.png cs8n3p08.png f04n0g08.png g25n0g16.png s01n3p01.png s32n3p04.png tbbn2c16.png xd3n2c08.png
809- basi0g02.png basn0g16.png bgyn6a16.png ct0n0g04.png f04n2c08.png g25n2c08.png s02i3p01.png s33i3p04.png tbbn3p08.png xd9n2c08.png
810- basi0g04.png basn2c08.png ccwn2c08.png ct1n0g04.png f99n0g04.png g25n3p04.png s02n3p01.png s33n3p04.png tbgn2c16.png xdtn0g01.png
811- basi0g08.png basn2c16.png ccwn3p08.png cten0g04.png g03n0g16.png oi1n0g16.png s03i3p01.png s34i3p04.png tbgn3p08.png xhdn0g08.png
812- basi0g16.png basn3p01.png cdfn2c08.png ctfn0g04.png g03n2c08.png oi1n2c16.png s03n3p01.png s34n3p04.png tbrn2c08.png xlfn0g04.png
813- basi2c08.png basn3p02.png cdhn2c08.png ctgn0g04.png g03n3p04.png oi2n0g16.png s04i3p01.png s35i3p04.png tbwn0g16.png xs1n0g01.png
814- basi2c16.png basn3p04.png cdsn2c08.png cthn0g04.png g04n0g16.png oi2n2c16.png s04n3p01.png s35n3p04.png tbwn3p08.png xs2n0g01.png
815- basi3p01.png basn3p08.png cdun2c08.png ctjn0g04.png g04n2c08.png oi4n0g16.png s05i3p02.png s36i3p04.png tbyn3p08.png xs4n0g01.png
816- basi3p02.png basn4a08.png ch1n3p04.png ctzn0g04.png g04n3p04.png oi4n2c16.png s05n3p02.png s36n3p04.png tm3n3p02.png xs7n0g01.png
817- basi3p04.png basn4a16.png ch2n3p08.png exif2c08.png g05n0g16.png oi9n0g16.png s06i3p02.png s37i3p04.png tp0n0g08.png z00n2c08.png
818- basi3p08.png basn6a08.png cm0n0g04.png f00n0g08.png g05n2c08.png oi9n2c16.png s06n3p02.png s37n3p04.png tp0n2c08.png z03n2c08.png
819- basi4a08.png basn6a16.png cm7n0g04.png f00n2c08.png g05n3p04.png pp0n2c16.png s07i3p02.png s38i3p04.png tp0n3p08.png z06n2c08.png
820- basi4a16.png bgai4a08.png cm9n0g04.png f01n0g08.png g07n0g16.png pp0n6a08.png s07n3p02.png s38n3p04.png tp1n3p08.png z09n2c08.png
821- basi6a08.png bgai4a16.png cs3n2c16.png f01n2c08.png g07n2c08.png ps1n0g08.png s08i3p02.png s39i3p04.png xc1n0g08.png
822- basi6a16.png bgan6a08.png cs3n3p08.png f02n0g08.png g07n3p04.png ps1n2c16.png s08n3p02.png s39n3p04.png xc9n2c08.png
823- basn0g01.png bgan6a16.png cs5n2c08.png f02n2c08.png g10n0g16.png ps2n0g08.png s09i3p02.png s40i3p04.png xcrn0g04.png
824- basn0g02.png bgbn4a08.png cs5n3p08.png f03n0g08.png g10n2c08.png ps2n2c16.png s09n3p02.png s40n3p04.png xcsn0g01.png`
825- . replace ( / \s + / g, ' ' )
826- . split ( ' ' )
827- . map ( fn => `tests/image-testfiles/${ fn } ` ) ;
828-
829- async function main ( ) {
830- for ( const fileName of FILES ) {
831- console . log ( `file: ${ fileName } ` ) ;
832- const nodeBuf = fs . readFileSync ( fileName ) ;
833- const ab = nodeBuf . buffer . slice ( nodeBuf . byteOffset , nodeBuf . byteOffset + nodeBuf . length ) ;
834- const parser = new PngParser ( ab ) ;
835- parser . onImageHeader ( evt => {
836- // console.dir(evt.imageHeader);
837- } ) ;
838- parser . onGamma ( evt => {
839- // console.dir(evt.imageGamma);
840- } ) ;
841- parser . onSignificantBits ( evt => {
842- // console.dir(evt.sigBits);
843- } ) ;
844- parser . onChromaticities ( evt => {
845- // console.dir(evt.chromaticities);
846- } ) ;
847- parser . onPalette ( evt => {
848- // console.dir(evt.palette);
849- } ) ;
850- parser . onTransparency ( evt => {
851- // console.dir(evt.transparency);
852- } ) ;
853- parser . onImageData ( evt => {
854- // console.dir(evt);
855- } ) ;
856- parser . onTextualData ( evt => {
857- // console.dir(evt.textualData);
858- } ) ;
859- parser . onCompressedTextualData ( evt => {
860- // console.dir(evt.compressedTextualData);
861- } ) ;
862- parser . onIntlTextualData ( evt => {
863- // console.dir(evt.intlTextualdata);
864- } ) ;
865- parser . onBackgroundColor ( evt => {
866- // console.dir(evt.backgroundColor);
867- } ) ;
868- parser . onLastModTime ( evt => {
869- // console.dir(evt.lastModTime);
870- } ) ;
871- parser . onPhysicalPixelDimensions ( evt => {
872- // console.dir(evt.physicalPixelDimensions);
873- } ) ;
874- parser . onExifProfile ( evt => {
875- // console.dir(evt.exifProfile);
876- } ) ;
877- parser . onHistogram ( evt => {
878- // console.dir(evt.histogram);
879- } ) ;
880-
881- try {
882- await parser . start ( ) ;
883- } catch ( err ) {
884- if ( ! fileName . startsWith ( 'tests/image-testfiles/x' ) ) throw err ;
885- }
886- }
887- }
888-
889- // main();
0 commit comments