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
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.
@@ -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.
- 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
### 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
import PBM from "./src/pbm.js";
import parsePBM from "./src/pbm.js";
fetch("/assets/TEST.LBM")
.then((response) => {
return response.arrayBuffer();
})
.then((buffer) => {
const image = new PBM(buffer);
const image = parsePBM(buffer);
console.log(image);
});
```
@@ -34,10 +36,10 @@ fetch("/assets/TEST.LBM")
```javascript
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 image = new PBM(data.buffer);
const image = parsePBM(data.buffer);
console.log(image);
```

View File

@@ -9,9 +9,33 @@
<title>IFF PBM image viewer</title>
</head>
<body>
<input type="file" id="imagefile" />
<canvas id="thumbnail-canvas"></canvas>
<canvas id="image-canvas"></canvas>
<p>
<input type="file" id="imagefile" />
</p>
<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>
</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>
</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 thumbnailContext = thumbnailCanvas.getContext("2d");
const imageCanvas = document.getElementById("image-canvas");
const imageContext = imageCanvas.getContext("2d");
const inputElement = document.getElementById("imagefile");
inputElement.addEventListener("change", handleFile, false);
const paletteCanvas = document.getElementById("palette-canvas");
const paletteContext = paletteCanvas.getContext("2d");
fetch("/assets/TEST.LBM")
.then((response) => {
return response.arrayBuffer();
})
.then((buffer) => {
const image = loadImage(buffer);
drawImage(image.thumbnail, thumbnailContext);
drawImage(image, imageContext);
});
let currentPalettePage = 0;
let image = null;
let running = false;
function handleFile() {
const imageFile = this.files[0];
const reader = new FileReader();
let cycleSpeed = 15.0;
reader.onload = (evt) => {
const image = loadImage(evt.target.result);
drawImage(image.thumbnail, thumbnailContext);
drawImage(image, imageContext);
};
// Drawing
function drawPalette() {
const colorSize = 20; // in pixels
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) {
const image = new PBM(buffer);
thumbnailCanvas.width = image.thumbnail.width;
thumbnailCanvas.height = image.thumbnail.height;
imageCanvas.width = image.width;
imageCanvas.height = image.height;
function drawImage(anImage, ctx) {
if (!anImage) return;
return image;
}
ctx.clearRect(0, 0, anImage.width, anImage.height);
const pixels = ctx.createImageData(anImage.width, anImage.height);
function drawImage(image, context) {
context.clearRect(0, 0, image.width, image.height);
let pixels = context.createImageData(image.width, image.height);
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];
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 r = image.palette[paletteIndex][0];
const g = image.palette[paletteIndex][1];
const b = image.palette[paletteIndex][2];
const r = anImage.palette[paletteIndex][0];
const g = anImage.palette[paletteIndex][1];
const b = anImage.palette[paletteIndex][2];
pixels.data[pixelIndex] = r;
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",
"devDependencies": {
"@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"
}
}

View File

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

View File

@@ -26,223 +26,208 @@
import BinaryStream from "./binarystream.js";
class PBM {
constructor(arrayBuffer) {
this.binaryStream = new BinaryStream(arrayBuffer);
function decompress(binaryStream, length) {
const result = [];
const endOfChunkIndex = binaryStream.index + length;
// Image properties taken from BMHD chunk
this.width = null;
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;
while (binaryStream.index < endOfChunkIndex) {
const byte = binaryStream.readByte();
// Palette information taken from CMAP chunk
this.palette = [];
// Color cycling information taken from CRNG chunk
this.cyclingRanges = [];
// Thumbnail information taken from TINY chunk
this.thumbnail = {
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 {
throw error; // re-throw the error unchanged
if (byte > 128) {
const nextByte = binaryStream.readByte();
for (let i = 0; i < 257 - byte; i++) {
result.push(nextByte);
}
}
}
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;
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}`
);
} else if (byte < 128) {
for (let i = 0; i < byte + 1; i++) {
result.push(binaryStream.readByte());
}
// Skip chunk padding byte when chunkLength is not a multiple of 2
if (chunkLength % 2 === 1) this.binaryStream.jump(1);
}
}
// Parse Bitmap Header chunk
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
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);
}
// Parse Thumbnail chunk
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;
// Decompress pixel data if necessary
if (this.compression === 1) {
this.thumbnail.pixelData = this.decompress(endOfChunkIndex);
} else {
this.thumbnail.pixelData = this.readUncompressed(endOfChunkIndex);
break;
}
}
// Parse Image data chunk
parseBODY(chunkLength) {
const endOfChunkIndex = this.binaryStream.index + chunkLength;
return result;
}
// Decompress pixel data if necessary
if (this.compression === 1) {
this.pixelData = this.decompress(endOfChunkIndex);
} else {
this.pixelData = this.readUncompressed(endOfChunkIndex);
// Parse Bitmap Header chunk
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);
}
decompress(endOfChunkIndex) {
let result = [];
return palette;
}
while (this.binaryStream.index < endOfChunkIndex) {
const byte = this.binaryStream.readByte();
// Parse Color range chunk
function parseCRNG(binaryStream) {
binaryStream.jump(2); // 2 bytes padding according to https://en.wikipedia.org/wiki/ILBM#CRNG:_Colour_range
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 {
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
if (chunkId !== "FORM") {
throw new Error(
`Invalid chunkId: "${chunkId}" at byte ${binaryStream.index}. Expected "FORM".`
);
}
if (chunkLength !== binaryStream.length - 8) {
throw new Error(
`Invalid chunk length: ${chunkLength} bytes. Expected ${
binaryStream.length - 8
} bytes.`
);
}
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;
}
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;
}
// 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;
// Skip chunk padding byte when chunkLength is not a multiple of 2
if (chunkLength % 2 === 1) binaryStream.jump(1);
}
}
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;
}

View File

@@ -1,17 +1,25 @@
#image-canvas {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 640px;
height: 480px;
.container {
display: flex;
justify-content: space-evenly;
}
#thumbnail-canvas {
position: absolute;
top: 50%;
left: 15%;
transform: translate(-50%, -50%);
width: 80px;
height: 60px;
.sidebar {
height: 500px;
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
}
.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
@@ -26,48 +27,45 @@
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", () => {
const fs = require("fs");
const data = fs.readFileSync("./tests/fixtures/VALID.LBM");
expect(() => {
new PBM(data.buffer);
parsePBM(data.buffer);
}).not.toThrowError();
});
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);
parsePBM(data.buffer);
}).toThrowError(/^Invalid chunkId: "FARM" at byte 12. Expected "FORM".$/);
});
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");
expect(() => {
new PBM(data.buffer);
parsePBM(data.buffer);
}).toThrowError(/^Invalid chunk length: 7070 bytes. Expected 7012 bytes.$/);
});
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");
expect(() => {
new PBM(data.buffer);
parsePBM(data.buffer);
}).toThrowError(/^Invalid formatId: "ILBM". Expected "PBM ".$/);
});
test("Parse a PBM bitmap header", () => {
const fs = require("fs");
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.height).toStrictEqual(480);
@@ -85,26 +83,23 @@ test("Parse a PBM bitmap header", () => {
});
test("Parse PBM palette information", () => {
const fs = require("fs");
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[10]).toStrictEqual([87, 255, 87]);
});
test("Parse PBM color cycling information", () => {
const fs = require("fs");
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);
});
test("Parse PBM thumbnail", () => {
const fs = require("fs");
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.height).toStrictEqual(60);
@@ -112,9 +107,8 @@ test("Parse PBM thumbnail", () => {
});
test("Decode PBM thumbnail pixel data", () => {
const fs = require("fs");
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);
// 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", () => {
const fs = require("fs");
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);
// FIXME(m): Verify these values are correct in the test image: