Repository: image-js/tiff
Branch: main
Commit: a3a91a905c3e
Files: 76
Total size: 97.9 MB
Directory structure:
gitextract_4e2k9nc9/
├── .github/
│ └── workflows/
│ ├── nodejs.yml
│ ├── release.yml
│ └── typedoc.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── eslint.config.js
├── img/
│ ├── array-sample-format.tif
│ ├── black.tif
│ ├── bw1bit.tif
│ ├── bwCross.tif
│ ├── cells.tif
│ ├── color-1px.tif
│ ├── color-5x5-deflate.tif
│ ├── color-5x5-lzw.tif
│ ├── color-5x5.tif
│ ├── color-alpha-2x2.tif
│ ├── color-alpha-5x5-lzw.tif
│ ├── color-alpha-5x5.tif
│ ├── color16-lzw.tif
│ ├── color16-multi.tif
│ ├── color16.tif
│ ├── color8-alpha.tif
│ ├── color8-lzw.tif
│ ├── color8-multi.tif
│ ├── color8.tif
│ ├── crosshair_tiled.tif
│ ├── dog.tiff
│ ├── female.tiff
│ ├── float32.tif
│ ├── float64.tif
│ ├── grey16-multi.tif
│ ├── grey16.tif
│ ├── grey8-lzw.tif
│ ├── grey8-multi.tif
│ ├── grey8.tif
│ ├── greya16.tif
│ ├── greya32.tif
│ ├── image-deflate.tif
│ ├── image-lzw.tif
│ ├── image.tif
│ ├── jellybeans.tiff
│ ├── palette.tif
│ ├── stack.tif
│ ├── tile_rgb_deflate.tif
│ ├── tiled.tif
│ └── whiteIsZero.tif
├── package.json
├── src/
│ ├── .npmignore
│ ├── __tests__/
│ │ ├── data/
│ │ │ ├── 1.strip
│ │ │ ├── 173.strip
│ │ │ └── 174.strip
│ │ ├── decode.lzw.test.ts
│ │ ├── decode.test.ts
│ │ ├── is_multi_page.test.ts
│ │ ├── lzw.test.ts
│ │ └── page_count.test.ts
│ ├── hacks.ts
│ ├── horizontal_differencing.ts
│ ├── ifd.ts
│ ├── ifd_value.ts
│ ├── index.ts
│ ├── lzw.ts
│ ├── tags/
│ │ ├── exif.ts
│ │ ├── gps.ts
│ │ └── standard.ts
│ ├── tiff_decoder.ts
│ ├── tiff_ifd.ts
│ ├── types.ts
│ └── zlib.ts
├── tsconfig.build.json
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/nodejs.yml
================================================
name: Node.js CI
on:
push:
branches:
- main
pull_request:
jobs:
nodejs:
# Documentation: https://github.com/zakodium/workflows#nodejs-ci
uses: zakodium/workflows/.github/workflows/nodejs.yml@nodejs-v1
with:
lint-check-types: true
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
push:
branches:
- main
jobs:
release:
# Documentation: https://github.com/zakodium/workflows#release
uses: zakodium/workflows/.github/workflows/release.yml@release-v1
with:
npm: true
secrets:
github-token: ${{ secrets.BOT_TOKEN }}
npm-token: ${{ secrets.NPM_BOT_TOKEN }}
================================================
FILE: .github/workflows/typedoc.yml
================================================
name: TypeDoc
on:
workflow_dispatch:
release:
types: [published]
jobs:
typedoc:
# Documentation: https://github.com/zakodium/workflows#typedoc
uses: zakodium/workflows/.github/workflows/typedoc.yml@typedoc-v1
with:
entry: 'src/index.ts'
secrets:
github-token: ${{ secrets.BOT_TOKEN }}
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Locally Packged Module
*.tgz
# Optional REPL history
.node_repl_history
lib
lib-esm
.idea/
.DS_Store
================================================
FILE: .npmrc
================================================
ignore-scripts=true
================================================
FILE: .prettierignore
================================================
CHANGELOG.md
================================================
FILE: .prettierrc.json
================================================
{
"arrowParens": "always",
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all"
}
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## [7.1.3](https://github.com/image-js/tiff/compare/v7.1.2...v7.1.3) (2025-11-25)
### Bug Fixes
* use fflate instead of pako ([#81](https://github.com/image-js/tiff/issues/81)) ([a6ea694](https://github.com/image-js/tiff/commit/a6ea694a4eef06c8d3b856f2d9dce3d3e2510a0f))
## [7.1.2](https://github.com/image-js/tiff/compare/v7.1.1...v7.1.2) (2025-10-07)
### Bug Fixes
* fix bug with rowsPerStrip ([#79](https://github.com/image-js/tiff/issues/79)) ([7abbe97](https://github.com/image-js/tiff/commit/7abbe97dcba8ffcc581977c512bfeaeca000a79d))
## [7.1.1](https://github.com/image-js/tiff/compare/v7.1.0...v7.1.1) (2025-08-13)
### Bug Fixes
* exports ([#75](https://github.com/image-js/tiff/issues/75)) ([c1f2448](https://github.com/image-js/tiff/commit/c1f2448bf460aea86aca6b6c41414713ee5b50e4))
## [7.1.0](https://github.com/image-js/tiff/compare/v7.0.0...v7.1.0) (2025-07-23)
### Features
* add support for 1 bit depth bw files ([#73](https://github.com/image-js/tiff/issues/73)) ([f101c2b](https://github.com/image-js/tiff/commit/f101c2b477c92fd2c4542b5a2452ed4cc5361a65))
### Performance Improvements
* improve performance of data decoding ([#70](https://github.com/image-js/tiff/issues/70)) ([be54e07](https://github.com/image-js/tiff/commit/be54e0790216b87b269dbcfe4671c48ac512d465))
## [7.0.0](https://github.com/image-js/tiff/compare/v6.2.0...v7.0.0) (2025-06-15)
### ⚠ BREAKING CHANGES
* migrate to ESM ([#68](https://github.com/image-js/tiff/issues/68))
### Code Refactoring
* migrate to ESM ([#68](https://github.com/image-js/tiff/issues/68)) ([3345547](https://github.com/image-js/tiff/commit/3345547d0665da124fdf9f3a61cdf99442bd4040))
## [6.2.0](https://github.com/image-js/tiff/compare/v6.1.1...v6.2.0) (2025-03-06)
### Features
* add float64 support ([#59](https://github.com/image-js/tiff/issues/59)) ([a164636](https://github.com/image-js/tiff/commit/a1646369615fe775fc0f3bcd166cbf9ed816138b))
* add tiled image support ([#62](https://github.com/image-js/tiff/issues/62)) ([8b1c922](https://github.com/image-js/tiff/commit/8b1c9220343831305967a220b0a9d39e0855aca7))
## [6.1.1](https://github.com/image-js/tiff/compare/v6.1.0...v6.1.1) (2024-09-12)
### Bug Fixes
* support Deflate compression code ([#56](https://github.com/image-js/tiff/issues/56)) ([d965602](https://github.com/image-js/tiff/commit/d965602d6f9f2857a84732298947b5fbeb75009b))
## [6.1.0](https://github.com/image-js/tiff/compare/v6.0.0...v6.1.0) (2024-05-21)
### Features
* export map of tag ids to names ([#53](https://github.com/image-js/tiff/issues/53)) ([9774e80](https://github.com/image-js/tiff/commit/9774e80c7d9d9d9db4fc05795701ec20961ad46d))
## [6.0.0](https://github.com/image-js/tiff/compare/v5.0.3...v6.0.0) (2024-04-08)
### ⚠ BREAKING CHANGES
* removed `firstImage` option, use `pages` option instead.
### Features
* add `pages` option ([#47](https://github.com/image-js/tiff/issues/47)) ([f0f0bac](https://github.com/image-js/tiff/commit/f0f0bac57a1f8790a9866fb4e476ac0a6e86b345)), closes [#37](https://github.com/image-js/tiff/issues/37) [#46](https://github.com/image-js/tiff/issues/46)
### Bug Fixes
* Support array sample format ([#51](https://github.com/image-js/tiff/issues/51)) ([42d778b](https://github.com/image-js/tiff/commit/42d778b333764b6e1b74b9af5718416991623fda))
### [5.0.3](https://www.github.com/image-js/tiff/compare/v5.0.2...v5.0.3) (2021-11-05)
### Bug Fixes
* support some malformed images without StripByteCounts ([56058a9](https://www.github.com/image-js/tiff/commit/56058a99c9e0b1e7e129b4150e4a705061555e20))
### [5.0.2](https://www.github.com/image-js/tiff/compare/v5.0.1...v5.0.2) (2021-10-31)
### Bug Fixes
* correctly decode images from pooled buffers ([fcbbc34](https://www.github.com/image-js/tiff/commit/fcbbc348028b97ee6f99186628fe11c8135a6e6a))
* make LZW more robust ([72a6180](https://www.github.com/image-js/tiff/commit/72a61809b8399b4d30d3657a248de2e7a2d2cdd6))
### [5.0.1](https://www.github.com/image-js/tiff/compare/v5.0.0...v5.0.1) (2021-10-12)
### Bug Fixes
* set TypeScript target to ES2020 ([#33](https://www.github.com/image-js/tiff/issues/33)) ([30e18e9](https://www.github.com/image-js/tiff/commit/30e18e956859bf64dc836ae53da3204cfefb9851))
## [5.0.0](https://www.github.com/image-js/tiff/compare/v4.3.0...v5.0.0) (2021-07-06)
### ⚠ BREAKING CHANGES
* Removed support for Node.js 10.
### Miscellaneous Chores
* stop testing on Node.js 10 ([220822f](https://www.github.com/image-js/tiff/commit/220822f008a3b8c6b047f4f78d2f01b202cda8b0))
## [4.3.0](https://github.com/image-js/tiff/compare/v4.2.0...v4.3.0) (2020-12-03)
### Features
* add support for Zlib/deflate compression ([7d3a04c](https://github.com/image-js/tiff/commit/7d3a04c04c44d75373ccd6c7928cb69b3b725077))
# [4.2.0](https://github.com/image-js/tiff/compare/v4.1.3...v4.2.0) (2020-08-21)
### Features
* add support for alpha channel and compressed 16-bit images ([5f2e612](https://github.com/image-js/tiff/commit/5f2e6128ed7b096290c1ebe0b861a61d3849b255))
## [4.1.3](https://github.com/image-js/tiff/compare/v4.1.2...v4.1.3) (2020-08-07)
### Bug Fixes
* support images that do not define `samplesPerPixel` ([2c2587b](https://github.com/image-js/tiff/commit/2c2587b4307ef22225d38f5d24968c678bf6fa54))
## [4.1.2](https://github.com/image-js/tiff/compare/v4.1.1...v4.1.2) (2020-08-06)
### Bug Fixes
* really correct decoding of RGB images ([5595c53](https://github.com/image-js/tiff/commit/5595c539469dfd3e8e2b617511f8327b94c86a77))
## [4.1.1](https://github.com/image-js/tiff/compare/v4.1.0...v4.1.1) (2020-08-06)
### Bug Fixes
* correctly support RGB images ([d546610](https://github.com/image-js/tiff/commit/d5466101845fd90c8a5225857ff0d7216d51b88d))
# [4.1.0](https://github.com/image-js/tiff/compare/v4.0.0...v4.1.0) (2020-08-04)
### Features
* support LZW compression ([20fbb50](https://github.com/image-js/tiff/commit/20fbb501b8855489e91ae22f519760c2112aae68))
# [4.0.0](https://github.com/image-js/tiff/compare/v3.0.1...v4.0.0) (2020-01-23)
### chore
* stop supporting Node.js 6 and 8 ([1156f52](https://github.com/image-js/tiff/commit/1156f52aaa4210dfb9ee2fef052b775298b86b81))
### Features
* add support for palette images ([d31413b](https://github.com/image-js/tiff/commit/d31413b09ed8f589107f7c1ffae06d5ea2e22b49))
### BREAKING CHANGES
* Node.js 6 and 8 are no longer supported.
## [3.0.1](https://github.com/image-js/tiff/compare/v3.0.0...v3.0.1) (2018-09-12)
### Bug Fixes
* support WhiteIsZero ([bf7a4ca](https://github.com/image-js/tiff/commit/bf7a4ca)), closes [#14](https://github.com/image-js/tiff/issues/14)
# [3.0.0](https://github.com/image-js/tiff/compare/v2.1.0...v3.0.0) (2017-10-25)
### Chores
* remove Node 4 from travis ([5c743d2](https://github.com/image-js/tiff/commit/5c743d2))
### Features
* add pageCount and isMultiPage functions ([7dac89f](https://github.com/image-js/tiff/commit/7dac89f))
### BREAKING CHANGES
* Stop support for Node 4
# [2.1.0](https://github.com/image-js/tiff/compare/v2.0.1...v2.1.0) (2016-11-05)
### Features
* add support for uncompressed RGB data ([b3ffff7](https://github.com/image-js/tiff/commit/b3ffff7))
## [2.0.1](https://github.com/image-js/tiff/compare/v2.0.0...v2.0.1) (2016-09-20)
### Bug Fixes
* return decimal numbers for rational types ([c3bad6c](https://github.com/image-js/tiff/commit/c3bad6c))
# [2.0.0](https://github.com/image-js/tiff/compare/v1.1.1...v2.0.0) (2016-09-20)
### Code Refactoring
* hide the decoder class behind a decode function ([78603ff](https://github.com/image-js/tiff/commit/78603ff))
### Features
* add support for decoding EXIF and GPS IFDs ([b2766a5](https://github.com/image-js/tiff/commit/b2766a5))
* allow to pass iobuffer options to the decoder ([97f0f8e](https://github.com/image-js/tiff/commit/97f0f8e))
### BREAKING CHANGES
* The API has changed. Use `tiff.decode()` instead of `TIFFDecoder`.
## [1.1.1](https://github.com/image-js/tiff/compare/v1.1.0...v1.1.1) (2016-04-25)
### Bug Fixes
* default value for compression field is 1 ([14c13a4](https://github.com/image-js/tiff/commit/14c13a4))
# [1.1.0](https://github.com/image-js/tiff/compare/v1.0.0...v1.1.0) (2015-12-04)
# [1.0.0](https://github.com/image-js/tiff/compare/v0.0.2...v1.0.0) (2015-11-23)
## [0.0.2](https://github.com/image-js/tiff/compare/v0.0.1...v0.0.2) (2015-09-20)
## 0.0.1 (2015-09-19)
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015 Michaël Zasso
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.
================================================
FILE: README.md
================================================
Maintained by Zakodium
# tiff
[](https://www.npmjs.com/package/tiff)
[](https://www.npmjs.com/package/tiff)
[](https://codecov.io/gh/image-js/tiff)
[](https://github.com/image-js/tiff/blob/main/LICENSE)
TIFF image decoder written entirely in JavaScript.
## Installation
```console
npm install tiff
```
## Compatibility
### [TIFF standard](./TIFF6.pdf)
The library can currently decode greyscale and RGB images (8, 16 or 32 bits).
It supports LZW compression and images with an additional alpha channel.
### Extensions
Images compressed with Zlib/deflate algorithm are also supported.
## API
### [Complete API documentation](https://image-js.github.io/tiff/)
### `tiff.decode(data[, options])`
Decodes the file and returns TIFF IFDs.
#### IFD object
Each decoded image is stored in an `IFD`.
##### IFD#data
The `data` property is a Typed Array containing the pixel data. It is a
`Uint8Array` for 8bit images, a `Uint16Array` for 16bit images and a
`Float32Array` for 32bit images.
##### Other properties of IFD
- `size` - number of pixels
- `width` - number of columns
- `height` - number of rows
- `bitsPerSample` - bit depth
- `alpha` - `true` if the image has an additional alpha channel
- `xResolution`
- `yResolution`
- `resolutionUnit`
### `tiff.pageCount(data)`
Returns the number of IFDs (pages) in the file.
### `tiff.isMultiPage(data)`
Returns true if the file has 2 or more IFDs (pages) and false if it has 1.
This is slightly more efficient than calling `pageCount()` if all you need to
know is whether the file has multiple pages or not.
## License
[MIT](./LICENSE)
================================================
FILE: eslint.config.js
================================================
import { defineConfig, globalIgnores } from 'eslint/config';
import cheminfo from 'eslint-config-cheminfo-typescript';
export default defineConfig(globalIgnores(['coverage', 'lib']), cheminfo);
================================================
FILE: img/black.tif
================================================
[File too large to display: 47.2 MB]
================================================
FILE: img/image-deflate.tif
================================================
[File too large to display: 13.7 MB]
================================================
FILE: img/image.tif
================================================
[File too large to display: 22.7 MB]
================================================
FILE: img/tiled.tif
================================================
[File too large to display: 14.3 MB]
================================================
FILE: package.json
================================================
{
"name": "tiff",
"version": "7.1.3",
"license": "MIT",
"description": "TIFF image decoder written entirely in JavaScript",
"author": "Michaël Zasso",
"type": "module",
"exports": {
".": "./lib/index.js"
},
"files": [
"lib",
"src"
],
"scripts": {
"check-types": "tsc --noEmit",
"clean": "rimraf coverage lib",
"eslint": "eslint .",
"eslint-fix": "eslint . --fix",
"prepack": "npm run tsc",
"prettier": "prettier --check .",
"prettier-write": "prettier --write .",
"test": "npm run test-only && npm run check-types && npm run eslint && npm run prettier",
"test-only": "vitest run --coverage",
"tsc": "npm run clean && npm run tsc-build",
"tsc-build": "tsc --project tsconfig.build.json"
},
"dependencies": {
"fflate": "^0.8.2",
"iobuffer": "^6.0.1"
},
"devDependencies": {
"@types/node": "^25.0.3",
"@vitest/coverage-v8": "^4.0.16",
"@zakodium/tsconfig": "^1.0.2",
"eslint": "^9.39.2",
"eslint-config-cheminfo-typescript": "^21.0.1",
"prettier": "^3.7.4",
"rimraf": "^6.1.2",
"typescript": "^5.9.3",
"vitest": "^4.0.16"
},
"repository": {
"type": "git",
"url": "git+https://github.com/image-js/tiff.git"
},
"bugs": {
"url": "https://github.com/image-js/tiff/issues"
},
"homepage": "https://image-js.github.io/tiff/"
}
================================================
FILE: src/.npmignore
================================================
__tests__
.npmignore
================================================
FILE: src/__tests__/data/1.strip
================================================
8`@$
BaPd6DbQ8V-FcQv=HdR9$M'JeRd]/LfS9m7NgS}?PhT:%GRiTe6OTjU:VWVkUv_XlV;%gZmVeo\nW;w^oW
================================================
FILE: src/__tests__/data/173.strip
================================================
8`@$
BaPd6DbQ8V-FcQv=HdR9$M'JeRd]/H9>fÀ373% {
it('image', { timeout: 30_000 }, () => {
const lzwBuffer = readFileSync(
join(import.meta.dirname, '../../img/image-lzw.tif'),
);
const imageLzw = decode(lzwBuffer);
const buffer = readFileSync(
join(import.meta.dirname, '../../img/image.tif'),
);
const image = decode(buffer);
expect(
dataEqual(imageLzw[0].data as Uint8Array, image[0].data as Uint8Array),
).toBe(true);
});
it('color8', () => {
const lzwBuffer = readFileSync(
join(import.meta.dirname, '../../img/color8-lzw.tif'),
);
const imageLzw = decode(lzwBuffer);
const buffer = readFileSync(
join(import.meta.dirname, '../../img/color8.tif'),
);
const image = decode(buffer);
expect(
dataEqual(imageLzw[0].data as Uint8Array, image[0].data as Uint8Array),
).toBe(true);
});
});
function dataEqual(data1: Uint8Array, data2: Uint8Array): boolean {
if (data1.length !== data2.length) return false;
for (let i = 0; i < data1.length; i++) {
if (data1[i] !== data2[i]) {
return false;
}
}
return true;
}
================================================
FILE: src/__tests__/decode.test.ts
================================================
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { expect, test } from 'vitest';
import { decode } from '../index.ts';
function readImage(file: string): Buffer {
return readFileSync(join(import.meta.dirname, '../../img', file));
}
interface TiffFile {
name: string;
width: number;
height: number;
bitsPerSample: number;
components: number;
alpha?: boolean;
pages?: number;
}
const files: TiffFile[] = [
{
name: 'color-1px.tif',
width: 1,
height: 1,
bitsPerSample: 8,
components: 3,
},
{
name: 'image-lzw.tif',
width: 2590,
height: 3062,
bitsPerSample: 8,
components: 3,
},
{
name: 'image-deflate.tif',
width: 5100,
height: 3434,
bitsPerSample: 8,
components: 3,
},
{
name: 'color8.tif',
width: 160,
height: 120,
bitsPerSample: 8,
components: 3,
},
{
name: 'color8-lzw.tif',
width: 160,
height: 120,
bitsPerSample: 8,
components: 3,
},
{
name: 'color8-alpha.tif',
width: 800,
height: 600,
bitsPerSample: 8,
components: 4,
alpha: true,
},
{
name: 'color16.tif',
width: 160,
height: 120,
bitsPerSample: 16,
components: 3,
},
{
name: 'color16-lzw.tif',
width: 160,
height: 120,
bitsPerSample: 16,
components: 3,
},
{ name: 'grey8.tif', width: 30, height: 90, bitsPerSample: 8, components: 1 },
{
name: 'grey8-lzw.tif',
width: 30,
height: 90,
bitsPerSample: 8,
components: 1,
},
{
name: 'grey16.tif',
width: 30,
height: 90,
bitsPerSample: 16,
components: 1,
},
{
name: 'whiteIsZero.tif',
width: 1248,
height: 1248,
bitsPerSample: 16,
components: 1,
},
{
name: 'cells.tif',
width: 2048,
height: 2048,
bitsPerSample: 16,
components: 1,
},
{
name: 'color-5x5.tif',
width: 5,
height: 5,
bitsPerSample: 8,
components: 3,
},
{
name: 'color-5x5-lzw.tif',
width: 5,
height: 5,
bitsPerSample: 8,
components: 3,
},
{
name: 'color-5x5-deflate.tif',
width: 5,
height: 5,
bitsPerSample: 8,
components: 3,
},
{
name: 'color-alpha-2x2.tif',
width: 2,
height: 2,
bitsPerSample: 8,
components: 4,
alpha: true,
},
{
name: 'color-alpha-5x5.tif',
width: 5,
height: 5,
bitsPerSample: 8,
components: 4,
alpha: true,
},
{
name: 'color-alpha-5x5-lzw.tif',
width: 5,
height: 5,
bitsPerSample: 8,
components: 4,
alpha: true,
},
{
name: 'float32.tif',
width: 141,
height: 125,
bitsPerSample: 32,
components: 1,
alpha: false,
},
{
name: 'float64.tif',
width: 851,
height: 338,
bitsPerSample: 64,
components: 1,
alpha: false,
},
{
name: 'black.tif',
width: 9192,
height: 2690,
bitsPerSample: 16,
components: 1,
alpha: false,
},
{
name: 'array-sample-format.tif',
width: 2,
height: 2,
bitsPerSample: 32,
components: 3,
alpha: false,
},
{
name: 'tiled.tif',
width: 2501,
height: 2001,
bitsPerSample: 32,
components: 1,
alpha: false,
},
{
name: 'bw1bit.tif',
width: 2,
height: 2,
bitsPerSample: 1,
components: 1,
alpha: false,
},
{
name: 'bwCross.tif',
width: 10,
height: 10,
bitsPerSample: 1,
components: 1,
alpha: false,
},
{
name: 'jellybeans.tiff',
width: 256,
height: 256,
bitsPerSample: 8,
components: 3,
alpha: false,
},
{
name: 'female.tiff',
width: 256,
height: 256,
bitsPerSample: 8,
components: 3,
alpha: false,
},
// Checks only the first frame of the image.
{
name: 'dog.tiff',
width: 16,
height: 16,
bitsPerSample: 1,
components: 4,
alpha: true,
pages: 8,
},
];
const cases = files.map(
(file) => [file.name, file, readImage(file.name)] as const,
);
const stack = readImage('stack.tif');
test.each(cases)(
'should decode %s',
{ timeout: 30_000 },
(name, file, image) => {
const result = decode(image);
expect(result).toHaveLength(file.pages ?? 1);
const { data, bitsPerSample, width, height, components, alpha } = result[0];
expect(width).toBe(file.width);
expect(height).toBe(file.height);
expect(components).toBe(file.components);
expect(bitsPerSample).toBe(file.bitsPerSample);
const size = file.width * file.height * file.components;
expect(data).toHaveLength(size);
expect(alpha).toBe(Boolean(file.alpha));
},
);
// prettier-ignore
const expectedRgb8BitData = Uint8Array.from([
255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0,
255, 0, 0, 0, 255, 0, 0, 0, 255, 128, 128, 128, 128, 128, 128,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
0, 255, 255, 255, 0, 255, 255, 255, 0, 128, 128, 128, 128, 128, 128,
0, 255, 255, 255, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0,
]);
test('should decode RGB 8bit data', () => {
const [result] = decode(readImage('color-5x5.tif'));
expect(result.data).toStrictEqual(expectedRgb8BitData);
});
test('should decode RGB 8bit data with LZW compression', () => {
const [result] = decode(readImage('color-5x5-lzw.tif'));
expect(result.data).toStrictEqual(expectedRgb8BitData);
});
// prettier-ignore
const expectedRgb8BitAlphaData = Uint8Array.from([
0, 0, 0, 0, 0, 255, 0, 54, 0, 0, 255, 102, 0, 0, 0, 152, 0, 0, 0, 203,
255, 0, 0, 31, 0, 255, 0, 78, 0, 0, 255, 255, 128, 128, 128, 255, 128, 128, 128, 255,
255, 255, 255, 54, 255, 255, 255, 255, 255, 255, 255, 127, 255, 255, 255, 255, 255, 255, 255, 255,
0, 255, 255, 78, 255, 0, 255, 255, 255, 255, 0, 255, 128, 128, 128, 177, 128, 128, 128, 255,
0, 255, 255, 102, 255, 0, 255, 255, 255, 255, 0, 255, 0, 0, 0, 255, 0, 0, 0, 229
]);
test('should decode RGB 8bit data with pre-multiplied alpha', () => {
const [result] = decode(readImage('color-alpha-5x5.tif'));
expect(result.data).toStrictEqual(expectedRgb8BitAlphaData);
});
test('should decode RGB 8bit data with pre-multiplied alpha and LZW compression', () => {
const [result] = decode(readImage('color-alpha-5x5-lzw.tif'));
expect(result.data).toStrictEqual(expectedRgb8BitAlphaData);
});
test('should decode RGB 8bit data with pre-multiplied alpha and lost precision', () => {
// prettier-ignore
const expectedData = Uint8Array.from([
255, 0, 0, 6, 255, 0, 0, 6,
128, 0, 0, 6, 128, 0, 0, 6,
]);
const [result] = decode(readImage('color-alpha-2x2.tif'));
expect(result.data).toStrictEqual(expectedData);
});
test('should decode with onlyFirst', () => {
const result = decode(readImage('grey8.tif'), { pages: [0] });
expect(result[0]).toHaveProperty('data');
});
test('should omit data', () => {
const result = decode(readImage('grey8.tif'), { ignoreImageData: true });
expect(result[0].data).toStrictEqual(new Uint8Array());
});
test('should read exif data', () => {
const result = decode(readImage('grey8.tif'), {
pages: [0],
ignoreImageData: true,
});
// @ts-expect-error We know exif is defined.
expect(result[0].exif.map).toStrictEqual({
ColorSpace: 65535,
PixelXDimension: 30,
PixelYDimension: 90,
});
});
test('should decode stacks', () => {
const decoded = decode(stack);
expect(decoded).toHaveLength(10);
for (const image of decoded) {
expect(image.width).toBe(128);
expect(image.height).toBe(128);
}
});
test('specify pages to decode', () => {
const decoded = decode(stack, { pages: [0, 2, 4, 6, 8] });
expect(decoded).toHaveLength(5);
for (const image of decoded) {
expect(image.width).toBe(128);
expect(image.height).toBe(128);
}
});
test('should throw if pages invalid', () => {
expect(() => decode(stack, { pages: [-1] })).toThrowError(
'Index -1 is invalid. Must be a positive integer.',
);
expect(() => decode(stack, { pages: [0.5] })).toThrowError(
'Index 0.5 is invalid. Must be a positive integer.',
);
expect(() => decode(stack, { pages: [20] })).toThrowError(
'Index 20 is out of bounds. The stack only contains 10 images.',
);
});
test('should decode palette', () => {
const decoded = decode(readImage('palette.tif'));
expect(decoded).toHaveLength(1);
const { palette } = decoded[0];
expect(palette).toHaveLength(256);
// @ts-expect-error We know palette is defined.
expect(palette[0]).toStrictEqual([65535, 0, 0]);
});
test('should decode image compressed with deflate algorithm', () => {
const decoded = decode(readImage('tile_rgb_deflate.tif'));
expect(decoded).toHaveLength(1);
expect(decoded[0]).toMatchObject({
alpha: false,
bitsPerSample: 16,
components: 3,
compression: 8,
width: 128,
height: 128,
});
});
test('should decode basic 2x2 1-bit image', () => {
const decoded = decode(readImage('bw1bit.tif'));
expect(decoded).toHaveLength(1);
expect(decoded[0]).toMatchObject({
alpha: false,
bitsPerSample: 1,
components: 1,
compression: 1,
width: 2,
height: 2,
});
expect(decoded[0].data).toStrictEqual(new Uint8Array([1, 0, 0, 1]));
});
test('should decode 10x10 1-bit image as a cross', () => {
const decoded = decode(readImage('bwCross.tif'));
expect(decoded).toHaveLength(1);
expect(decoded[0]).toMatchObject({
alpha: false,
bitsPerSample: 1,
components: 1,
compression: 1,
width: 10,
height: 10,
});
expect(decoded[0].data).toStrictEqual(
new Uint8Array(
[
[0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
].flat(),
),
);
});
test('should decode 15x15 image with tile data', () => {
const decoded = decode(readImage('crosshair_tiled.tif'));
expect(decoded).toHaveLength(1);
expect(decoded[0]).toMatchObject({
alpha: false,
bitsPerSample: 1,
components: 1,
compression: 1,
width: 15,
height: 15,
});
expect(decoded[0].data).toStrictEqual(
new Uint8Array(
[
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1],
[1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1],
[1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1],
[1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1],
[1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
].flat(),
),
);
});
test('should decode multiframe image', () => {
const decoded = decode(readImage('dog.tiff'));
expect(decoded).toHaveLength(8);
expect(decoded[0]).toMatchObject({
alpha: true,
bitsPerSample: 1,
components: 4,
compression: 1,
width: 16,
height: 16,
});
expect(decoded[0].imageWidth).toBe(16);
// last row of the first frame
const firstFrameLastRow = new Uint8Array([
1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1,
1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1,
0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0,
]);
expect(decoded[1].imageLength).toBe(16);
expect(decoded[1].imageWidth).toBe(16);
expect(decoded[0].data.slice(960, 1024)).toStrictEqual(firstFrameLastRow);
// twelveth row of the second frame
const secondFrameTwelvethRow = new Uint8Array([
1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1,
0, 1, 0, 1, 0, 1, 0,
]);
expect(decoded[1].samplesPerPixel).toBe(2);
expect(decoded[1].data.slice(352, 384)).toStrictEqual(secondFrameTwelvethRow);
});
test('should decode image with undefined rowsPerStrip', () => {
const decodedFemale = decode(readImage('female.tiff'));
expect(decodedFemale[0].rowsPerStrip).toStrictEqual(2 ** 32 - 1);
expect(decodedFemale[0].data.slice(0, 10)).toStrictEqual(
new Uint8Array([94, 0, 115, 27, 61, 103, 26, 60, 104, 27]),
);
const decodedJellybeans = decode(readImage('jellybeans.tiff'));
expect(decodedJellybeans[0].rowsPerStrip).toStrictEqual(2 ** 32 - 1);
expect(decodedJellybeans[0].data.slice(0, 10)).toStrictEqual(
new Uint8Array([139, 132, 90, 141, 136, 92, 142, 131, 89, 143]),
);
});
================================================
FILE: src/__tests__/is_multi_page.test.ts
================================================
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { expect, test } from 'vitest';
import { isMultiPage } from '../index.ts';
const files = [
{ name: 'grey8.tif', pages: 1 },
{ name: 'grey16.tif', pages: 1 },
{ name: 'color8.tif', pages: 1 },
{ name: 'color16.tif', pages: 1 },
{ name: 'grey8-multi.tif', pages: 2 },
{ name: 'grey16-multi.tif', pages: 2 },
{ name: 'color8-multi.tif', pages: 2 },
{ name: 'color16-multi.tif', pages: 2 },
];
// const files = ['color8c.tif'];//'grey8.tif', 'grey16.tif', 'color8.tif', 'color16.tif'];
const contents = files.map((file) =>
readFileSync(join(import.meta.dirname, '../../img', file.name)),
);
test('TIFF isMultiPage', () => {
for (let i = 0; i < contents.length; i++) {
const result = isMultiPage(contents[i]);
expect(result).toBe(files[i].pages > 1);
}
});
================================================
FILE: src/__tests__/lzw.test.ts
================================================
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import { decompressLzw } from '../lzw.ts';
describe('lzw', () => {
it('1', () => {
const buffer = readFileSync(join(import.meta.dirname, 'data/1.strip'));
const result = decompressLzw(
new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength),
);
expect(result.byteLength).toBe(7770);
expect(
new Uint8Array(
result.buffer,
result.byteOffset,
result.byteLength,
).reduce((sum, current) => sum + current, 0),
).toBe(675);
});
it('173', () => {
const buffer = readFileSync(join(import.meta.dirname, 'data/173.strip'));
const result = decompressLzw(
new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength),
);
expect(result.byteLength).toBe(7770);
expect(
new Uint8Array(
result.buffer,
result.byteOffset,
result.byteLength,
).reduce((sum, current) => sum + current, 0),
).toBe(38307);
});
it('174', () => {
const buffer = readFileSync(join(import.meta.dirname, 'data/174.strip'));
const result = decompressLzw(
new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength),
);
expect(result.byteLength).toBe(7770);
});
});
================================================
FILE: src/__tests__/page_count.test.ts
================================================
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { expect, test } from 'vitest';
import { pageCount } from '../index.ts';
const files = [
{ name: 'grey8.tif', pages: 1 },
{ name: 'grey16.tif', pages: 1 },
{ name: 'color8.tif', pages: 1 },
{ name: 'color16.tif', pages: 1 },
{ name: 'grey8-multi.tif', pages: 2 },
{ name: 'grey16-multi.tif', pages: 2 },
{ name: 'color8-multi.tif', pages: 2 },
{ name: 'color16-multi.tif', pages: 2 },
];
// const files = ['color8c.tif'];//'grey8.tif', 'grey16.tif', 'color8.tif', 'color16.tif'];
const contents = files.map((file) =>
readFileSync(join(import.meta.dirname, '../../img', file.name)),
);
test('TIFF pageCount', () => {
for (let i = 0; i < contents.length; i++) {
const result = pageCount(contents[i]);
expect(result).toBe(files[i].pages);
}
});
================================================
FILE: src/hacks.ts
================================================
import type TiffIfd from './tiff_ifd.ts';
export function guessStripByteCounts(ifd: TiffIfd): number[] {
if (ifd.compression !== 1) {
throw new Error(
'missing mandatory StripByteCounts field in compressed image',
);
}
const bytesPerStrip =
ifd.rowsPerStrip *
ifd.width *
ifd.samplesPerPixel *
(ifd.bitsPerSample / 8);
return new Array(ifd.stripOffsets.length).fill(bytesPerStrip);
}
================================================
FILE: src/horizontal_differencing.ts
================================================
// Section 14: Differencing Predictor (p. 64)
export function applyHorizontalDifferencing8Bit(
data: Uint8Array,
width: number,
components: number,
): void {
let i = 0;
while (i < data.length) {
for (let j = components; j < width * components; j += components) {
for (let k = 0; k < components; k++) {
data[i + j + k] =
(data[i + j + k] + data[i + j - (components - k)]) & 255;
}
}
i += width * components;
}
}
export function applyHorizontalDifferencing16Bit(
data: Uint16Array,
width: number,
components: number,
): void {
let i = 0;
while (i < data.length) {
for (let j = components; j < width * components; j += components) {
for (let k = 0; k < components; k++) {
data[i + j + k] =
(data[i + j + k] + data[i + j - (components - k)]) & 65535;
}
}
i += width * components;
}
}
================================================
FILE: src/ifd.ts
================================================
import * as exif from './tags/exif.ts';
import * as gps from './tags/gps.ts';
import * as standard from './tags/standard.ts';
import type { DataArray, IFDKind } from './types.ts';
const tags = {
standard,
exif,
gps,
};
export default class IFD {
public kind: IFDKind;
public data: DataArray;
public fields: Map;
public exif: IFD | undefined;
public gps: IFD | undefined;
private _hasMap: boolean;
private _map: any;
public constructor(kind: IFDKind) {
if (!kind) {
throw new Error('missing kind');
}
this.data = new Uint8Array();
this.fields = new Map();
this.kind = kind;
this._hasMap = false;
this._map = {};
}
public get(tag: number | string): any {
if (typeof tag === 'number') {
return this.fields.get(tag);
} else if (typeof tag === 'string') {
return this.fields.get(tags[this.kind].tagsByName[tag]);
} else {
throw new Error('expected a number or string');
}
}
public get map(): Record {
if (!this._hasMap) {
const taglist = tags[this.kind].tagsById;
for (const key of this.fields.keys()) {
if (taglist[key]) {
this._map[taglist[key]] = this.fields.get(key);
}
}
this._hasMap = true;
}
return this._map;
}
}
================================================
FILE: src/ifd_value.ts
================================================
import type TIFFDecoder from './tiff_decoder.ts';
const types = new Map<
number,
[number, (decoder: TIFFDecoder, count: number) => any]
>([
[1, [1, readByte]], // BYTE
[2, [1, readASCII]], // ASCII
[3, [2, readShort]], // SHORT
[4, [4, readLong]], // LONG
[5, [8, readRational]], // RATIONAL
[6, [1, readSByte]], // SBYTE
[7, [1, readByte]], // UNDEFINED
[8, [2, readSShort]], // SSHORT
[9, [4, readSLong]], // SLONG
[10, [8, readSRational]], // SRATIONAL
[11, [4, readFloat]], // FLOAT
[12, [8, readDouble]], // DOUBLE
]);
export function getByteLength(type: number, count: number): number {
const val = types.get(type);
if (!val) throw new Error(`type not found: ${type}`);
return val[0] * count;
}
export function readData(
decoder: TIFFDecoder,
type: number,
count: number,
): any {
const val = types.get(type);
if (!val) throw new Error(`type not found: ${type}`);
return val[1](decoder, count);
}
function readByte(decoder: TIFFDecoder, count: number): number | Uint8Array {
if (count === 1) return decoder.readUint8();
const array = new Uint8Array(count);
for (let i = 0; i < count; i++) {
array[i] = decoder.readUint8();
}
return array;
}
function readASCII(decoder: TIFFDecoder, count: number): string | string[] {
const strings = [];
let currentString = '';
for (let i = 0; i < count; i++) {
// eslint-disable-next-line unicorn/prefer-code-point
const char = String.fromCharCode(decoder.readUint8());
if (char === '\0') {
strings.push(currentString);
currentString = '';
} else {
currentString += char;
}
}
if (strings.length === 1) {
return strings[0];
} else {
return strings;
}
}
function readShort(decoder: TIFFDecoder, count: number): number | Uint16Array {
if (count === 1) return decoder.readUint16();
const array = new Uint16Array(count);
for (let i = 0; i < count; i++) {
array[i] = decoder.readUint16();
}
return array;
}
function readLong(decoder: TIFFDecoder, count: number): number | Uint32Array {
if (count === 1) return decoder.readUint32();
const array = new Uint32Array(count);
for (let i = 0; i < count; i++) {
array[i] = decoder.readUint32();
}
return array;
}
function readRational(decoder: TIFFDecoder, count: number): number | number[] {
if (count === 1) {
return decoder.readUint32() / decoder.readUint32();
}
const rationals = new Array(count);
for (let i = 0; i < count; i++) {
rationals[i] = decoder.readUint32() / decoder.readUint32();
}
return rationals;
}
function readSByte(decoder: TIFFDecoder, count: number): number | Int8Array {
if (count === 1) return decoder.readInt8();
const array = new Int8Array(count);
for (let i = 0; i < count; i++) {
array[i] = decoder.readInt8();
}
return array;
}
function readSShort(decoder: TIFFDecoder, count: number): number | Int16Array {
if (count === 1) return decoder.readInt16();
const array = new Int16Array(count);
for (let i = 0; i < count; i++) {
array[i] = decoder.readInt16();
}
return array;
}
function readSLong(decoder: TIFFDecoder, count: number): number | Int32Array {
if (count === 1) return decoder.readInt32();
const array = new Int32Array(count);
for (let i = 0; i < count; i++) {
array[i] = decoder.readInt32();
}
return array;
}
function readSRational(decoder: TIFFDecoder, count: number): number | number[] {
if (count === 1) {
return decoder.readInt32() / decoder.readInt32();
}
const rationals = new Array(count);
for (let i = 0; i < count; i++) {
rationals[i] = decoder.readInt32() / decoder.readInt32();
}
return rationals;
}
function readFloat(decoder: TIFFDecoder, count: number): number | Float32Array {
if (count === 1) return decoder.readFloat32();
const array = new Float32Array(count);
for (let i = 0; i < count; i++) {
array[i] = decoder.readFloat32();
}
return array;
}
function readDouble(
decoder: TIFFDecoder,
count: number,
): number | Float64Array {
if (count === 1) return decoder.readFloat64();
const array = new Float64Array(count);
for (let i = 0; i < count; i++) {
array[i] = decoder.readFloat64();
}
return array;
}
================================================
FILE: src/index.ts
================================================
import type { InputData } from 'iobuffer';
import { tagsById as exif } from './tags/exif.ts';
import { tagsById as gps } from './tags/gps.ts';
import { tagsById as standard } from './tags/standard.ts';
import TIFFDecoder from './tiff_decoder.ts';
import type TiffIfd from './tiff_ifd.ts';
import type { DecodeOptions } from './types.ts';
function decodeTIFF(data: InputData, options?: DecodeOptions): TiffIfd[] {
const decoder = new TIFFDecoder(data);
return decoder.decode(options);
}
function isMultiPage(data: InputData): boolean {
const decoder = new TIFFDecoder(data);
return decoder.isMultiPage;
}
function pageCount(data: InputData): number {
const decoder = new TIFFDecoder(data);
return decoder.pageCount;
}
const tagNames = {
exif,
gps,
standard,
};
export { decodeTIFF as decode, isMultiPage, pageCount, tagNames };
export { type DecodeOptions } from './types.ts';
export { default as TiffIfd } from './tiff_ifd.ts';
================================================
FILE: src/lzw.ts
================================================
import { IOBuffer } from 'iobuffer';
const CLEAR_CODE = 256;
const EOI_CODE = 257;
// 0-255 from the table + 256 for clear code + 257 for end of information code.
const TABLE_START = 258;
const MIN_BIT_LENGTH = 9;
let stringTable: number[][] = [];
function initializeStringTable() {
if (stringTable.length === 0) {
for (let i = 0; i < 256; i++) {
stringTable.push([i]);
}
// Fill the table with dummy data.
// Elements at indices > 257 will be replaced during decompression.
const dummyString: number[] = [];
for (let i = 256; i < 4096; i++) {
stringTable.push(dummyString);
}
}
}
const andTable = [511, 1023, 2047, 4095];
const bitJumps = [0, 0, 0, 0, 0, 0, 0, 0, 0, 511, 1023, 2047, 4095];
class LzwDecoder {
private stripArray: Uint8Array;
private nextData = 0;
private nextBits = 0;
private bytePointer = 0;
private tableLength = TABLE_START;
private currentBitLength = MIN_BIT_LENGTH;
private outData: IOBuffer;
public constructor(data: DataView) {
this.stripArray = new Uint8Array(
data.buffer,
data.byteOffset,
data.byteLength,
);
this.outData = new IOBuffer(data.byteLength);
this.initializeTable();
}
public decode(): DataView {
let code = 0;
let oldCode = 0;
while ((code = this.getNextCode()) !== EOI_CODE) {
if (code === CLEAR_CODE) {
this.initializeTable();
code = this.getNextCode();
if (code === EOI_CODE) {
break;
}
this.writeString(this.stringFromCode(code));
oldCode = code;
} else if (this.isInTable(code)) {
this.writeString(this.stringFromCode(code));
this.addStringToTable(
this.stringFromCode(oldCode).concat(this.stringFromCode(code)[0]),
);
oldCode = code;
} else {
const outString = this.stringFromCode(oldCode).concat(
this.stringFromCode(oldCode)[0],
);
this.writeString(outString);
this.addStringToTable(outString);
oldCode = code;
}
}
const outArray = this.outData.toArray();
return new DataView(
outArray.buffer,
outArray.byteOffset,
outArray.byteLength,
);
}
private initializeTable(): void {
initializeStringTable();
this.tableLength = TABLE_START;
this.currentBitLength = MIN_BIT_LENGTH;
}
private writeString(string: number[]): void {
this.outData.writeBytes(string);
}
private stringFromCode(code: number): number[] {
// At this point, `code` must be defined in the table.
return stringTable[code];
}
private isInTable(code: number): boolean {
return code < this.tableLength;
}
private addStringToTable(string: number[]): void {
stringTable[this.tableLength++] = string;
if (stringTable.length > 4096) {
stringTable = [];
throw new Error(
'LZW decoding error. Please open an issue at https://github.com/image-js/tiff/issues/new/choose (include a test image).',
);
}
if (this.tableLength === bitJumps[this.currentBitLength]) {
this.currentBitLength++;
}
}
private getNextCode(): number {
this.nextData =
(this.nextData << 8) | (this.stripArray[this.bytePointer++] & 0xff);
this.nextBits += 8;
if (this.nextBits < this.currentBitLength) {
this.nextData =
(this.nextData << 8) | (this.stripArray[this.bytePointer++] & 0xff);
this.nextBits += 8;
}
const code =
(this.nextData >> (this.nextBits - this.currentBitLength)) &
andTable[this.currentBitLength - 9];
this.nextBits -= this.currentBitLength;
// This should not really happen but is present in other codes as well.
// See: https://github.com/sugark/Tiffus/blob/15a60123813d1612f4ae9e4fab964f9f7d71cf63/src/org/eclipse/swt/internal/image/TIFFLZWDecoder.java
if (this.bytePointer > this.stripArray.length) {
return 257;
}
return code;
}
}
export function decompressLzw(stripData: DataView): DataView {
return new LzwDecoder(stripData).decode();
}
================================================
FILE: src/tags/exif.ts
================================================
const tagsById: Record = {
0x829a: 'ExposureTime',
0x829d: 'FNumber',
0x8822: 'ExposureProgram',
0x8824: 'SpectralSensitivity',
0x8827: 'ISOSpeedRatings',
0x8828: 'OECF',
0x8830: 'SensitivityType',
0x8831: 'StandardOutputSensitivity',
0x8832: 'RecommendedExposureIndex',
0x8833: 'ISOSpeed',
0x8834: 'ISOSpeedLatitudeyyy',
0x8835: 'ISOSpeedLatitudezzz',
0x9000: 'ExifVersion',
0x9003: 'DateTimeOriginal',
0x9004: 'DateTimeDigitized',
0x9101: 'ComponentsConfiguration',
0x9102: 'CompressedBitsPerPixel',
0x9201: 'ShutterSpeedValue',
0x9202: 'ApertureValue',
0x9203: 'BrightnessValue',
0x9204: 'ExposureBiasValue',
0x9205: 'MaxApertureValue',
0x9206: 'SubjectDistance',
0x9207: 'MeteringMode',
0x9208: 'LightSource',
0x9209: 'Flash',
0x920a: 'FocalLength',
0x9214: 'SubjectArea',
0x927c: 'MakerNote',
0x9286: 'UserComment',
0x9290: 'SubsecTime',
0x9291: 'SubsecTimeOriginal',
0x9292: 'SubsecTimeDigitized',
0xa000: 'FlashpixVersion',
0xa001: 'ColorSpace',
0xa002: 'PixelXDimension',
0xa003: 'PixelYDimension',
0xa004: 'RelatedSoundFile',
0xa20b: 'FlashEnergy',
0xa20c: 'SpatialFrequencyResponse',
0xa20e: 'FocalPlaneXResolution',
0xa20f: 'FocalPlaneYResolution',
0xa210: 'FocalPlaneResolutionUnit',
0xa214: 'SubjectLocation',
0xa215: 'ExposureIndex',
0xa217: 'SensingMethod',
0xa300: 'FileSource',
0xa301: 'SceneType',
0xa302: 'CFAPattern',
0xa401: 'CustomRendered',
0xa402: 'ExposureMode',
0xa403: 'WhiteBalance',
0xa404: 'DigitalZoomRatio',
0xa405: 'FocalLengthIn35mmFilm',
0xa406: 'SceneCaptureType',
0xa407: 'GainControl',
0xa408: 'Contrast',
0xa409: 'Saturation',
0xa40a: 'Sharpness',
0xa40b: 'DeviceSettingDescription',
0xa40c: 'SubjectDistanceRange',
0xa420: 'ImageUniqueID',
0xa430: 'CameraOwnerName',
0xa431: 'BodySerialNumber',
0xa432: 'LensSpecification',
0xa433: 'LensMake',
0xa434: 'LensModel',
0xa435: 'LensSerialNumber',
0xa500: 'Gamma',
};
const tagsByName: Record = {};
for (const i in tagsById) {
tagsByName[tagsById[i]] = Number(i);
}
export { tagsById, tagsByName };
================================================
FILE: src/tags/gps.ts
================================================
const tagsById: Record = {
0x0000: 'GPSVersionID',
0x0001: 'GPSLatitudeRef',
0x0002: 'GPSLatitude',
0x0003: 'GPSLongitudeRef',
0x0004: 'GPSLongitude',
0x0005: 'GPSAltitudeRef',
0x0006: 'GPSAltitude',
0x0007: 'GPSTimeStamp',
0x0008: 'GPSSatellites',
0x0009: 'GPSStatus',
0x000a: 'GPSMeasureMode',
0x000b: 'GPSDOP',
0x000c: 'GPSSpeedRef',
0x000d: 'GPSSpeed',
0x000e: 'GPSTrackRef',
0x000f: 'GPSTrack',
0x0010: 'GPSImgDirectionRef',
0x0011: 'GPSImgDirection',
0x0012: 'GPSMapDatum',
0x0013: 'GPSDestLatitudeRef',
0x0014: 'GPSDestLatitude',
0x0015: 'GPSDestLongitudeRef',
0x0016: 'GPSDestLongitude',
0x0017: 'GPSDestBearingRef',
0x0018: 'GPSDestBearing',
0x0019: 'GPSDestDistanceRef',
0x001a: 'GPSDestDistance',
0x001b: 'GPSProcessingMethod',
0x001c: 'GPSAreaInformation',
0x001d: 'GPSDateStamp',
0x001e: 'GPSDifferential',
0x001f: 'GPSHPositioningError',
};
const tagsByName: Record = {};
for (const i in tagsById) {
tagsByName[tagsById[i]] = Number(i);
}
export { tagsById, tagsByName };
================================================
FILE: src/tags/standard.ts
================================================
const tagsById: Record = {
// Baseline tags
0x00fe: 'NewSubfileType',
0x00ff: 'SubfileType',
0x0100: 'ImageWidth',
0x0101: 'ImageLength',
0x0102: 'BitsPerSample',
0x0103: 'Compression',
0x0106: 'PhotometricInterpretation',
0x0107: 'Threshholding',
0x0108: 'CellWidth',
0x0109: 'CellLength',
0x010a: 'FillOrder',
0x010e: 'ImageDescription',
0x010f: 'Make',
0x0110: 'Model',
0x0111: 'StripOffsets',
0x0112: 'Orientation',
0x0115: 'SamplesPerPixel',
0x0116: 'RowsPerStrip',
0x0117: 'StripByteCounts',
0x0118: 'MinSampleValue',
0x0119: 'MaxSampleValue',
0x011a: 'XResolution',
0x011b: 'YResolution',
0x011c: 'PlanarConfiguration',
0x0120: 'FreeOffsets',
0x0121: 'FreeByteCounts',
0x0122: 'GrayResponseUnit',
0x0123: 'GrayResponseCurve',
0x0128: 'ResolutionUnit',
0x0131: 'Software',
0x0132: 'DateTime',
0x013b: 'Artist',
0x013c: 'HostComputer',
0x0140: 'ColorMap',
0x0152: 'ExtraSamples',
0x8298: 'Copyright',
// Extension tags
0x010d: 'DocumentName',
0x011d: 'PageName',
0x011e: 'XPosition',
0x011f: 'YPosition',
0x0124: 'T4Options',
0x0125: 'T6Options',
0x0129: 'PageNumber',
0x012d: 'TransferFunction',
0x013d: 'Predictor',
0x013e: 'WhitePoint',
0x013f: 'PrimaryChromaticities',
0x0141: 'HalftoneHints',
0x0142: 'TileWidth',
0x0143: 'TileLength',
0x0144: 'TileOffsets',
0x0145: 'TileByteCounts',
0x0146: 'BadFaxLines',
0x0147: 'CleanFaxData',
0x0148: 'ConsecutiveBadFaxLines',
0x014a: 'SubIFDs',
0x014c: 'InkSet',
0x014d: 'InkNames',
0x014e: 'NumberOfInks',
0x0150: 'DotRange',
0x0151: 'TargetPrinter',
0x0153: 'SampleFormat',
0x0154: 'SMinSampleValue',
0x0155: 'SMaxSampleValue',
0x0156: 'TransferRange',
0x0157: 'ClipPath',
0x0158: 'XClipPathUnits',
0x0159: 'YClipPathUnits',
0x015a: 'Indexed',
0x015b: 'JPEGTables',
0x015f: 'OPIProxy',
0x0190: 'GlobalParametersIFD',
0x0191: 'ProfileType',
0x0192: 'FaxProfile',
0x0193: 'CodingMethods',
0x0194: 'VersionYear',
0x0195: 'ModeNumber',
0x01b1: 'Decode',
0x01b2: 'DefaultImageColor',
0x0200: 'JPEGProc',
0x0201: 'JPEGInterchangeFormat',
0x0202: 'JPEGInterchangeFormatLength',
0x0203: 'JPEGRestartInterval',
0x0205: 'JPEGLosslessPredictors',
0x0206: 'JPEGPointTransforms',
0x0207: 'JPEGQTables',
0x0208: 'JPEGDCTables',
0x0209: 'JPEGACTables',
0x0211: 'YCbCrCoefficients',
0x0212: 'YCbCrSubSampling',
0x0213: 'YCbCrPositioning',
0x0214: 'ReferenceBlackWhite',
0x022f: 'StripRowCounts',
0x02bc: 'XMP',
0x800d: 'ImageID',
0x87ac: 'ImageLayer',
// Private tags
0x80a4: 'WangAnnotatio',
0x82a5: 'MDFileTag',
0x82a6: 'MDScalePixel',
0x82a7: 'MDColorTable',
0x82a8: 'MDLabName',
0x82a9: 'MDSampleInfo',
0x82aa: 'MDPrepDate',
0x82ab: 'MDPrepTime',
0x82ac: 'MDFileUnits',
0x830e: 'ModelPixelScaleTag',
0x83bb: 'IPTC',
0x847e: 'INGRPacketDataTag',
0x847f: 'INGRFlagRegisters',
0x8480: 'IrasBTransformationMatrix',
0x8482: 'ModelTiepointTag',
0x85d8: 'ModelTransformationTag',
0x8649: 'Photoshop',
0x8769: 'ExifIFD',
0x8773: 'ICCProfile',
0x87af: 'GeoKeyDirectoryTag',
0x87b0: 'GeoDoubleParamsTag',
0x87b1: 'GeoAsciiParamsTag',
0x8825: 'GPSIFD',
0x885c: 'HylaFAXFaxRecvParams',
0x885d: 'HylaFAXFaxSubAddress',
0x885e: 'HylaFAXFaxRecvTime',
0x935c: 'ImageSourceData',
0xa005: 'InteroperabilityIFD',
0xa480: 'GDAL_METADATA',
0xa481: 'GDAL_NODATA',
0xc427: 'OceScanjobDescription',
0xc428: 'OceApplicationSelector',
0xc429: 'OceIdentificationNumber',
0xc42a: 'OceImageLogicCharacteristics',
0xc612: 'DNGVersion',
0xc613: 'DNGBackwardVersion',
0xc614: 'UniqueCameraModel',
0xc615: 'LocalizedCameraModel',
0xc616: 'CFAPlaneColor',
0xc617: 'CFALayout',
0xc618: 'LinearizationTable',
0xc619: 'BlackLevelRepeatDim',
0xc61a: 'BlackLevel',
0xc61b: 'BlackLevelDeltaH',
0xc61c: 'BlackLevelDeltaV',
0xc61d: 'WhiteLevel',
0xc61e: 'DefaultScale',
0xc61f: 'DefaultCropOrigin',
0xc620: 'DefaultCropSize',
0xc621: 'ColorMatrix1',
0xc622: 'ColorMatrix2',
0xc623: 'CameraCalibration1',
0xc624: 'CameraCalibration2',
0xc625: 'ReductionMatrix1',
0xc626: 'ReductionMatrix2',
0xc627: 'AnalogBalance',
0xc628: 'AsShotNeutral',
0xc629: 'AsShotWhiteXY',
0xc62a: 'BaselineExposure',
0xc62b: 'BaselineNoise',
0xc62c: 'BaselineSharpness',
0xc62d: 'BayerGreenSplit',
0xc62e: 'LinearResponseLimit',
0xc62f: 'CameraSerialNumber',
0xc630: 'LensInfo',
0xc631: 'ChromaBlurRadius',
0xc632: 'AntiAliasStrength',
0xc634: 'DNGPrivateData',
0xc635: 'MakerNoteSafety',
0xc65a: 'CalibrationIlluminant1',
0xc65b: 'CalibrationIlluminant2',
0xc65c: 'BestQualityScale',
0xc660: 'AliasLayerMetadata',
};
const tagsByName: Record = {};
for (const i in tagsById) {
tagsByName[tagsById[i]] = Number(i);
}
export { tagsById, tagsByName };
================================================
FILE: src/tiff_decoder.ts
================================================
import type { InputData } from 'iobuffer';
import { IOBuffer } from 'iobuffer';
import { guessStripByteCounts } from './hacks.ts';
import {
applyHorizontalDifferencing16Bit,
applyHorizontalDifferencing8Bit,
} from './horizontal_differencing.ts';
import IFD from './ifd.ts';
import { getByteLength, readData } from './ifd_value.ts';
import { decompressLzw } from './lzw.ts';
import TiffIfd from './tiff_ifd.ts';
import type { DataArray, DecodeOptions, IFDKind } from './types.ts';
import { decompressZlib } from './zlib.ts';
const defaultOptions: DecodeOptions = {
ignoreImageData: false,
};
interface InternalOptions extends DecodeOptions {
kind?: IFDKind;
}
export default class TIFFDecoder extends IOBuffer {
private _nextIFD: number;
public constructor(data: InputData) {
super(data);
this._nextIFD = 0;
}
public get isMultiPage(): boolean {
let c = 0;
this.decodeHeader();
while (this._nextIFD) {
c++;
this.decodeIFD({ ignoreImageData: true }, true);
if (c === 2) {
return true;
}
}
if (c === 1) {
return false;
}
throw unsupported('ifdCount', c);
}
public get pageCount(): number {
let c = 0;
this.decodeHeader();
while (this._nextIFD) {
c++;
this.decodeIFD({ ignoreImageData: true }, true);
}
if (c > 0) {
return c;
}
throw unsupported('ifdCount', c);
}
public decode(options: DecodeOptions = {}): TiffIfd[] {
const { pages } = options;
checkPages(pages);
const maxIndex = pages ? Math.max(...pages) : Infinity;
options = { ...defaultOptions, ...options };
const result = [];
this.decodeHeader();
let index = 0;
while (this._nextIFD) {
if (pages) {
if (pages.includes(index)) {
result.push(this.decodeIFD(options, true));
} else {
this.decodeIFD({ ignoreImageData: true }, true);
}
if (index === maxIndex) {
break;
}
} else {
result.push(this.decodeIFD(options, true));
}
index++;
}
if (index < maxIndex && maxIndex !== Infinity) {
throw new RangeError(
`Index ${maxIndex} is out of bounds. The stack only contains ${index} images.`,
);
}
return result;
}
private decodeHeader(): void {
// Byte offset
const value = this.readUint16();
if (value === 0x4949) {
this.setLittleEndian();
} else if (value === 0x4d4d) {
this.setBigEndian();
} else {
throw new Error(`invalid byte order: 0x${value.toString(16)}`);
}
// Magic number
if (this.readUint16() !== 42) {
throw new Error('not a TIFF file');
}
// Offset of the first IFD
this._nextIFD = this.readUint32();
}
private decodeIFD(options: InternalOptions, tiff: true): TiffIfd;
private decodeIFD(options: InternalOptions, tiff: false): IFD;
private decodeIFD(options: InternalOptions, tiff: boolean): TiffIfd | IFD {
this.seek(this._nextIFD);
let ifd: TiffIfd | IFD;
if (tiff) {
ifd = new TiffIfd();
} else {
if (!options.kind) {
throw new Error(`kind is missing`);
}
ifd = new IFD(options.kind);
}
const numEntries = this.readUint16();
for (let i = 0; i < numEntries; i++) {
this.decodeIFDEntry(ifd);
}
if (!options.ignoreImageData) {
if (!(ifd instanceof TiffIfd)) {
throw new Error('must be a tiff ifd');
}
this.decodeImageData(ifd);
}
this._nextIFD = this.readUint32();
return ifd;
}
private decodeIFDEntry(ifd: IFD): void {
const offset = this.offset;
const tag = this.readUint16();
const type = this.readUint16();
const numValues = this.readUint32();
if (type < 1 || type > 12) {
this.skip(4); // unknown type, skip this value
return;
}
const valueByteLength = getByteLength(type, numValues);
if (valueByteLength > 4) {
this.seek(this.readUint32());
}
const value = readData(this, type, numValues);
ifd.fields.set(tag, value);
// Read sub-IFDs
if (tag === 0x8769 || tag === 0x8825) {
const currentOffset = this.offset;
let kind: IFDKind = 'exif';
if (tag === 0x8769) {
kind = 'exif';
} else if (tag === 0x8825) {
kind = 'gps';
}
this._nextIFD = value;
ifd[kind] = this.decodeIFD(
{
kind,
ignoreImageData: true,
},
false,
);
this.offset = currentOffset;
}
// go to the next entry
this.seek(offset);
this.skip(12);
}
private decodeImageData(ifd: TiffIfd): void {
const orientation = ifd.orientation;
if (orientation && orientation !== 1) {
throw unsupported('orientation', orientation);
}
switch (ifd.type) {
case 0: // WhiteIsZero
case 1: // BlackIsZero
case 2: // RGB
case 3: // Palette color
if (ifd.tiled) {
this.readTileData(ifd);
} else {
this.readStripData(ifd);
}
break;
default:
throw unsupported('image type', ifd.type);
}
this.applyPredictor(ifd);
this.convertAlpha(ifd);
if (ifd.bitsPerSample === 1) {
this.split1BitData(ifd);
}
if (ifd.type === 0) {
// WhiteIsZero: we invert the values
const bitDepth = ifd.bitsPerSample;
const maxValue = 2 ** bitDepth - 1;
for (let i = 0; i < ifd.data.length; i++) {
ifd.data[i] = maxValue - ifd.data[i];
}
}
}
private split1BitData(ifd: TiffIfd) {
const { imageWidth, imageLength, samplesPerPixel } = ifd;
const data = new Uint8Array(imageLength * imageWidth * samplesPerPixel);
const bytesPerRow = Math.ceil((imageWidth * samplesPerPixel) / 8);
let dataIndex = 0;
for (let row = 0; row < imageLength; row++) {
const rowStartByte = row * bytesPerRow;
for (let col = 0; col < imageWidth * samplesPerPixel; col++) {
const byteIndex = rowStartByte + Math.floor(col / 8);
const bitIndex = 7 - (col % 8);
const bit = (ifd.data[byteIndex] >> bitIndex) & 1;
data[dataIndex++] = bit;
}
}
ifd.data = data;
}
private static uncompress(data: DataView, compression = 1): DataView {
switch (compression) {
// No compression, nothing to do
case 1: {
return data;
}
// LZW compression
case 5: {
return decompressLzw(data);
}
// Zlib and Deflate compressions. They are identical.
case 8:
case 32946: {
return decompressZlib(data);
}
case 2: // CCITT Group 3 1-Dimensional Modified Huffman run length encoding
throw unsupported('Compression', 'CCITT Group 3');
case 32773: // PackBits compression
throw unsupported('Compression', 'PackBits');
default:
throw unsupported('Compression', compression);
}
}
private createSampleReader(
sampleFormat: number,
bitDepth: number,
littleEndian: boolean,
): (data: DataView, index: number) => number {
if (bitDepth === 8 || bitDepth === 1) {
return (data: DataView, index: number) => data.getUint8(index);
} else if (bitDepth === 16) {
return (data: DataView, index: number) =>
data.getUint16(2 * index, littleEndian);
} else if (bitDepth === 32 && sampleFormat === 3) {
return (data: DataView, index: number) =>
data.getFloat32(4 * index, littleEndian);
} else if (bitDepth === 64 && sampleFormat === 3) {
return (data: DataView, index: number) =>
data.getFloat64(8 * index, littleEndian);
} else {
throw unsupported('bitDepth', bitDepth);
}
}
private readStripData(ifd: TiffIfd): void {
// General Image Dimensions
const width = ifd.width;
const height = ifd.height;
const size =
ifd.bitsPerSample !== 1
? width * ifd.samplesPerPixel * height
: Math.ceil((width * ifd.samplesPerPixel) / 8) * height;
// Compressed Strip Layout
const stripOffsets = ifd.stripOffsets;
const stripByteCounts = ifd.stripByteCounts || guessStripByteCounts(ifd);
const littleEndian = this.isLittleEndian();
// For 1-bit images, calculate pixels per strip correctly
const stripLength =
ifd.bitsPerSample !== 1
? width * ifd.samplesPerPixel * ifd.rowsPerStrip
: Math.ceil((width * ifd.samplesPerPixel) / 8) * ifd.rowsPerStrip;
const readSamples = this.createSampleReader(
ifd.sampleFormat,
ifd.bitsPerSample,
littleEndian,
);
// Output Data Buffer
const output = getDataArray(size, ifd.bitsPerSample, ifd.sampleFormat);
// Iterate over Number of Strips
let start = 0;
for (let i = 0; i < stripOffsets.length; i++) {
// Extract Strip Data, Uncompress
const stripData = new DataView(
this.buffer,
this.byteOffset + stripOffsets[i],
stripByteCounts[i],
);
const uncompressed = TIFFDecoder.uncompress(stripData, ifd.compression);
// Last strip can be smaller
const length = Math.min(stripLength, size - start);
// Write Uncompressed Strip Data to Output (Linear Layout)
for (let index = 0; index < length; ++index) {
const value = readSamples(uncompressed, index);
output[start + index] = value;
}
start += length;
}
ifd.data = output;
// For 1-bit images, we need to convert the data to bits
}
private readTileData(ifd: TiffIfd): void {
if (!ifd.tileWidth || !ifd.tileHeight) {
return;
}
const width = ifd.width;
const height = ifd.height;
const size =
ifd.bitsPerSample !== 1
? width * height * ifd.samplesPerPixel
: Math.ceil((width * ifd.samplesPerPixel) / 8) * height;
const twidth = ifd.tileWidth;
const theight = ifd.tileHeight;
const nwidth = Math.ceil(width / twidth);
const nheight = Math.ceil(height / theight);
const tileOffsets = ifd.tileOffsets;
const tileByteCounts = ifd.tileByteCounts;
const littleEndian = this.isLittleEndian();
const readSamples = this.createSampleReader(
ifd.sampleFormat,
ifd.bitsPerSample,
littleEndian,
);
const output = getDataArray(size, ifd.bitsPerSample, ifd.sampleFormat);
for (let nx = 0; nx < nwidth; ++nx) {
for (let ny = 0; ny < nheight; ++ny) {
const nind = ny * nwidth + nx;
const tileData = new DataView(
this.buffer,
this.byteOffset + tileOffsets[nind],
tileByteCounts[nind],
);
const uncompressed = TIFFDecoder.uncompress(tileData, ifd.compression);
if (ifd.bitsPerSample === 1) {
// For 1-bit: read sequentially by bytes
const bytesPerRow = Math.ceil(width / 8);
const tileBytesPerRow = Math.ceil(twidth / 8);
for (let ty = 0; ty < theight && ny * theight + ty < height; ty++) {
const iy = ny * theight + ty;
const srcStart = ty * tileBytesPerRow;
const dstStart = iy * bytesPerRow + Math.floor((nx * twidth) / 8);
// Copy the row of bytes from tile to output
const bytesToCopy = Math.min(
tileBytesPerRow,
bytesPerRow - Math.floor((nx * twidth) / 8),
);
for (let b = 0; b < bytesToCopy; b++) {
output[dstStart + b] = readSamples(uncompressed, srcStart + b);
}
}
} else {
// For 8/16/32-bit: read by pixels
for (let ty = 0; ty < theight; ty++) {
for (let tx = 0; tx < twidth; tx++) {
const ix = nx * twidth + tx;
const iy = ny * theight + ty;
if (ix >= width || iy >= height) continue;
const tilePixelIndex = ty * twidth + tx;
const value = readSamples(uncompressed, tilePixelIndex);
const outputPixelIndex = (iy * width + ix) * ifd.samplesPerPixel;
output[outputPixelIndex] = value;
}
}
}
}
}
ifd.data = output;
}
private applyPredictor(ifd: TiffIfd): void {
const bitDepth = ifd.bitsPerSample;
switch (ifd.predictor) {
case 1: {
// No prediction scheme, nothing to do
break;
}
case 2: {
if (bitDepth === 8) {
applyHorizontalDifferencing8Bit(
ifd.data as Uint8Array,
ifd.width,
ifd.components,
);
} else if (bitDepth === 16) {
applyHorizontalDifferencing16Bit(
ifd.data as Uint16Array,
ifd.width,
ifd.components,
);
} else {
throw new Error(
`Horizontal differencing is only supported for images with a bit depth of ${bitDepth}`,
);
}
break;
}
default:
throw new Error(`invalid predictor: ${ifd.predictor}`);
}
}
private convertAlpha(ifd: TiffIfd): void {
if (ifd.alpha && ifd.associatedAlpha) {
const { data, components, maxSampleValue } = ifd;
for (let i = 0; i < data.length; i += components) {
const alphaValue = data[i + components - 1];
for (let j = 0; j < components - 1; j++) {
data[i + j] = Math.round((data[i + j] * maxSampleValue) / alphaValue);
}
}
}
}
}
function getDataArray(
size: number,
bitDepth: number,
sampleFormat: number,
): DataArray {
if (bitDepth === 8 || bitDepth === 1) {
return new Uint8Array(size);
} else if (bitDepth === 16) {
return new Uint16Array(size);
} else if (bitDepth === 32 && sampleFormat === 3) {
return new Float32Array(size);
} else if (bitDepth === 64 && sampleFormat === 3) {
return new Float64Array(size);
} else {
throw unsupported(
'bit depth / sample format',
`${bitDepth} / ${sampleFormat}`,
);
}
}
function unsupported(type: string, value: any): Error {
return new Error(`Unsupported ${type}: ${value}`);
}
function checkPages(pages: number[] | undefined) {
if (pages) {
for (const page of pages) {
if (page < 0 || !Number.isInteger(page)) {
throw new RangeError(
`Index ${page} is invalid. Must be a positive integer.`,
);
}
}
}
}
================================================
FILE: src/tiff_ifd.ts
================================================
import Ifd from './ifd.ts';
// eslint-disable-next-line prefer-named-capture-group
const dateTimeRegex = /^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})$/;
export default class TiffIfd extends Ifd {
public constructor() {
super('standard');
}
// Custom fields
public get size(): number {
return this.width * this.height;
}
public get width(): number {
return this.imageWidth;
}
public get height(): number {
return this.imageLength;
}
public get components(): number {
return this.samplesPerPixel;
}
public get date(): Date {
const date = new Date();
const result = dateTimeRegex.exec(this.dateTime);
if (result === null) {
throw new Error(`invalid dateTime: ${this.dateTime}`);
}
date.setFullYear(
Number(result[1]),
Number(result[2]) - 1,
Number(result[3]),
);
date.setHours(Number(result[4]), Number(result[5]), Number(result[6]));
return date;
}
// IFD fields
public get newSubfileType(): number {
return this.get('NewSubfileType');
}
public get imageWidth(): number {
return this.get('ImageWidth');
}
public get imageLength(): number {
return this.get('ImageLength');
}
public get bitsPerSample(): number {
const data = this.get('BitsPerSample');
if (data && typeof data !== 'number') {
return data[0];
}
return data;
}
public get alpha(): boolean {
const extraSamples = this.extraSamples;
if (!extraSamples) return false;
return extraSamples[0] !== 0;
}
public get associatedAlpha(): boolean {
const extraSamples = this.extraSamples;
if (!extraSamples) return false;
return extraSamples[0] === 1;
}
public get extraSamples(): number[] | undefined {
return alwaysArray(this.get('ExtraSamples'));
}
public get compression(): number {
return this.get('Compression') || 1;
}
public get type(): number {
return this.get('PhotometricInterpretation');
}
public get fillOrder(): number {
return this.get('FillOrder') || 1;
}
public get documentName(): string | undefined {
return this.get('DocumentName');
}
public get imageDescription(): string | undefined {
return this.get('ImageDescription');
}
public get stripOffsets(): number[] {
return alwaysArray(this.get('StripOffsets'));
}
public get orientation(): number {
return this.get('Orientation');
}
public get samplesPerPixel(): number {
return this.get('SamplesPerPixel') || 1;
}
public get rowsPerStrip(): number {
return this.get('RowsPerStrip') || 2 ** 32 - 1;
}
public get stripByteCounts(): number[] {
return alwaysArray(this.get('StripByteCounts'));
}
public get minSampleValue(): number {
return this.get('MinSampleValue') || 0;
}
public get maxSampleValue(): number {
return this.get('MaxSampleValue') || 2 ** this.bitsPerSample - 1;
}
public get xResolution(): number {
return this.get('XResolution');
}
public get yResolution(): number {
return this.get('YResolution');
}
public get planarConfiguration(): number {
return this.get('PlanarConfiguration') || 1;
}
public get resolutionUnit(): number {
return this.get('ResolutionUnit') || 2;
}
public get dateTime(): string {
return this.get('DateTime');
}
public get predictor(): number {
return this.get('Predictor') || 1;
}
public get sampleFormat(): number {
const data = alwaysArray(this.get('SampleFormat') || 1);
return data[0];
}
public get sMinSampleValue(): number {
return this.get('SMinSampleValue') || this.minSampleValue;
}
public get sMaxSampleValue(): number {
return this.get('SMaxSampleValue') || this.maxSampleValue;
}
public get palette(): Array<[number, number, number]> | undefined {
const totalColors = 2 ** this.bitsPerSample;
const colorMap: number[] = this.get('ColorMap');
if (!colorMap) return undefined;
if (colorMap.length !== 3 * totalColors) {
throw new Error(`ColorMap size must be ${totalColors}`);
}
const palette: Array<[number, number, number]> = [];
for (let i = 0; i < totalColors; i++) {
palette.push([
colorMap[i],
colorMap[i + totalColors],
colorMap[i + 2 * totalColors],
]);
}
return palette;
}
public get tileWidth(): number | undefined {
return this.get('TileWidth');
}
public get tileHeight(): number | undefined {
return this.get('TileLength');
}
public get tileOffsets(): number[] {
return alwaysArray(this.get('TileOffsets'));
}
public get tileByteCounts(): number[] {
return alwaysArray(this.get('TileByteCounts'));
}
public get tiled(): boolean {
return (
this.tileWidth !== undefined &&
this.tileHeight !== undefined &&
this.tileOffsets !== undefined &&
this.tileByteCounts !== undefined
);
}
}
function alwaysArray(value: number | number[]): number[] {
if (typeof value === 'number') return [value];
return value;
}
================================================
FILE: src/types.ts
================================================
export interface DecodeOptions {
ignoreImageData?: boolean;
/**
* Specify the indices of the pages to decode in case of a multi-page TIFF.
*/
pages?: number[];
}
export type IFDKind = 'standard' | 'exif' | 'gps';
export type DataArray = Uint8Array | Uint16Array | Float32Array | Float64Array;
================================================
FILE: src/zlib.ts
================================================
import { decompressSync } from 'fflate';
export function decompressZlib(stripData: DataView): DataView {
const stripUint8 = new Uint8Array(
stripData.buffer,
stripData.byteOffset,
stripData.byteLength,
);
const inflated = decompressSync(stripUint8);
return new DataView(
inflated.buffer,
inflated.byteOffset,
inflated.byteLength,
);
}
================================================
FILE: tsconfig.build.json
================================================
{
"extends": "./tsconfig.json",
"include": ["src"],
"exclude": ["**/__tests__", "**/*.test.ts"]
}
================================================
FILE: tsconfig.json
================================================
{
"extends": "@zakodium/tsconfig",
"compilerOptions": {
"noUncheckedIndexedAccess": false,
"types": ["node"],
"outDir": "lib"
},
"include": ["src", "vite*.ts"]
}