Initial commit

This commit is contained in:
Michael Smith 2023-05-11 23:39:28 +02:00 committed by Michael Smith
commit d9ce760d19
19 changed files with 4185 additions and 0 deletions

132
.gitignore vendored Normal file
View File

@ -0,0 +1,132 @@
# ---> Node
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Michael Smith <root@retrospace.be>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

63
README.md Normal file
View File

@ -0,0 +1,63 @@
# pbm-js
JavaScript library and proof of concept viewer for IFF PBM files.
This is the format used by the PC version of Deluxe Paint II and is different from the Amiga version.
## Features
- Written using ES6 modules, runs out of the box in modern browsers.
- 100% plain JavaScript. No dependencies.
## 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._
```javascript
import PBM from "./src/pbm.js";
fetch("/assets/TEST.LBM")
.then((response) => {
return response.arrayBuffer();
})
.then((buffer) => {
const image = new PBM(buffer);
console.log(image);
});
```
### In Node.js
```javascript
import * as fs from "fs";
import PBM from "./src/pbm.js";
const data = fs.readFileSync("./tests/fixtures/VALID.LBM");
const image = new PBM(data.buffer);
console.log(image);
```
## Testing
```sh
# Install test framework and dependencies (requires Node.js)
npm install
# Run the tests
npm run test
```
## Missing features
- Color Cycling
## References
- [libiff](https://github.com/svanderburg/libiff): Portable, extensible parser for the Interchange File Format (IFF). Well documented and very detailed C implementation of the IFF file format by [Sander van der Burg](http://sandervanderburg.nl/index.php).
- [DPaint-js](https://github.com/steffest/DPaint-js): Web based image editor, modeled after the legendary Deluxe Paint with a focus on retro Amiga file formats: read and write Amiga icon files and IFF ILBM images.
## Contributing
This software is currently in alpha version. Bug reports and pull requests welcome.

BIN
assets/TEST.LBM Normal file

Binary file not shown.

1424
docs/IFF.asc Normal file

File diff suppressed because it is too large Load Diff

19
docs/PBM.asc Normal file
View File

@ -0,0 +1,19 @@
NOTE(m): Copied from https://github.com/svanderburg/libilbm/blob/master/doc/additions/PBM.asc
Apparently, The PC version of Deluxe Paint stores images in a slightly different
format compared to the Amiga version of the Deluxe Paint. After doing some
experiments, I have discovered the following differences:
- The IFF form type used is: 'PBM ' instead of 'ILBM'.
- As PC hardware does not know anything about bitplanes, its BODY uses chunky
format, in which every byte represents a pixel.
- Interleaving scanlines is to smoothly render planar graphics data on an Amiga,
so that no visual corruption is visible while calculating the index values of
each pixel. On the PC this is not necessary, so we don't have to interleave or
deinterleave anything.
- Some Amiga specific chunks, such as CAMG are not used.
Sander van der Burg

17
index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css" />
<script type="module" src="main.js"></script>
<title>IFF PBM image viewer</title>
</head>
<body>
<input type="file" id="imagefile" />
<canvas id="thumbnail-canvas"></canvas>
<canvas id="image-canvas"></canvas>
</body>
</html>

91
main.js Normal file
View File

@ -0,0 +1,91 @@
/*
MIT License
Copyright (c) 2023 Michael Smith <root@retrospace.be>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import PBM 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);
fetch("/assets/TEST.LBM")
.then((response) => {
return response.arrayBuffer();
})
.then((buffer) => {
const image = loadImage(buffer);
drawImage(image.thumbnail, thumbnailContext);
drawImage(image, imageContext);
});
function handleFile() {
const imageFile = this.files[0];
const reader = new FileReader();
reader.onload = (evt) => {
const image = loadImage(evt.target.result);
drawImage(image.thumbnail, thumbnailContext);
drawImage(image, imageContext);
};
reader.readAsArrayBuffer(imageFile);
}
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;
return image;
}
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];
const pixelIndex = index * 4;
const r = image.palette[paletteIndex][0];
const g = image.palette[paletteIndex][1];
const b = image.palette[paletteIndex][2];
pixels.data[pixelIndex] = r;
pixels.data[pixelIndex + 1] = g;
pixels.data[pixelIndex + 2] = b;
pixels.data[pixelIndex + 3] = 255;
}
}
context.putImageData(pixels, 0, 0);
}

1906
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
package.json Normal file
View File

@ -0,0 +1,18 @@
{
"type": "module",
"name": "pbm-js",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "vitest",
"coverage": "vitest run --coverage"
},
"keywords": [],
"author": "Michael Smith",
"license": "MIT",
"devDependencies": {
"@vitest/coverage-c8": "^0.31.0",
"vitest": "^0.31.0"
}
}

94
src/binarystream.js Normal file
View File

@ -0,0 +1,94 @@
/*
MIT License
Copyright (c) 2023 Michael Smith <root@retrospace.be>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
// NOTE(m): Partially copied from
// https://github.com/steffest/DPaint-js/blob/master/_script/util/binarystream.js
class BinaryStream {
constructor(arrayBuffer) {
this.index = 0;
this.data = arrayBuffer;
this.dataView = new DataView(arrayBuffer);
this.length = arrayBuffer.byteLength;
}
EOF() {
return this.index === this.length;
}
jump(offset) {
this.index += offset;
}
readByte() {
const byte = this.dataView.getUint8(this.index);
this.index++;
return byte;
}
readInt16BE() {
const value = this.dataView.getInt16(this.index);
this.index += 2;
return value;
}
readString(length) {
let string = "";
for (let i = 0; i < length; i++) {
const byte = this.dataView.getUint8(this.index + i);
string += String.fromCharCode(byte);
}
this.index += length;
return string;
}
readUint8() {
const value = this.dataView.getUint8(this.index);
this.index += 1;
return value;
}
readUint16BE() {
const value = this.dataView.getUint16(this.index);
this.index += 2;
return value;
}
readUint32BE() {
const value = this.dataView.getUint32(this.index);
this.index += 4;
return value;
}
}
export default BinaryStream;

240
src/pbm.js Normal file
View File

@ -0,0 +1,240 @@
/*
MIT License
Copyright (c) 2023 Michael Smith <root@retrospace.be>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import BinaryStream from "./binarystream.js";
class PBM {
constructor(arrayBuffer) {
this.binaryStream = new BinaryStream(arrayBuffer);
// 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;
// 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
}
}
}
parseFORM() {
// Parse "FORM" chunk
let chunkID = this.binaryStream.readString(4);
const lenChunk = 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 (lenChunk !== this.binaryStream.length - 8) {
throw new Error(
`Invalid chunk length: ${lenChunk} 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);
this.binaryStream.jump(4); // Skip 4 bytes chunk length value
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();
break;
case "BODY":
this.parseBODY();
break;
default:
throw new Error(
`Unsupported chunkID: ${chunkID} at byte ${this.binaryStream.index}`
);
}
}
}
// 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;
// FIXME(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() {
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);
}
}
}
// Parse Image data chunk
parseBODY() {
// NOTE(m): Should we make use of the chunk length here instead?
while (this.pixelData.length < this.size) {
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;
}
} else {
// Data is not compressed, just copy the bytes
this.pixelData.push(byte);
}
}
}
}
export default PBM;

17
style.css Normal file
View File

@ -0,0 +1,17 @@
#image-canvas {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 640px;
height: 480px;
}
#thumbnail-canvas {
position: absolute;
top: 50%;
left: 15%;
transform: translate(-50%, -50%);
width: 80px;
height: 60px;
}

BIN
tests/fixtures/ASH.LBM vendored Normal file

Binary file not shown.

BIN
tests/fixtures/INVALID_CHUNK_ID.LBM vendored Normal file

Binary file not shown.

BIN
tests/fixtures/INVALID_CHUNK_LENGTH.LBM vendored Normal file

Binary file not shown.

BIN
tests/fixtures/SEASCAPE.LBM vendored Normal file

Binary file not shown.

BIN
tests/fixtures/VALID.LBM vendored Normal file

Binary file not shown.

143
tests/pbm.test.js Normal file
View File

@ -0,0 +1,143 @@
/*
MIT License
Copyright (c) 2023 Michael Smith <root@retrospace.be>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import { expect, test } from "vitest";
import PBM from "../src/pbm.js";
test("Successfully parse a PBM file", () => {
const fs = require("fs");
const data = fs.readFileSync("./tests/fixtures/VALID.LBM");
expect(() => {
new PBM(data.buffer);
}).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".$/);
});
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);
}).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);
}).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);
expect(image.width).toStrictEqual(640);
expect(image.height).toStrictEqual(480);
expect(image.size).toStrictEqual(307_200);
expect(image.xOrigin).toStrictEqual(0);
expect(image.yOrigin).toStrictEqual(0);
expect(image.numPlanes).toStrictEqual(8);
expect(image.mask).toStrictEqual(0);
expect(image.compression).toStrictEqual(1);
expect(image.transClr).toStrictEqual(255);
expect(image.xAspect).toStrictEqual(1);
expect(image.yAspect).toStrictEqual(1);
expect(image.pageWidth).toStrictEqual(640);
expect(image.pageHeight).toStrictEqual(480);
});
test("Parse PBM palette information", () => {
const fs = require("fs");
const data = fs.readFileSync("./tests/fixtures/VALID.LBM");
const image = new PBM(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);
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);
expect(image.thumbnail.width).toStrictEqual(80);
expect(image.thumbnail.height).toStrictEqual(60);
expect(image.thumbnail.size).toStrictEqual(4800);
});
test("Decode PBM thumbnail pixel data", () => {
const fs = require("fs");
const data = fs.readFileSync("./tests/fixtures/VALID.LBM");
const image = new PBM(data.buffer);
expect(image.thumbnail.pixelData.length).toStrictEqual(4800);
// FIXME(m): Verify these values are correct in the test image thumbnail:
expect(image.thumbnail.pixelData[0]).toStrictEqual(14);
expect(image.palette[14]).toStrictEqual([255, 255, 87]);
});
test("Decode PBM image pixel data", () => {
const fs = require("fs");
const data = fs.readFileSync("./tests/fixtures/VALID.LBM");
const image = new PBM(data.buffer);
expect(image.pixelData.length).toStrictEqual(307_200);
// FIXME(m): Verify these values are correct in the test image:
expect(image.pixelData[0]).toStrictEqual(14);
expect(image.palette[14]).toStrictEqual([255, 255, 87]);
});