Compatibility improvements and minor fixes

* Account for padding byte when chunk length is not a multiple of 2
* Use chunkLength to parse TINY and BODY chunk data
* Rename chunkID, lenChunk and formatID
* Deduplicate decompression code
This commit is contained in:
Michael Smith 2023-05-13 21:38:01 +02:00
parent d97a039cd8
commit 9dcbdd0327
2 changed files with 67 additions and 68 deletions

View File

@ -76,35 +76,35 @@ class PBM {
parseFORM() { parseFORM() {
// Parse "FORM" chunk // Parse "FORM" chunk
let chunkID = this.binaryStream.readString(4); let chunkId = this.binaryStream.readString(4);
const lenChunk = this.binaryStream.readUint32BE(); let chunkLength = this.binaryStream.readUint32BE();
const formatID = this.binaryStream.readString(4); const formatId = this.binaryStream.readString(4);
// Validate chunk according to notes on https://en.wikipedia.org/wiki/ILBM // Validate chunk according to notes on https://en.wikipedia.org/wiki/ILBM
if (chunkID !== "FORM") { if (chunkId !== "FORM") {
throw new Error( throw new Error(
`Invalid chunkID: "${chunkID}" at byte ${this.binaryStream.index}. Expected "FORM".` `Invalid chunkId: "${chunkId}" at byte ${this.binaryStream.index}. Expected "FORM".`
); );
} }
if (lenChunk !== this.binaryStream.length - 8) { if (chunkLength !== this.binaryStream.length - 8) {
throw new Error( throw new Error(
`Invalid chunk length: ${lenChunk} bytes. Expected ${ `Invalid chunk length: ${chunkLength} bytes. Expected ${
this.binaryStream.length - 8 this.binaryStream.length - 8
} bytes.` } bytes.`
); );
} }
if (formatID !== "PBM ") { if (formatId !== "PBM ") {
throw new Error(`Invalid formatID: "${formatID}". Expected "PBM ".`); throw new Error(`Invalid formatId: "${formatId}". Expected "PBM ".`);
} }
// Parse all other chunks // Parse all other chunks
while (!this.binaryStream.EOF()) { while (!this.binaryStream.EOF()) {
chunkID = this.binaryStream.readString(4); chunkId = this.binaryStream.readString(4);
this.binaryStream.jump(4); // Skip 4 bytes chunk length value chunkLength = this.binaryStream.readUint32BE();
switch (chunkID) { switch (chunkId) {
case "BMHD": case "BMHD":
this.parseBMHD(); this.parseBMHD();
break; break;
@ -119,16 +119,19 @@ class PBM {
this.parseCRNG(); this.parseCRNG();
break; break;
case "TINY": case "TINY":
this.parseTINY(); this.parseTINY(chunkLength);
break; break;
case "BODY": case "BODY":
this.parseBODY(); this.parseBODY(chunkLength);
break; break;
default: default:
throw new Error( throw new Error(
`Unsupported chunkID: ${chunkID} at byte ${this.binaryStream.index}` `Unsupported chunkId: ${chunkId} at byte ${this.binaryStream.index}`
); );
} }
// Skip chunk padding byte when chunkLength is not a multiple of 2
if (chunkLength % 2 === 1) this.binaryStream.jump(1);
} }
} }
@ -154,7 +157,7 @@ class PBM {
parseCMAP() { parseCMAP() {
const numColors = 2 ** this.numPlanes; const numColors = 2 ** this.numPlanes;
// FIXME(m): Read 3 bytes at a time? // TODO(m): Read 3 bytes at a time?
for (let i = 0; i < numColors; i++) { for (let i = 0; i < numColors; i++) {
let rgb = []; let rgb = [];
for (let j = 0; j < 3; j++) { for (let j = 0; j < 3; j++) {
@ -178,62 +181,67 @@ class PBM {
} }
// Parse Thumbnail chunk // Parse Thumbnail chunk
parseTINY() { parseTINY(chunkLength) {
const endOfChunkIndex = this.binaryStream.index + chunkLength;
this.thumbnail.width = this.binaryStream.readUint16BE(); this.thumbnail.width = this.binaryStream.readUint16BE();
this.thumbnail.height = this.binaryStream.readUint16BE(); this.thumbnail.height = this.binaryStream.readUint16BE();
this.thumbnail.size = this.thumbnail.width * this.thumbnail.height; this.thumbnail.size = this.thumbnail.width * this.thumbnail.height;
while (this.thumbnail.pixelData.length < this.thumbnail.size) { // Decompress pixel data if necessary
const byte = this.binaryStream.readByte();
// TODO(m): Deduplicate decompression code for thumbnail and image data
if (this.compression === 1) { if (this.compression === 1) {
// Decompress the data this.thumbnail.pixelData = this.decompress(endOfChunkIndex);
if (byte > 128) {
const nextByte = this.binaryStream.readByte();
for (let i = 0; i < 257 - byte; i++) {
this.thumbnail.pixelData.push(nextByte);
}
} else if (byte < 128) {
for (let i = 0; i < byte + 1; i++) {
this.thumbnail.pixelData.push(this.binaryStream.readByte());
}
} else { } else {
break; this.thumbnail.pixelData = this.readUncompressed(endOfChunkIndex);
}
} else {
// Data is not compressed, just copy the bytes
this.thumbnail.pixelData.push(byte);
}
} }
} }
// Parse Image data chunk // Parse Image data chunk
parseBODY() { parseBODY(chunkLength) {
// NOTE(m): Should we make use of the chunk length here instead? const endOfChunkIndex = this.binaryStream.index + chunkLength;
while (this.pixelData.length < this.size) { // Decompress pixel data if necessary
if (this.compression === 1) {
this.pixelData = this.decompress(endOfChunkIndex);
} else {
this.pixelData = this.readUncompressed(endOfChunkIndex);
}
}
decompress(endOfChunkIndex) {
let result = [];
while (this.binaryStream.index < endOfChunkIndex) {
const byte = this.binaryStream.readByte(); const byte = this.binaryStream.readByte();
if (this.compression === 1) {
// Decompress the data
if (byte > 128) { if (byte > 128) {
const nextByte = this.binaryStream.readByte(); const nextByte = this.binaryStream.readByte();
for (let i = 0; i < 257 - byte; i++) { for (let i = 0; i < 257 - byte; i++) {
this.pixelData.push(nextByte); result.push(nextByte);
} }
} else if (byte < 128) { } else if (byte < 128) {
for (let i = 0; i < byte + 1; i++) { for (let i = 0; i < byte + 1; i++) {
this.pixelData.push(this.binaryStream.readByte()); result.push(this.binaryStream.readByte());
} }
} else { } else {
break; break;
} }
} else {
// Data is not compressed, just copy the bytes
this.pixelData.push(byte);
} }
return result;
} }
// TODO(m): Read a range of bytes straight into an array?
// Use arrayBuffers throughout instead?
readUncompressed(endOfChunkIndex) {
let result = [];
while (this.binaryStream.index < endOfChunkIndex) {
const byte = this.binaryStream.readByte();
result.push(byte);
}
return result;
} }
} }

View File

@ -37,22 +37,13 @@ test("Successfully parse a PBM file", () => {
}).not.toThrowError(); }).not.toThrowError();
}); });
test("Fail to parse", () => {
const fs = require("fs");
const data = fs.readFileSync("./tests/fixtures/ASH.LBM");
expect(() => {
new PBM(data.buffer);
}).toThrowError(/^Failed to parse file.$/);
});
test("Fail to parse a PBM file with an invalid chunk id", () => { test("Fail to parse a PBM file with an invalid chunk id", () => {
const fs = require("fs"); const fs = require("fs");
const data = fs.readFileSync("./tests/fixtures/INVALID_CHUNK_ID.LBM"); const data = fs.readFileSync("./tests/fixtures/INVALID_CHUNK_ID.LBM");
expect(() => { expect(() => {
new PBM(data.buffer); new PBM(data.buffer);
}).toThrowError(/^Invalid chunkID: "FARM" at byte 12. Expected "FORM".$/); }).toThrowError(/^Invalid chunkId: "FARM" at byte 12. Expected "FORM".$/);
}); });
test("Fail to parse a PBM file with an invalid chunk length", () => { test("Fail to parse a PBM file with an invalid chunk length", () => {
@ -70,7 +61,7 @@ test("Fail to parse an IFF file that is not a PBM file", () => {
expect(() => { expect(() => {
new PBM(data.buffer); new PBM(data.buffer);
}).toThrowError(/^Invalid formatID: "ILBM". Expected "PBM ".$/); }).toThrowError(/^Invalid formatId: "ILBM". Expected "PBM ".$/);
}); });
test("Parse a PBM bitmap header", () => { test("Parse a PBM bitmap header", () => {