Initial commit
This commit is contained in:
commit
d9ce760d19
132
.gitignore
vendored
Normal file
132
.gitignore
vendored
Normal 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
21
LICENSE
Normal 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
63
README.md
Normal 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
BIN
assets/TEST.LBM
Normal file
Binary file not shown.
1424
docs/IFF.asc
Normal file
1424
docs/IFF.asc
Normal file
File diff suppressed because it is too large
Load Diff
19
docs/PBM.asc
Normal file
19
docs/PBM.asc
Normal 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
17
index.html
Normal 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
91
main.js
Normal 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
1906
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
package.json
Normal file
18
package.json
Normal 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
94
src/binarystream.js
Normal 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
240
src/pbm.js
Normal 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
17
style.css
Normal 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
BIN
tests/fixtures/ASH.LBM
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/INVALID_CHUNK_ID.LBM
vendored
Normal file
BIN
tests/fixtures/INVALID_CHUNK_ID.LBM
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/INVALID_CHUNK_LENGTH.LBM
vendored
Normal file
BIN
tests/fixtures/INVALID_CHUNK_LENGTH.LBM
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/SEASCAPE.LBM
vendored
Normal file
BIN
tests/fixtures/SEASCAPE.LBM
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/VALID.LBM
vendored
Normal file
BIN
tests/fixtures/VALID.LBM
vendored
Normal file
Binary file not shown.
143
tests/pbm.test.js
Normal file
143
tests/pbm.test.js
Normal 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]);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user