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:
parent
d97a039cd8
commit
9dcbdd0327
122
src/pbm.js
122
src/pbm.js
@ -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();
|
if (this.compression === 1) {
|
||||||
|
this.thumbnail.pixelData = this.decompress(endOfChunkIndex);
|
||||||
// TODO(m): Deduplicate decompression code for thumbnail and image data
|
} else {
|
||||||
if (this.compression === 1) {
|
this.thumbnail.pixelData = this.readUncompressed(endOfChunkIndex);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
if (byte > 128) {
|
||||||
// Decompress the data
|
const nextByte = this.binaryStream.readByte();
|
||||||
if (byte > 128) {
|
for (let i = 0; i < 257 - byte; i++) {
|
||||||
const nextByte = this.binaryStream.readByte();
|
result.push(nextByte);
|
||||||
for (let i = 0; i < 257 - byte; i++) {
|
}
|
||||||
this.pixelData.push(nextByte);
|
} else if (byte < 128) {
|
||||||
}
|
for (let i = 0; i < byte + 1; i++) {
|
||||||
} else if (byte < 128) {
|
result.push(this.binaryStream.readByte());
|
||||||
for (let i = 0; i < byte + 1; i++) {
|
|
||||||
this.pixelData.push(this.binaryStream.readByte());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Data is not compressed, just copy the bytes
|
break;
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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", () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user