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() {
// Parse "FORM" chunk
let chunkID = this.binaryStream.readString(4);
const lenChunk = this.binaryStream.readUint32BE();
const formatID = this.binaryStream.readString(4);
let chunkId = this.binaryStream.readString(4);
let chunkLength = this.binaryStream.readUint32BE();
const formatId = this.binaryStream.readString(4);
// Validate chunk according to notes on https://en.wikipedia.org/wiki/ILBM
if (chunkID !== "FORM") {
if (chunkId !== "FORM") {
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(
`Invalid chunk length: ${lenChunk} bytes. Expected ${
`Invalid chunk length: ${chunkLength} bytes. Expected ${
this.binaryStream.length - 8
} bytes.`
);
}
if (formatID !== "PBM ") {
throw new Error(`Invalid formatID: "${formatID}". Expected "PBM ".`);
if (formatId !== "PBM ") {
throw new Error(`Invalid formatId: "${formatId}". Expected "PBM ".`);
}
// Parse all other chunks
while (!this.binaryStream.EOF()) {
chunkID = this.binaryStream.readString(4);
this.binaryStream.jump(4); // Skip 4 bytes chunk length value
chunkId = this.binaryStream.readString(4);
chunkLength = this.binaryStream.readUint32BE();
switch (chunkID) {
switch (chunkId) {
case "BMHD":
this.parseBMHD();
break;
@ -119,16 +119,19 @@ class PBM {
this.parseCRNG();
break;
case "TINY":
this.parseTINY();
this.parseTINY(chunkLength);
break;
case "BODY":
this.parseBODY();
this.parseBODY(chunkLength);
break;
default:
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() {
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++) {
let rgb = [];
for (let j = 0; j < 3; j++) {
@ -178,62 +181,67 @@ class PBM {
}
// Parse Thumbnail chunk
parseTINY() {
parseTINY(chunkLength) {
const endOfChunkIndex = this.binaryStream.index + chunkLength;
this.thumbnail.width = this.binaryStream.readUint16BE();
this.thumbnail.height = this.binaryStream.readUint16BE();
this.thumbnail.size = this.thumbnail.width * this.thumbnail.height;
while (this.thumbnail.pixelData.length < this.thumbnail.size) {
const byte = this.binaryStream.readByte();
// TODO(m): Deduplicate decompression code for thumbnail and image data
if (this.compression === 1) {
// Decompress the data
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 {
break;
}
} else {
// Data is not compressed, just copy the bytes
this.thumbnail.pixelData.push(byte);
}
// Decompress pixel data if necessary
if (this.compression === 1) {
this.thumbnail.pixelData = this.decompress(endOfChunkIndex);
} else {
this.thumbnail.pixelData = this.readUncompressed(endOfChunkIndex);
}
}
// Parse Image data chunk
parseBODY() {
// NOTE(m): Should we make use of the chunk length here instead?
parseBODY(chunkLength) {
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();
if (this.compression === 1) {
// Decompress the data
if (byte > 128) {
const nextByte = this.binaryStream.readByte();
for (let i = 0; i < 257 - byte; i++) {
this.pixelData.push(nextByte);
}
} else if (byte < 128) {
for (let i = 0; i < byte + 1; i++) {
this.pixelData.push(this.binaryStream.readByte());
}
} else {
break;
if (byte > 128) {
const nextByte = this.binaryStream.readByte();
for (let i = 0; i < 257 - byte; i++) {
result.push(nextByte);
}
} else if (byte < 128) {
for (let i = 0; i < byte + 1; i++) {
result.push(this.binaryStream.readByte());
}
} else {
// Data is not compressed, just copy the bytes
this.pixelData.push(byte);
break;
}
}
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();
});
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", () => {
const fs = require("fs");
const data = fs.readFileSync("./tests/fixtures/INVALID_CHUNK_ID.LBM");
expect(() => {
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", () => {
@ -70,7 +61,7 @@ test("Fail to parse an IFF file that is not a PBM file", () => {
expect(() => {
new PBM(data.buffer);
}).toThrowError(/^Invalid formatID: "ILBM". Expected "PBM ".$/);
}).toThrowError(/^Invalid formatId: "ILBM". Expected "PBM ".$/);
});
test("Parse a PBM bitmap header", () => {