Compare commits

...

11 Commits

Author SHA1 Message Date
Michael Smith
bb96f405b7 Fix crash in viewer example code when image does not contain a thumbnail 2023-06-04 22:08:40 +02:00
Michael Smith
e28b88aedf Slowly work towards more pure functions 2023-05-31 22:20:40 +02:00
Michael Smith
f937499e6f Remove unnecessary wrapping of functionality in a class 2023-05-31 20:49:44 +02:00
Michael Smith
bd68eb6940 Switch to prettier default config of using double quotes 2023-05-30 10:42:26 +02:00
Michael Smith
24875d989b Refactor viewer example 2023-05-27 21:16:18 +02:00
Michael Smith
8375261eb1 Fix problems highlighted by eslint 2023-05-27 15:31:55 +02:00
Michael Smith
f7a90d5433 Add and configure eslint 2023-05-27 15:31:55 +02:00
Michael Smith
fc3469ec48 Remove extraneous console.log() statement 2023-05-15 14:32:48 +02:00
Michael Smith
9f4a734558 Update README 2023-05-14 22:54:00 +02:00
Michael Smith
c623db79a3 Removed unused test fixture 2023-05-14 22:44:59 +02:00
Michael Smith
85f426aee7 Update README 2023-05-14 22:41:18 +02:00
9 changed files with 2094 additions and 339 deletions

19
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,19 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: ["airbnb-base", "prettier"],
overrides: [],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
rules: {
"import/extensions": [2, "always"],
"no-plusplus": ["error", { allowForLoopAfterthoughts: true }],
"no-param-reassign": ["error", { props: false }],
"no-bitwise": ["error", { allow: ["<<", "&"] }],
},
};

View File

