Compare commits

..

13 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
Michael Smith
6f779d0c04 Add palette viewer and color cycling to proof of concept viewer 2023-05-14 22:38:12 +02:00
Michael Smith
940523f72f Parse CRNG chunk bit flags
See https://en.wikipedia.org/wiki/ILBM#CRNG:_Colour_range
2023-05-14 22:38:12 +02:00
11 changed files with 2199 additions and 289 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,21 +10,23 @@ 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
_Also see `index.html` and `main.js` for a more elaborate example that renders the palletized image data to a html5 canvas._ _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);
``` ```

View File

@@ -9,9 +9,33 @@
<title>IFF PBM image viewer</title> <title>IFF PBM image viewer</title>
</head> </head>
<body> <body>
<p>
<input type="file" id="imagefile" /> <input type="file" id="imagefile" />
</p>
<canvas id="thumbnail-canvas"></canvas> <p>
<button id="cycleColors">Cycle colors</button>
<input type="range" min="10" max="50" value="15" id="cyclingSpeedSlider">
<span id="cyclingSpeedLabel"></span>
</p>
<div class="container">
<div class="item">
<canvas id="image-canvas"></canvas> <canvas id="image-canvas"></canvas>
</div>
<div class="item">
<div class="sidebar">
<div class="item">
<canvas id="thumbnail-canvas"></canvas>
</div>
<div class="item">
<canvas id="palette-canvas"></canvas>
<div class="buttons">
<button id="paletteLeft"><-</button>
<span id="palettePageLabel">1</span>
<button id="paletteRight">-></button>
</div>
</div>
</div>
</div>
</div>
</body> </body>
</html> </html>

178
main.js
View File

