From 466e0430497f1ded75d0af54731b9454d75ab63e Mon Sep 17 00:00:00 2001 From: FAL Date: Mon, 22 Dec 2025 14:20:20 +0900 Subject: [PATCH 01/17] =?UTF-8?q?build:=20publish=E5=AF=BE=E8=B1=A1?= =?UTF-8?q?=E3=81=AEfiles=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 40a5d15f1..1ee959288 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,9 @@ "LICENSE.txt", "package.json", "README.md", - "MODIFICATIONS.md" + "MODIFICATIONS.md", + "tsconfig.json", + "yarn.lock" ], "engines": { "node": ">=20" @@ -135,4 +137,4 @@ "javascript", "library" ] -} \ No newline at end of file +} From ec88a40e74ff1a07440b78ca1a46eb3ed7380a5f Mon Sep 17 00:00:00 2001 From: FAL Date: Mon, 22 Dec 2025 17:46:04 +0900 Subject: [PATCH 02/17] refactor: rename local variables --- src/utils/png.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/utils/png.ts b/src/utils/png.ts index bdbf72c12..a5977204e 100644 --- a/src/utils/png.ts +++ b/src/utils/png.ts @@ -67,17 +67,17 @@ export class PNG { readonly bitsPerComponent: number; private constructor(pngData: Uint8Array) { - let upng: ReturnType; + let decoded: ReturnType; try { // @ts-ignore : It internally does new Uint8Array() - upng = UPNG.decode(pngData); + decoded = UPNG.decode(pngData); } catch (error) { throw mapUpngError(error, 'Failed to decode PNG:'); } let frames: ArrayBuffer[]; try { - frames = UPNG.toRGBA8(upng); + frames = UPNG.toRGBA8(decoded); } catch (error) { throw mapUpngError(error, 'Failed to convert PNG to RGBA8:'); } @@ -85,8 +85,9 @@ export class PNG { if (frames.length > 1) { throw new AnimatedPngNotSupportedError(); } + const rgbaBuffer = frames[0]; - const frame = new Uint8Array(frames[0]); + const frame = new Uint8Array(rgbaBuffer); const { rgbChannel, alphaChannel } = splitAlphaChannel(frame); this.rgbChannel = rgbChannel; @@ -94,10 +95,10 @@ export class PNG { const hasAlphaValues = alphaChannel.some((a) => a < 255); if (hasAlphaValues) this.alphaChannel = alphaChannel; - this.type = getImageType(upng.ctype); + this.type = getImageType(decoded.ctype); - this.width = upng.width; - this.height = upng.height; + this.width = decoded.width; + this.height = decoded.height; this.bitsPerComponent = 8; } } From f5a94a44ae2e776728787019fd63590dbdeb0254 Mon Sep 17 00:00:00 2001 From: FAL Date: Mon, 22 Dec 2025 17:48:39 +0900 Subject: [PATCH 03/17] refactor: improve local error wrapper --- src/utils/png.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/utils/png.ts b/src/utils/png.ts index a5977204e..427a9738a 100644 --- a/src/utils/png.ts +++ b/src/utils/png.ts @@ -13,9 +13,13 @@ const UPNG: typeof UPNGModule = (UPNGModule as any)?.default ?? (UPNGModule as any); -const mapUpngError = (error: unknown, msgPrefix: string): InvalidPngError => { +const mapPngError = (error: unknown, msgPrefix: string): InvalidPngError => { const message = - error instanceof Error ? `${error.name}: ${error.message}` : String(error); + error instanceof InvalidPngError + ? error.message + : error instanceof Error + ? `${error.name}: ${error.message}` + : String(error); return new InvalidPngError(`${msgPrefix} ${message}`); }; @@ -72,14 +76,14 @@ export class PNG { // @ts-ignore : It internally does new Uint8Array() decoded = UPNG.decode(pngData); } catch (error) { - throw mapUpngError(error, 'Failed to decode PNG:'); + throw mapPngError(error, 'Failed to decode PNG:'); } let frames: ArrayBuffer[]; try { frames = UPNG.toRGBA8(decoded); } catch (error) { - throw mapUpngError(error, 'Failed to convert PNG to RGBA8:'); + throw mapPngError(error, 'Failed to convert PNG to RGBA8:'); } if (frames.length > 1) { From 5d902e94f12c0f8b71f3bd5efc2ebfa6ff80029a Mon Sep 17 00:00:00 2001 From: FAL Date: Mon, 22 Dec 2025 17:50:45 +0900 Subject: [PATCH 04/17] =?UTF-8?q?fix:=20Animated=20PNG=20=E3=82=92?= =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=AB=E3=81=9B=E3=81=9A=E5=87=A6?= =?UTF-8?q?=E7=90=86=E7=B6=9A=E8=A1=8C=E3=81=99=E3=82=8B=E3=82=88=E3=81=86?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/errors.ts | 9 --------- src/utils/png.ts | 9 +-------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 09a84136f..1aaac080b 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -49,15 +49,6 @@ export class InvalidPngError extends PDFLibUtilsError { } } -export class AnimatedPngNotSupportedError extends PDFLibUtilsError { - constructor() { - super( - PDFLibErrorTypes.UNSUPPORTED_EXTERNAL_BINARY_DATA, - 'Animated PNGs are not supported', - ); - } -} - export class InvalidOptionPassedError extends PDFLibUtilsError { constructor(valueName: string, allowedValues: Primitive[], actual: any) { const allowed = allowedValues.map(formatValue).join(' or '); diff --git a/src/utils/png.ts b/src/utils/png.ts index 427a9738a..34d0494a2 100644 --- a/src/utils/png.ts +++ b/src/utils/png.ts @@ -1,8 +1,5 @@ import UPNGModule from '@pdf-lib/upng'; -import { - AnimatedPngNotSupportedError, - InvalidPngError, -} from 'src/utils/errors.js'; +import { InvalidPngError } from 'src/utils/errors.js'; /** * UPNGModule has different shapes depending on the bundler / module system. @@ -85,10 +82,6 @@ export class PNG { } catch (error) { throw mapPngError(error, 'Failed to convert PNG to RGBA8:'); } - - if (frames.length > 1) { - throw new AnimatedPngNotSupportedError(); - } const rgbaBuffer = frames[0]; const frame = new Uint8Array(rgbaBuffer); From 6dd24e83c37f2944608617aba772add5b538d253 Mon Sep 17 00:00:00 2001 From: FAL Date: Mon, 22 Dec 2025 18:02:16 +0900 Subject: [PATCH 05/17] refactor: rename local variables --- src/utils/png.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/png.ts b/src/utils/png.ts index 34d0494a2..92f7684fc 100644 --- a/src/utils/png.ts +++ b/src/utils/png.ts @@ -82,10 +82,10 @@ export class PNG { } catch (error) { throw mapPngError(error, 'Failed to convert PNG to RGBA8:'); } - const rgbaBuffer = frames[0]; + const frame = frames[0]; + const rgbaBuffer = new Uint8Array(frame); - const frame = new Uint8Array(rgbaBuffer); - const { rgbChannel, alphaChannel } = splitAlphaChannel(frame); + const { rgbChannel, alphaChannel } = splitAlphaChannel(rgbaBuffer); this.rgbChannel = rgbChannel; From d6632756557e3851e3bf6dec2adcce33dd129712 Mon Sep 17 00:00:00 2001 From: FAL Date: Mon, 22 Dec 2025 21:14:02 +0900 Subject: [PATCH 06/17] fix: improve PNG handling by introducing `fast-png` library --- package.json | 1 + src/utils/fast-png-helper.ts | 188 +++++++++++++++++++++++++++++++++++ src/utils/png.ts | 39 +++----- yarn.lock | 64 ++++++++++++ 4 files changed, 267 insertions(+), 25 deletions(-) create mode 100644 src/utils/fast-png-helper.ts diff --git a/package.json b/package.json index 1ee959288..f00ecb0f0 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "@pdf-lib/standard-fonts": "^1.0.0", "@pdf-lib/upng": "^1.0.1", "crypto-js": "^4.2.0", + "fast-png": "^8.0.0", "pako": "^2.1.0", "tslib": "^2.8.1" }, diff --git a/src/utils/fast-png-helper.ts b/src/utils/fast-png-helper.ts new file mode 100644 index 000000000..2b2fe653c --- /dev/null +++ b/src/utils/fast-png-helper.ts @@ -0,0 +1,188 @@ +import { convertIndexedToRgb, type DecodedPng } from 'fast-png'; +import { InvalidPngError } from 'src/utils/errors.js'; + +const toByte = (value: number, depth: number) => { + if (depth === 8) return value; + if (depth === 16) return Math.round(value / 257); + const maxValue = (1 << depth) - 1; + return Math.round((value * 255) / maxValue); +}; + +const unpackSamples = (image: DecodedPng): Uint8Array | Uint16Array => { + const { data, depth, width, height, channels } = image; + if (depth === 16) { + if (data instanceof Uint16Array) return data; + return new Uint16Array(data.buffer, data.byteOffset, data.byteLength / 2); + } + + if (depth === 8) { + if (data instanceof Uint8Array) return data; + return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); + } + + const raw = + data instanceof Uint8Array + ? data + : new Uint8Array(data.buffer, data.byteOffset, data.byteLength); + const pixelCount = width * height; + const samplesPerPixel = channels; + const samplesPerRow = width * samplesPerPixel; + const samples = new Uint8Array(pixelCount * samplesPerPixel); + const mask = (1 << depth) - 1; + + let dataIndex = 0; + let sampleIndex = 0; + + for (let row = 0; row < height; row++) { + let bitsRemaining = 0; + let currentByte = 0; + + for (let sample = 0; sample < samplesPerRow; sample++) { + if (bitsRemaining < depth) { + currentByte = raw[dataIndex++]; + bitsRemaining = 8; + } + + bitsRemaining -= depth; + samples[sampleIndex++] = (currentByte >> bitsRemaining) & mask; + } + } + + return samples; +}; + +const expandIndexedToRgba = (image: DecodedPng) => { + if (!image.palette || image.palette.length === 0) { + throw new InvalidPngError('Indexed PNG is missing a color palette'); + } + + const paletteChannels = image.palette[0].length; + if (paletteChannels !== 3 && paletteChannels !== 4) { + throw new InvalidPngError( + `Unsupported palette channel count: ${paletteChannels}`, + ); + } + + const paletteData = convertIndexedToRgb(image); + const pixelCount = image.width * image.height; + const rgba = new Uint8Array(pixelCount * 4); + + for (let i = 0; i < pixelCount; i++) { + const paletteOffset = i * paletteChannels; + const rgbaOffset = i * 4; + + rgba[rgbaOffset] = paletteData[paletteOffset]; + rgba[rgbaOffset + 1] = paletteData[paletteOffset + 1]; + rgba[rgbaOffset + 2] = paletteData[paletteOffset + 2]; + rgba[rgbaOffset + 3] = + paletteChannels === 4 ? paletteData[paletteOffset + 3] : 255; + } + + return rgba; +}; + +export const toRgba8 = (image: DecodedPng): Uint8Array => { + if (image.palette) return expandIndexedToRgba(image); + + const samples = unpackSamples(image); + const { channels, depth, width, height, transparency } = image; + const pixelCount = width * height; + + const expectedSamples = pixelCount * channels; + if (samples.length < expectedSamples) { + throw new InvalidPngError( + `PNG data is truncated (expected at least ${expectedSamples} samples, got ${samples.length})`, + ); + } + + const rgba = new Uint8Array(pixelCount * 4); + + switch (channels) { + case 4: { + for (let i = 0; i < pixelCount; i++) { + const sampleOffset = i * 4; + const rgbaOffset = i * 4; + + rgba[rgbaOffset] = toByte(samples[sampleOffset], depth); + rgba[rgbaOffset + 1] = toByte(samples[sampleOffset + 1], depth); + rgba[rgbaOffset + 2] = toByte(samples[sampleOffset + 2], depth); + rgba[rgbaOffset + 3] = toByte(samples[sampleOffset + 3], depth); + } + break; + } + + case 3: { + const transparentColor = + transparency && transparency.length >= 3 + ? [ + toByte(transparency[0], depth), + toByte(transparency[1], depth), + toByte(transparency[2], depth), + ] + : undefined; + + for (let i = 0; i < pixelCount; i++) { + const sampleOffset = i * 3; + const rgbaOffset = i * 4; + + const r = toByte(samples[sampleOffset], depth); + const g = toByte(samples[sampleOffset + 1], depth); + const b = toByte(samples[sampleOffset + 2], depth); + + rgba[rgbaOffset] = r; + rgba[rgbaOffset + 1] = g; + rgba[rgbaOffset + 2] = b; + rgba[rgbaOffset + 3] = + transparentColor && + r === transparentColor[0] && + g === transparentColor[1] && + b === transparentColor[2] + ? 0 + : 255; + } + break; + } + + case 2: { + for (let i = 0; i < pixelCount; i++) { + const sampleOffset = i * 2; + const rgbaOffset = i * 4; + + const gray = toByte(samples[sampleOffset], depth); + const alpha = toByte(samples[sampleOffset + 1], depth); + + rgba[rgbaOffset] = gray; + rgba[rgbaOffset + 1] = gray; + rgba[rgbaOffset + 2] = gray; + rgba[rgbaOffset + 3] = alpha; + } + break; + } + + case 1: { + const transparentSample = + transparency && transparency.length > 0 + ? toByte(transparency[0], depth) + : undefined; + + for (let i = 0; i < pixelCount; i++) { + const rgbaOffset = i * 4; + const gray = toByte(samples[i], depth); + + rgba[rgbaOffset] = gray; + rgba[rgbaOffset + 1] = gray; + rgba[rgbaOffset + 2] = gray; + rgba[rgbaOffset + 3] = + transparentSample !== undefined && gray === transparentSample + ? 0 + : 255; + } + break; + } + + default: + throw new InvalidPngError(`Unknown channel count: ${channels}`); + } + + return rgba; +}; diff --git a/src/utils/png.ts b/src/utils/png.ts index 92f7684fc..088cc9307 100644 --- a/src/utils/png.ts +++ b/src/utils/png.ts @@ -1,15 +1,7 @@ -import UPNGModule from '@pdf-lib/upng'; +import { decode } from 'fast-png'; +import { toRgba8 } from 'src/utils/fast-png-helper.js'; import { InvalidPngError } from 'src/utils/errors.js'; -/** - * UPNGModule has different shapes depending on the bundler / module system. - * The following attempts to cover the most common cases. - */ -const UPNG: typeof UPNGModule = - (UPNGModule as any)?.default?.default ?? - (UPNGModule as any)?.default ?? - (UPNGModule as any); - const mapPngError = (error: unknown, msgPrefix: string): InvalidPngError => { const message = error instanceof InvalidPngError @@ -20,13 +12,13 @@ const mapPngError = (error: unknown, msgPrefix: string): InvalidPngError => { return new InvalidPngError(`${msgPrefix} ${message}`); }; -const getImageType = (ctype: number) => { - if (ctype === 0) return PngType.Greyscale; - if (ctype === 2) return PngType.Truecolour; - if (ctype === 3) return PngType.IndexedColour; - if (ctype === 4) return PngType.GreyscaleWithAlpha; - if (ctype === 6) return PngType.TruecolourWithAlpha; - throw new InvalidPngError(`Unknown color type: ${ctype}`); +const getImageType = (channels: number, palette?: Array): PngType => { + if (palette) return PngType.IndexedColour; + if (channels === 1) return PngType.Greyscale; + if (channels === 2) return PngType.GreyscaleWithAlpha; + if (channels === 3) return PngType.Truecolour; + if (channels === 4) return PngType.TruecolourWithAlpha; + throw new InvalidPngError(`Unknown channel count: ${channels}`); }; const splitAlphaChannel = (rgbaChannel: Uint8Array) => { @@ -68,22 +60,19 @@ export class PNG { readonly bitsPerComponent: number; private constructor(pngData: Uint8Array) { - let decoded: ReturnType; + let decoded: ReturnType; try { - // @ts-ignore : It internally does new Uint8Array() - decoded = UPNG.decode(pngData); + decoded = decode(pngData); } catch (error) { throw mapPngError(error, 'Failed to decode PNG:'); } - let frames: ArrayBuffer[]; + let rgbaBuffer: Uint8Array; try { - frames = UPNG.toRGBA8(decoded); + rgbaBuffer = toRgba8(decoded); } catch (error) { throw mapPngError(error, 'Failed to convert PNG to RGBA8:'); } - const frame = frames[0]; - const rgbaBuffer = new Uint8Array(frame); const { rgbChannel, alphaChannel } = splitAlphaChannel(rgbaBuffer); @@ -92,7 +81,7 @@ export class PNG { const hasAlphaValues = alphaChannel.some((a) => a < 255); if (hasAlphaValues) this.alphaChannel = alphaChannel; - this.type = getImageType(decoded.ctype); + this.type = getImageType(decoded.channels, decoded.palette); this.width = decoded.width; this.height = decoded.height; diff --git a/yarn.lock b/yarn.lock index d39adf51b..3b3dfe779 100644 --- a/yarn.lock +++ b/yarn.lock @@ -959,11 +959,66 @@ expect-type@^1.2.2: resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.3.0.tgz#0d58ed361877a31bbc4dd6cf71bbfef7faf6bd68" integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA== +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@^29.0.0, expect@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" + integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== + dependencies: + "@jest/expect-utils" "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-png@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/fast-png/-/fast-png-8.0.0.tgz#99e1070786cd3fda92895dce3b32295fcd632ad8" + integrity sha512-gCysNasJ8KEMgfdYIKd/wTDo6ENK1PWT0RJO7O+0pgmuHPw2O6tA1WvdxFRJoLf9V8yFYpG0FA1YgI8X97OhJA== + dependencies: + fflate "^0.8.2" + iobuffer "^6.0.1" + +fb-watchman@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + dependencies: + bser "2.1.1" + fdir@^6.2.0, fdir@^6.5.0: version "6.5.0" resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== +fflate@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" + integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" flamebearer@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/flamebearer/-/flamebearer-1.1.3.tgz#aac0cb56305e1af2b16528aca649c76103f75c08" @@ -1187,6 +1242,15 @@ ini@^4.1.3: resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.3.tgz#4c359675a6071a46985eb39b14e4a2c0ec98a795" integrity sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg== +iobuffer@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/iobuffer/-/iobuffer-6.0.1.tgz#e550d671384362d541b5a23e1a81ed46ecab36d8" + integrity sha512-SZWYkWNfjIXIBYSDpXDYIgshqtbOPsi4lviawAEceR1Kqk+sHDlcQjWrzNQsii80AyBY0q5c8HCTNjqo74ul+Q== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== is-core-module@^2.16.1: version "2.16.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" From da03de016a61a5d08df74258d38513a25926d404 Mon Sep 17 00:00:00 2001 From: FAL Date: Tue, 6 Jan 2026 14:37:05 +0900 Subject: [PATCH 07/17] =?UTF-8?q?build:=20publish=E5=AF=BE=E8=B1=A1?= =?UTF-8?q?=E3=81=AEfiles=E3=81=AE=E8=BF=BD=E5=8A=A0=E3=82=92revert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/package.json b/package.json index f00ecb0f0..5c9405073 100644 --- a/package.json +++ b/package.json @@ -81,9 +81,7 @@ "LICENSE.txt", "package.json", "README.md", - "MODIFICATIONS.md", - "tsconfig.json", - "yarn.lock" + "MODIFICATIONS.md" ], "engines": { "node": ">=20" From 1351b07935ba26987d31d1a25a1ba83723c7a408 Mon Sep 17 00:00:00 2001 From: FAL Date: Tue, 6 Jan 2026 16:43:51 +0900 Subject: [PATCH 08/17] =?UTF-8?q?test:=20=E7=A0=B4=E6=90=8DPNG=E3=81=A7?= =?UTF-8?q?=E3=83=95=E3=83=AA=E3=83=BC=E3=82=BA=E3=81=97=E3=81=AA=E3=81=84?= =?UTF-8?q?=E3=81=93=E3=81=A8=E3=82=92=E7=A2=BA=E8=AA=8D=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=82=B1=E3=83=BC=E3=82=B9=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/api/PDFDocument.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/api/PDFDocument.spec.ts b/tests/api/PDFDocument.spec.ts index 32e65173c..f9dc05415 100644 --- a/tests/api/PDFDocument.spec.ts +++ b/tests/api/PDFDocument.spec.ts @@ -652,6 +652,18 @@ describe(`PDFDocument`, () => { InvalidPngError, ); }); + + it(`throws an error when the PNG data is truncated`, async () => { + const pdfDoc = await PDFDocument.create(); + const truncatedPng = examplePngImage.slice( + 0, + Math.floor(examplePngImage.length / 2), + ); + + await expect(pdfDoc.embedPng(truncatedPng)).rejects.toThrow( + InvalidPngError, + ); + }); }); describe(`save() method`, () => { From d00b331b8c1cb873ae529a432b43c2678b4c7547 Mon Sep 17 00:00:00 2001 From: FAL Date: Tue, 6 Jan 2026 16:47:02 +0900 Subject: [PATCH 09/17] chore: remove UPNG dependency --- package.json | 1 - yarn.lock | 55 +--------------------------------------------------- 2 files changed, 1 insertion(+), 55 deletions(-) diff --git a/package.json b/package.json index 5c9405073..1e9c61130 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,6 @@ "dependencies": { "@denkiyagi/fontkit": "2.0.4-mod.2025.5", "@pdf-lib/standard-fonts": "^1.0.0", - "@pdf-lib/upng": "^1.0.1", "crypto-js": "^4.2.0", "fast-png": "^8.0.0", "pako": "^2.1.0", diff --git a/yarn.lock b/yarn.lock index 3b3dfe779..edbf2dc93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -217,13 +217,6 @@ dependencies: pako "^1.0.6" -"@pdf-lib/upng@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@pdf-lib/upng/-/upng-1.0.1.tgz#7dc9c636271aca007a9df4deaf2dd7e7960280cb" - integrity sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ== - dependencies: - pako "^1.0.10" - "@rollup/plugin-commonjs@^29.0.0": version "29.0.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.0.tgz#42a6cc0eeb8ae7aabfc6f9bdc28fe22d65abd15a" @@ -959,27 +952,6 @@ expect-type@^1.2.2: resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.3.0.tgz#0d58ed361877a31bbc4dd6cf71bbfef7faf6bd68" integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA== -exit@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" - integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== - -expect@^29.0.0, expect@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" - integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== - dependencies: - "@jest/expect-utils" "^29.7.0" - jest-get-type "^29.6.3" - jest-matcher-utils "^29.7.0" - jest-message-util "^29.7.0" - jest-util "^29.7.0" - -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - fast-png@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/fast-png/-/fast-png-8.0.0.tgz#99e1070786cd3fda92895dce3b32295fcd632ad8" @@ -988,13 +960,6 @@ fast-png@^8.0.0: fflate "^0.8.2" iobuffer "^6.0.1" -fb-watchman@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" - integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== - dependencies: - bser "2.1.1" - fdir@^6.2.0, fdir@^6.5.0: version "6.5.0" resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" @@ -1005,20 +970,6 @@ fflate@^0.8.2: resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -find-up@^4.0.0, find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" flamebearer@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/flamebearer/-/flamebearer-1.1.3.tgz#aac0cb56305e1af2b16528aca649c76103f75c08" @@ -1247,10 +1198,6 @@ iobuffer@^6.0.1: resolved "https://registry.yarnpkg.com/iobuffer/-/iobuffer-6.0.1.tgz#e550d671384362d541b5a23e1a81ed46ecab36d8" integrity sha512-SZWYkWNfjIXIBYSDpXDYIgshqtbOPsi4lviawAEceR1Kqk+sHDlcQjWrzNQsii80AyBY0q5c8HCTNjqo74ul+Q== -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== is-core-module@^2.16.1: version "2.16.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" @@ -1431,7 +1378,7 @@ pako@^0.2.5: resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA== -pako@^1.0.10, pako@^1.0.6: +pako@^1.0.6: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== From aa1a625d77aea655cff97d357fe05ced82979417 Mon Sep 17 00:00:00 2001 From: FAL Date: Tue, 6 Jan 2026 18:48:32 +0900 Subject: [PATCH 10/17] fix: unpackSamples() --- src/utils/fast-png-helper.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/fast-png-helper.ts b/src/utils/fast-png-helper.ts index 2b2fe653c..3e90a809e 100644 --- a/src/utils/fast-png-helper.ts +++ b/src/utils/fast-png-helper.ts @@ -46,6 +46,8 @@ const unpackSamples = (image: DecodedPng): Uint8Array | Uint16Array => { bitsRemaining -= depth; samples[sampleIndex++] = (currentByte >> bitsRemaining) & mask; } + + bitsRemaining = 0; } return samples; From 21545d65813ba05d16933842c2d5358e9dbf24dc Mon Sep 17 00:00:00 2001 From: FAL Date: Wed, 7 Jan 2026 00:38:43 +0900 Subject: [PATCH 11/17] style: code formatting --- src/utils/png.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils/png.ts b/src/utils/png.ts index 088cc9307..fb47be145 100644 --- a/src/utils/png.ts +++ b/src/utils/png.ts @@ -12,7 +12,10 @@ const mapPngError = (error: unknown, msgPrefix: string): InvalidPngError => { return new InvalidPngError(`${msgPrefix} ${message}`); }; -const getImageType = (channels: number, palette?: Array): PngType => { +const getImageType = ( + channels: number, + palette?: unknown[][], +): PngType => { if (palette) return PngType.IndexedColour; if (channels === 1) return PngType.Greyscale; if (channels === 2) return PngType.GreyscaleWithAlpha; From e7460457017960f826812a454854ed06d32e5a30 Mon Sep 17 00:00:00 2001 From: FAL Date: Wed, 7 Jan 2026 00:39:20 +0900 Subject: [PATCH 12/17] =?UTF-8?q?test:=20png.ts=20=E3=81=AE=E3=83=A6?= =?UTF-8?q?=E3=83=8B=E3=83=83=E3=83=88=E3=83=86=E3=82=B9=E3=83=88=E3=81=AB?= =?UTF-8?q?=E3=81=A6=E3=80=81=E6=A7=98=E3=80=85=E3=81=AAPNG=E5=BD=A2?= =?UTF-8?q?=E5=BC=8F=E3=81=AB=E5=AF=BE=E3=81=99=E3=82=8B=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=B1=E3=83=BC=E3=82=B9=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/utils/png.spec.ts | 194 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 193 insertions(+), 1 deletion(-) diff --git a/tests/utils/png.spec.ts b/tests/utils/png.spec.ts index 57f0ea93d..b62c376a9 100644 --- a/tests/utils/png.spec.ts +++ b/tests/utils/png.spec.ts @@ -1,4 +1,5 @@ -import { PNG } from 'src/utils/png.js'; +import { encode } from 'fast-png'; +import { PNG, PngType } from 'src/utils/png.js'; describe(`PNG`, () => { it(`can load images with alpha values greater than 1`, () => { @@ -24,4 +25,195 @@ describe(`PNG`, () => { expect(pngImage.rgbChannel).toEqual(new Uint8Array([255, 120, 80])); expect(pngImage.alphaChannel).toEqual(new Uint8Array([128])); }); + + describe(`color type detection`, () => { + it(`detects greyscale PNGs`, () => { + const pngImage = PNG.load( + encode({ + width: 1, + height: 1, + data: new Uint8Array([32]), + channels: 1, + depth: 8, + }), + ); + + expect(pngImage.type).toBe(PngType.Greyscale); + expect(pngImage.rgbChannel).toEqual(new Uint8Array([32, 32, 32])); + expect(pngImage.alphaChannel).toBeUndefined(); + }); + + it(`detects truecolour PNGs`, () => { + const pngImage = PNG.load( + encode({ + width: 1, + height: 1, + data: new Uint8Array([10, 20, 30]), + channels: 3, + depth: 8, + }), + ); + + expect(pngImage.type).toBe(PngType.Truecolour); + expect(pngImage.rgbChannel).toEqual(new Uint8Array([10, 20, 30])); + expect(pngImage.alphaChannel).toBeUndefined(); + }); + + it(`detects indexed-colour PNGs`, () => { + const pngImage = PNG.load( + encode({ + width: 1, + height: 1, + data: new Uint8Array([0]), + channels: 1, + depth: 8, + palette: [[100, 110, 120]], + }), + ); + + expect(pngImage.type).toBe(PngType.IndexedColour); + expect(pngImage.rgbChannel).toEqual(new Uint8Array([100, 110, 120])); + expect(pngImage.alphaChannel).toBeUndefined(); + }); + + it(`detects greyscale-with-alpha PNGs`, () => { + const pngImage = PNG.load( + encode({ + width: 1, + height: 1, + data: new Uint8Array([40, 200]), + channels: 2, + depth: 8, + }), + ); + + expect(pngImage.type).toBe(PngType.GreyscaleWithAlpha); + expect(pngImage.rgbChannel).toEqual(new Uint8Array([40, 40, 40])); + expect(pngImage.alphaChannel).toEqual(new Uint8Array([200])); + }); + + it(`detects truecolour-with-alpha PNGs`, () => { + const pngImage = PNG.load( + encode({ + width: 1, + height: 1, + data: new Uint8Array([1, 2, 3, 4]), + channels: 4, + depth: 8, + }), + ); + + expect(pngImage.type).toBe(PngType.TruecolourWithAlpha); + expect(pngImage.rgbChannel).toEqual(new Uint8Array([1, 2, 3])); + expect(pngImage.alphaChannel).toEqual(new Uint8Array([4])); + }); + }); + + describe(`alpha channel handling`, () => { + it(`drops fully opaque alpha channels`, () => { + const pngImage = PNG.load( + encode({ + width: 1, + height: 1, + data: new Uint8Array([9, 8, 7, 255]), + channels: 4, + depth: 8, + }), + ); + + expect(pngImage.rgbChannel).toEqual(new Uint8Array([9, 8, 7])); + expect(pngImage.alphaChannel).toBeUndefined(); + }); + + it(`preserves palette alpha for indexed PNGs`, () => { + const pngImage = PNG.load( + encode({ + width: 1, + height: 1, + data: new Uint8Array([0]), + channels: 1, // indexed + depth: 8, + palette: [[12, 34, 56, 128]], + }), + ); + + expect(pngImage.type).toBe(PngType.IndexedColour); + expect(pngImage.rgbChannel).toEqual(new Uint8Array([12, 34, 56])); + expect(pngImage.alphaChannel).toEqual(new Uint8Array([128])); + }); + }); + + describe(`bit depth normalization`, () => { + it(`handles 1-bit PNGs`, () => { + const pngImage = PNG.load( + encode({ + width: 1, + height: 2, + data: new Uint8Array([0b10000000, 0b00000000]), + channels: 1, // greyscale + depth: 1, + }), + ); + + expect(pngImage.rgbChannel).toEqual( + new Uint8Array([255, 255, 255, 0, 0, 0]), + ); + }); + + it(`handles 2-bit PNGs`, () => { + const pngImage = PNG.load( + encode({ + width: 1, + height: 1, + data: new Uint8Array([0b11000000]), + channels: 1, // greyscale + depth: 2, + }), + ); + + expect(pngImage.rgbChannel).toEqual(new Uint8Array([255, 255, 255])); + }); + + it(`handles 4-bit PNGs`, () => { + const pngImage = PNG.load( + encode({ + width: 1, + height: 1, + data: new Uint8Array([0b11110000]), + channels: 1, // greyscale + depth: 4, + }), + ); + + expect(pngImage.rgbChannel).toEqual(new Uint8Array([255, 255, 255])); + }); + + it(`handles 8-bit PNGs`, () => { + const pngImage = PNG.load( + encode({ + width: 1, + height: 1, + data: new Uint8Array([12, 34, 56]), + channels: 3, // RGB + depth: 8, + }), + ); + + expect(pngImage.rgbChannel).toEqual(new Uint8Array([12, 34, 56])); + }); + + it(`handles 16-bit PNGs`, () => { + const pngImage = PNG.load( + encode({ + width: 1, + height: 1, + data: new Uint16Array([0xffff, 0x8000, 0x0000]), + channels: 3, // RGB + depth: 16, + }), + ); + + expect(pngImage.rgbChannel).toEqual(new Uint8Array([255, 128, 0])); + }); + }); }); From c5de710b0921c656d68ef155d743a90c80680ca7 Mon Sep 17 00:00:00 2001 From: FAL Date: Wed, 7 Jan 2026 01:51:00 +0900 Subject: [PATCH 13/17] docs: update changelog --- MODIFICATIONS.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MODIFICATIONS.md b/MODIFICATIONS.md index 61b397aa8..dc7113acc 100644 --- a/MODIFICATIONS.md +++ b/MODIFICATIONS.md @@ -2,10 +2,17 @@ ## [Unreleased] +### Build System Changes + - Native ESM everywhere: all src/tests/apps use `.js`-suffixed local imports; Jest replaced with vitest; CJS build removed. - Build outputs relocated to `dist/`: `dist/es` (native ESM + typings) and `dist/umd` (bundled). - Package now uses conditional exports; consumers must import via the published entry points (`import`/`exports` map) rather than deep-linking files. +### PNG Handling Changes + +- Switched PNG decoding from `@pdf-lib/upng` to `fast-png`, preventing freezes on truncated PNG inputs. +- Animated PNGs are now accepted and decoded as a single image. + ## [1.17.1-mod.2025.8] ### Feature Removals From b5f4afff70415fd81b95bfe4f9d0cde086f5ed6b Mon Sep 17 00:00:00 2001 From: FAL Date: Wed, 7 Jan 2026 01:51:55 +0900 Subject: [PATCH 14/17] v1.17.1-mod.2026.1 --- MODIFICATIONS.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MODIFICATIONS.md b/MODIFICATIONS.md index dc7113acc..638c37f76 100644 --- a/MODIFICATIONS.md +++ b/MODIFICATIONS.md @@ -1,6 +1,6 @@ # Modifications -## [Unreleased] +## [1.17.1-mod.2026.1] ### Build System Changes diff --git a/package.json b/package.json index 1e9c61130..bc1afc8eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@denkiyagi/pdf-lib", - "version": "1.17.1-mod.2025.8", + "version": "1.17.1-mod.2026.1", "description": "A modified version of pdf-lib. Create and modify PDF files with JavaScript", "author": "DenkiYagi Inc. (modified version author), Andrew Dillon (orignal version author)", "contributors": [ From 17fac93f202cb37f04e956183ac65684c1e91e06 Mon Sep 17 00:00:00 2001 From: FAL Date: Wed, 7 Jan 2026 02:09:17 +0900 Subject: [PATCH 15/17] style: code formatting --- src/utils/png.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/utils/png.ts b/src/utils/png.ts index fb47be145..ab419cd87 100644 --- a/src/utils/png.ts +++ b/src/utils/png.ts @@ -12,10 +12,7 @@ const mapPngError = (error: unknown, msgPrefix: string): InvalidPngError => { return new InvalidPngError(`${msgPrefix} ${message}`); }; -const getImageType = ( - channels: number, - palette?: unknown[][], -): PngType => { +const getImageType = (channels: number, palette?: unknown[][]): PngType => { if (palette) return PngType.IndexedColour; if (channels === 1) return PngType.Greyscale; if (channels === 2) return PngType.GreyscaleWithAlpha; From 5d5d9adceb79e90cbf6ee287c213981812ef436c Mon Sep 17 00:00:00 2001 From: FAL Date: Wed, 7 Jan 2026 03:03:09 +0900 Subject: [PATCH 16/17] test: test 2x2 pixels PNG --- tests/utils/png.spec.ts | 349 +++++++++++++++++++++++++++++++--------- 1 file changed, 276 insertions(+), 73 deletions(-) diff --git a/tests/utils/png.spec.ts b/tests/utils/png.spec.ts index b62c376a9..340a72f07 100644 --- a/tests/utils/png.spec.ts +++ b/tests/utils/png.spec.ts @@ -3,109 +3,211 @@ import { PNG, PngType } from 'src/utils/png.js'; describe(`PNG`, () => { it(`can load images with alpha values greater than 1`, () => { - // This Uint8Array contains a PNG image composed of a single pixel. It was - // generated with the following code in a browser: - // ``` - // const ctx = c.getContext('2d'); - // ctx.fillStyle = 'rgba(255, 120, 80, 0.5)'; - // ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); - // ``` - // The pixel has the following values: R=255, G=120, B=80, A=128 - // - // prettier-ignore - const input = new Uint8Array([ - 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, - 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 13, 73, 68, 65, 84, - 24, 87, 99, 248, 95, 17, 208, 0, 0, 6, 137, 2, 72, 25, 58, 220, 62, 0, 0, - 0, 0, 73, 69, 78, 68, 174, 66, 96, 130, - ]); - - const pngImage = PNG.load(input); - - expect(pngImage.rgbChannel).toEqual(new Uint8Array([255, 120, 80])); - expect(pngImage.alphaChannel).toEqual(new Uint8Array([128])); + const pngImage = PNG.load( + encode({ + width: 2, + height: 2, + // prettier-ignore + data: new Uint8Array([ + 255, 120, 80, 128, // pixel 1 + 10, 20, 30, 200, // pixel 2 + 5, 15, 25, 64, // pixel 3 + 250, 240, 230, 5, // pixel 4 + ]), + channels: 4, + depth: 8, + }), + ); + + expect(pngImage.rgbChannel).toEqual( + // prettier-ignore + new Uint8Array([ + 255, 120, 80, // pixel 1 + 10, 20, 30, // pixel 2 + 5, 15, 25, // pixel 3 + 250, 240, 230, // pixel 4 + ]), + ); + expect(pngImage.alphaChannel).toEqual( + // prettier-ignore + new Uint8Array([ + 128, // pixel 1 + 200, // pixel 2 + 64, // pixel 3 + 5, // pixel 4 + ]), + ); }); describe(`color type detection`, () => { it(`detects greyscale PNGs`, () => { const pngImage = PNG.load( encode({ - width: 1, - height: 1, - data: new Uint8Array([32]), + width: 2, + height: 2, + // prettier-ignore + data: new Uint8Array([ + 32, // pixel 1 + 64, // pixel 2 + 96, // pixel 3 + 128, // pixel 4 + ]), channels: 1, depth: 8, }), ); expect(pngImage.type).toBe(PngType.Greyscale); - expect(pngImage.rgbChannel).toEqual(new Uint8Array([32, 32, 32])); + expect(pngImage.rgbChannel).toEqual( + // prettier-ignore + new Uint8Array([ + 32, 32, 32, // pixel 1 + 64, 64, 64, // pixel 2 + 96, 96, 96, // pixel 3 + 128, 128, 128, // pixel 4 + ]), + ); expect(pngImage.alphaChannel).toBeUndefined(); }); it(`detects truecolour PNGs`, () => { const pngImage = PNG.load( encode({ - width: 1, - height: 1, - data: new Uint8Array([10, 20, 30]), + width: 2, + height: 2, + // prettier-ignore + data: new Uint8Array([ + 10, 20, 30, // pixel 1 + 40, 50, 60, // pixel 2 + 70, 80, 90, // pixel 3 + 100, 110, 120, // pixel 4 + ]), channels: 3, depth: 8, }), ); expect(pngImage.type).toBe(PngType.Truecolour); - expect(pngImage.rgbChannel).toEqual(new Uint8Array([10, 20, 30])); + expect(pngImage.rgbChannel).toEqual( + // prettier-ignore + new Uint8Array([ + 10, 20, 30, // pixel 1 + 40, 50, 60, // pixel 2 + 70, 80, 90, // pixel 3 + 100, 110, 120, // pixel 4 + ]), + ); expect(pngImage.alphaChannel).toBeUndefined(); }); it(`detects indexed-colour PNGs`, () => { const pngImage = PNG.load( encode({ - width: 1, - height: 1, - data: new Uint8Array([0]), + width: 2, + height: 2, + // prettier-ignore + data: new Uint8Array([ + 0, // pixel 1 + 1, // pixel 2 + 1, // pixel 3 + 0, // pixel 4 + ]), channels: 1, depth: 8, - palette: [[100, 110, 120]], + palette: [ + [100, 110, 120], + [10, 20, 30], + ], }), ); expect(pngImage.type).toBe(PngType.IndexedColour); - expect(pngImage.rgbChannel).toEqual(new Uint8Array([100, 110, 120])); + expect(pngImage.rgbChannel).toEqual( + // prettier-ignore + new Uint8Array([ + 100, 110, 120, // pixel 1 + 10, 20, 30, // pixel 2 + 10, 20, 30, // pixel 3 + 100, 110, 120, // pixel 4 + ]), + ); expect(pngImage.alphaChannel).toBeUndefined(); }); it(`detects greyscale-with-alpha PNGs`, () => { const pngImage = PNG.load( encode({ - width: 1, - height: 1, - data: new Uint8Array([40, 200]), + width: 2, + height: 2, + // prettier-ignore + data: new Uint8Array([ + 40, 200, // pixel 1 + 50, 255, // pixel 2 + 60, 128, // pixel 3 + 70, 0, // pixel 4 + ]), channels: 2, depth: 8, }), ); expect(pngImage.type).toBe(PngType.GreyscaleWithAlpha); - expect(pngImage.rgbChannel).toEqual(new Uint8Array([40, 40, 40])); - expect(pngImage.alphaChannel).toEqual(new Uint8Array([200])); + expect(pngImage.rgbChannel).toEqual( + // prettier-ignore + new Uint8Array([ + 40, 40, 40, // pixel 1 + 50, 50, 50, // pixel 2 + 60, 60, 60, // pixel 3 + 70, 70, 70, // pixel 4 + ]), + ); + expect(pngImage.alphaChannel).toEqual( + // prettier-ignore + new Uint8Array([ + 200, // pixel 1 + 255, // pixel 2 + 128, // pixel 3 + 0, // pixel 4 + ]), + ); }); it(`detects truecolour-with-alpha PNGs`, () => { const pngImage = PNG.load( encode({ - width: 1, - height: 1, - data: new Uint8Array([1, 2, 3, 4]), + width: 2, + height: 2, + // prettier-ignore + data: new Uint8Array([ + 1, 2, 3, 4, // pixel 1 + 5, 6, 7, 255, // pixel 2 + 8, 9, 10, 0, // pixel 3 + 11, 12, 13, 128, // pixel 4 + ]), channels: 4, depth: 8, }), ); expect(pngImage.type).toBe(PngType.TruecolourWithAlpha); - expect(pngImage.rgbChannel).toEqual(new Uint8Array([1, 2, 3])); - expect(pngImage.alphaChannel).toEqual(new Uint8Array([4])); + expect(pngImage.rgbChannel).toEqual( + // prettier-ignore + new Uint8Array([ + 1, 2, 3, // pixel 1 + 5, 6, 7, // pixel 2 + 8, 9, 10, // pixel 3 + 11, 12, 13, // pixel 4 + ]), + ); + expect(pngImage.alphaChannel).toEqual( + // prettier-ignore + new Uint8Array([ + 4, // pixel 1 + 255, // pixel 2 + 0, // pixel 3 + 128, // pixel 4 + ]), + ); }); }); @@ -113,33 +215,72 @@ describe(`PNG`, () => { it(`drops fully opaque alpha channels`, () => { const pngImage = PNG.load( encode({ - width: 1, - height: 1, - data: new Uint8Array([9, 8, 7, 255]), + width: 2, + height: 2, + // prettier-ignore + data: new Uint8Array([ + 9, 8, 7, 255, // pixel 1 + 1, 2, 3, 255, // pixel 2 + 4, 5, 6, 255, // pixel 3 + 7, 8, 9, 255, // pixel 4 + ]), channels: 4, depth: 8, }), ); - expect(pngImage.rgbChannel).toEqual(new Uint8Array([9, 8, 7])); + expect(pngImage.rgbChannel).toEqual( + // prettier-ignore + new Uint8Array([ + 9, 8, 7, // pixel 1 + 1, 2, 3, // pixel 2 + 4, 5, 6, // pixel 3 + 7, 8, 9, // pixel 4 + ]), + ); expect(pngImage.alphaChannel).toBeUndefined(); }); it(`preserves palette alpha for indexed PNGs`, () => { const pngImage = PNG.load( encode({ - width: 1, - height: 1, - data: new Uint8Array([0]), + width: 2, + height: 2, + // prettier-ignore + data: new Uint8Array([ + 0, // pixel 1 + 1, // pixel 2 + 1, // pixel 3 + 0, // pixel 4 + ]), channels: 1, // indexed depth: 8, - palette: [[12, 34, 56, 128]], + palette: [ + [12, 34, 56, 128], + [98, 76, 54, 255], + ], }), ); expect(pngImage.type).toBe(PngType.IndexedColour); - expect(pngImage.rgbChannel).toEqual(new Uint8Array([12, 34, 56])); - expect(pngImage.alphaChannel).toEqual(new Uint8Array([128])); + expect(pngImage.rgbChannel).toEqual( + // prettier-ignore + new Uint8Array([ + 12, 34, 56, // pixel 1 + 98, 76, 54, // pixel 2 + 98, 76, 54, // pixel 3 + 12, 34, 56, // pixel 4 + ]), + ); + expect(pngImage.alphaChannel).toEqual( + // prettier-ignore + new Uint8Array([ + 128, // pixel 1 + 255, // pixel 2 + 255, // pixel 3 + 128, // pixel 4 + ]), + ); }); }); @@ -147,73 +288,135 @@ describe(`PNG`, () => { it(`handles 1-bit PNGs`, () => { const pngImage = PNG.load( encode({ - width: 1, + width: 2, height: 2, - data: new Uint8Array([0b10000000, 0b00000000]), + // prettier-ignore + data: new Uint8Array([ + 0b10000000, // row 1: pixel 1=1, pixel 2=0 + 0b01000000, // row 2: pixel 3=0, pixel 4=1 + ]), channels: 1, // greyscale depth: 1, }), ); expect(pngImage.rgbChannel).toEqual( - new Uint8Array([255, 255, 255, 0, 0, 0]), + // prettier-ignore + new Uint8Array([ + 255, 255, 255, // pixel 1 + 0, 0, 0, // pixel 2 + 0, 0, 0, // pixel 3 + 255, 255, 255, // pixel 4 + ]), ); }); it(`handles 2-bit PNGs`, () => { const pngImage = PNG.load( encode({ - width: 1, - height: 1, - data: new Uint8Array([0b11000000]), + width: 2, + height: 2, + // prettier-ignore + data: new Uint8Array([ + 0b11000000, // row 1: pixel 1=3, pixel 2=0 + 0b01100000, // row 2: pixel 3=1, pixel 4=2 + ]), channels: 1, // greyscale depth: 2, }), ); - expect(pngImage.rgbChannel).toEqual(new Uint8Array([255, 255, 255])); + expect(pngImage.rgbChannel).toEqual( + // prettier-ignore + new Uint8Array([ + 255, 255, 255, // pixel 1 (3/3) + 0, 0, 0, // pixel 2 (0/3) + 85, 85, 85, // pixel 3 (1/3) + 170, 170, 170, // pixel 4 (2/3) + ]), + ); }); it(`handles 4-bit PNGs`, () => { const pngImage = PNG.load( encode({ - width: 1, - height: 1, - data: new Uint8Array([0b11110000]), + width: 2, + height: 2, + // prettier-ignore + data: new Uint8Array([ + 0b11110000, // row 1: pixel 1=15, pixel 2=0 + 0b10000100, // row 2: pixel 3=8, pixel 4=4 + ]), channels: 1, // greyscale depth: 4, }), ); - expect(pngImage.rgbChannel).toEqual(new Uint8Array([255, 255, 255])); + expect(pngImage.rgbChannel).toEqual( + // prettier-ignore + new Uint8Array([ + 255, 255, 255, // pixel 1 + 0, 0, 0, // pixel 2 + 136, 136, 136, // pixel 3 + 68, 68, 68, // pixel 4 + ]), + ); }); it(`handles 8-bit PNGs`, () => { const pngImage = PNG.load( encode({ - width: 1, - height: 1, - data: new Uint8Array([12, 34, 56]), + width: 2, + height: 2, + // prettier-ignore + data: new Uint8Array([ + 12, 34, 56, // pixel 1 + 78, 90, 12, // pixel 2 + 34, 56, 78, // pixel 3 + 90, 12, 34, // pixel 4 + ]), channels: 3, // RGB depth: 8, }), ); - expect(pngImage.rgbChannel).toEqual(new Uint8Array([12, 34, 56])); + expect(pngImage.rgbChannel).toEqual( + // prettier-ignore + new Uint8Array([ + 12, 34, 56, // pixel 1 + 78, 90, 12, // pixel 2 + 34, 56, 78, // pixel 3 + 90, 12, 34, // pixel 4 + ]), + ); }); it(`handles 16-bit PNGs`, () => { const pngImage = PNG.load( encode({ - width: 1, - height: 1, - data: new Uint16Array([0xffff, 0x8000, 0x0000]), + width: 2, + height: 2, + // prettier-ignore + data: new Uint16Array([ + 0xffff, 0x8000, 0x0000, // pixel 1 + 0x0000, 0xffff, 0x8000, // pixel 2 + 0x8000, 0x0000, 0xffff, // pixel 3 + 0x4000, 0xc000, 0x2000, // pixel 4 + ]), channels: 3, // RGB depth: 16, }), ); - expect(pngImage.rgbChannel).toEqual(new Uint8Array([255, 128, 0])); + expect(pngImage.rgbChannel).toEqual( + // prettier-ignore + new Uint8Array([ + 255, 128, 0, // pixel 1 + 0, 255, 128, // pixel 2 + 128, 0, 255, // pixel 3 + 64, 191, 32, // pixel 4 + ]), + ); }); }); }); From cd43eac662c46eb180870085ceb9c866fe178762 Mon Sep 17 00:00:00 2001 From: FAL Date: Wed, 7 Jan 2026 03:12:46 +0900 Subject: [PATCH 17/17] test: remove redundant case --- tests/utils/png.spec.ts | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/tests/utils/png.spec.ts b/tests/utils/png.spec.ts index 340a72f07..0d1ebf140 100644 --- a/tests/utils/png.spec.ts +++ b/tests/utils/png.spec.ts @@ -2,43 +2,6 @@ import { encode } from 'fast-png'; import { PNG, PngType } from 'src/utils/png.js'; describe(`PNG`, () => { - it(`can load images with alpha values greater than 1`, () => { - const pngImage = PNG.load( - encode({ - width: 2, - height: 2, - // prettier-ignore - data: new Uint8Array([ - 255, 120, 80, 128, // pixel 1 - 10, 20, 30, 200, // pixel 2 - 5, 15, 25, 64, // pixel 3 - 250, 240, 230, 5, // pixel 4 - ]), - channels: 4, - depth: 8, - }), - ); - - expect(pngImage.rgbChannel).toEqual( - // prettier-ignore - new Uint8Array([ - 255, 120, 80, // pixel 1 - 10, 20, 30, // pixel 2 - 5, 15, 25, // pixel 3 - 250, 240, 230, // pixel 4 - ]), - ); - expect(pngImage.alphaChannel).toEqual( - // prettier-ignore - new Uint8Array([ - 128, // pixel 1 - 200, // pixel 2 - 64, // pixel 3 - 5, // pixel 4 - ]), - ); - }); - describe(`color type detection`, () => { it(`detects greyscale PNGs`, () => { const pngImage = PNG.load(