-
Notifications
You must be signed in to change notification settings - Fork 1
fix: PNG/APNG CRC verification, GIF bounds check, adjustBrightness LUT hoisting, PAM docs #106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
8061186
8341271
54bf326
e583259
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -127,6 +127,7 @@ export class APNGFormat extends PNGBase implements ImageFormat { | |||||||||
| while (pos < data.length) { | ||||||||||
| const length = this.readUint32(data, pos); | ||||||||||
| pos += 4; | ||||||||||
| const typePos = pos; | ||||||||||
| const type = String.fromCharCode( | ||||||||||
| data[pos], | ||||||||||
| data[pos + 1], | ||||||||||
|
|
@@ -137,7 +138,13 @@ export class APNGFormat extends PNGBase implements ImageFormat { | |||||||||
| const chunkData = data.slice(pos, pos + length); | ||||||||||
| const chunkPos = pos; | ||||||||||
| pos += length; | ||||||||||
| pos += 4; // Skip CRC | ||||||||||
| const storedCrc = this.readUint32(data, pos); | ||||||||||
| pos += 4; | ||||||||||
|
|
||||||||||
| // Verify CRC (covers chunk type + chunk data) | ||||||||||
| if (storedCrc !== this.crc32(data.slice(typePos, typePos + 4 + length))) { | ||||||||||
| throw new Error(`PNG chunk '${type}' has invalid CRC`); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| chunkList.push({ type, data: chunkData, pos: chunkPos }); | ||||||||||
|
|
||||||||||
|
|
@@ -522,6 +529,7 @@ export class APNGFormat extends PNGBase implements ImageFormat { | |||||||||
|
|
||||||||||
| const length = this.readUint32(data, pos); | ||||||||||
| pos += 4; | ||||||||||
| const typePos = pos; | ||||||||||
| const type = String.fromCharCode( | ||||||||||
| data[pos], | ||||||||||
| data[pos + 1], | ||||||||||
|
|
@@ -534,7 +542,13 @@ export class APNGFormat extends PNGBase implements ImageFormat { | |||||||||
|
|
||||||||||
| const chunkData = data.slice(pos, pos + length); | ||||||||||
| pos += length; | ||||||||||
| pos += 4; // Skip CRC | ||||||||||
| const storedCrc = this.readUint32(data, pos); | ||||||||||
| pos += 4; | ||||||||||
|
|
||||||||||
| // Verify CRC (covers chunk type + chunk data) | ||||||||||
| if (storedCrc !== this.crc32(data.slice(typePos, typePos + 4 + length))) { | ||||||||||
|
||||||||||
| if (storedCrc !== this.crc32(data.slice(typePos, typePos + 4 + length))) { | |
| if ( | |
| storedCrc !== this.crc32(data.subarray(typePos, typePos + 4 + length)) | |
| ) { |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -60,6 +60,7 @@ export class PNGFormat extends PNGBase implements ImageFormat { | |||||||||
| if (pos + 8 > data.length) break; | ||||||||||
| const length = this.readUint32(data, pos); | ||||||||||
| pos += 4; | ||||||||||
| const typePos = pos; | ||||||||||
| const type = String.fromCharCode( | ||||||||||
| data[pos], | ||||||||||
| data[pos + 1], | ||||||||||
|
|
@@ -69,7 +70,13 @@ export class PNGFormat extends PNGBase implements ImageFormat { | |||||||||
| pos += 4; | ||||||||||
| const chunkData = data.slice(pos, pos + length); | ||||||||||
| pos += length; | ||||||||||
| pos += 4; // Skip CRC | ||||||||||
| const storedCrc = this.readUint32(data, pos); | ||||||||||
| pos += 4; | ||||||||||
|
Comment on lines
74
to
+77
|
||||||||||
|
|
||||||||||
| // Verify CRC (covers chunk type + chunk data) | ||||||||||
| if (storedCrc !== this.crc32(data.slice(typePos, typePos + 4 + length))) { | ||||||||||
| throw new Error(`PNG chunk '${type}' has invalid CRC`); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (type === "IHDR") { | ||||||||||
| width = this.readUint32(chunkData, 0); | ||||||||||
|
|
@@ -217,6 +224,7 @@ export class PNGFormat extends PNGBase implements ImageFormat { | |||||||||
|
|
||||||||||
| const length = this.readUint32(data, pos); | ||||||||||
| pos += 4; | ||||||||||
| const typePos = pos; | ||||||||||
| const type = String.fromCharCode( | ||||||||||
| data[pos], | ||||||||||
| data[pos + 1], | ||||||||||
|
|
@@ -229,7 +237,13 @@ export class PNGFormat extends PNGBase implements ImageFormat { | |||||||||
|
|
||||||||||
| const chunkData = data.slice(pos, pos + length); | ||||||||||
| pos += length; | ||||||||||
| pos += 4; // Skip CRC | ||||||||||
| const storedCrc = this.readUint32(data, pos); | ||||||||||
| pos += 4; | ||||||||||
|
|
||||||||||
| // Verify CRC (covers chunk type + chunk data) | ||||||||||
| if (storedCrc !== this.crc32(data.slice(typePos, typePos + 4 + length))) { | ||||||||||
|
||||||||||
| if (storedCrc !== this.crc32(data.slice(typePos, typePos + 4 + length))) { | |
| if ( | |
| storedCrc !== this.crc32(data.subarray(typePos, typePos + 4 + length)) | |
| ) { |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -18,6 +18,17 @@ function isLittleEndian(): boolean { | |||||||||
| // Cache the endianness check result | ||||||||||
| const IS_LITTLE_ENDIAN = isLittleEndian(); | ||||||||||
|
|
||||||||||
| // Pre-computed clamp lookup table for adjustBrightness. | ||||||||||
| // Covers the range [-255, 511] offset by 255 so the index is always non-negative. | ||||||||||
| // Index: (pixelValue + adjustInt + 255), result: clamped value in [0, 255]. | ||||||||||
| const BRIGHTNESS_LUT_SIZE = 767; | ||||||||||
| const BRIGHTNESS_LUT_OFFSET = 255; | ||||||||||
| const _BRIGHTNESS_LUT = new Uint8Array(BRIGHTNESS_LUT_SIZE); | ||||||||||
| for (let _i = 0; _i < BRIGHTNESS_LUT_SIZE; _i++) { | ||||||||||
| const _v = _i - BRIGHTNESS_LUT_OFFSET; | ||||||||||
| _BRIGHTNESS_LUT[_i] = _v < 0 ? 0 : (_v > 255 ? 255 : _v); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Composite one image on top of another at a specified position | ||||||||||
| * @param base Base image data (RGBA) | ||||||||||
|
|
@@ -111,23 +122,13 @@ export function adjustBrightness( | |||||||||
| const clampedAmount = Math.max(-1, Math.min(1, amount)); | ||||||||||
| const adjust = clampedAmount * 255; | ||||||||||
|
|
||||||||||
| // Pre-compute lookup table for clamping | ||||||||||
| // Range: -255 to 511 (data value 0-255 + adjust -255 to 255), offset by 255 for zero-based index | ||||||||||
| const LUT_SIZE = 767; | ||||||||||
| const LUT_OFFSET = 255; | ||||||||||
| const lut = new Uint8Array(LUT_SIZE); | ||||||||||
| for (let i = 0; i < LUT_SIZE; i++) { | ||||||||||
| const value = i - LUT_OFFSET; | ||||||||||
| lut[i] = value < 0 ? 0 : (value > 255 ? 255 : value); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Use bitwise OR for fast rounding (equivalent to Math.round for positive numbers) | ||||||||||
| const adjustInt = (adjust + 0.5) | 0; | ||||||||||
|
||||||||||
| // Use bitwise OR for fast rounding (equivalent to Math.round for positive numbers) | |
| const adjustInt = (adjust + 0.5) | 0; | |
| // Use symmetric rounding so negative and positive brightness adjustments behave consistently | |
| const adjustInt = Math.round(adjust); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
APNGFormat.decodeFrames first-pass chunk parsing does not validate that there are at least 8 bytes for length+type, nor that
pos + length + 4is withindata.lengthbefore slicing chunk data and reading the stored CRC. WithreadUint32's out-of-range behavior, truncated/corrupt inputs can be mis-parsed and can produce misleading CRC errors (or be bypassed if CRC reads as 0). Add the same bounds checks used in extractMetadata before reading type/data/CRC, and throw a clear truncation/invalid chunk error.