@@ -1,6 +1,6 @@
# pbm-js # pbm-js
JavaScript library for IFF PBM files. JavaScript library for parsing IFF PBM files.
This is the format used by the PC version of Deluxe Paint II and is different from the Amiga version. This is the format used by the PC version of Deluxe Paint II and is different from the Amiga version.
@@ -10,6 +10,8 @@ This is the format used by the PC version of Deluxe Paint II and is different fr
- 100% plain JavaScript. No dependencies. - 100% plain JavaScript. No dependencies.
- Compatible with all [Mark J. Ferrari](https://www.markferrari.com/about/)'s artwork I could find. - Compatible with all [Mark J. Ferrari](https://www.markferrari.com/about/)'s artwork I could find.
Try it out at [https://michaelshmitty.github.io/pbm-js/](https://michaelshmitty.github.io/pbm-js/). You will need to supply your own IFF PBM files. All processing is done in your browser, no data is sent to any server.
## Usage ## Usage
### In the browser ### In the browser
@@ -17,14 +19,14 @@ This is the format used by the PC version of Deluxe Paint II and is different fr
_Also see `index.html` and `main.js` for a more elaborate example that renders the image and palette data to an html5 canvas and supports color cycling._ _Also see `index.html` and `main.js` for a more elaborate example that renders the image and palette data to an html5 canvas and supports color cycling._
```javascript ```javascript
import PBM from "./src/pbm.js"; import parsePBM from "./src/pbm.js";
fetch("/assets/TEST.LBM") fetch("/assets/TEST.LBM")
.then((response) => { .then((response) => {
return response.arrayBuffer(); return response.arrayBuffer();
}) })
.then((buffer) => { .then((buffer) => {
const image = new PBM(buffer); const image = parsePBM(buffer);
console.log(image); console.log(image);
}); });
``` ```
@@ -34,10 +36,10 @@ fetch("/assets/TEST.LBM")
```javascript ```javascript
import * as fs from "fs"; import * as fs from "fs";
import PBM from "./src/pbm.js"; import parsePBM from "./src/pbm.js";
const data = fs.readFileSync("./tests/fixtures/VALID.LBM"); const data = fs.readFileSync("./tests/fixtures/VALID.LBM");
const image = new PBM(data.buffer); const image = parsePBM(data.buffer);
console.log(image); console.log(image);
``` ```

176
main.js
View File

@@ -24,7 +24,7 @@
*/ */
import PBM from "./src/pbm.js"; import parsePBM from "./src/pbm.js";
const thumbnailCanvas = document.getElementById("thumbnail-canvas"); const thumbnailCanvas = document.getElementById("thumbnail-canvas");
const thumbnailContext = thumbnailCanvas.getContext("2d"); const thumbnailContext = thumbnailCanvas.getContext("2d");
@@ -33,94 +33,13 @@ const imageContext = imageCanvas.getContext("2d");
const paletteCanvas = document.getElementById("palette-canvas"); const paletteCanvas = document.getElementById("palette-canvas");
const paletteContext = paletteCanvas.getContext("2d"); const paletteContext = paletteCanvas.getContext("2d");
const inputElement = document.getElementById("imagefile");
inputElement.addEventListener("change", handleFile, false);
document.getElementById("paletteLeft").addEventListener("click", paletteLeft);
document.getElementById("paletteRight").addEventListener("click", paletteRight);
document.getElementById("cycleColors").addEventListener("click", () => {
if (running) {
running = false;
} else {
running = true;
animate();
}
});
const palettePageLabelEl = document.getElementById("palettePageLabel");
const cyclingSpeedLabel = document.getElementById("cyclingSpeedLabel");
cyclingSpeedLabel.innerText = cyclingSpeedSlider.value;
let currentPalettePage = 0; let currentPalettePage = 0;
let image = null; let image = null;
let running = false; let running = false;
let cycleSpeed = 15.0; let cycleSpeed = 15.0;
document // Drawing
.getElementById("cyclingSpeedSlider")
.addEventListener("input", (evt) => {
cycleSpeed = evt.target.value;
cyclingSpeedLabel.innerText = cycleSpeed;
});
fetch("/assets/TEST.LBM")
.then((response) => {
return response.arrayBuffer();
})
.then((buffer) => {
image = loadImage(buffer);
drawPalette(image.palette, currentPalettePage, paletteContext);
drawImage(image.thumbnail, thumbnailContext);
drawImage(image, imageContext);
console.log(image.cyclingRanges);
});
function handleFile() {
const imageFile = this.files[0];
const reader = new FileReader();
reader.onload = (evt) => {
image = loadImage(evt.target.result);
drawPalette(image.palette, currentPalettePage, paletteContext);
drawImage(image.thumbnail, thumbnailContext);
drawImage(image, imageContext);
};
reader.readAsArrayBuffer(imageFile);
}
function loadImage(buffer) {
image = new PBM(buffer);
thumbnailCanvas.width = image.thumbnail.width;
thumbnailCanvas.height = image.thumbnail.height;
imageCanvas.width = image.width;
imageCanvas.height = image.height;
return image;
}
function paletteLeft(evt) {
if (currentPalettePage === 0) {
currentPalettePage = 3;
} else {
currentPalettePage--;
}
palettePageLabelEl.innerText = currentPalettePage + 1;
drawPalette();
}
function paletteRight(evt) {
if (currentPalettePage === 3) {
currentPalettePage = 0;
} else {
currentPalettePage++;
}
palettePageLabelEl.innerText = currentPalettePage + 1;
drawPalette();
}
function drawPalette() { function drawPalette() {
const colorSize = 20; // in pixels const colorSize = 20; // in pixels
const width = 4 * colorSize; // 4 columns const width = 4 * colorSize; // 4 columns
@@ -143,19 +62,21 @@ function drawPalette() {
} }
} }
function drawImage(image, ctx) { function drawImage(anImage, ctx) {
ctx.clearRect(0, 0, image.width, image.height); if (!anImage) return;
let pixels = ctx.createImageData(image.width, image.height);
for (let x = 0; x < image.width; x++) { ctx.clearRect(0, 0, anImage.width, anImage.height);
for (let y = 0; y < image.height; y++) { const pixels = ctx.createImageData(anImage.width, anImage.height);
const index = y * image.width + x;
const paletteIndex = image.pixelData[index]; for (let x = 0; x < anImage.width; x++) {
for (let y = 0; y < anImage.height; y++) {
const index = y * anImage.width + x;
const paletteIndex = anImage.pixelData[index];
const pixelIndex = index * 4; const pixelIndex = index * 4;
const r = image.palette[paletteIndex][0]; const r = anImage.palette[paletteIndex][0];
const g = image.palette[paletteIndex][1]; const g = anImage.palette[paletteIndex][1];
const b = image.palette[paletteIndex][2]; const b = anImage.palette[paletteIndex][2];
pixels.data[pixelIndex] = r; pixels.data[pixelIndex] = r;
pixels.data[pixelIndex + 1] = g; pixels.data[pixelIndex + 1] = g;
@@ -167,6 +88,59 @@ function drawImage(image, ctx) {
ctx.putImageData(pixels, 0, 0); ctx.putImageData(pixels, 0, 0);
} }
// Image loading
function loadImage(buffer) {
image = parsePBM(buffer);
thumbnailCanvas.width = image.thumbnail?.width || 80;
thumbnailCanvas.height = image.thumbnail?.height || 60;
imageCanvas.width = image.width;
imageCanvas.height = image.height;
return image;
}
document.getElementById("imagefile").addEventListener(
"change",
(e) => {
const imageFile = e.target.files[0];
const reader = new FileReader();
reader.onload = (evt) => {
image = loadImage(evt.target.result);
drawPalette(image.palette, currentPalettePage, paletteContext);
drawImage(image.thumbnail, thumbnailContext);
drawImage(image, imageContext);
};
reader.readAsArrayBuffer(imageFile);
},
false
);
// Palette navigation
document.getElementById("paletteLeft").addEventListener("click", () => {
if (currentPalettePage === 0) {
currentPalettePage = 3;
} else {
currentPalettePage -= 1;
}
document.getElementById("palettePageLabel").innerText =
currentPalettePage + 1;
drawPalette();
});
document.getElementById("paletteRight").addEventListener("click", () => {
if (currentPalettePage === 3) {
currentPalettePage = 0;
} else {
currentPalettePage += 1;
}
document.getElementById("palettePageLabel").innerText =
currentPalettePage + 1;
drawPalette();
});
// Color cycling
function cycleColors(now) { function cycleColors(now) {
image.cyclingRanges.forEach((range) => { image.cyclingRanges.forEach((range) => {
if (range.active) { if (range.active) {
@@ -195,3 +169,19 @@ function animate(now) {
if (running) requestAnimationFrame(animate); if (running) requestAnimationFrame(animate);
} }
document
.getElementById("cyclingSpeedSlider")
.addEventListener("input", (evt) => {
cycleSpeed = evt.target.value;
document.getElementById("cyclingSpeedLabel").innerText = cycleSpeed;
});
document.getElementById("cycleColors").addEventListener("click", () => {
if (running) {
running = false;
} else {
running = true;
animate();
}
});

1772
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,10 @@
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@vitest/coverage-c8": "^0.31.0", "@vitest/coverage-c8": "^0.31.0",
"eslint": "^8.41.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"vitest": "^0.31.0" "vitest": "^0.31.0"
} }
} }

View File

@@ -44,10 +44,17 @@ class BinaryStream {
} }
readByte() { readByte() {
const byte = this.dataView.getUint8(this.index); return this.readUint8();
}
this.index++; readBytes(length) {
return byte; const bytes = [];
for (let i = 0; i < length; i++) {
bytes.push(this.readUint8());
}
return bytes;
} }
readInt16BE() { readInt16BE() {

View File

@@ -26,155 +26,73 @@
import BinaryStream from "./binarystream.js"; import BinaryStream from "./binarystream.js";
class PBM { function decompress(binaryStream, length) {
constructor(arrayBuffer) { const result = [];
this.binaryStream = new BinaryStream(arrayBuffer); const endOfChunkIndex = binaryStream.index + length;
// Image properties taken from BMHD chunk while (binaryStream.index < endOfChunkIndex) {
this.width = null; const byte = binaryStream.readByte();
this.height = null;
this.size = null;
this.xOrigin = null;
this.yOrigin = null;
this.numPlanes = null;
this.mask = null;
this.compression = null;
this.transClr = null;
this.xAspect = null;
this.yAspect = null;
this.pageWidth = null;
this.pageHeight = null;
// Palette information taken from CMAP chunk if (byte > 128) {
this.palette = []; const nextByte = binaryStream.readByte();
for (let i = 0; i < 257 - byte; i++) {
// Color cycling information taken from CRNG chunk result.push(nextByte);
this.cyclingRanges = []; }
} else if (byte < 128) {
// Thumbnail information taken from TINY chunk for (let i = 0; i < byte + 1; i++) {
this.thumbnail = { result.push(binaryStream.readByte());
width: null, }
height: null,
size: null,
palette: this.palette,
pixelData: [],
};
// Uncompressed pixel data referencing palette colors
this.pixelData = [];
try {
this.parseFORM();
} catch (error) {
if (error instanceof RangeError) {
throw new Error(`Failed to parse file.`);
} else { } else {
throw error; // re-throw the error unchanged
}
}
}
parseFORM() {
// Parse "FORM" chunk
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") {
throw new Error(
`Invalid chunkId: "${chunkId}" at byte ${this.binaryStream.index}. Expected "FORM".`
);
}
if (chunkLength !== this.binaryStream.length - 8) {
throw new Error(
`Invalid chunk length: ${chunkLength} bytes. Expected ${
this.binaryStream.length - 8
} bytes.`
);
}
if (formatId !== "PBM ") {
throw new Error(`Invalid formatId: "${formatId}". Expected "PBM ".`);
}
// Parse all other chunks
while (!this.binaryStream.EOF()) {
chunkId = this.binaryStream.readString(4);
chunkLength = this.binaryStream.readUint32BE();
switch (chunkId) {
case "BMHD":
this.parseBMHD();
break; break;
case "CMAP":
this.parseCMAP();
break;
case "DPPS":
// NOTE(m): Ignore unknown DPPS chunk of size 110 bytes
this.binaryStream.jump(110);
break;
case "CRNG":
this.parseCRNG();
break;
case "TINY":
this.parseTINY(chunkLength);
break;
case "BODY":
this.parseBODY(chunkLength);
break;
default:
throw new Error(
`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);
} }
} }
// Parse Bitmap Header chunk return result;
parseBMHD() { }
this.width = this.binaryStream.readUint16BE();
this.height = this.binaryStream.readUint16BE();
this.size = this.width * this.height;
this.xOrigin = this.binaryStream.readInt16BE();
this.yOrigin = this.binaryStream.readInt16BE();
this.numPlanes = this.binaryStream.readUint8();
this.mask = this.binaryStream.readUint8();
this.compression = this.binaryStream.readUint8();
this.binaryStream.readUint8(); // Ignore pad1 field left "for future compatibility"
this.transClr = this.binaryStream.readUint16BE();
this.xAspect = this.binaryStream.readUint8();
this.yAspect = this.binaryStream.readUint8();
this.pageWidth = this.binaryStream.readInt16BE();
this.pageHeight = this.binaryStream.readInt16BE();
}
// Parse Palette chunk // Parse Bitmap Header chunk
parseCMAP() { function parseBMHD(binaryStream, image) {
const numColors = 2 ** this.numPlanes; image.width = binaryStream.readUint16BE();
image.height = binaryStream.readUint16BE();
image.size = image.width * image.height;
image.xOrigin = binaryStream.readInt16BE();
image.yOrigin = binaryStream.readInt16BE();
image.numPlanes = binaryStream.readUint8();
image.mask = binaryStream.readUint8();
image.compression = binaryStream.readUint8();
binaryStream.readUint8(); // Ignore pad1 field left "for future compatibility"
image.transClr = binaryStream.readUint16BE();
image.xAspect = binaryStream.readUint8();
image.yAspect = binaryStream.readUint8();
image.pageWidth = binaryStream.readInt16BE();
image.pageHeight = binaryStream.readInt16BE();
}
// Parse Palette chunk
function parseCMAP(binaryStream, numPlanes) {
const palette = [];
const numColors = 2 ** numPlanes;
// TODO(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 = []; const rgb = [];
for (let j = 0; j < 3; j++) { for (let j = 0; j < 3; j++) {
rgb.push(this.binaryStream.readByte()); rgb.push(binaryStream.readByte());
}
this.palette.push(rgb);
} }
palette.push(rgb);
} }
// Parse Color range chunk return palette;
parseCRNG() { }
this.binaryStream.jump(2); // 2 bytes padding according to https://en.wikipedia.org/wiki/ILBM#CRNG:_Colour_range
const rate = this.binaryStream.readInt16BE(); // Parse Color range chunk
const flags = this.binaryStream.readInt16BE(); function parseCRNG(binaryStream) {
const low = this.binaryStream.readUint8(); binaryStream.jump(2); // 2 bytes padding according to https://en.wikipedia.org/wiki/ILBM#CRNG:_Colour_range
const high = this.binaryStream.readUint8();
const rate = binaryStream.readInt16BE();
const flags = binaryStream.readInt16BE();
const low = binaryStream.readUint8();
const high = binaryStream.readUint8();
// Parse flags according to https://en.wikipedia.org/wiki/ILBM#CRNG:_Colour_range // Parse flags according to https://en.wikipedia.org/wiki/ILBM#CRNG:_Colour_range
// If bit 0 is 1, the color should cycle, otherwise this color register range is inactive // If bit 0 is 1, the color should cycle, otherwise this color register range is inactive
@@ -188,78 +106,128 @@ class PBM {
const activeBitMask = 1 << 0; const activeBitMask = 1 << 0;
const directionBitMask = 1 << 1; const directionBitMask = 1 << 1;
this.cyclingRanges.push({ return {
rate: rate, rate,
active: (flags & activeBitMask) !== 0, active: (flags & activeBitMask) !== 0,
direction: (flags & directionBitMask) !== 0 ? "reverse" : "forward", direction: (flags & directionBitMask) !== 0 ? "reverse" : "forward",
low: low, low,
high: high, high,
}); };
} }
// Parse Thumbnail chunk // Parse Thumbnail chunk
parseTINY(chunkLength) { function parseTINY(binaryStream, compression, chunkLength) {
const endOfChunkIndex = this.binaryStream.index + chunkLength; const thumbnail = {};
this.thumbnail.width = this.binaryStream.readUint16BE(); thumbnail.width = binaryStream.readUint16BE();
this.thumbnail.height = this.binaryStream.readUint16BE(); thumbnail.height = binaryStream.readUint16BE();
this.thumbnail.size = this.thumbnail.width * this.thumbnail.height; thumbnail.size = thumbnail.width * thumbnail.height;
// Decompress pixel data if necessary if (compression === 1) {
if (this.compression === 1) { thumbnail.pixelData = decompress(binaryStream, chunkLength - 4);
this.thumbnail.pixelData = this.decompress(endOfChunkIndex);
} else { } else {
this.thumbnail.pixelData = this.readUncompressed(endOfChunkIndex); thumbnail.pixelData = binaryStream.readBytes(chunkLength);
}
} }
// Parse Image data chunk return thumbnail;
parseBODY(chunkLength) { }
const endOfChunkIndex = this.binaryStream.index + chunkLength;
// Decompress pixel data if necessary // Parse Image data chunk
if (this.compression === 1) { function parseBODY(binaryStream, compression, chunkLength) {
this.pixelData = this.decompress(endOfChunkIndex); if (compression === 1) {
} else { return decompress(binaryStream, chunkLength);
this.pixelData = this.readUncompressed(endOfChunkIndex);
}
} }
decompress(endOfChunkIndex) { return binaryStream.readBytes(chunkLength);
let result = []; }
while (this.binaryStream.index < endOfChunkIndex) { // Parse FORM chunk
const byte = this.binaryStream.readByte(); function parseFORM(binaryStream, image) {
let chunkId = binaryStream.readString(4);
let chunkLength = binaryStream.readUint32BE();
const formatId = binaryStream.readString(4);
if (byte > 128) { // Validate chunk according to notes on https://en.wikipedia.org/wiki/ILBM
const nextByte = this.binaryStream.readByte(); if (chunkId !== "FORM") {
for (let i = 0; i < 257 - byte; i++) { throw new Error(
result.push(nextByte); `Invalid chunkId: "${chunkId}" at byte ${binaryStream.index}. Expected "FORM".`
);
} }
} else if (byte < 128) {
for (let i = 0; i < byte + 1; i++) { if (chunkLength !== binaryStream.length - 8) {
result.push(this.binaryStream.readByte()); throw new Error(
`Invalid chunk length: ${chunkLength} bytes. Expected ${
binaryStream.length - 8
} bytes.`
);
} }
} else {
if (formatId !== "PBM ") {
throw new Error(`Invalid formatId: "${formatId}". Expected "PBM ".`);
}
// Parse all other chunks
while (!binaryStream.EOF()) {
chunkId = binaryStream.readString(4);
chunkLength = binaryStream.readUint32BE();
switch (chunkId) {
case "BMHD":
parseBMHD(binaryStream, image);
break; break;
} case "CMAP":
image.palette = parseCMAP(binaryStream, image.numPlanes);
break;
case "DPPS":
// NOTE(m): Ignore unknown DPPS chunk of size 110 bytes
binaryStream.jump(110);
break;
case "CRNG":
image.cyclingRanges.push(parseCRNG(binaryStream));
break;
case "TINY":
image.thumbnail = parseTINY(
binaryStream,
image.compression,
chunkLength
);
// FIXME(m): Remove need for reference to image palette in thumbnail data
image.thumbnail.palette = image.palette;
break;
case "BODY":
image.pixelData = parseBODY(
binaryStream,
image.compression,
chunkLength
);
break;
default:
throw new Error(
`Unsupported chunkId: ${chunkId} at byte ${binaryStream.index}`
);
} }
return result; // Skip chunk padding byte when chunkLength is not a multiple of 2
} if (chunkLength % 2 === 1) binaryStream.jump(1);
// 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;
} }
} }
export default PBM; export default function parsePBM(arrayBuffer) {
const binaryStream = new BinaryStream(arrayBuffer);
const image = {
cyclingRanges: [],
};
try {
parseFORM(binaryStream, image);
} catch (error) {
if (error instanceof RangeError) {
throw new Error(`Failed to parse file.`);
} else {
throw error; // re-throw the error unchanged
}
}
return image;
}

BIN
tests/fixtures/ASH.LBM vendored

Binary file not shown.

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-new */
/* /*
MIT License MIT License
@@ -26,48 +27,45 @@
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import PBM from "../src/pbm.js"; import parsePBM from "../src/pbm.js";
const fs = require("fs");
test("Successfully parse a PBM file", () => { test("Successfully parse a PBM file", () => {
const fs = require("fs");
const data = fs.readFileSync("./tests/fixtures/VALID.LBM"); const data = fs.readFileSync("./tests/fixtures/VALID.LBM");
expect(() => { expect(() => {
new PBM(data.buffer); parsePBM(data.buffer);
}).not.toThrowError(); }).not.toThrowError();
}); });
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 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); parsePBM(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", () => {
const fs = require("fs");
const data = fs.readFileSync("./tests/fixtures/INVALID_CHUNK_LENGTH.LBM"); const data = fs.readFileSync("./tests/fixtures/INVALID_CHUNK_LENGTH.LBM");
expect(() => { expect(() => {
new PBM(data.buffer); parsePBM(data.buffer);
}).toThrowError(/^Invalid chunk length: 7070 bytes. Expected 7012 bytes.$/); }).toThrowError(/^Invalid chunk length: 7070 bytes. Expected 7012 bytes.$/);
}); });
test("Fail to parse an IFF file that is not a PBM file", () => { test("Fail to parse an IFF file that is not a PBM file", () => {
const fs = require("fs");
const data = fs.readFileSync("./tests/fixtures/SEASCAPE.LBM"); const data = fs.readFileSync("./tests/fixtures/SEASCAPE.LBM");
expect(() => { expect(() => {
new PBM(data.buffer); parsePBM(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", () => {
const fs = require("fs");
const data = fs.readFileSync("./tests/fixtures/VALID.LBM"); const data = fs.readFileSync("./tests/fixtures/VALID.LBM");
const image = new PBM(data.buffer); const image = parsePBM(data.buffer);
expect(image.width).toStrictEqual(640); expect(image.width).toStrictEqual(640);
expect(image.height).toStrictEqual(480); expect(image.height).toStrictEqual(480);
@@ -85,26 +83,23 @@ test("Parse a PBM bitmap header", () => {
}); });
test("Parse PBM palette information", () => { test("Parse PBM palette information", () => {
const fs = require("fs");
const data = fs.readFileSync("./tests/fixtures/VALID.LBM"); const data = fs.readFileSync("./tests/fixtures/VALID.LBM");
const image = new PBM(data.buffer); const image = parsePBM(data.buffer);
expect(image.palette.length).toStrictEqual(256); expect(image.palette.length).toStrictEqual(256);
expect(image.palette[10]).toStrictEqual([87, 255, 87]); expect(image.palette[10]).toStrictEqual([87, 255, 87]);
}); });
test("Parse PBM color cycling information", () => { test("Parse PBM color cycling information", () => {
const fs = require("fs");
const data = fs.readFileSync("./tests/fixtures/VALID.LBM"); const data = fs.readFileSync("./tests/fixtures/VALID.LBM");
const image = new PBM(data.buffer); const image = parsePBM(data.buffer);
expect(image.cyclingRanges.length).toStrictEqual(16); expect(image.cyclingRanges.length).toStrictEqual(16);
}); });
test("Parse PBM thumbnail", () => { test("Parse PBM thumbnail", () => {
const fs = require("fs");
const data = fs.readFileSync("./tests/fixtures/VALID.LBM"); const data = fs.readFileSync("./tests/fixtures/VALID.LBM");
const image = new PBM(data.buffer); const image = parsePBM(data.buffer);
expect(image.thumbnail.width).toStrictEqual(80); expect(image.thumbnail.width).toStrictEqual(80);
expect(image.thumbnail.height).toStrictEqual(60); expect(image.thumbnail.height).toStrictEqual(60);
@@ -112,9 +107,8 @@ test("Parse PBM thumbnail", () => {
}); });
test("Decode PBM thumbnail pixel data", () => { test("Decode PBM thumbnail pixel data", () => {
const fs = require("fs");
const data = fs.readFileSync("./tests/fixtures/VALID.LBM"); const data = fs.readFileSync("./tests/fixtures/VALID.LBM");
const image = new PBM(data.buffer); const image = parsePBM(data.buffer);
expect(image.thumbnail.pixelData.length).toStrictEqual(4800); expect(image.thumbnail.pixelData.length).toStrictEqual(4800);
// FIXME(m): Verify these values are correct in the test image thumbnail: // FIXME(m): Verify these values are correct in the test image thumbnail:
@@ -123,9 +117,8 @@ test("Decode PBM thumbnail pixel data", () => {
}); });
test("Decode PBM image pixel data", () => { test("Decode PBM image pixel data", () => {
const fs = require("fs");
const data = fs.readFileSync("./tests/fixtures/VALID.LBM"); const data = fs.readFileSync("./tests/fixtures/VALID.LBM");
const image = new PBM(data.buffer); const image = parsePBM(data.buffer);
expect(image.pixelData.length).toStrictEqual(307_200); expect(image.pixelData.length).toStrictEqual(307_200);
// FIXME(m): Verify these values are correct in the test image: // FIXME(m): Verify these values are correct in the test image: