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