mirror of
https://github.com/codedread/bitjs
synced 2025-10-03 17:49:16 +02:00
PngParser: Add support for sPLT chunk
This commit is contained in:
parent
7b1906a91b
commit
f1096f0ab2
3 changed files with 115 additions and 89 deletions
|
@ -8,7 +8,6 @@
|
|||
* Copyright(c) 2024 Google Inc.
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs'; // TODO: Remove.
|
||||
import { ByteStream } from '../../io/bytestream.js';
|
||||
import { getExifProfile } from './exif.js';
|
||||
|
||||
|
@ -17,10 +16,6 @@ import { getExifProfile } from './exif.js';
|
|||
// https://www.w3.org/TR/png-3/
|
||||
// https://en.wikipedia.org/wiki/PNG#File_format
|
||||
|
||||
// TODO: Ancillary chunks: sPLT.
|
||||
|
||||
// let DEBUG = true;
|
||||
let DEBUG = false;
|
||||
const SIG = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
|
||||
|
||||
/** @enum {string} */
|
||||
|
@ -39,6 +34,7 @@ export const PngParseEventType = {
|
|||
iTXt: 'intl_text_data',
|
||||
pHYs: 'physical_pixel_dims',
|
||||
sBIT: 'significant_bits',
|
||||
sPLT: 'suggested_palette',
|
||||
tEXt: 'textual_data',
|
||||
tIME: 'last_mod_time',
|
||||
tRNS: 'transparency',
|
||||
|
@ -314,6 +310,31 @@ export class PngHistogramEvent extends Event {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef PngSuggestedPaletteEntry
|
||||
* @property {number} red
|
||||
* @property {number} green
|
||||
* @property {number} blue
|
||||
* @property {number} alpha
|
||||
* @property {number} frequency
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef PngSuggestedPalette
|
||||
* @property {string} paletteName
|
||||
* @property {number} sampleDepth Either 8 or 16.
|
||||
* @property {PngSuggestedPaletteEntry[]} entries
|
||||
*/
|
||||
|
||||
export class PngSuggestedPaletteEvent extends Event {
|
||||
/** @param {PngSuggestedPalette} suggestedPalette */
|
||||
constructor(suggestedPalette) {
|
||||
super(PngParseEventType.sPLT);
|
||||
/** @type {PngSuggestedPalette} */
|
||||
this.suggestedPalette = suggestedPalette;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef PngChunk Internal use only.
|
||||
* @property {number} length
|
||||
|
@ -478,6 +499,16 @@ export class PngParser extends EventTarget {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe way to bind a listener for a PngSuggestedPaletteEvent.
|
||||
* @param {function(PngSuggestedPaletteEvent): void} listener
|
||||
* @returns {PngParser} for chaining
|
||||
*/
|
||||
onSuggestedPalette(listener) {
|
||||
super.addEventListener(PngParseEventType.sPLT, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe way to bind a listener for a PngTextualDataEvent.
|
||||
* @param {function(PngTextualDataEvent): void} listener
|
||||
|
@ -784,6 +815,42 @@ export class PngParser extends EventTarget {
|
|||
this.dispatchEvent(new PngHistogramEvent(hist));
|
||||
break;
|
||||
|
||||
// https://www.w3.org/TR/png-3/#11sPLT
|
||||
case 'sPLT':
|
||||
const spByteArr = chStream.peekBytes(length);
|
||||
const spNameNullIndex = spByteArr.indexOf(0);
|
||||
|
||||
/** @type {PngSuggestedPalette} */
|
||||
const sPalette = {
|
||||
paletteName: chStream.readString(spNameNullIndex),
|
||||
sampleDepth: chStream.skip(1).readNumber(1),
|
||||
entries: [],
|
||||
};
|
||||
|
||||
const sampleDepth = sPalette.sampleDepth;
|
||||
if (![8, 16].includes(sampleDepth)) throw `Invalid sPLT sample depth: ${sampleDepth}`;
|
||||
|
||||
const remainingByteLength = length - spNameNullIndex - 1 - 1;
|
||||
const compByteLength = sPalette.sampleDepth === 8 ? 1 : 2;
|
||||
const entryByteLength = 4 * compByteLength + 2;
|
||||
if (remainingByteLength % entryByteLength !== 0) {
|
||||
throw `Invalid # of bytes left in sPLT: ${remainingByteLength}`;
|
||||
}
|
||||
|
||||
const numEntries = remainingByteLength / entryByteLength;
|
||||
for (let e = 0; e < numEntries; ++e) {
|
||||
sPalette.entries.push({
|
||||
red: chStream.readNumber(compByteLength),
|
||||
green: chStream.readNumber(compByteLength),
|
||||
blue: chStream.readNumber(compByteLength),
|
||||
alpha: chStream.readNumber(compByteLength),
|
||||
frequency: chStream.readNumber(2),
|
||||
});
|
||||
}
|
||||
|
||||
this.dispatchEvent(new PngSuggestedPaletteEvent(sPalette));
|
||||
break;
|
||||
|
||||
// https://www.w3.org/TR/png-3/#11IDAT
|
||||
case 'IDAT':
|
||||
/** @type {PngImageData} */
|
||||
|
@ -803,87 +870,3 @@ export class PngParser extends EventTarget {
|
|||
} while (chunk.chunkType !== 'IEND');
|
||||
}
|
||||
}
|
||||
|
||||
const FILES = `PngSuite.png basn0g04.png bggn4a16.png cs8n2c08.png f03n2c08.png g10n3p04.png s01i3p01.png s32i3p04.png tbbn0g04.png xd0n2c08.png
|
||||
basi0g01.png basn0g08.png bgwn6a08.png cs8n3p08.png f04n0g08.png g25n0g16.png s01n3p01.png s32n3p04.png tbbn2c16.png xd3n2c08.png
|
||||
basi0g02.png basn0g16.png bgyn6a16.png ct0n0g04.png f04n2c08.png g25n2c08.png s02i3p01.png s33i3p04.png tbbn3p08.png xd9n2c08.png
|
||||
basi0g04.png basn2c08.png ccwn2c08.png ct1n0g04.png f99n0g04.png g25n3p04.png s02n3p01.png s33n3p04.png tbgn2c16.png xdtn0g01.png
|
||||
basi0g08.png basn2c16.png ccwn3p08.png cten0g04.png g03n0g16.png oi1n0g16.png s03i3p01.png s34i3p04.png tbgn3p08.png xhdn0g08.png
|
||||
basi0g16.png basn3p01.png cdfn2c08.png ctfn0g04.png g03n2c08.png oi1n2c16.png s03n3p01.png s34n3p04.png tbrn2c08.png xlfn0g04.png
|
||||
basi2c08.png basn3p02.png cdhn2c08.png ctgn0g04.png g03n3p04.png oi2n0g16.png s04i3p01.png s35i3p04.png tbwn0g16.png xs1n0g01.png
|
||||
basi2c16.png basn3p04.png cdsn2c08.png cthn0g04.png g04n0g16.png oi2n2c16.png s04n3p01.png s35n3p04.png tbwn3p08.png xs2n0g01.png
|
||||
basi3p01.png basn3p08.png cdun2c08.png ctjn0g04.png g04n2c08.png oi4n0g16.png s05i3p02.png s36i3p04.png tbyn3p08.png xs4n0g01.png
|
||||
basi3p02.png basn4a08.png ch1n3p04.png ctzn0g04.png g04n3p04.png oi4n2c16.png s05n3p02.png s36n3p04.png tm3n3p02.png xs7n0g01.png
|
||||
basi3p04.png basn4a16.png ch2n3p08.png exif2c08.png g05n0g16.png oi9n0g16.png s06i3p02.png s37i3p04.png tp0n0g08.png z00n2c08.png
|
||||
basi3p08.png basn6a08.png cm0n0g04.png f00n0g08.png g05n2c08.png oi9n2c16.png s06n3p02.png s37n3p04.png tp0n2c08.png z03n2c08.png
|
||||
basi4a08.png basn6a16.png cm7n0g04.png f00n2c08.png g05n3p04.png pp0n2c16.png s07i3p02.png s38i3p04.png tp0n3p08.png z06n2c08.png
|
||||
basi4a16.png bgai4a08.png cm9n0g04.png f01n0g08.png g07n0g16.png pp0n6a08.png s07n3p02.png s38n3p04.png tp1n3p08.png z09n2c08.png
|
||||
basi6a08.png bgai4a16.png cs3n2c16.png f01n2c08.png g07n2c08.png ps1n0g08.png s08i3p02.png s39i3p04.png xc1n0g08.png
|
||||
basi6a16.png bgan6a08.png cs3n3p08.png f02n0g08.png g07n3p04.png ps1n2c16.png s08n3p02.png s39n3p04.png xc9n2c08.png
|
||||
basn0g01.png bgan6a16.png cs5n2c08.png f02n2c08.png g10n0g16.png ps2n0g08.png s09i3p02.png s40i3p04.png xcrn0g04.png
|
||||
basn0g02.png bgbn4a08.png cs5n3p08.png f03n0g08.png g10n2c08.png ps2n2c16.png s09n3p02.png s40n3p04.png xcsn0g01.png`
|
||||
.replace(/\s+/g, ' ')
|
||||
.split(' ')
|
||||
.map(fn => `tests/image-testfiles/${fn}`);
|
||||
|
||||
async function main() {
|
||||
for (const fileName of FILES) {
|
||||
console.log(`file: ${fileName}`);
|
||||
const nodeBuf = fs.readFileSync(fileName);
|
||||
const ab = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.length);
|
||||
const parser = new PngParser(ab);
|
||||
parser.onImageHeader(evt => {
|
||||
// console.dir(evt.imageHeader);
|
||||
});
|
||||
parser.onGamma(evt => {
|
||||
// console.dir(evt.imageGamma);
|
||||
});
|
||||
parser.onSignificantBits(evt => {
|
||||
// console.dir(evt.sigBits);
|
||||
});
|
||||
parser.onChromaticities(evt => {
|
||||
// console.dir(evt.chromaticities);
|
||||
});
|
||||
parser.onPalette(evt => {
|
||||
// console.dir(evt.palette);
|
||||
});
|
||||
parser.onTransparency(evt => {
|
||||
// console.dir(evt.transparency);
|
||||
});
|
||||
parser.onImageData(evt => {
|
||||
// console.dir(evt);
|
||||
});
|
||||
parser.onTextualData(evt => {
|
||||
// console.dir(evt.textualData);
|
||||
});
|
||||
parser.onCompressedTextualData(evt => {
|
||||
// console.dir(evt.compressedTextualData);
|
||||
});
|
||||
parser.onIntlTextualData(evt => {
|
||||
// console.dir(evt.intlTextualdata);
|
||||
});
|
||||
parser.onBackgroundColor(evt => {
|
||||
// console.dir(evt.backgroundColor);
|
||||
});
|
||||
parser.onLastModTime(evt => {
|
||||
// console.dir(evt.lastModTime);
|
||||
});
|
||||
parser.onPhysicalPixelDimensions(evt => {
|
||||
// console.dir(evt.physicalPixelDimensions);
|
||||
});
|
||||
parser.onExifProfile(evt => {
|
||||
// console.dir(evt.exifProfile);
|
||||
});
|
||||
parser.onHistogram(evt => {
|
||||
// console.dir(evt.histogram);
|
||||
});
|
||||
|
||||
try {
|
||||
await parser.start();
|
||||
} catch (err) {
|
||||
if (!fileName.startsWith('tests/image-testfiles/x')) throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// main();
|
||||
|
|
|
@ -9,6 +9,7 @@ import { ExifDataFormat, ExifTagNumber } from '../image/parsers/exif.js';
|
|||
/** @typedef {import('../image/parsers/png.js').PngBackgroundColor} PngBackgroundColor */
|
||||
/** @typedef {import('../image/parsers/png.js').PngChromaticies} PngChromaticies */
|
||||
/** @typedef {import('../image/parsers/png.js').PngCompressedTextualData} PngCompressedTextualData */
|
||||
/** @typedef {import('../image/parsers/png.js').PngHistogram} PngHistogram */
|
||||
/** @typedef {import('../image/parsers/png.js').PngImageData} PngImageData */
|
||||
/** @typedef {import('../image/parsers/png.js').PngImageGamma} PngImageGamma */
|
||||
/** @typedef {import('../image/parsers/png.js').PngImageHeader} PngImageHeader */
|
||||
|
@ -17,6 +18,7 @@ import { ExifDataFormat, ExifTagNumber } from '../image/parsers/exif.js';
|
|||
/** @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').PngSuggestedPalette} PngSuggestedPalette */
|
||||
/** @typedef {import('../image/parsers/png.js').PngTextualData} PngTextualData */
|
||||
/** @typedef {import('../image/parsers/png.js').PngTransparency} PngTransparency */
|
||||
|
||||
|
@ -275,4 +277,45 @@ describe('bitjs.image.parsers.PngParser', () => {
|
|||
expect(descVal.dataFormat).equals(ExifDataFormat.ASCII_STRING);
|
||||
expect(descVal.stringValue).equals('2017 Willem van Schaik');
|
||||
});
|
||||
|
||||
it('extracts hIST', async () => {
|
||||
/** @type {PngPalette} */
|
||||
let palette;
|
||||
/** @type {PngHistogram} */
|
||||
let hist;
|
||||
await getPngParser('tests/image-testfiles/ch1n3p04.png')
|
||||
.onHistogram(evt => { hist = evt.histogram })
|
||||
.onPalette(evt => { palette = evt.palette })
|
||||
.start();
|
||||
|
||||
expect(hist.frequencies.length).equals(palette.entries.length);
|
||||
expect(hist.frequencies[0]).equals(64);
|
||||
expect(hist.frequencies[1]).equals(112);
|
||||
});
|
||||
|
||||
it('extracts sPLT', async () => {
|
||||
/** @type {PngSuggestedPalette} */
|
||||
let sPalette;
|
||||
await getPngParser('tests/image-testfiles/ps1n0g08.png')
|
||||
.onSuggestedPalette(evt => { sPalette = evt.suggestedPalette })
|
||||
.start();
|
||||
|
||||
expect(sPalette.entries.length).equals(216);
|
||||
expect(sPalette.paletteName).equals('six-cube');
|
||||
expect(sPalette.sampleDepth).equals(8);
|
||||
|
||||
const entry0 = sPalette.entries[0];
|
||||
expect(entry0.red).equals(0);
|
||||
expect(entry0.green).equals(0);
|
||||
expect(entry0.blue).equals(0);
|
||||
expect(entry0.alpha).equals(255);
|
||||
expect(entry0.frequency).equals(0);
|
||||
|
||||
const entry1 = sPalette.entries[1];
|
||||
expect(entry1.red).equals(0);
|
||||
expect(entry1.green).equals(0);
|
||||
expect(entry1.blue).equals(51);
|
||||
expect(entry1.alpha).equals(255);
|
||||
expect(entry1.frequency).equals(0);
|
||||
});
|
||||
});
|
||||
|
|
BIN
tests/image-testfiles/ps1n0g08.png
Normal file
BIN
tests/image-testfiles/ps1n0g08.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
Loading…
Add table
Add a link
Reference in a new issue