@@ -24,61 +24,59 @@
*/ */
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");
const imageCanvas = document.getElementById("image-canvas"); const imageCanvas = document.getElementById("image-canvas");
const imageContext = imageCanvas.getContext("2d"); const imageContext = imageCanvas.getContext("2d");
const inputElement = document.getElementById("imagefile"); const paletteCanvas = document.getElementById("palette-canvas");
inputElement.addEventListener("change", handleFile, false); const paletteContext = paletteCanvas.getContext("2d");
fetch("/assets/TEST.LBM") let currentPalettePage = 0;
.then((response) => { let image = null;
return response.arrayBuffer(); let running = false;
})
.then((buffer) => {
const image = loadImage(buffer);
drawImage(image.thumbnail, thumbnailContext);
drawImage(image, imageContext);
});
function handleFile() { let cycleSpeed = 15.0;
const imageFile = this.files[0];
const reader = new FileReader();
reader.onload = (evt) => { // Drawing
const image = loadImage(evt.target.result); function drawPalette() {
drawImage(image.thumbnail, thumbnailContext); const colorSize = 20; // in pixels
drawImage(image, imageContext); const width = 4 * colorSize; // 4 columns
}; const height = 16 * colorSize; // 16 rows
reader.readAsArrayBuffer(imageFile); paletteCanvas.width = width;
paletteCanvas.height = height;
paletteContext.clearRect(0, 0, width, height);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 4; x++) {
const index = currentPalettePage * 64 + (y * 4 + x);
const color = `rgb(${image.palette[index][0]}, ${image.palette[index][1]}, ${image.palette[index][2]})`;
paletteContext.fillStyle = color;
paletteContext.fillRect(x * colorSize, y * colorSize, width, height);
}
}
} }
function loadImage(buffer) { function drawImage(anImage, ctx) {
const image = new PBM(buffer); if (!anImage) return;
thumbnailCanvas.width = image.thumbnail.width;
thumbnailCanvas.height = image.thumbnail.height;
imageCanvas.width = image.width;
imageCanvas.height = image.height;
return image; ctx.clearRect(0, 0, anImage.width, anImage.height);
} const pixels = ctx.createImageData(anImage.width, anImage.height);
function drawImage(image, context) { for (let x = 0; x < anImage.width; x++) {
context.clearRect(0, 0, image.width, image.height); for (let y = 0; y < anImage.height; y++) {
let pixels = context.createImageData(image.width, image.height); const index = y * anImage.width + x;
const paletteIndex = anImage.pixelData[index];
for (let x = 0; x < image.width; x++) {
for (let y = 0; y < image.height; y++) {
const index = y * image.width + x;
const paletteIndex = image.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;
@@ -87,5 +85,103 @@ function drawImage(image, context) {
} }
} }
context.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) {
image.cyclingRanges.forEach((range) => {
if (range.active) {
if (!range.lastTime) range.lastTime = now;
if (now - range.lastTime > range.rate / cycleSpeed) {
if (range.direction === "forward") {
// Move last color to first position
const lastColor = image.palette.splice(range.high, 1)[0];
image.palette.splice(range.low, 0, lastColor);
} else if (range.direction === "reverse") {
// Move first color to last position
const firstColor = image.palette.splice(range.low, 1)[0];
image.palette.splice(range.high, 0, firstColor);
}
range.lastTime = now;
}
}
});
}
function animate(now) {
cycleColors(now);
drawPalette();
drawImage(image, imageContext);
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,71 +26,138 @@
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 break;
}
} }
} }
parseFORM() { return result;
// Parse "FORM" chunk }
let chunkId = this.binaryStream.readString(4);
let chunkLength = this.binaryStream.readUint32BE(); // Parse Bitmap Header chunk
const formatId = this.binaryStream.readString(4); function parseBMHD(binaryStream, image) {
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?
for (let i = 0; i < numColors; i++) {
const rgb = [];
for (let j = 0; j < 3; j++) {
rgb.push(binaryStream.readByte());
}
palette.push(rgb);
}
return palette;
}
// Parse Color range chunk
function parseCRNG(binaryStream) {
binaryStream.jump(2); // 2 bytes padding according to https://en.wikipedia.org/wiki/ILBM#CRNG:_Colour_range
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
// If bit 0 is 1, the color should cycle, otherwise this color register range is inactive
// and should have no effect.
//
// If bit 1 is 0, the colors cycle upwards (forward), i.e. each color moves into the next
// index position in the palette and the uppermost color in the range moves down to the
// lowest position.
// If bit 1 is 1, the colors cycle in the opposite direction (reverse).
// Only those colors between the low and high entries in the palette should cycle.
const activeBitMask = 1 << 0;
const directionBitMask = 1 << 1;
return {
rate,
active: (flags & activeBitMask) !== 0,
direction: (flags & directionBitMask) !== 0 ? "reverse" : "forward",
low,
high,
};
}
// Parse Thumbnail chunk
function parseTINY(binaryStream, compression, chunkLength) {
const thumbnail = {};
thumbnail.width = binaryStream.readUint16BE();
thumbnail.height = binaryStream.readUint16BE();
thumbnail.size = thumbnail.width * thumbnail.height;
if (compression === 1) {
thumbnail.pixelData = decompress(binaryStream, chunkLength - 4);
} else {
thumbnail.pixelData = binaryStream.readBytes(chunkLength);
}
return thumbnail;
}
// Parse Image data chunk
function parseBODY(binaryStream, compression, chunkLength) {
if (compression === 1) {
return decompress(binaryStream, chunkLength);
}
return binaryStream.readBytes(chunkLength);
}
// Parse FORM chunk
function parseFORM(binaryStream, image) {
let chunkId = binaryStream.readString(4);
let chunkLength = binaryStream.readUint32BE();
const formatId = 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 ${binaryStream.index}. Expected "FORM".`
); );
} }
if (chunkLength !== this.binaryStream.length - 8) { if (chunkLength !== binaryStream.length - 8) {
throw new Error( throw new Error(
`Invalid chunk length: ${chunkLength} bytes. Expected ${ `Invalid chunk length: ${chunkLength} bytes. Expected ${
this.binaryStream.length - 8 binaryStream.length - 8
} bytes.` } bytes.`
); );
} }
@@ -100,149 +167,67 @@ class PBM {
} }
// Parse all other chunks // Parse all other chunks
while (!this.binaryStream.EOF()) { while (!binaryStream.EOF()) {
chunkId = this.binaryStream.readString(4); chunkId = binaryStream.readString(4);
chunkLength = this.binaryStream.readUint32BE(); chunkLength = binaryStream.readUint32BE();
switch (chunkId) { switch (chunkId) {
case "BMHD": case "BMHD":
this.parseBMHD(); parseBMHD(binaryStream, image);
break; break;
case "CMAP": case "CMAP":
this.parseCMAP(); image.palette = parseCMAP(binaryStream, image.numPlanes);
break; break;
case "DPPS": case "DPPS":
// NOTE(m): Ignore unknown DPPS chunk of size 110 bytes // NOTE(m): Ignore unknown DPPS chunk of size 110 bytes
this.binaryStream.jump(110); binaryStream.jump(110);
break; break;
case "CRNG": case "CRNG":
this.parseCRNG(); image.cyclingRanges.push(parseCRNG(binaryStream));
break; break;
case "TINY": case "TINY":
this.parseTINY(chunkLength); 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; break;
case "BODY": case "BODY":
this.parseBODY(chunkLength); image.pixelData = parseBODY(
binaryStream,
image.compression,
chunkLength
);
break; break;
default: default:
throw new Error( throw new Error(
`Unsupported chunkId: ${chunkId} at byte ${this.binaryStream.index}` `Unsupported chunkId: ${chunkId} at byte ${binaryStream.index}`
); );
} }
// Skip chunk padding byte when chunkLength is not a multiple of 2 // Skip chunk padding byte when chunkLength is not a multiple of 2
if (chunkLength % 2 === 1) this.binaryStream.jump(1); if (chunkLength % 2 === 1) binaryStream.jump(1);
} }
} }
// Parse Bitmap Header chunk export default function parsePBM(arrayBuffer) {
parseBMHD() { const binaryStream = new BinaryStream(arrayBuffer);
this.width = this.binaryStream.readUint16BE(); const image = {
this.height = this.binaryStream.readUint16BE(); cyclingRanges: [],
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
parseCMAP() {
const numColors = 2 ** this.numPlanes;
// TODO(m): Read 3 bytes at a time?
for (let i = 0; i < numColors; i++) {
let rgb = [];
for (let j = 0; j < 3; j++) {
rgb.push(this.binaryStream.readByte());
}
this.palette.push(rgb);
}
}
// Parse Color range chunk
parseCRNG() {
this.binaryStream.jump(2); // 2 bytes padding according to https://en.wikipedia.org/wiki/ILBM#CRNG:_Colour_range
const cyclingRange = {
rate: this.binaryStream.readInt16BE(),
flags: this.binaryStream.readInt16BE(),
low: this.binaryStream.readUint8(),
hight: this.binaryStream.readUint8(),
}; };
this.cyclingRanges.push(cyclingRange); try {
} parseFORM(binaryStream, image);
} catch (error) {
// Parse Thumbnail chunk if (error instanceof RangeError) {
parseTINY(chunkLength) { throw new Error(`Failed to parse file.`);
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;
// Decompress pixel data if necessary
if (this.compression === 1) {
this.thumbnail.pixelData = this.decompress(endOfChunkIndex);
} else { } else {
this.thumbnail.pixelData = this.readUncompressed(endOfChunkIndex); throw error; // re-throw the error unchanged
} }
} }
// Parse Image data chunk return image;
parseBODY(chunkLength) {
const endOfChunkIndex = this.binaryStream.index + chunkLength;
// 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 (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 {
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;
}
}
export default PBM;

View File

@@ -1,17 +1,25 @@
#image-canvas { .container {
position: absolute; display: flex;
top: 50%; justify-content: space-evenly;
left: 50%;
transform: translate(-50%, -50%);
width: 640px;
height: 480px;
} }
#thumbnail-canvas { .sidebar {
position: absolute; height: 500px;
top: 50%; display: flex;
left: 15%; flex-direction: column;
transform: translate(-50%, -50%); justify-content: space-evenly;
width: 80px; align-items: center;
height: 60px; }
.buttons {
display: flex;
justify-content: space-around;
}
#palette-canvas {
border: 1px solid black;
}
#cyclingSpeedSlider {
width: 70%;
} }

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: