├── .eslintrc.cjs ├── .github ├── dependabot.yml └── workflows │ ├── push.yml │ └── release-please.yml ├── .gitignore ├── .hyperfine.json ├── .kodiak.toml ├── .npmignore ├── .prettierrc.cjs ├── .release-please-manifest.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── lerna.json ├── package.json ├── packages ├── cli │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── bin │ │ └── cogeotiff.js │ ├── package.json │ ├── src │ │ ├── __test__ │ │ │ └── exports.test.ts │ │ ├── action.util.ts │ │ ├── bin.ts │ │ ├── cli.table.ts │ │ ├── commands │ │ │ ├── dump.ts │ │ │ └── info.ts │ │ ├── common.ts │ │ ├── fs.ts │ │ ├── index.ts │ │ ├── log.ts │ │ ├── tags.ts │ │ ├── util.bytes.ts │ │ └── util.tile.ts │ └── tsconfig.json ├── core │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── data │ │ ├── DEM_BS28_2016_1000_1141.tif │ │ ├── README.md │ │ ├── be_cog.tiff │ │ ├── big_cog.tiff │ │ ├── cog.tiff │ │ ├── east_coast_phase3_2023_AY31_1000_3335.tif.gz │ │ ├── rgba8_strip.tiff │ │ ├── rgba8_tiled.tiff │ │ └── sparse.tiff │ ├── package.json │ ├── src │ │ ├── __benchmark__ │ │ │ ├── cog.read.benchmark.ts │ │ │ ├── source.file.ts │ │ │ └── source.memory.ts │ │ ├── __test__ │ │ │ ├── cog.image.test.ts │ │ │ ├── cog.read.test.ts │ │ │ └── example.ts │ │ ├── const │ │ │ ├── index.ts │ │ │ ├── tiff.endian.ts │ │ │ ├── tiff.mime.ts │ │ │ ├── tiff.tag.id.ts │ │ │ ├── tiff.tag.value.ts │ │ │ └── tiff.version.ts │ │ ├── index.ts │ │ ├── read │ │ │ ├── data.view.offset.ts │ │ │ ├── tiff.gdal.ts │ │ │ ├── tiff.ifd.config.ts │ │ │ ├── tiff.tag.factory.ts │ │ │ ├── tiff.tag.ts │ │ │ └── tiff.value.reader.ts │ │ ├── source.ts │ │ ├── tiff.image.ts │ │ ├── tiff.ts │ │ ├── util │ │ │ ├── bytes.ts │ │ │ └── util.hex.ts │ │ └── vector.ts │ └── tsconfig.json └── examples │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ └── browser │ │ ├── example.single.tile.ts │ │ └── index.ts │ └── tsconfig.json ├── release-please-config.json ├── tsconfig.base.json ├── tsconfig.json └── yarn.lock /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | const cfg = { 2 | ...require('@linzjs/style/.eslintrc.cjs'), 3 | }; 4 | 5 | // Disable require await as we use `async foo() { throws Bar }` in a few places 6 | const tsRules = cfg.overrides.find((ovr) => ovr.files.find((f) => f.endsWith('*.ts'))); 7 | tsRules.rules['@typescript-eslint/require-await'] = 'off'; 8 | 9 | module.exports = cfg; 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: '17:00' 8 | open-pull-requests-limit: 10 9 | versioning-strategy: increase-if-necessary 10 | groups: 11 | chunkd: 12 | patterns: 13 | - "@chunkd/*" -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: linz/action-typescript@v3 11 | with: 12 | package-manager: yarn 13 | - uses: blacha/hyperfine-action@v1 14 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | 6 | name: release-please 7 | jobs: 8 | release-please: 9 | runs-on: ubuntu-latest 10 | outputs: 11 | releases_created: ${{ steps.release.outputs.releases_created }} 12 | steps: 13 | - uses: google-github-actions/release-please-action@v3 14 | id: release 15 | with: 16 | command: manifest 17 | release-type: node 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | publish-release: 21 | needs: release-please 22 | runs-on: ubuntu-latest 23 | if: ${{ needs.release-please.outputs.releases_created }} 24 | environment: 25 | name: prod 26 | 27 | steps: 28 | - name: Build and test 29 | uses: linz/action-typescript@v3 30 | with: 31 | package-manager: yarn 32 | 33 | - name: Publish to NPM 34 | run: npx lerna publish from-package --no-push --no-private --yes 35 | env: 36 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | .cache 4 | dist/ 5 | output 6 | tsconfig.tsbuildinfo 7 | -------------------------------------------------------------------------------- /.hyperfine.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Read 15,000 tiles from sparse.tiff", 4 | "command": "node packages/core/build/__benchmark__/cog.read.benchmark.js packages/core/data/sparse.tiff" 5 | } 6 | ] -------------------------------------------------------------------------------- /.kodiak.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | 4 | [merge] 5 | method = "squash" 6 | automerge_label = "automerge 🚀" 7 | prioritize_ready_to_merge = true 8 | 9 | [merge.message] 10 | title = "pull_request_title" 11 | 12 | [approve] 13 | auto_approve_usernames = ["dependabot"] 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !build/** 3 | !bin/** 4 | !src/** 5 | !package.json 6 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('@linzjs/style/.prettierrc.cjs') 3 | }; -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages/cli": "9.0.3", 3 | "packages/core": "9.0.3", 4 | "packages/examples": "9.0.3", 5 | "packages/fetchable": "7.0.0" 6 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing 3 | 4 | This repository uses [Conventional Commits](https://www.conventionalcommits.org/) 5 | 6 | Example options: 7 | - **build**: Changes that affect the build system or external dependencies 8 | - **ci**: Changes to our CI configuration files and scripts 9 | - **docs**: Documentation only changes 10 | - **feat**: A new feature 11 | - **fix**: A bug fix 12 | - **perf**: A code change that improves performance 13 | - **refactor**: A code change that neither fixes a bug nor adds a feature 14 | - **style**: Changes that do not affect the meaning of the code 15 | - **test**: Adding missing tests or correcting existing tests 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Blayne Chard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cogeotiff 2 | 3 | 4 | Tools to work with [Cloud optimized GeogTiff](https://www.cogeo.org/) (COG) 5 | 6 | - Completely javascript based, works in the browser and nodejs 7 | - Lazy load Tiffs images and metadata 8 | - Supports huge 100GB+ COGs 9 | - Uses GDAL Tiffs optimizations, generally only one or two reads per tile! 10 | - Loads Tiffs from URL, File, Google Cloud or AWS S3 11 | - Used in production for [LINZ's Basemaps](https://github.com/linz/basemaps) with billions of tiles fetched from COGs! 12 | 13 | ## Usage 14 | 15 | Load a COG from a URL using `fetch` 16 | 17 | ```typescript 18 | import { SourceHttp } from '@chunkd/source-http'; 19 | 20 | const source = new SourceHttp('https://example.com/cog.tif'); 21 | const cog = await Tiff.create(source); 22 | 23 | const img = cog.images[0]; 24 | if (img.isTiled()) throw new Error('Tiff is not tiled'); 25 | const tile = await img.getTile(2, 2); // Fetch a tile from a tiff x:2, y:2 26 | 27 | // Tiff tags can be directly accessed too 28 | img.value(TiffTag.GdalNoData); // "-9999" 29 | // or tag metadata can be fetched 30 | img.tags.get(TiffTag.GdalNoData); 31 | /** 32 | { 33 | type: 'inline', // How the tag was read "inline" vs "lazy" 34 | id: 42113, // Tag Id (@see TiffTag) 35 | name: 'GdalNoData', // Tag Name 36 | count: 6, // Number of values 37 | value: '-9999', 38 | dataType: 2, // Char 39 | tagOffset: 194 // Bytes into the file where the tag was read. 40 | } 41 | */ 42 | ``` 43 | 44 | ## Command Line Interface 45 | 46 | ```bash 47 | npm i -g @cogeotiff/cli 48 | ``` 49 | 50 | ### cogeotiff info 51 | 52 | Display basic information about COG 53 | 54 | ```shell 55 | cogeotiff info webp.cog.tif 56 | ``` 57 | 58 | Output: 59 | 60 | ``` 61 | COG File Info - /home/blacha/Downloads/tif-new/bg43.webp.cog.tif 62 | 63 | Tiff type BigTiff (v43) 64 | Chunk size 64 KB 65 | Bytes read 64 KB (1 Chunk) 66 | 67 | Images 68 | Compression image/webp 69 | Origin 18550349.52047286,-5596413.462927464,0 70 | Resolution 19.10925707129406,-19.10925707129415,0 71 | BoundingBox 18550349.52047286,-5713820.738373496,19098250.139221005,-5596413.462927464 72 | Info 73 | Id Size Tile Size Tile Count 74 | 0 28672x6144 56x12 672 75 | 1 14336x3072 28x6 168 76 | 2 7168x1536 14x3 42 77 | 3 3584x768 7x2 14 78 | 4 1792x384 4x1 4 79 | 5 896x192 2x1 2 80 | 6 448x96 1x1 1 81 | 82 | GDAL 83 | COG optimized true 84 | COG broken false 85 | Tile order RowMajor 86 | Tile leader uint32 - 4 Bytes 87 | Mask interleaved false 88 | ``` 89 | 90 | ### cogeotiff dump 91 | 92 | Dump all tiles for a image (**Warning** if you do this for a large cog this will create millions of files.) 93 | 94 | ``` 95 | cogeotiff dump --image 2 --output output 96 | ``` 97 | 98 | 99 | # Building 100 | 101 | This requires [NodeJs](https://nodejs.org/en/) >= 18 & [Yarn](https://yarnpkg.com/en/) 102 | 103 | Use [n](https://github.com/tj/n) to manage nodeJs versions 104 | 105 | ```bash 106 | # Download the latest nodejs & yarn 107 | n latest 108 | npm install -g yarn 109 | 110 | # Install node deps 111 | yarn 112 | 113 | # Build everything into /build 114 | yarn run build 115 | 116 | # Run the unit tests 117 | yarn run test 118 | ``` 119 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "yarn", 3 | "version": "independent", 4 | "packages": ["packages/*"] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cogeotiff/base", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "clean": "tsc -b --clean && rimraf 'packages/*/build'", 6 | "build": "tsc -b --pretty", 7 | "build-watch": "tsc -b --pretty --watch", 8 | "version": "eslint lerna.json --fix", 9 | "lint": "eslint . --quiet --fix --ignore-path .gitignore", 10 | "test": "node --test" 11 | }, 12 | "type": "module", 13 | "engines": { 14 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 15 | }, 16 | "private": true, 17 | "keywords": [], 18 | "author": "Blayne Chard", 19 | "license": "ISC", 20 | "description": "", 21 | "devDependencies": { 22 | "@linzjs/style": "^5.4.0", 23 | "@types/node": "^20.0.0", 24 | "lerna": "^8.0.0", 25 | "rimraf": "^4.1.2" 26 | }, 27 | "workspaces": { 28 | "packages": [ 29 | "packages/*" 30 | ], 31 | "nohoist": [ 32 | "**/@types/**" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/cli/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.tsbuildinfo 2 | -------------------------------------------------------------------------------- /packages/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### Dependencies 4 | 5 | * The following workspace dependencies were updated 6 | * dependencies 7 | * @cogeotiff/core bumped from ^9.0.0 to ^9.0.1 8 | 9 | ### Dependencies 10 | 11 | * The following workspace dependencies were updated 12 | * dependencies 13 | * @cogeotiff/core bumped from ^9.0.2 to ^9.0.3 14 | 15 | ## [9.0.2](https://github.com/blacha/cogeotiff/compare/cli-v9.0.1...cli-v9.0.2) (2023-12-15) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * **cli:** remove excess console.log ([60e9d47](https://github.com/blacha/cogeotiff/commit/60e9d47b1d9846e9ba2cfd9b3135661ed16418c1)) 21 | 22 | 23 | ### Dependencies 24 | 25 | * The following workspace dependencies were updated 26 | * dependencies 27 | * @cogeotiff/core bumped from ^9.0.1 to ^9.0.2 28 | 29 | ## [9.0.0](https://github.com/blacha/cogeotiff/compare/cli-v8.1.1...cli-v9.0.0) (2023-12-11) 30 | 31 | 32 | ### ⚠ BREAKING CHANGES 33 | 34 | * rename all type from CogTiff to just Tiff ([#1227](https://github.com/blacha/cogeotiff/issues/1227)) 35 | * modify structure of tiff tags ([#1225](https://github.com/blacha/cogeotiff/issues/1225)) 36 | 37 | ### Features 38 | 39 | * **cli:** expose stats on tiles, empty, avg and overview size ([62bc6a7](https://github.com/blacha/cogeotiff/commit/62bc6a727615907c318fc8b4b06375b81bc17a00)) 40 | * **cli:** include file size if known ([5e90764](https://github.com/blacha/cogeotiff/commit/5e907640022953d396a14d4023633dfe8e14289e)) 41 | * color more output and add more tags ([fe4088b](https://github.com/blacha/cogeotiff/commit/fe4088b3f1f88a1248d803c29a563872aab4205c)) 42 | * export all tag value constants ([#1229](https://github.com/blacha/cogeotiff/issues/1229)) ([44757e5](https://github.com/blacha/cogeotiff/commit/44757e5ba5c98e992bb9fd72eb9993c727648b74)) 43 | * modify structure of tiff tags ([#1225](https://github.com/blacha/cogeotiff/issues/1225)) ([049e0bc](https://github.com/blacha/cogeotiff/commit/049e0bc3c4e15f8c095a3da4442ef144d372cf60)) 44 | * rename all type from CogTiff to just Tiff ([#1227](https://github.com/blacha/cogeotiff/issues/1227)) ([872263b](https://github.com/blacha/cogeotiff/commit/872263b11f1ab06853cb872de54a9d9dd745b647)) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * **cli:** if the tiff is fully sparse dont print NaN ([368aad2](https://github.com/blacha/cogeotiff/commit/368aad2d9ed80508195fd3700934e026d1106ed3)) 50 | * **cli:** remove console.log ([b4f22cb](https://github.com/blacha/cogeotiff/commit/b4f22cb47c3e64f523ad4955bc5389f341ada207)) 51 | 52 | 53 | ### Dependencies 54 | 55 | * The following workspace dependencies were updated 56 | * dependencies 57 | * @cogeotiff/core bumped from ^8.1.1 to ^9.0.0 58 | 59 | ## [8.1.1](https://github.com/blacha/cogeotiff/compare/cli-v8.1.0...cli-v8.1.1) (2023-11-14) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * **cli:** correct html creation ([7156d63](https://github.com/blacha/cogeotiff/commit/7156d63626c56f868b694e8124fdd96fd83f09be)) 65 | 66 | 67 | ### Dependencies 68 | 69 | * The following workspace dependencies were updated 70 | * dependencies 71 | * @cogeotiff/core bumped from ^8.1.0 to ^8.1.1 72 | 73 | ## [8.1.0](https://github.com/blacha/cogeotiff/compare/cli-v8.0.2...cli-v8.1.0) (2023-08-23) 74 | 75 | 76 | ### Features 77 | 78 | * **cli:** fetch all tiff tags with --fetch-tags ([#1155](https://github.com/blacha/cogeotiff/issues/1155)) ([4067751](https://github.com/blacha/cogeotiff/commit/406775184eed18ab10ae2816ecbedea9706b20f5)) 79 | 80 | 81 | ### Dependencies 82 | 83 | * The following workspace dependencies were updated 84 | * dependencies 85 | * @cogeotiff/core bumped from ^8.0.2 to ^8.1.0 86 | 87 | ## [8.0.2](https://github.com/blacha/cogeotiff/compare/cli-v8.0.1...cli-v8.0.2) (2023-08-05) 88 | 89 | 90 | ### Bug Fixes 91 | 92 | * **core:** expose TiffTag and TiffTagGeo ([d538bdc](https://github.com/blacha/cogeotiff/commit/d538bdc833bf76ba8d730a1062156916715585b4)) 93 | 94 | 95 | ### Dependencies 96 | 97 | * The following workspace dependencies were updated 98 | * dependencies 99 | * @cogeotiff/core bumped from ^8.0.1 to ^8.0.2 100 | 101 | ## [8.0.1](https://github.com/blacha/cogeotiff/compare/cli-v8.0.0...cli-v8.0.1) (2023-08-05) 102 | 103 | 104 | ### Bug Fixes 105 | 106 | * broken changelog ordering ([31f8c8a](https://github.com/blacha/cogeotiff/commit/31f8c8ac5e2770427ed2dc0f5c7c34330c6cb0eb)) 107 | 108 | 109 | ### Dependencies 110 | 111 | * The following workspace dependencies were updated 112 | * dependencies 113 | * @cogeotiff/core bumped from ^8.0.0 to ^8.0.1 114 | -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 | # coeotiff 2 | 3 | CLI to work with [Cloud optimized GEOTiff](https://www.cogeo.org/) 4 | 5 | - Completely javascript based, works in the browser and nodejs 6 | - Lazy load COG images and metadata 7 | - Supports huge 100GB+ COGs 8 | - Uses GDAL COG optimizations, generally only one read per tile! 9 | - Loads COGs from URL, File or AWS S3 10 | 11 | ## Usage 12 | 13 | ```bash 14 | npm i -g @cogeotiff/cli 15 | ``` 16 | 17 | ### cogeotiff info 18 | 19 | Display basic information about COG 20 | 21 | ```shell 22 | cogeotiff info webp.cog.tiff 23 | ``` 24 | 25 | Output: 26 | 27 | ``` 28 | COG File Info - s3://linz-imagery/otago/otago_sn9457_1995-1997_0.75m/2193/rgb/ Tiff type BigTiff (v43) 29 | Bytes read 32 KB (1 Chunk) 30 | 31 | Images 32 | Compression image/webp 33 | Origin 1352800, 4851600, 0 34 | Resolution 0.75, -0.75, 0 35 | BoundingBox 1352800, 4844400, 1357600, 4851600 36 | EPSG EPSG:2193 (https://epsg.io/2193) 37 | Images 38 | Id Size Tile Size Tile Count Resolution 39 | 0 6400x9600 512x512 13x19 (247) 0.75 40 | 1 3200x4800 512x512 7x10 (70) 1.5 41 | 2 1600x2400 512x512 4x5 (20) 3 42 | 3 800x1200 512x512 2x3 (6) 6 43 | 4 400x600 512x512 1x2 (2) 12 44 | 5 200x300 512x512 1x1 (1) 24 45 | 46 | GDAL 47 | COG optimized true 48 | Ghost Options 49 | GDAL_STRUCTURAL_METADATA_SIZE = 000140 bytes 50 | LAYOUT = IFDS_BEFORE_DATA 51 | BLOCK_ORDER = ROW_MAJOR 52 | BLOCK_LEADER = SIZE_AS_UINT4 53 | BLOCK_TRAILER = LAST_4_BYTES_REPEATED 54 | KNOWN_INCOMPATIBLE_EDITION = NO 55 | ``` 56 | 57 | ### cogeotiff dump 58 | 59 | Dump all tiles for a image (**Warning** if you do this for a large cog this will create millions of files.) 60 | 61 | ``` 62 | cogeotiff dump webp.cog.tiff --image 2 --output output 63 | ``` 64 | -------------------------------------------------------------------------------- /packages/cli/bin/cogeotiff.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import '../build/bin.js'; 3 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cogeotiff/cli", 3 | "version": "9.0.3", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/blacha/cogeotiff.git", 7 | "directory": "packages/cli" 8 | }, 9 | "author": "Blayne Chard", 10 | "license": "MIT", 11 | "bin": { 12 | "cogeotiff": "bin/cogeotiff.js" 13 | }, 14 | "type": "module", 15 | "engines": { 16 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 17 | }, 18 | "scripts": {}, 19 | "dependencies": { 20 | "@chunkd/source": "^11.0.0", 21 | "@chunkd/middleware": "^11.0.0", 22 | "@chunkd/fs": "^11.0.2", 23 | "@chunkd/fs-aws": "^11.0.2", 24 | "@cogeotiff/core": "^9.0.3", 25 | "@linzjs/tracing": "^1.1.1", 26 | "ansi-colors": "^4.1.1", 27 | "cmd-ts": "^0.13.0", 28 | "p-limit": "^4.0.0", 29 | "pretty-json-log": "^1.4.0" 30 | }, 31 | "devDependencies": { 32 | "@types/node": "^20.0.0", 33 | "@types/pino": "^7.0.5" 34 | }, 35 | "publishConfig": { 36 | "access": "public" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/cli/src/__test__/exports.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | import { describe, it } from 'node:test'; 3 | 4 | import { 5 | AngularUnit, 6 | Compression, 7 | LinearUnit, 8 | OldSubFileType, 9 | Orientation, 10 | Photometric, 11 | PlanarConfiguration, 12 | RasterTypeKey, 13 | SampleFormat, 14 | SubFileType, 15 | } from '@cogeotiff/core'; 16 | 17 | // Ensure the tag constants are exported 18 | describe('Exports', () => { 19 | it('should export constants', () => { 20 | assert.equal(Photometric.Rgb, 2); 21 | assert.equal(SampleFormat.Float, 3); 22 | assert.equal(RasterTypeKey.PixelIsArea, 1); 23 | assert.equal(SubFileType.ReducedImage, 1); 24 | assert.equal(OldSubFileType.ReducedImage, 2); 25 | assert.equal(Compression.Lzw, 5); 26 | assert.equal(AngularUnit.Degree, 9102); 27 | assert.equal(LinearUnit.Metre, 9001); 28 | assert.equal(PlanarConfiguration.Contig, 1); 29 | assert.equal(Orientation.TopLeft, 1); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/cli/src/action.util.ts: -------------------------------------------------------------------------------- 1 | import c from 'ansi-colors'; 2 | 3 | export interface CLiResultMapLine { 4 | key: string; 5 | value: string | number | boolean | number[] | null; 6 | enabled?: boolean; 7 | } 8 | export interface CliResultMap { 9 | title?: string; 10 | keys: (CLiResultMapLine | null)[]; 11 | enabled?: boolean; 12 | } 13 | 14 | export const ActionUtil = { 15 | formatResult(title: string, result: CliResultMap[]): string[] { 16 | const msg: string[] = [title]; 17 | for (const group of result) { 18 | if (group.enabled === false) continue; 19 | msg.push(''); 20 | if (group.title) msg.push(c.bold(group.title)); 21 | for (const kv of group.keys) { 22 | if (kv == null) continue; 23 | if (kv.enabled === false) continue; 24 | if (kv.value == null || (typeof kv.value === 'string' && kv.value.trim() === '')) { 25 | continue; 26 | } 27 | msg.push(` ${kv.key.padEnd(14, ' ')} ${String(kv.value)}`); 28 | } 29 | } 30 | return msg; 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /packages/cli/src/bin.ts: -------------------------------------------------------------------------------- 1 | // Ensure connection reuse is enabled 2 | process.env['AWS_NODEJS_CONNECTION_REUSE_ENABLED'] = '1'; 3 | 4 | import { otError } from '@linzjs/tracing'; 5 | import { run } from 'cmd-ts'; 6 | 7 | import { cmd } from './index.js'; 8 | import { logger } from './log.js'; 9 | 10 | run(cmd, process.argv.slice(2)).catch((err) => { 11 | logger.fatal('Command:Failed', { ...otError(err) }); 12 | logger.pino.flush(); 13 | // Give the log some time to flush before exiting 14 | setTimeout(() => process.exit(1), 25); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/cli/src/cli.table.ts: -------------------------------------------------------------------------------- 1 | import c from 'ansi-colors'; 2 | 3 | export interface CliTableInfo { 4 | /** Header for the table */ 5 | name: string; 6 | /** Pad the field out to this width */ 7 | width: number; 8 | /** Get the field value */ 9 | get: (obj: T, index?: number) => string | null; 10 | /** 11 | * Is this field enabled, 12 | * Fields are only enabled if every data record returns true for enabled 13 | */ 14 | enabled?: (obj: T) => boolean; 15 | } 16 | 17 | export class CliTable { 18 | fields: CliTableInfo[] = []; 19 | add(fields: CliTableInfo): void { 20 | this.fields.push(fields); 21 | } 22 | 23 | print(data: T[], rowPadding = ''): string[] { 24 | const fields = this.fields.filter((f) => data.every((d) => f.enabled?.(d) ?? true)); 25 | const values = fields.map((f) => data.map((d, i) => f.get(d, i))); 26 | const sizes = values.map((val) => val.reduce((v, c) => Math.max(v, c?.length ?? 0), 0)); 27 | 28 | const rows: string[] = [rowPadding + fields.map((f, i) => c.bold(f.name.padEnd(sizes[i] + 2))).join('\t')]; 29 | for (let i = 0; i < data.length; i++) { 30 | const row: string[] = []; 31 | for (let f = 0; f < fields.length; f++) { 32 | const fValue = values[f][i]; 33 | const fSize = sizes[f]; 34 | row.push((fValue ?? '').padEnd(fSize + 2)); 35 | } 36 | rows.push(rowPadding + row.join('\t')); 37 | } 38 | 39 | return rows; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/cli/src/commands/dump.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs'; 2 | import { basename } from 'node:path'; 3 | import { pathToFileURL } from 'node:url'; 4 | 5 | import { fsa } from '@chunkd/fs'; 6 | import { Tiff, TiffMimeType } from '@cogeotiff/core'; 7 | import { log } from '@linzjs/tracing'; 8 | import { command, number, option, optional, restPositionals } from 'cmd-ts'; 9 | import pLimit from 'p-limit'; 10 | 11 | import { DefaultArgs, Url } from '../common.js'; 12 | import { ensureS3fs, setupLogger } from '../log.js'; 13 | import { getTileName, writeTile } from '../util.tile.js'; 14 | 15 | const TileQueue = pLimit(5); 16 | 17 | export interface GeoJsonPolygon { 18 | type: 'Feature'; 19 | geometry: { 20 | type: 'Polygon'; 21 | coordinates: [[[number, number], [number, number], [number, number], [number, number], [number, number]]]; 22 | }; 23 | properties: Record; 24 | } 25 | 26 | function makePolygon(xMin: number, yMin: number, xMax: number, yMax: number): GeoJsonPolygon { 27 | return { 28 | type: 'Feature', 29 | properties: {}, 30 | geometry: { 31 | type: 'Polygon', 32 | coordinates: [ 33 | [ 34 | [xMin, yMin], 35 | [xMin, yMax], 36 | [xMax, yMax], 37 | [xMax, yMin], 38 | [xMin, yMin], 39 | ], 40 | ], 41 | }, 42 | }; 43 | } 44 | 45 | export const commandDump = command({ 46 | name: 'dump', 47 | args: { 48 | ...DefaultArgs, 49 | path: option({ short: 'f', long: 'file', type: optional(Url) }), 50 | image: option({ short: 'i', long: 'image', description: 'Image Id to dump (starting from 0)', type: number }), 51 | output: option({ short: 'o', long: 'output', type: optional(Url) }), 52 | paths: restPositionals({ type: Url, description: 'Files to process' }), 53 | }, 54 | 55 | async handler(args) { 56 | const cwd = pathToFileURL(process.cwd() + '/'); 57 | const logger = setupLogger(args); 58 | for (const path of [args.path, ...args.paths]) { 59 | if (path == null) continue; 60 | if (path.protocol === 's3:') await ensureS3fs(); 61 | const source = fsa.source(path); 62 | const tiff = await new Tiff(source).init(); 63 | const img = tiff.images[args.image]; 64 | if (!img.isTiled()) throw Error(`Tiff: ${path.href} file is not tiled.`); 65 | const output = new URL(`${basename(path.href)}-i${args.image}/`, args.output ?? cwd); 66 | await fs.mkdir(output, { recursive: true }); 67 | 68 | await dumpTiles(tiff, output, args.image, logger); 69 | await dumpBounds(tiff, output, args.image); 70 | } 71 | }, 72 | }); 73 | 74 | async function dumpBounds(tiff: Tiff, target: URL, index: number): Promise { 75 | const img = tiff.images[index]; 76 | if (!img.isTiled() || !img.isGeoLocated) return; 77 | 78 | const features: GeoJsonPolygon[] = []; 79 | const featureCollection: Record = { 80 | type: 'FeatureCollection', 81 | features, 82 | }; 83 | 84 | const tileCount = img.tileCount; 85 | const tileInfo = img.tileSize; 86 | const tileSize = img.size; 87 | 88 | const firstImage = tiff.images[0]; 89 | if (firstImage.epsg !== 4326) { 90 | featureCollection['crs'] = { 91 | type: 'name', 92 | properties: { name: `epsg:${firstImage.epsg}` }, 93 | }; 94 | } 95 | firstImage.compression; 96 | const firstTileSize = firstImage.size; 97 | const origin = firstImage.origin; 98 | const resolution = firstImage.resolution; 99 | 100 | const xScale = (resolution[0] * firstTileSize.width) / tileSize.width; 101 | const yScale = (resolution[1] * firstTileSize.height) / tileSize.height; 102 | 103 | for (let y = 0; y < tileCount.y; y++) { 104 | const yMax = origin[1] + y * tileInfo.height * yScale; 105 | const yMin = yMax + tileInfo.height * yScale; 106 | for (let x = 0; x < tileCount.x; x++) { 107 | const xMin = origin[0] + x * tileInfo.width * xScale; 108 | const xMax = xMin + tileInfo.width * xScale; 109 | const poly = makePolygon(xMin, yMin, xMax, yMax); 110 | poly.properties = { tile: getTileName(firstImage.compression ?? TiffMimeType.None, index, x, y) }; 111 | features.push(poly); 112 | } 113 | } 114 | 115 | await fs.writeFile(new URL(`i${index}.bounds.geojson`, target), JSON.stringify(featureCollection, null, 2)); 116 | } 117 | 118 | async function dumpTiles(tiff: Tiff, target: URL, index: number, logger: typeof log): Promise { 119 | const promises: Promise[] = []; 120 | const img = tiff.images[index]; 121 | if (!img.isTiled()) return 0; 122 | 123 | logger.info('Tiff:Info', { source: tiff.source.url, ...img.tileSize, ...img.tileCount }); 124 | const { tileCount, tileSize } = img; 125 | 126 | const html = [ 127 | '', 128 | '', 129 | `\t`, 130 | '', 131 | '', 132 | ]; 133 | 134 | for (let y = 0; y < tileCount.y; y++) { 135 | for (let x = 0; x < tileCount.x; x++) { 136 | const promise = TileQueue(() => writeTile(tiff, x, y, index, target, logger)); 137 | promises.push(promise); 138 | } 139 | } 140 | 141 | const result = await Promise.all(promises); 142 | let i = 0; 143 | for (let y = 0; y < tileCount.y; y++) { 144 | html.push('\t
'); 145 | 146 | for (let x = 0; x < tileCount.x; x++) { 147 | const fileName = result[i]; 148 | i++; 149 | 150 | if (fileName == null) { 151 | html.push(`\t\t
`); 152 | continue; 153 | } 154 | html.push(`\t\t`); 155 | } 156 | html.push('\t
'); 157 | } 158 | 159 | html.push(''); 160 | html.push(''); 161 | await fs.writeFile(new URL('index.html', target), html.join('\n')); 162 | 163 | return promises.length; 164 | } 165 | -------------------------------------------------------------------------------- /packages/cli/src/commands/info.ts: -------------------------------------------------------------------------------- 1 | import { fsa } from '@chunkd/fs'; 2 | import { Tag, Tiff, TiffImage, TiffTag, TiffTagGeo, TiffTagValueType, TiffVersion } from '@cogeotiff/core'; 3 | import c from 'ansi-colors'; 4 | import { command, flag, option, optional, restPositionals } from 'cmd-ts'; 5 | 6 | import { ActionUtil, CliResultMap } from '../action.util.js'; 7 | import { CliTable, CliTableInfo } from '../cli.table.js'; 8 | import { DefaultArgs, Url } from '../common.js'; 9 | import { FetchLog } from '../fs.js'; 10 | import { ensureS3fs, setupLogger } from '../log.js'; 11 | import { TagFormatters, TagGeoFormatters } from '../tags.js'; 12 | import { toByteSizeString } from '../util.bytes.js'; 13 | 14 | function round(num: number): number { 15 | const opt = 10 ** 4; 16 | return Math.floor(num * opt) / opt; 17 | } 18 | 19 | export const commandInfo = command({ 20 | name: 'info', 21 | args: { 22 | ...DefaultArgs, 23 | path: option({ short: 'f', long: 'file', type: optional(Url) }), 24 | tags: flag({ short: 't', long: 'tags', description: 'Dump tiff tags' }), 25 | fetchTags: flag({ long: 'fetch-tags', description: 'Fetch extra tiff tag information' }), 26 | tileStats: flag({ 27 | long: 'tile-stats', 28 | description: 'Fetch tile information, like size [this can fetch a lot of data]', 29 | }), 30 | paths: restPositionals({ type: Url, description: 'Files to process' }), 31 | }, 32 | async handler(args) { 33 | const logger = setupLogger(args); 34 | const paths = [...args.paths, args.path].filter((f) => f != null); 35 | 36 | for (const path of paths) { 37 | if (path == null) continue; 38 | if (path.protocol === 's3:') await ensureS3fs(); 39 | logger.debug('Tiff:load', { path: path?.href }); 40 | FetchLog.reset(); 41 | 42 | const source = fsa.source(path); 43 | const tiff = await new Tiff(source).init(); 44 | 45 | if (args.tileStats) { 46 | await Promise.all(tiff.images.map((img) => img.fetch(TiffTag.TileByteCounts))); 47 | TiffImageInfoTable.add(tiffTileStats); 48 | } 49 | 50 | const header = [ 51 | { key: 'Tiff type', value: `${TiffVersion[tiff.version]} (v${String(tiff.version)})` }, 52 | { 53 | key: 'Bytes read', 54 | value: `${toByteSizeString(FetchLog.bytesRead)} (${FetchLog.fetches.length} Chunk${ 55 | FetchLog.fetches.length === 1 ? '' : 's' 56 | })`, 57 | }, 58 | tiff.source.metadata?.size 59 | ? { 60 | key: 'Size', 61 | value: toByteSizeString(tiff.source.metadata.size), 62 | } 63 | : null, 64 | ]; 65 | 66 | const firstImage = tiff.images[0]; 67 | const isGeoLocated = firstImage.isGeoLocated; 68 | const compression = firstImage.value(TiffTag.Compression); 69 | const images = [ 70 | { key: 'Compression', value: `${compression} - ${c.magenta(firstImage.compression ?? '??')}` }, 71 | isGeoLocated ? { key: 'Origin', value: firstImage.origin.map(round).join(', ') } : null, 72 | isGeoLocated ? { key: 'Resolution', value: firstImage.resolution.map(round).join(', ') } : null, 73 | isGeoLocated ? { key: 'BoundingBox', value: firstImage.bbox.map(round).join(', ') } : null, 74 | firstImage.epsg 75 | ? { key: 'EPSG', value: `EPSG:${firstImage.epsg} ${c.underline('https://epsg.io/' + firstImage.epsg)}` } 76 | : null, 77 | { key: 'Images', value: '\n' + TiffImageInfoTable.print(tiff.images, '\t').join('\n') }, 78 | ]; 79 | 80 | const ghostOptions = [...(tiff.options?.options.entries() ?? [])]; 81 | const gdalMetadata = parseGdalMetadata(firstImage); 82 | const gdal = [ 83 | { 84 | key: 'COG optimized', 85 | value: String(tiff.options?.isCogOptimized), 86 | enabled: tiff.options?.isCogOptimized === true, 87 | }, 88 | { key: 'COG broken', value: String(tiff.options?.isBroken), enabled: tiff.options?.isBroken === true }, 89 | { 90 | key: 'Ghost Options', 91 | value: '\n' + ghostOptions.map((c) => `\t\t${c[0]} = ${c[1]}`).join('\n'), 92 | enabled: ghostOptions.length > 0, 93 | }, 94 | { 95 | key: 'Metadata', 96 | value: '\n' + gdalMetadata?.map((c) => `\t\t${c}`).join('\n'), 97 | enabled: gdalMetadata != null, 98 | }, 99 | ]; 100 | 101 | const result: CliResultMap[] = [ 102 | { keys: header }, 103 | { title: 'Images', keys: images }, 104 | { title: 'GDAL', keys: gdal, enabled: gdal.filter((g) => g.enabled == null || g.enabled).length > 0 }, 105 | ]; 106 | if (args.tags) { 107 | for (const img of tiff.images) { 108 | const tiffTags = [...img.tags.values()]; 109 | 110 | if (args.fetchTags) await Promise.all(tiffTags.map((t) => img.fetch(t.id))); 111 | 112 | result.push({ 113 | title: `Image: ${img.id} - Tiff tags`, 114 | keys: tiffTags.map(formatTag), 115 | }); 116 | await img.loadGeoTiffTags(); 117 | if (img.tagsGeo.size > 0) { 118 | const tiffTagsGeo = [...img.tagsGeo.entries()]; 119 | const keys = tiffTagsGeo.map(([key, value]) => formatGeoTag(key, value)); 120 | if (keys.length > 0) { 121 | result.push({ title: `Image: ${img.id} - Geo Tiff tags`, keys }); 122 | } 123 | } 124 | } 125 | } 126 | 127 | const msg = ActionUtil.formatResult(`\n${c.bold('COG File Info')} - ${c.bold(path.href)}`, result); 128 | console.log(msg.join('\n')); 129 | 130 | await source.close?.(); 131 | } 132 | }, 133 | }); 134 | 135 | const TiffImageInfoTable = new CliTable(); 136 | TiffImageInfoTable.add({ name: 'Id', width: 4, get: (_i, index) => String(index) }); 137 | TiffImageInfoTable.add({ name: 'Size', width: 20, get: (i) => `${i.size.width}x${i.size.height}` }); 138 | TiffImageInfoTable.add({ 139 | name: 'Tile Size', 140 | width: 20, 141 | get: (i) => `${i.tileSize.width}x${i.tileSize.height}`, 142 | enabled: (i) => i.isTiled(), 143 | }); 144 | TiffImageInfoTable.add({ 145 | name: 'Tile Count', 146 | width: 20, 147 | get: (i) => { 148 | let tileCount = i.tileCount.x * i.tileCount.y; 149 | const offsets = i.value(TiffTag.TileByteCounts) ?? i.value(TiffTag.TileOffsets); 150 | if (offsets) tileCount = offsets.length; 151 | 152 | return `${i.tileCount.x}x${i.tileCount.y} (${tileCount})`; 153 | }, 154 | enabled: (i) => i.isTiled(), 155 | }); 156 | TiffImageInfoTable.add({ 157 | name: 'Strip Count', 158 | width: 20, 159 | get: (i) => `${i.tags.get(TiffTag.StripOffsets)?.count}`, 160 | enabled: (i) => !i.isTiled(), 161 | }); 162 | TiffImageInfoTable.add({ 163 | name: 'Resolution', 164 | width: 20, 165 | get: (i) => `${round(i.resolution[0])}`, 166 | enabled: (i) => i.isGeoLocated, 167 | }); 168 | 169 | // Show compression only if it varies between images 170 | TiffImageInfoTable.add({ 171 | name: 'Compression', 172 | width: 20, 173 | get: (i) => i.compression, 174 | enabled: (i) => { 175 | const formats = new Set(); 176 | i.tiff.images.forEach((f) => formats.add(f.compression)); 177 | return formats.size > 1; 178 | }, 179 | }); 180 | 181 | export const tiffTileStats: CliTableInfo = { 182 | name: 'Tile Stats', 183 | width: 20, 184 | get: (i) => { 185 | const sizes = i.value(TiffTag.TileByteCounts); 186 | if (sizes == null) return 'N/A'; 187 | 188 | const stats = { 189 | size: 0, 190 | empty: 0, 191 | }; 192 | for (const st of sizes) { 193 | if (st === 0) stats.empty++; 194 | stats.size += st; 195 | } 196 | if (stats.size === 0) return `${c.red('empty')} x${stats.empty}`; 197 | 198 | const empty = stats.empty > 0 ? ` (${c.red('empty')} x${stats.empty})` : ''; 199 | 200 | const avg = stats.size === 0 ? 0 : stats.size / (sizes.length - stats.empty); 201 | return toByteSizeString(stats.size) + ` (${c.blue('avg:')} ${toByteSizeString(avg)})` + empty; 202 | }, 203 | enabled: () => true, 204 | }; 205 | 206 | /** 207 | * Parse out the GDAL Metadata to be more friendly to read 208 | * 209 | * TODO using a XML Parser will make this even better 210 | * @param img 211 | */ 212 | function parseGdalMetadata(img: TiffImage): string[] | null { 213 | const metadata = img.value(TiffTag.GdalMetadata); 214 | if (typeof metadata !== 'string') return null; 215 | if (!metadata.startsWith('')) return null; 216 | return metadata 217 | .replace('\n', '') 218 | .replace('\n', '') 219 | .replace('\n\x00', '') 220 | .split('\n') 221 | .map((c) => c.trim()); 222 | } 223 | 224 | function isLoaded(tag: Tag): boolean { 225 | if (tag.type === 'offset' && tag.isLoaded === false) return false; 226 | if (tag.type === 'lazy' && tag.value == null) return false; 227 | return true; 228 | } 229 | 230 | function formatTag(tag: Tag): { key: string; value: string } { 231 | const tagName = TiffTag[tag.id]; 232 | const tagDebug = `(${TiffTagValueType[tag.dataType]}${tag.count > 1 ? ' x' + tag.count : ''}`; 233 | const key = `${c.dim(String(tag.id)).padEnd(7, ' ')} ${String(tagName)} ${c.dim(tagDebug)})`.padEnd(52, ' '); 234 | 235 | if (!isLoaded(tag)) { 236 | return { key, value: c.dim('Tag not Loaded, use --fetch-tags to force load') }; 237 | } 238 | 239 | let complexType = ''; 240 | // Array of values that is not a string! 241 | if (tag.count > 1 && tag.dataType !== TiffTagValueType.Ascii) { 242 | const val = [...(tag.value as number[])]; // Ensure the value is not a TypedArray 243 | if (TagFormatters[tag.id]) { 244 | complexType = ` - ${c.magenta(TagFormatters[tag.id](val) ?? '??')}`; 245 | } 246 | return { key, value: (val.length > 25 ? val.slice(0, 25).join(', ') + '...' : val.join(', ')) + complexType }; 247 | } 248 | 249 | let tagString = JSON.stringify(tag.value) ?? c.dim('null'); 250 | if (tagString.length > 256) tagString = tagString.slice(0, 250) + '...'; 251 | if (TagFormatters[tag.id]) { 252 | complexType = ` - ${c.magenta(TagFormatters[tag.id]([tag.value as number]) ?? '??')}`; 253 | } 254 | return { key, value: tagString + complexType }; 255 | } 256 | 257 | function formatGeoTag(tagId: TiffTagGeo, value: string | number | number[]): { key: string; value: string } { 258 | const tagName = TiffTagGeo[tagId]; 259 | const key = `${c.dim(String(tagId)).padEnd(7, ' ')} ${String(tagName).padEnd(30)}`; 260 | 261 | let complexType = ''; 262 | if (TagGeoFormatters[tagId]) { 263 | complexType = ` - ${c.magenta(TagGeoFormatters[tagId]([value as unknown as number]) ?? '??')}`; 264 | } 265 | 266 | let tagString = JSON.stringify(value) ?? c.dim('null'); 267 | if (tagString.length > 256) tagString = tagString.slice(0, 250) + '...'; 268 | return { key, value: tagString + complexType }; 269 | } 270 | -------------------------------------------------------------------------------- /packages/cli/src/common.ts: -------------------------------------------------------------------------------- 1 | import { flag, Type } from 'cmd-ts'; 2 | import { pathToFileURL } from 'url'; 3 | 4 | export const verbose = flag({ long: 'verbose', description: 'Verbose logging', short: 'v' }); 5 | export const extraVerbose = flag({ long: 'extra-verbose', description: 'Extra verbose logging', short: 'V' }); 6 | 7 | export const DefaultArgs = { 8 | verbose, 9 | extraVerbose, 10 | }; 11 | 12 | export const Url: Type = { 13 | async from(s: string): Promise { 14 | try { 15 | return new URL(s); 16 | } catch (e) { 17 | return pathToFileURL(s); 18 | } 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /packages/cli/src/fs.ts: -------------------------------------------------------------------------------- 1 | import { SourceCallback, SourceMiddleware, SourceRequest } from '@chunkd/source'; 2 | 3 | import { logger } from './log.js'; 4 | 5 | export const FetchLog: SourceMiddleware & { reset(): void; fetches: SourceRequest[]; bytesRead: number } = { 6 | name: 'source:log', 7 | fetch(req: SourceRequest, next: SourceCallback) { 8 | this.fetches.push(req); 9 | this.bytesRead += req.length ?? 0; 10 | logger.info('Tiff:fetch', { href: req.source.url.href, offset: req.offset, length: req.length }); 11 | return next(req); 12 | }, 13 | reset() { 14 | this.fetches = []; 15 | this.bytesRead = 0; 16 | }, 17 | fetches: [], 18 | bytesRead: 0, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | import { subcommands } from 'cmd-ts'; 2 | 3 | import { commandDump } from './commands/dump.js'; 4 | import { commandInfo } from './commands/info.js'; 5 | 6 | export const cmd = subcommands({ 7 | name: 'cogeotiff', 8 | description: 'COG utilities', 9 | cmds: { 10 | info: commandInfo, 11 | dump: commandDump, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /packages/cli/src/log.ts: -------------------------------------------------------------------------------- 1 | import { fsa, FsHttp } from '@chunkd/fs'; 2 | import { SourceCache, SourceChunk } from '@chunkd/middleware'; 3 | import { log } from '@linzjs/tracing'; 4 | 5 | import { FetchLog } from './fs.js'; 6 | 7 | // Cache the last 10MB of chunks for reuse 8 | export const sourceCache = new SourceCache({ size: 10 * 1024 * 1024 }); 9 | export const sourceChunk = new SourceChunk({ size: 32 * 1024 }); 10 | 11 | export function setupLogger(cfg: { verbose?: boolean; extraVerbose?: boolean }): typeof log { 12 | if (cfg.verbose) { 13 | log.level = 'debug'; 14 | } else if (cfg.extraVerbose) { 15 | log.level = 'trace'; 16 | } else { 17 | log.level = 'warn'; 18 | } 19 | 20 | fsa.register('http://', new FsHttp()); 21 | fsa.register('https://', new FsHttp()); 22 | 23 | // Order of these are really important 24 | // Chunk all requests into 32KB chunks 25 | fsa.middleware.push(sourceChunk); 26 | // Cache the last 10MB of chunks for reuse 27 | fsa.middleware.push(sourceCache); 28 | 29 | fsa.middleware.push(FetchLog); 30 | 31 | return log; 32 | } 33 | 34 | export const logger = log; 35 | 36 | /** S3 client adds approx 300ms to the cli startup time, so only register it if needed */ 37 | export async function ensureS3fs(): Promise { 38 | if (fsa.systems.find((f) => f.prefix.startsWith('s3'))) return; 39 | 40 | const S3Client = await import('@aws-sdk/client-s3'); 41 | const FsAwsS3 = await import('@chunkd/fs-aws'); 42 | 43 | fsa.register('s3://', new FsAwsS3.FsAwsS3(new S3Client.S3Client({}))); 44 | } 45 | -------------------------------------------------------------------------------- /packages/cli/src/tags.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AngularUnit, 3 | Compression, 4 | LinearUnit, 5 | ModelTypeCode, 6 | Orientation, 7 | Photometric, 8 | PlanarConfiguration, 9 | RasterTypeKey, 10 | SampleFormat, 11 | SubFileType, 12 | TiffTag, 13 | TiffTagGeo, 14 | TiffTagGeoType, 15 | TiffTagType, 16 | } from '@cogeotiff/core'; 17 | 18 | /** Convert enum values back to strings */ 19 | export const TagGeoFormatters = { 20 | [TiffTagGeo.GTRasterTypeGeoKey]: (v: TiffTagGeoType[TiffTagGeo.GTRasterTypeGeoKey]) => RasterTypeKey[v], 21 | [TiffTagGeo.GTModelTypeGeoKey]: (v: TiffTagGeoType[TiffTagGeo.GTModelTypeGeoKey]) => ModelTypeCode[v], 22 | [TiffTagGeo.GeogAngularUnitsGeoKey]: (v: TiffTagGeoType[TiffTagGeo.GeogAngularUnitsGeoKey]) => AngularUnit[v], 23 | [TiffTagGeo.ProjLinearUnitsGeoKey]: (v: TiffTagGeoType[TiffTagGeo.ProjLinearUnitsGeoKey]) => LinearUnit[v], 24 | [TiffTagGeo.VerticalUnitsGeoKey]: (v: TiffTagGeoType[TiffTagGeo.VerticalUnitsGeoKey]) => LinearUnit[v], 25 | } as Record string>; 26 | 27 | export const TagFormatters = { 28 | [TiffTag.LercParameters]: (value: TiffTagType[TiffTag.LercParameters]): string => { 29 | return `v${value[0]} - ${Compression[value[1]] ?? '??'}`; 30 | }, 31 | [TiffTag.SubFileType]: (value: TiffTagType[TiffTag.SubFileType]): string => SubFileType[value], 32 | [TiffTag.Compression]: (value: TiffTagType[TiffTag.Compression]): string => Compression[value], 33 | [TiffTag.Orientation]: (value: TiffTagType[TiffTag.Orientation]): string => Orientation[value], 34 | [TiffTag.SampleFormat]: (value: TiffTagType[TiffTag.SampleFormat]): string => { 35 | return value.map((m) => SampleFormat[m]).join(', '); 36 | }, 37 | [TiffTag.Photometric]: (value: TiffTagType[TiffTag.Photometric]): string => Photometric[value], 38 | [TiffTag.PlanarConfiguration]: (value: TiffTagType[TiffTag.PlanarConfiguration]): string => { 39 | return PlanarConfiguration[value]; 40 | }, 41 | } as Record string>; 42 | -------------------------------------------------------------------------------- /packages/cli/src/util.bytes.ts: -------------------------------------------------------------------------------- 1 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; 2 | 3 | /** 4 | * Convert a byte count into human readable byte numbers 5 | * 6 | * eg 1024 => 1KB 7 | * 8 | * @param bytes byte count to convert 9 | */ 10 | export function toByteSizeString(bytes: number): string { 11 | if (bytes === 0) return '0'; 12 | if (bytes === 1) return '1 Byte'; 13 | const i = Math.floor(Math.log(bytes) / Math.log(1024)); 14 | const output = bytes / Math.pow(1024, i); 15 | const byteSize = sizes[i]; 16 | 17 | if (i === 1) return `${Math.round(output)} ${byteSize}`; 18 | return `${Math.floor(output * 100) / 100} ${byteSize}`; 19 | } 20 | -------------------------------------------------------------------------------- /packages/cli/src/util.tile.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs'; 2 | 3 | import { Tiff, TiffMimeType } from '@cogeotiff/core'; 4 | import { log } from '@linzjs/tracing'; 5 | 6 | const FileExtension: Record = { 7 | [TiffMimeType.Jpeg]: 'jpeg', 8 | [TiffMimeType.Jp2000]: 'jp2', 9 | [TiffMimeType.Webp]: 'webp', 10 | [TiffMimeType.Lzw]: 'lzw', 11 | [TiffMimeType.Deflate]: 'deflate', 12 | [TiffMimeType.None]: 'bin', 13 | [TiffMimeType.JpegXl]: 'jpeg', 14 | [TiffMimeType.Zstd]: 'zstd', 15 | [TiffMimeType.Lerc]: 'lerc', 16 | [TiffMimeType.Lzma]: 'lzma', 17 | }; 18 | 19 | /** 20 | * Get a human readable tile name 21 | * 22 | * @param mimeType image type of tile @see FileExtension 23 | * @param index Image index 24 | * @param x Tile X 25 | * @param y Tile Y 26 | * 27 | * @returns tile name eg `001_002_12.png` 28 | */ 29 | export function getTileName(mimeType: string, index: number, x: number, y: number): string { 30 | const xS = `${x}`.padStart(3, '0'); 31 | const yS = `${y}`.padStart(3, '0'); 32 | const fileExt = FileExtension[mimeType] ?? 'unknown'; 33 | return `${xS}_${yS}_${index}.${fileExt}`; 34 | } 35 | 36 | export async function writeTile( 37 | tiff: Tiff, 38 | x: number, 39 | y: number, 40 | index: number, 41 | outputPath: URL, 42 | logger: typeof log, 43 | ): Promise { 44 | const tile = await tiff.images[index].getTile(x, y); 45 | if (tile == null) { 46 | logger.debug('Tile:Empty', { source: tiff.source.url.href, index, x, y }); 47 | return null; 48 | } 49 | const fileName = getTileName(tile.mimeType, index, x, y); 50 | await fs.writeFile(new URL(fileName, outputPath), Buffer.from(tile.bytes)); 51 | logger.debug('Tile:Write', { source: tiff.source.url.href, index, x, y, fileName, bytes: tile.bytes.byteLength }); 52 | return fileName; 53 | } 54 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./build", 6 | "lib": ["es2018", "dom"] 7 | }, 8 | "include": ["src"], 9 | "references": [{ "path": "../core" }] 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/.npmignore: -------------------------------------------------------------------------------- 1 | data 2 | tsconfig.tsbuildinfo 3 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [9.0.3](https://github.com/blacha/cogeotiff/compare/core-v9.0.2...core-v9.0.3) (2024-01-08) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * **core:** correctly get image width/height ([3697ade](https://github.com/blacha/cogeotiff/commit/3697aded0267f133bc273f9d80d2fa53485cf2f3)) 9 | * **core:** load more projection information ([57dd0a9](https://github.com/blacha/cogeotiff/commit/57dd0a9443231a1f2bb8be1be66e811467840d1a)) 10 | 11 | ## [9.0.2](https://github.com/blacha/cogeotiff/compare/core-v9.0.1...core-v9.0.2) (2023-12-15) 12 | 13 | 14 | ### Bug Fixes 15 | 16 | * **core:** do not read past the end of the offset arrays ([8699bc3](https://github.com/blacha/cogeotiff/commit/8699bc332360895cbc26f4a124d3de22eaea48f2)) 17 | 18 | ## [9.0.1](https://github.com/blacha/cogeotiff/compare/core-v9.0.0...core-v9.0.1) (2023-12-13) 19 | 20 | 21 | ### Bug Fixes 22 | 23 | * **core:** do not read past the end of a buffer ([c810ada](https://github.com/blacha/cogeotiff/commit/c810adacd9a508858a28d85f75afa620ec94b355)) 24 | 25 | ## [9.0.0](https://github.com/blacha/cogeotiff/compare/core-v8.1.1...core-v9.0.0) (2023-12-11) 26 | 27 | 28 | ### ⚠ BREAKING CHANGES 29 | 30 | * rename all type from CogTiff to just Tiff ([#1227](https://github.com/blacha/cogeotiff/issues/1227)) 31 | * modify structure of tiff tags ([#1225](https://github.com/blacha/cogeotiff/issues/1225)) 32 | 33 | ### Features 34 | 35 | * color more output and add more tags ([fe4088b](https://github.com/blacha/cogeotiff/commit/fe4088b3f1f88a1248d803c29a563872aab4205c)) 36 | * export all tag value constants ([#1229](https://github.com/blacha/cogeotiff/issues/1229)) ([44757e5](https://github.com/blacha/cogeotiff/commit/44757e5ba5c98e992bb9fd72eb9993c727648b74)) 37 | * expose default read size so it can be easily overridden ([5786246](https://github.com/blacha/cogeotiff/commit/57862469229503c95ee274b555fc75d828b58529)) 38 | * expose gdal's NO_DATA as a getter on the image ([#1230](https://github.com/blacha/cogeotiff/issues/1230)) ([fc21a30](https://github.com/blacha/cogeotiff/commit/fc21a30d6754f37923b92ee4fe26c557ff6d9378)) 39 | * force some tags to always be arrays ([#1228](https://github.com/blacha/cogeotiff/issues/1228)) ([acc8f93](https://github.com/blacha/cogeotiff/commit/acc8f93eac6f311bdb9d0a6e97e28e2457867c91)) 40 | * modify structure of tiff tags ([#1225](https://github.com/blacha/cogeotiff/issues/1225)) ([049e0bc](https://github.com/blacha/cogeotiff/commit/049e0bc3c4e15f8c095a3da4442ef144d372cf60)) 41 | * rename all type from CogTiff to just Tiff ([#1227](https://github.com/blacha/cogeotiff/issues/1227)) ([872263b](https://github.com/blacha/cogeotiff/commit/872263b11f1ab06853cb872de54a9d9dd745b647)) 42 | * Tag SampleFormat should also be a array ([4216ddd](https://github.com/blacha/cogeotiff/commit/4216dddc1601bf44a1e604ff78e515f90ccdbdfa)) 43 | 44 | 45 | ### Bug Fixes 46 | 47 | * allow unknown compression types to be read ([9247a70](https://github.com/blacha/cogeotiff/commit/9247a709d6f049785614fa41b79bbadf2061a07e)) 48 | 49 | ## [8.1.1](https://github.com/blacha/cogeotiff/compare/core-v8.1.0...core-v8.1.1) (2023-11-14) 50 | 51 | 52 | ### Bug Fixes 53 | 54 | * **core:** correct loading of sub array geotags ([#1214](https://github.com/blacha/cogeotiff/issues/1214)) ([a67ec0a](https://github.com/blacha/cogeotiff/commit/a67ec0a0ca77313fdfb298ea72c532f496562d68)) 55 | * **core:** expose CogTiffImage ([aca2c58](https://github.com/blacha/cogeotiff/commit/aca2c58f2c6ad0ccf95310eedd7402d50b9e77bd)) 56 | 57 | ## [8.1.0](https://github.com/blacha/cogeotiff/compare/core-v8.0.2...core-v8.1.0) (2023-08-23) 58 | 59 | 60 | ### Features 61 | 62 | * **cli:** fetch all tiff tags with --fetch-tags ([#1155](https://github.com/blacha/cogeotiff/issues/1155)) ([4067751](https://github.com/blacha/cogeotiff/commit/406775184eed18ab10ae2816ecbedea9706b20f5)) 63 | 64 | 65 | ### Bug Fixes 66 | 67 | * **core:** do not read past the end of a file ([#1152](https://github.com/blacha/cogeotiff/issues/1152)) ([fd0be56](https://github.com/blacha/cogeotiff/commit/fd0be56eee6944239502cd8ffd7a6fe89e76b984)) 68 | 69 | ## [8.0.2](https://github.com/blacha/cogeotiff/compare/core-v8.0.1...core-v8.0.2) (2023-08-05) 70 | 71 | 72 | ### Bug Fixes 73 | 74 | * **core:** expose TiffTag and TiffTagGeo ([d538bdc](https://github.com/blacha/cogeotiff/commit/d538bdc833bf76ba8d730a1062156916715585b4)) 75 | 76 | ## [8.0.1](https://github.com/blacha/cogeotiff/compare/core-v8.0.0...core-v8.0.1) (2023-08-05) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * broken changelog ordering ([31f8c8a](https://github.com/blacha/cogeotiff/commit/31f8c8ac5e2770427ed2dc0f5c7c34330c6cb0eb)) 82 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # @cogeotiff/core 2 | 3 | Working with [Cloud optimized GEOTiff](https://www.cogeo.org/) and Tiffs 4 | 5 | - Completely javascript based, works in the browser and nodejs 6 | - Lazy load Tiffs images and metadata 7 | - Supports huge 100GB+ COG 8 | - Uses GDAL COG optimizations, generally only one or two reads per tile! 9 | - Loads Tiffs from URL, File, Google Cloud or AWS S3 10 | - Used in production for [LINZ's Basemaps](https://github.com/linz/basemaps) with billions of tiles fetched from Tiffs! 11 | 12 | ## Usage 13 | 14 | Load a COG from a remote http source 15 | 16 | ```typescript 17 | import { SourceHttp } from '@chunkd/source-url'; 18 | import { Tiff } from '@cogeotiff/core' 19 | 20 | const source = new SourceHttp('https://example.com/cog.tif'); 21 | const tiff = await Tiff.create(source); 22 | 23 | /** Load a specific tile from a specific image */ 24 | const tile = await tiff.images[5].getTile(2, 2); 25 | 26 | /** Load the 5th image in the Tiff */ 27 | const img = tiff.images[5]; 28 | if (img.isTiled()) { 29 | /** Load tile x:10 y:10 */ 30 | const tile = await img.getTile(10, 10); 31 | tile.mimeType; // image/jpeg 32 | tile.bytes; // Raw image buffer 33 | } 34 | 35 | /** Get the origin point of the tiff */ 36 | const origin = img.origin; 37 | /** Bounding box of the tiff */ 38 | const bbox = img.bbox; 39 | 40 | // Tiff tags can be accessed via some helpers 41 | const noData = img.noData; // -9999 42 | const noDataTag = img.tags.get(TiffTag.GdalNoData) // Tag information such as file offset or tagId 43 | const noDataValue = img.value(TiffTag.GdalNoData) // "-9999" (tag is stored as a string) 44 | ``` 45 | 46 | ### Tags 47 | 48 | Tags are somewhat typesafe for most common use cases in typescript, 49 | 50 | ```typescript 51 | img.value(TiffTag.ImageWidth) // number 52 | img.value(TiffTag.BitsPerSample) // number[] 53 | ``` 54 | 55 | Some tags have exported constants to make them easier to work with 56 | 57 | ```typescript 58 | import {Photometric} from '@cogeotiff/core' 59 | 60 | 61 | const photometric = img.value(TiffTag.Photometric) 62 | 63 | if( photometric == Photometric.Rgb) { 64 | // Tiff is a RGB photometric tiff 65 | } 66 | ``` 67 | 68 | For a full list of constants see [./src/index.ts](./src/index.ts) 69 | 70 | GeoTiff tags are loaded into a separate location 71 | 72 | ```typescript 73 | import {RasterTypeKey} from '@cogeotiff/core' 74 | 75 | const pixelIsArea = img.valueGeo(TiffTagGeo.GTRasterTypeGeoKey) == RasterTypeKey.PixelIsArea 76 | ``` 77 | 78 | ### Examples 79 | 80 | More examples can bee seen 81 | 82 | - [@cogeotiff/example](https://github.com/blacha/cogeotiff/tree/master/packages/examples) 83 | - [CogViewer](https://github.com/blacha/cogeotiff-web) 84 | - [@chunkd](https://github.com/blacha/chunkd) Additional sources eg file:// and s3:// -------------------------------------------------------------------------------- /packages/core/data/DEM_BS28_2016_1000_1141.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacha/cogeotiff/35a01121a9e076690bba25d778832dcbb86b97f9/packages/core/data/DEM_BS28_2016_1000_1141.tif -------------------------------------------------------------------------------- /packages/core/data/README.md: -------------------------------------------------------------------------------- 1 | # Testing cogs taken from 2 | 3 | - https://github.com/mapbox/COGDumper 4 | - `sparse.tiff` Contains data sourced from [LINZ](https://linz.govt.nz) licensed for reuse under CC BY 4.0 5 | - `DEM_BS28_2016_1000_1141.tif` Contains data sourced from [LINZ](https://linz.govt.nz) licensed for reuse under CC BY 4.0 6 | -------------------------------------------------------------------------------- /packages/core/data/be_cog.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacha/cogeotiff/35a01121a9e076690bba25d778832dcbb86b97f9/packages/core/data/be_cog.tiff -------------------------------------------------------------------------------- /packages/core/data/big_cog.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacha/cogeotiff/35a01121a9e076690bba25d778832dcbb86b97f9/packages/core/data/big_cog.tiff -------------------------------------------------------------------------------- /packages/core/data/cog.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacha/cogeotiff/35a01121a9e076690bba25d778832dcbb86b97f9/packages/core/data/cog.tiff -------------------------------------------------------------------------------- /packages/core/data/east_coast_phase3_2023_AY31_1000_3335.tif.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacha/cogeotiff/35a01121a9e076690bba25d778832dcbb86b97f9/packages/core/data/east_coast_phase3_2023_AY31_1000_3335.tif.gz -------------------------------------------------------------------------------- /packages/core/data/rgba8_strip.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacha/cogeotiff/35a01121a9e076690bba25d778832dcbb86b97f9/packages/core/data/rgba8_strip.tiff -------------------------------------------------------------------------------- /packages/core/data/rgba8_tiled.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacha/cogeotiff/35a01121a9e076690bba25d778832dcbb86b97f9/packages/core/data/rgba8_tiled.tiff -------------------------------------------------------------------------------- /packages/core/data/sparse.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blacha/cogeotiff/35a01121a9e076690bba25d778832dcbb86b97f9/packages/core/data/sparse.tiff -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cogeotiff/core", 3 | "version": "9.0.3", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/blacha/cogeotiff.git", 7 | "directory": "packages/core" 8 | }, 9 | "type": "module", 10 | "engines": { 11 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 12 | }, 13 | "main": "./build/index.js", 14 | "types": "./build/index.d.ts", 15 | "author": "Blayne Chard", 16 | "license": "MIT", 17 | "scripts": { 18 | "test": "node --test" 19 | }, 20 | "dependencies": {}, 21 | "devDependencies": { 22 | "@types/node": "^20.0.0" 23 | }, 24 | "publishConfig": { 25 | "access": "public" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/__benchmark__/cog.read.benchmark.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs/promises'; 2 | 3 | import { TiffTag } from '../index.js'; 4 | import { Tiff } from '../tiff.js'; 5 | import { SourceMemory } from './source.memory.js'; 6 | 7 | // console.log = console.trace; 8 | /** Read a tile from every image inside of a tiff 300 tiles read */ 9 | async function main(): Promise { 10 | const buf = await readFile(process.argv[process.argv.length - 1]); 11 | const source = new SourceMemory(buf); 12 | for (let i = 0; i < 5_000; i++) { 13 | performance.mark('tiff:init'); 14 | const tiff = new Tiff(source); 15 | await tiff.init(); 16 | performance.mark('tiff:init:done'); 17 | 18 | // 6 images 19 | for (const img of tiff.images) await img.getTile(0, 0); 20 | 21 | // Force loading all the byte arrays in which benchmarks the bulk array loading 22 | await tiff.images[0].fetch(TiffTag.TileByteCounts); 23 | await tiff.images[0].fetch(TiffTag.TileOffsets); 24 | } 25 | } 26 | 27 | void main(); 28 | -------------------------------------------------------------------------------- /packages/core/src/__benchmark__/source.file.ts: -------------------------------------------------------------------------------- 1 | import { readFile, stat } from 'node:fs/promises'; 2 | import { promisify } from 'node:util'; 3 | import { gunzip } from 'node:zlib'; 4 | 5 | import { Source } from '../source.js'; 6 | 7 | const gunzipP = promisify(gunzip); 8 | 9 | export class TestFileSource implements Source { 10 | url: URL; 11 | data: Promise; 12 | 13 | constructor(fileName: URL) { 14 | this.url = fileName; 15 | this.data = readFile(this.url).then((buf) => { 16 | if (this.url.pathname.endsWith('gz')) return gunzipP(buf); 17 | return buf; 18 | }); 19 | } 20 | 21 | async fetch(offset: number, length: number): Promise { 22 | const fileData = await this.data; 23 | return fileData.buffer.slice(fileData.byteOffset + offset, fileData.byteOffset + offset + length); 24 | } 25 | 26 | get size(): Promise { 27 | return Promise.resolve() 28 | .then(() => stat(this.url)) 29 | .then((f) => f.size); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/__benchmark__/source.memory.ts: -------------------------------------------------------------------------------- 1 | import { Source } from '../source.js'; 2 | 3 | export class SourceMemory implements Source { 4 | url: URL; 5 | data: ArrayBuffer; 6 | metadata: { size: number }; 7 | 8 | static toArrayBuffer(buf: Buffer | Uint8Array | ArrayBuffer): ArrayBuffer { 9 | if (buf instanceof ArrayBuffer) return buf; 10 | if (buf.byteLength === buf.buffer.byteLength) return buf.buffer; 11 | return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); 12 | } 13 | 14 | constructor(bytes: Buffer | Uint8Array | ArrayBuffer) { 15 | this.url = new URL('memory://fake-file'); 16 | this.data = SourceMemory.toArrayBuffer(bytes); 17 | this.metadata = { size: this.data.byteLength }; 18 | } 19 | 20 | async fetch(offset: number, length?: number): Promise { 21 | // console.log('Fetch', offset, length); 22 | if (offset < 0) offset = this.data.byteLength + offset; 23 | if (offset > this.data.byteLength) { 24 | throw new Error(`Read offset outside bounds ${offset}-${length}`); 25 | } 26 | 27 | if (length && offset + length > this.data.byteLength) { 28 | throw new Error(`Read length outside bounds ${offset}-${length}`); 29 | } 30 | 31 | return this.data.slice(offset, length == null ? undefined : offset + length); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/core/src/__test__/cog.image.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { beforeEach, describe, it } from 'node:test'; 3 | 4 | import { promises as fs } from 'fs'; 5 | 6 | import { TestFileSource } from '../__benchmark__/source.file.js'; 7 | import { SourceMemory } from '../__benchmark__/source.memory.js'; 8 | import { TiffMimeType } from '../const/tiff.mime.js'; 9 | import { Photometric, TiffTag } from '../const/tiff.tag.id.js'; 10 | import { Tiff } from '../tiff.js'; 11 | import { ByteSize } from '../util/bytes.js'; 12 | 13 | // 900913 properties. 14 | const A = 6378137.0; 15 | const InitialResolution = (2 * Math.PI * A) / 256; 16 | 17 | function getResolution(zoom: number): number { 18 | return InitialResolution / 2 ** zoom; 19 | } 20 | 21 | describe('TiffTiled', () => { 22 | const cogSourceFile = new TestFileSource(new URL('../../data/rgba8_tiled.tiff', import.meta.url)); 23 | const cog = new Tiff(cogSourceFile); 24 | 25 | beforeEach(() => cog.init()); 26 | 27 | it('should match resolutions to web mercator zoom levels', () => { 28 | for (let i = 0; i < 14; i++) { 29 | assert.equal(cog.getImageByResolution(getResolution(i)).id, 4); 30 | } 31 | 32 | assert.equal(cog.getImageByResolution(getResolution(14)).id, 3); 33 | assert.equal(cog.getImageByResolution(getResolution(15)).id, 2); 34 | assert.equal(cog.getImageByResolution(getResolution(16)).id, 1); 35 | assert.equal(cog.getImageByResolution(getResolution(17)).id, 0); 36 | assert.equal(cog.getImageByResolution(getResolution(18)).id, 0); 37 | 38 | for (let i = 19; i < 32; i++) { 39 | assert.equal(cog.getImageByResolution(getResolution(i)).id, 0); 40 | } 41 | }); 42 | 43 | it('should get origin from all images', () => { 44 | const baseOrigin = cog.images[0].origin; 45 | for (const img of cog.images) { 46 | assert.deepEqual(img.origin, baseOrigin); 47 | } 48 | }); 49 | 50 | it('should get bounding box from all images', () => { 51 | const baseOrigin = cog.images[0].bbox; 52 | for (const img of cog.images) { 53 | assert.deepEqual(img.bbox, baseOrigin); 54 | } 55 | }); 56 | 57 | it('should be geolocated', () => { 58 | for (const img of cog.images) assert.equal(img.isGeoLocated, true); 59 | }); 60 | 61 | it('should scale image resolution for all images', () => { 62 | const [resX, resY, resZ] = cog.images[0].resolution; 63 | for (let i = 0; i < cog.images.length; i++) { 64 | const img = cog.images[i]; 65 | const scale = 2 ** i; // This tiff is scaled at a factor of two per zoom level 66 | assert.deepEqual(img.resolution, [resX * scale, resY * scale, resZ]); 67 | } 68 | }); 69 | 70 | it('should have tile information', () => { 71 | const [firstImage] = cog.images; 72 | assert.equal(firstImage.stripCount, 0); 73 | assert.equal(firstImage.isTiled(), true); 74 | }); 75 | 76 | it('should hasTile for every tile', async () => { 77 | const [firstImage] = cog.images; 78 | 79 | for (let x = 0; x < firstImage.tileCount.x; x++) { 80 | for (let y = 0; y < firstImage.tileCount.y; y++) { 81 | assert.equal(await firstImage.hasTile(x, y), true); 82 | } 83 | } 84 | }); 85 | }); 86 | 87 | describe('Cog.Big', () => { 88 | it('should support reading from memory', async () => { 89 | const fullSource = new TestFileSource(new URL('../../data/sparse.tiff', import.meta.url)); 90 | 91 | const cog = new Tiff(fullSource); 92 | await cog.init(); 93 | 94 | const [firstImage] = cog.images; 95 | assert.equal(firstImage.stripCount, 0); 96 | assert.equal(firstImage.isTiled(), true); 97 | assert.equal(firstImage.epsg, 2193); 98 | 99 | const img = cog.images[4]; 100 | 101 | assert.deepEqual(img.tileCount, { x: 2, y: 2 }); 102 | }); 103 | 104 | it('should read using a memory source', async () => { 105 | const bytes = await fs.readFile(new URL('../../data/sparse.tiff', import.meta.url)); 106 | const source = new SourceMemory(bytes.buffer); 107 | const cog = new Tiff(source); 108 | await cog.init(); 109 | 110 | const [firstImage] = cog.images; 111 | assert.equal(firstImage.stripCount, 0); 112 | assert.equal(firstImage.isTiled(), true); 113 | 114 | const img = cog.images[4]; 115 | assert.deepEqual(img.tileCount, { x: 2, y: 2 }); 116 | }); 117 | }); 118 | 119 | describe('Cog.Sparse', () => { 120 | const cogSourceFile = new TestFileSource(new URL('../../data/sparse.tiff', import.meta.url)); 121 | const cog = new Tiff(cogSourceFile); 122 | 123 | it('should read metadata', async () => { 124 | await cog.init(); 125 | assert.equal(cog.images[0].epsg, 2193); 126 | }); 127 | 128 | it('should be geolocated', () => { 129 | for (const img of cog.images) assert.equal(img.isGeoLocated, true); 130 | }); 131 | 132 | it('should support sparse cogs', async () => { 133 | const z = 4; 134 | const img = cog.images[z]; 135 | 136 | const { tileCount } = img; 137 | assert.deepEqual(tileCount, { x: 2, y: 2 }); 138 | 139 | assert.equal(img.value(TiffTag.SamplesPerPixel), 4); // 4 bands 140 | assert.deepEqual(img.value(TiffTag.BitsPerSample), [8, 8, 8, 8]); 141 | assert.equal(img.value(TiffTag.Photometric), Photometric.Rgb); 142 | assert.equal(img.value(TiffTag.GdalNoData), null); 143 | 144 | for (let x = 0; x < tileCount.x; x++) { 145 | for (let y = 0; y < tileCount.y; y++) { 146 | const hasTile = await img.hasTile(x, y); 147 | assert.equal(hasTile, false); 148 | const tileXy = await img.getTile(x, y); 149 | const tileXyz = await cog.images[z].getTile(x, y); 150 | assert.equal(tileXy, null, `Tile x:${x} y:${y} should be empty`); 151 | assert.equal(tileXyz, null, `Tile x:${x} y:${y} z: ${z} should be empty`); 152 | } 153 | } 154 | }); 155 | 156 | it('should have ghost options', () => { 157 | assert.equal(cog.options?.options.size, 6); 158 | assert.equal(cog.options?.tileLeaderByteSize, ByteSize.UInt32); 159 | assert.equal(cog.options?.isCogOptimized, true); 160 | 161 | const entries = [...(cog.options?.options.entries() ?? [])]; 162 | assert.deepEqual(entries, [ 163 | ['GDAL_STRUCTURAL_METADATA_SIZE', '000140 bytes'], 164 | ['LAYOUT', 'IFDS_BEFORE_DATA'], 165 | ['BLOCK_ORDER', 'ROW_MAJOR'], 166 | ['BLOCK_LEADER', 'SIZE_AS_UINT4'], 167 | ['BLOCK_TRAILER', 'LAST_4_BYTES_REPEATED'], 168 | ['KNOWN_INCOMPATIBLE_EDITION', 'NO'], 169 | ]); 170 | }); 171 | }); 172 | 173 | describe('CogStrip', () => { 174 | const cogSourceFile = new TestFileSource(new URL('../../data/rgba8_strip.tiff', import.meta.url)); 175 | const cog = new Tiff(cogSourceFile); 176 | 177 | beforeEach(() => cog.init()); 178 | 179 | it('should get origin from all images', () => { 180 | const baseOrigin = cog.images[0].origin; 181 | for (const img of cog.images) { 182 | assert.deepEqual(img.origin, baseOrigin); 183 | } 184 | }); 185 | 186 | it('should get bounding box from all images', () => { 187 | const baseOrigin = cog.images[0].bbox; 188 | for (const img of cog.images) { 189 | assert.deepEqual(img.bbox, baseOrigin); 190 | } 191 | }); 192 | 193 | it('should scale image resolution for all images', () => { 194 | const [resX, resY, resZ] = cog.images[0].resolution; 195 | for (let i = 0; i < cog.images.length; i++) { 196 | const img = cog.images[i]; 197 | const scale = 2 ** i; // This tiff is scaled at a factor of two per zoom level 198 | assert.deepEqual(img.resolution, [resX * scale, resY * scale, resZ]); 199 | } 200 | }); 201 | 202 | it('should have strip information', async () => { 203 | const [firstImage] = cog.images; 204 | assert.equal(firstImage.epsg, 3857); 205 | assert.equal(firstImage.isTiled(), false); 206 | assert.equal(firstImage.stripCount, 2); 207 | 208 | const stripA = await firstImage.getStrip(0); 209 | assert.equal(stripA?.mimeType, TiffMimeType.Webp); 210 | assert.equal(stripA?.bytes.byteLength, 152); 211 | 212 | const stripB = await firstImage.getStrip(1); 213 | assert.equal(stripB?.mimeType, TiffMimeType.Webp); 214 | assert.equal(stripB?.bytes.byteLength, 152); 215 | }); 216 | }); 217 | -------------------------------------------------------------------------------- /packages/core/src/__test__/cog.read.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { readFile } from 'node:fs/promises'; 3 | import { describe, it } from 'node:test'; 4 | 5 | import { TestFileSource } from '../__benchmark__/source.file.js'; 6 | import { SourceMemory } from '../__benchmark__/source.memory.js'; 7 | import { TiffMimeType } from '../const/tiff.mime.js'; 8 | import { Photometric, SampleFormat } from '../const/tiff.tag.id.js'; 9 | import { TiffVersion } from '../const/tiff.version.js'; 10 | import { TiffTag, TiffTagGeo } from '../index.js'; 11 | import { Tiff } from '../tiff.js'; 12 | 13 | function validate(tif: Tiff): void { 14 | assert.equal(tif.images.length, 5); 15 | 16 | const [firstTif] = tif.images; 17 | assert.equal(firstTif.isTiled(), true); 18 | assert.deepEqual(firstTif.tileSize, { width: 256, height: 256 }); 19 | assert.deepEqual(firstTif.size, { width: 64, height: 64 }); 20 | } 21 | 22 | describe('CogRead', () => { 23 | // TODO this does not load 100% yet 24 | // it('should read big endian', async () => { 25 | // const source = new TestFileSource(new URL('../../data/big_cog.tiff', import.meta.url)); 26 | // const tiff = new Tiff(source); 27 | 28 | // await tiff.init(); 29 | 30 | // assert.equal(tiff.isLittleEndian, false); 31 | // assert.equal(tiff.version, TiffVersion.BigTiff); 32 | // validate(tiff); 33 | // }); 34 | 35 | it('should read big tiff', async () => { 36 | const source = new TestFileSource(new URL('../../data/big_cog.tiff', import.meta.url)); 37 | const tiff = new Tiff(source); 38 | 39 | await tiff.init(); 40 | 41 | assert.equal(tiff.isLittleEndian, true); 42 | assert.equal(tiff.version, TiffVersion.BigTiff); 43 | assert.equal(tiff.images[0].epsg, null); 44 | validate(tiff); 45 | }); 46 | 47 | it('should read tiff', async () => { 48 | const source = new TestFileSource(new URL('../../data/cog.tiff', import.meta.url)); 49 | const tiff = new Tiff(source); 50 | 51 | await tiff.init(); 52 | 53 | assert.equal(tiff.isLittleEndian, true); 54 | assert.equal(tiff.version, TiffVersion.Tiff); 55 | assert.equal(tiff.images[0].epsg, null); 56 | 57 | validate(tiff); 58 | 59 | const [firstTif] = tiff.images; 60 | assert.equal(firstTif.compression, TiffMimeType.Jpeg); 61 | }); 62 | 63 | it('should allow multiple init', async () => { 64 | const source = new TestFileSource(new URL('../../data/cog.tiff', import.meta.url)); 65 | const tiff = new Tiff(source); 66 | 67 | assert.equal(tiff.isInitialized, false); 68 | await tiff.init(); 69 | assert.equal(tiff.isInitialized, true); 70 | assert.equal(tiff.images.length, 5); 71 | 72 | assert.equal(tiff.isInitialized, true); 73 | await tiff.init(); 74 | assert.equal(tiff.images.length, 5); 75 | }); 76 | 77 | it('should read ifds from anywhere in the file', async () => { 78 | const source = new TestFileSource(new URL('../../data/DEM_BS28_2016_1000_1141.tif', import.meta.url)); 79 | const tiff = await Tiff.create(source); 80 | 81 | assert.equal(tiff.images.length, 1); 82 | const im = tiff.images[0]; 83 | 84 | assert.equal(im.isGeoTagsLoaded, true); 85 | assert.equal(im.epsg, 2193); 86 | assert.equal(im.compression, TiffMimeType.None); 87 | assert.equal(im.isTiled(), false); 88 | 89 | // 32 bit float DEM 90 | assert.deepEqual(im.value(TiffTag.BitsPerSample), [32]); 91 | assert.deepEqual(im.value(TiffTag.SampleFormat), [SampleFormat.Float]); 92 | assert.equal(im.value(TiffTag.Photometric), Photometric.MinIsBlack); 93 | 94 | assert.equal(im.value(TiffTag.GdalNoData), '-9999'); 95 | assert.equal(im.noData, -9999); 96 | 97 | assert.equal(im.valueGeo(TiffTagGeo.GTCitationGeoKey), 'NZGD2000 / New Zealand Transverse Mercator 2000'); 98 | assert.equal(im.valueGeo(TiffTagGeo.GeodeticCitationGeoKey), 'NZGD2000'); 99 | assert.deepEqual(await im.fetch(TiffTag.StripByteCounts), [8064, 8064, 8064, 8064, 8064, 8064, 8064, 5040]); 100 | }); 101 | 102 | it('should read sub array ifds', async () => { 103 | const source = new TestFileSource( 104 | new URL('../../data/east_coast_phase3_2023_AY31_1000_3335.tif.gz', import.meta.url), 105 | ); 106 | const tiff = await Tiff.create(source); 107 | 108 | assert.equal(tiff.images.length, 5); 109 | const im = tiff.images[0]; 110 | 111 | assert.equal(im.isGeoTagsLoaded, true); 112 | assert.equal(im.epsg, 2193); 113 | assert.equal(im.compression, TiffMimeType.Lzw); 114 | assert.deepEqual(im.size, { width: 9600, height: 14400 }); 115 | assert.deepEqual(im.value(TiffTag.BitsPerSample), [8, 8, 8, 8]); 116 | 117 | const geoTags = [...im.tagsGeo.keys()].map((key) => TiffTagGeo[key]); 118 | assert.deepEqual(geoTags, [ 119 | 'GTModelTypeGeoKey', 120 | 'GTRasterTypeGeoKey', 121 | 'GTCitationGeoKey', 122 | 'GeodeticCRSGeoKey', 123 | 'GeogAngularUnitsGeoKey', 124 | 'EllipsoidGeoKey', 125 | 'EllipsoidSemiMajorAxisGeoKey', 126 | 'EllipsoidSemiMinorAxisGeoKey', 127 | 'EllipsoidInvFlatteningGeoKey', 128 | 'GeogTOWGS84GeoKey', 129 | 'ProjectedCRSGeoKey', 130 | 'ProjectedCitationGeoKey', 131 | 'ProjLinearUnitsGeoKey', 132 | ]); 133 | 134 | assert.deepEqual(im.valueGeo(TiffTagGeo.GeogTOWGS84GeoKey), [0, 0, 0, 0, 0, 0, 0]); 135 | }); 136 | 137 | it('should allow invalid compression', async () => { 138 | const source = new TestFileSource(new URL('../../data/cog.tiff', import.meta.url)); 139 | const tiff = await Tiff.create(source); 140 | 141 | // Overwrite the loaded compression type to a invalid value 142 | tiff.images[0].tags.get(TiffTag.Compression)!.value = -1; 143 | 144 | const tile = await tiff.images[0].getTile(0, 0); 145 | assert.deepEqual(tile?.mimeType, 'application/octet-stream'); 146 | }); 147 | 148 | it('should load small tiffs', async () => { 149 | const cogSourceFile = new URL('../../data/rgba8_tiled.tiff', import.meta.url); 150 | 151 | const buf = await readFile(cogSourceFile); 152 | const source = new SourceMemory(buf); 153 | 154 | const tiff = await Tiff.create(source); 155 | assert.equal(tiff.images.length, 5); 156 | assert.equal(tiff.images[0].epsg, 3857); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /packages/core/src/__test__/example.ts: -------------------------------------------------------------------------------- 1 | import { SourceHttp } from '@chunkd/source-http'; 2 | 3 | import { Tiff } from '../index.js'; 4 | 5 | async function main(): Promise { 6 | const source = new SourceHttp('https://example.com/cog.tif'); 7 | const tiff = await Tiff.create(source); 8 | 9 | /** Load a specific tile from a specific image */ 10 | const tile = await tiff.images[5].getTile(2, 2); 11 | if (tile != null) { 12 | tile.bytes; // Raw image buffer or null if tile doesn't exist 13 | } 14 | 15 | /** Load the 5th image in the Tiff */ 16 | const img = tiff.images[5]; 17 | if (img.isTiled()) { 18 | /** Load tile x:10 y:10 */ 19 | const tile = await img.getTile(10, 10); 20 | if (tile != null) { 21 | tile.mimeType; // image/jpeg 22 | tile.bytes; // Raw image buffer 23 | } 24 | } 25 | 26 | /** Get the origin point of the tiff */ 27 | img.origin; 28 | /** Bounding box of the tiff */ 29 | img.bbox; 30 | } 31 | 32 | void main(); 33 | -------------------------------------------------------------------------------- /packages/core/src/const/index.ts: -------------------------------------------------------------------------------- 1 | export { TiffEndian } from './tiff.endian.js'; 2 | export { TiffCompressionMimeType as TiffCompression, TiffMimeType } from './tiff.mime.js'; 3 | export { TiffTag as TiffTag, TiffTagGeo as TiffTagGeo } from './tiff.tag.id.js'; 4 | export { TiffTagValueType } from './tiff.tag.value.js'; 5 | export { TiffVersion } from './tiff.version.js'; 6 | -------------------------------------------------------------------------------- /packages/core/src/const/tiff.endian.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tiff format 3 | * 4 | * The header of a Tiff file contains the endianness of the file 5 | */ 6 | export enum TiffEndian { 7 | Big = 0x4d4d, 8 | Little = 0x4949, 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/const/tiff.mime.ts: -------------------------------------------------------------------------------- 1 | import { Compression } from './tiff.tag.id.js'; 2 | 3 | /** 4 | * MimeType conversion for common tif image compresson types 5 | */ 6 | export enum TiffMimeType { 7 | None = 'application/octet-stream', 8 | Jpeg = 'image/jpeg', 9 | Jp2000 = 'image/jp2', 10 | JpegXl = 'image/jpegxl', 11 | Webp = 'image/webp', 12 | Zstd = 'application/zstd', 13 | Lzw = 'application/lzw', 14 | Deflate = 'application/deflate', 15 | Lerc = 'application/lerc', 16 | Lzma = 'application/x-lzma', 17 | } 18 | 19 | export const TiffCompressionMimeType: Record = { 20 | [Compression.None]: TiffMimeType.None, 21 | [Compression.Lzw]: TiffMimeType.Lzw, 22 | [Compression.Jpeg6]: TiffMimeType.Jpeg, 23 | [Compression.Jpeg]: TiffMimeType.Jpeg, 24 | [Compression.DeflateOther]: TiffMimeType.Deflate, 25 | [Compression.Deflate]: TiffMimeType.Deflate, 26 | [Compression.Lerc]: TiffMimeType.Lerc, 27 | [Compression.Lzma]: TiffMimeType.Lzma, 28 | [Compression.Jp2000]: TiffMimeType.Jp2000, 29 | [Compression.Zstd]: TiffMimeType.Zstd, 30 | [Compression.Webp]: TiffMimeType.Webp, 31 | [Compression.JpegXl]: TiffMimeType.JpegXl, 32 | }; 33 | 34 | /** 35 | * Lookup the related mimetype for a compression id 36 | * 37 | * @param c Compression id 38 | * @returns mime type for compression 39 | */ 40 | export function getCompressionMimeType(c: Compression | null): TiffMimeType | null { 41 | if (c == null) return null; 42 | return TiffCompressionMimeType[c]; 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/src/const/tiff.tag.id.ts: -------------------------------------------------------------------------------- 1 | /** Sub file type for tag 254 {@link TiffTag.SubFileType} */ 2 | export enum SubFileType { 3 | /** Reduced resolution version */ 4 | ReducedImage = 1, 5 | /** One page of many */ 6 | Page = 2, 7 | /** Transparency mask */ 8 | Mask = 4, 9 | } 10 | 11 | export enum Orientation { 12 | /* row 0 top, col 0 lhs */ 13 | TopLeft = 1, 14 | /* row 0 top, col 0 rhs */ 15 | TopRight = 2, 16 | /* row 0 bottom, col 0 rhs */ 17 | BottomRight = 3, 18 | /* row 0 Bottom, col 0 lhs */ 19 | BottomLeft = 4, 20 | /* row 0 lhs, col 0 Top */ 21 | LeftTop = 5, 22 | /* row 0 rhs, col 0 Top */ 23 | RightTOP = 6, 24 | /* row 0 rhs, col 0 Bottom */ 25 | RightBottom = 7, 26 | /* row 0 lhs, col 0 Bottom */ 27 | LeftBottom = 8, 28 | } 29 | 30 | export enum RasterTypeKey { 31 | /** 32 | * PixelIsArea (default) a pixel is treated as an area, 33 | * the raster coordinate (0,0) is the top left corner of the top left pixel. 34 | */ 35 | PixelIsArea = 1, 36 | 37 | /** 38 | * PixelIsPoint treats pixels as point samples with empty space between the "pixel" samples. 39 | * the raster coordinate (0,0) is the location of the top left raster pixel. 40 | */ 41 | PixelIsPoint = 2, 42 | } 43 | 44 | export enum ModelTypeCode { 45 | Unknown = 0, 46 | /** Projection Coordinate System */ 47 | Projected = 1, 48 | /** Geographic latitude-longitude System */ 49 | Geographic = 2, 50 | /** Geocentric (X,Y,Z) Coordinate System */ 51 | Geocentric = 3, 52 | 53 | UserDefined = 32767, 54 | } 55 | 56 | /** Sub file type for tag 255 {@link TiffTag.OldSubFileType} */ 57 | export enum OldSubFileType { 58 | /** Full resolution image data */ 59 | Image = 1, 60 | /** Reduced resolution version */ 61 | ReducedImage = 2, 62 | /** One page of many */ 63 | Page = 3, 64 | } 65 | 66 | /** Tiff compression types */ 67 | export enum Compression { 68 | None = 1, 69 | Lzw = 5, 70 | Jpeg6 = 6, 71 | Jpeg = 7, 72 | DeflateOther = 8, 73 | Deflate = 32946, 74 | Jp2000 = 3417, 75 | Lerc = 34887, 76 | Lzma = 34925, 77 | Zstd = 50000, 78 | Webp = 50001, 79 | JpegXl = 50002, 80 | } 81 | 82 | export enum PlanarConfiguration { 83 | /** single image plane */ 84 | Contig = 1, 85 | /** separate planes of data */ 86 | Separate = 2, 87 | } 88 | 89 | export enum SampleFormat { 90 | /** Unsigned integer data */ 91 | Uint = 1, 92 | /** Signed integer data */ 93 | Int = 2, 94 | /** IEEE floating point data */ 95 | Float = 3, 96 | /** Untyped data */ 97 | Void = 4, 98 | /** Complex signed int */ 99 | ComplexInt = 5, 100 | /** Complex ieee floating */ 101 | ComplexFloat = 6, 102 | } 103 | 104 | export enum Photometric { 105 | /** min value is white */ 106 | MinIsWhite = 0, 107 | /** min value is black */ 108 | MinIsBlack = 1, 109 | /** RGB color model */ 110 | Rgb = 2, 111 | /** color map indexed */ 112 | Palette = 3, 113 | /** $holdout mask */ 114 | Mask = 4, 115 | /** !color separations */ 116 | Separated = 5, 117 | /** !CCIR 601 */ 118 | Ycbcr = 6, 119 | /** !1976 CIE L*a*b* */ 120 | Cielab = 8, 121 | /** ICC L*a*b* [Adobe TIFF Technote 4] */ 122 | Icclab = 9, 123 | /** ITU L*a*b* */ 124 | Itulab = 10, 125 | /** color filter array */ 126 | Cfa = 32803, 127 | /** CIE Log2(L) */ 128 | Logl = 32844, 129 | Logluv = 32845, 130 | } 131 | 132 | /** 133 | * Tiff tags as defined by libtiff and libgeotiff 134 | * 135 | * - {@link https://gitlab.com/libtiff/libtiff} 136 | * - {@link https://github.com/OSGeo/libgeotiff/} 137 | */ 138 | export enum TiffTag { 139 | /** 140 | * Type of the sub file 141 | * 142 | * @see {@link SubFileType} 143 | */ 144 | SubFileType = 254, 145 | 146 | /** 147 | * Type of sub file 148 | * 149 | * @see {@link OldSubFileType} 150 | */ 151 | OldSubFileType = 255, 152 | 153 | /** Width of image in pixels */ 154 | ImageWidth = 256, 155 | 156 | /** Height of image in pixels */ 157 | ImageHeight = 257, 158 | 159 | /** 160 | * Number of bits per channel 161 | * 162 | * @example 163 | * ```typescript 164 | * [8,8,8] // 8 bit RGB 165 | * [16] // 16bit 166 | * ``` 167 | */ 168 | BitsPerSample = 258, 169 | 170 | /** 171 | * 172 | * Data type of the image 173 | * 174 | * See {@link SampleFormat} 175 | * 176 | * @example 177 | * ```typescript 178 | * [1] // SampleFormat.Uint 179 | * [1,1,1,1] // 4 band Uint 180 | * ``` 181 | */ 182 | SampleFormat = 339, 183 | 184 | /** 185 | * Compression Type 186 | * 187 | * @see {@link Compression} 188 | * 189 | * @example 190 | * ```typescript 191 | * 5 // Compression.Lzw 192 | * ``` 193 | */ 194 | Compression = 259, 195 | 196 | /** 197 | * Photometric interpretation 198 | * 199 | * @see {@link Photometric} 200 | * 201 | * @example 202 | * ```typescript 203 | * 2 // Photometric.Rgb 204 | * ``` 205 | */ 206 | Photometric = 262, 207 | 208 | /** Tile width in pixels */ 209 | TileWidth = 322, 210 | /** Tile height in pixels */ 211 | TileHeight = 323, 212 | 213 | /** 214 | * Offsets to data tiles 215 | * `0` means the tile has no data (sparse tiff) 216 | * 217 | * @example 218 | * ```typescript 219 | * [0, 3200, 1406] // three tiles, first tile does not exist 220 | * ``` 221 | */ 222 | TileOffsets = 324, 223 | /** 224 | * Byte counts for tiles 225 | * `0 means the tile does not exist (sparse tiff) 226 | * 227 | * @example 228 | * ```typescript 229 | * [0, 3200, 1406] // three tiles, first tile does not exist 230 | * ``` 231 | **/ 232 | TileByteCounts = 325, 233 | 234 | /** JPEG table stream */ 235 | JpegTables = 347, 236 | 237 | StripOffsets = 273, 238 | StripByteCounts = 279, 239 | 240 | // GDAL 241 | /** 242 | * GDAL metadata 243 | * Generally a xml document with lots of information about the tiff and how it was created 244 | */ 245 | GdalMetadata = 42112, 246 | 247 | /** 248 | * No data value encoded as a string 249 | * 250 | * @example "-9999" 251 | */ 252 | GdalNoData = 42113, 253 | 254 | /** GeoTiff Tags */ 255 | 256 | /** 257 | * Pixel scale in meters 258 | * in the format [scaleX, scaleY, scaleZ] 259 | * 260 | * Requires {@link ModelTiePoint} to be set and {@link ModelTransformation} not to be set 261 | * 262 | * @example 263 | * ```typescript 264 | * [100.0, 100.0, 0.0] 265 | * ``` 266 | */ 267 | ModelPixelScale = 33550, 268 | /** 269 | * Position of the tiff 270 | * 271 | * contains a list of tie points that contain 272 | * [x,y,z] of position in the in the tiff, generally [0,0,0] 273 | * [x,y,z] of the position in the projected 274 | * 275 | * @example 276 | * Mapping tiff point `[0,0,0]` to projected coordinates `[350807.4, 5316081.3, 0.0]` 277 | * ``` 278 | * [0, 0, 0, 350807.4, 5316081.3, 0.0] 279 | * ``` 280 | */ 281 | ModelTiePoint = 33922, 282 | 283 | /** 284 | * Exact affine transformation between the tiff and the projected location 285 | * 286 | * this tag should not be defined when {@link ModelTiePoint} or {@link ModelPixelScale} are used 287 | * 288 | * @example 289 | *```typescript 290 | * [ 0, 100.0, 0, 400000.0, 291 | * 100.0, 0, 0, 500000.0, 292 | * 0, 0, 0, 0, 293 | * 0, 0, 0, 1] 294 | * ``` 295 | */ 296 | ModelTransformation = 34744, 297 | /** 298 | * List of GeoTiff tags 299 | * {@link TiffTagGeo} 300 | * 301 | * {@link https://docs.ogc.org/is/19-008r4/19-008r4.html#_requirements_class_geokeydirectorytag} 302 | */ 303 | GeoKeyDirectory = 34735, 304 | /** 305 | * Double Parameters for GeoTiff Tags 306 | * 307 | * {@link TiffTagGeo} 308 | */ 309 | GeoDoubleParams = 34736, 310 | /** 311 | * Ascii Parameters for GeoTiff Tags 312 | * 313 | * {@link TiffTagGeo} 314 | */ 315 | GeoAsciiParams = 34737, 316 | 317 | /** 318 | * Stores the LERC version and additional compression 319 | * 320 | * @example 321 | * ```typescript 322 | * [4, 0] // version 4, no extra compression 323 | * ``` 324 | */ 325 | LercParameters = 50674, 326 | 327 | PlanarConfiguration = 284, 328 | 329 | /** Untyped values */ 330 | CellLength = 265, 331 | CellWidth = 264, 332 | ColorMap = 320, 333 | Copyright = 33432, 334 | DateTime = 306, 335 | ExtraSamples = 338, 336 | FillOrder = 266, 337 | FreeByteCounts = 289, 338 | FreeOffsets = 288, 339 | GrayResponseCurve = 291, 340 | GrayResponseUnit = 290, 341 | HostComputer = 316, 342 | ImageDescription = 270, 343 | Make = 271, 344 | MaxSampleValue = 281, 345 | MinSampleValue = 280, 346 | Model = 272, 347 | Orientation = 274, 348 | ResolutionUnit = 296, 349 | RowsPerStrip = 278, 350 | SamplesPerPixel = 277, 351 | Software = 305, 352 | 353 | Threshholding = 263, 354 | XResolution = 282, 355 | YResolution = 283, 356 | BadFaxLines = 326, 357 | CleanFaxData = 327, 358 | ClipPath = 343, 359 | ConsecutiveBadFaxLines = 328, 360 | Decode = 433, 361 | DefaultImageColor = 434, 362 | DocumentName = 269, 363 | DotRange = 336, 364 | HalftoneHints = 321, 365 | Indexed = 346, 366 | PageName = 285, 367 | PageNumber = 297, 368 | Predictor = 317, 369 | PrimaryChromaticities = 319, 370 | ReferenceBlackWhite = 532, 371 | SMinSampleValue = 340, 372 | SMaxSampleValue = 341, 373 | StripRowCounts = 559, 374 | SubIFDs = 330, 375 | T4Options = 292, 376 | T6Options = 293, 377 | 378 | TransferFunction = 301, 379 | WhitePoint = 318, 380 | XClipPathUnits = 344, 381 | XPosition = 286, 382 | YCbCrCoefficients = 529, 383 | YCbCrPositioning = 531, 384 | YCbCrSubSampling = 530, 385 | YClipPathUnits = 345, 386 | YPosition = 287, 387 | ApertureValue = 37378, 388 | ColorSpace = 40961, 389 | DateTimeDigitized = 36868, 390 | DateTimeOriginal = 36867, 391 | ExifIFD = 34665, 392 | ExifVersion = 36864, 393 | ExposureTime = 33434, 394 | FileSource = 41728, 395 | Flash = 37385, 396 | FlashpixVersion = 40960, 397 | FNumber = 33437, 398 | ImageUniqueID = 42016, 399 | LightSource = 37384, 400 | MakerNote = 37500, 401 | ShutterSpeedValue = 37377, 402 | UserComment = 37510, 403 | IPTC = 33723, 404 | ICCProfile = 34675, 405 | XMP = 700, 406 | } 407 | 408 | /** Define the expected types for all the tiff tags */ 409 | export interface TiffTagType { 410 | [TiffTag.ImageHeight]: number; 411 | [TiffTag.ImageWidth]: number; 412 | [TiffTag.SubFileType]: SubFileType; 413 | [TiffTag.BitsPerSample]: number[]; 414 | [TiffTag.Compression]: Compression; 415 | [TiffTag.OldSubFileType]: OldSubFileType; 416 | [TiffTag.Photometric]: Photometric; 417 | 418 | [TiffTag.TileWidth]: number; 419 | [TiffTag.TileHeight]: number; 420 | [TiffTag.TileOffsets]: number[]; 421 | [TiffTag.TileByteCounts]: number[]; 422 | [TiffTag.JpegTables]: number[]; 423 | 424 | [TiffTag.StripByteCounts]: number[]; 425 | [TiffTag.StripOffsets]: number[]; 426 | 427 | [TiffTag.SampleFormat]: SampleFormat[]; 428 | [TiffTag.GdalMetadata]: string; 429 | [TiffTag.GdalNoData]: string; 430 | [TiffTag.ModelPixelScale]: number[]; 431 | [TiffTag.ModelTiePoint]: number[]; 432 | [TiffTag.ModelTransformation]: number[]; 433 | [TiffTag.GeoKeyDirectory]: number[]; 434 | [TiffTag.GeoDoubleParams]: number[]; 435 | [TiffTag.GeoAsciiParams]: string; 436 | 437 | [TiffTag.PlanarConfiguration]: PlanarConfiguration; 438 | [TiffTag.Orientation]: Orientation; 439 | 440 | [TiffTag.LercParameters]: number[]; 441 | 442 | // Untyped values 443 | 444 | [TiffTag.CellLength]: unknown; 445 | [TiffTag.CellWidth]: unknown; 446 | [TiffTag.ColorMap]: unknown; 447 | [TiffTag.Copyright]: unknown; 448 | [TiffTag.DateTime]: unknown; 449 | [TiffTag.ExtraSamples]: unknown; 450 | [TiffTag.FillOrder]: unknown; 451 | [TiffTag.FreeByteCounts]: unknown; 452 | [TiffTag.FreeOffsets]: unknown; 453 | [TiffTag.GrayResponseCurve]: unknown; 454 | [TiffTag.GrayResponseUnit]: unknown; 455 | [TiffTag.HostComputer]: unknown; 456 | [TiffTag.ImageDescription]: unknown; 457 | [TiffTag.Make]: unknown; 458 | [TiffTag.MaxSampleValue]: unknown; 459 | [TiffTag.MinSampleValue]: unknown; 460 | [TiffTag.Model]: unknown; 461 | [TiffTag.ResolutionUnit]: unknown; 462 | [TiffTag.RowsPerStrip]: unknown; 463 | [TiffTag.SamplesPerPixel]: unknown; 464 | [TiffTag.Software]: unknown; 465 | 466 | [TiffTag.Threshholding]: unknown; 467 | [TiffTag.XResolution]: unknown; 468 | [TiffTag.YResolution]: unknown; 469 | [TiffTag.BadFaxLines]: unknown; 470 | [TiffTag.CleanFaxData]: unknown; 471 | [TiffTag.ClipPath]: unknown; 472 | [TiffTag.ConsecutiveBadFaxLines]: unknown; 473 | [TiffTag.Decode]: unknown; 474 | [TiffTag.DefaultImageColor]: unknown; 475 | [TiffTag.DocumentName]: unknown; 476 | [TiffTag.DotRange]: unknown; 477 | [TiffTag.HalftoneHints]: unknown; 478 | [TiffTag.Indexed]: unknown; 479 | [TiffTag.PageName]: unknown; 480 | [TiffTag.PageNumber]: unknown; 481 | [TiffTag.Predictor]: unknown; 482 | [TiffTag.PrimaryChromaticities]: unknown; 483 | [TiffTag.ReferenceBlackWhite]: unknown; 484 | [TiffTag.SMinSampleValue]: unknown; 485 | [TiffTag.SMaxSampleValue]: unknown; 486 | [TiffTag.StripRowCounts]: unknown; 487 | [TiffTag.SubIFDs]: unknown; 488 | [TiffTag.T4Options]: unknown; 489 | [TiffTag.T6Options]: unknown; 490 | 491 | [TiffTag.TransferFunction]: unknown; 492 | [TiffTag.WhitePoint]: unknown; 493 | [TiffTag.XClipPathUnits]: unknown; 494 | [TiffTag.XPosition]: unknown; 495 | [TiffTag.YCbCrCoefficients]: unknown; 496 | [TiffTag.YCbCrPositioning]: unknown; 497 | [TiffTag.YCbCrSubSampling]: unknown; 498 | [TiffTag.YClipPathUnits]: unknown; 499 | [TiffTag.YPosition]: unknown; 500 | [TiffTag.ApertureValue]: unknown; 501 | [TiffTag.ColorSpace]: unknown; 502 | [TiffTag.DateTimeDigitized]: unknown; 503 | [TiffTag.DateTimeOriginal]: unknown; 504 | [TiffTag.ExifIFD]: unknown; 505 | [TiffTag.ExifVersion]: unknown; 506 | [TiffTag.ExposureTime]: unknown; 507 | [TiffTag.FileSource]: unknown; 508 | [TiffTag.Flash]: unknown; 509 | [TiffTag.FlashpixVersion]: unknown; 510 | [TiffTag.FNumber]: unknown; 511 | [TiffTag.ImageUniqueID]: unknown; 512 | [TiffTag.LightSource]: unknown; 513 | [TiffTag.MakerNote]: unknown; 514 | [TiffTag.ShutterSpeedValue]: unknown; 515 | [TiffTag.UserComment]: unknown; 516 | [TiffTag.IPTC]: unknown; 517 | [TiffTag.ICCProfile]: unknown; 518 | [TiffTag.XMP]: unknown; 519 | } 520 | 521 | /** 522 | * Geotiff tags as defined by OGC GeoTiff 1.1 523 | * 524 | * {@link https://docs.ogc.org/is/19-008r4/19-008r4.html#_summary_of_geokey_ids_and_names} 525 | */ 526 | export enum TiffTagGeo { 527 | // GeoTIFF Configuration Keys 528 | 529 | /** 530 | * This GeoKey defines the type of Model coordinate reference system used, to which the transformation from the raster space is made: 531 | * 532 | * {@link https://docs.ogc.org/is/19-008r4/19-008r4.html#_requirements_class_gtmodeltypegeokey} 533 | * 534 | * {@link ModelTypeCode} 535 | */ 536 | GTModelTypeGeoKey = 1024, 537 | /** 538 | * There are currently only two options: `RasterPixelIsPoint` and `RasterPixelIsArea` 539 | * 540 | * {@link https://docs.ogc.org/is/19-008r4/19-008r4.html#_requirements_class_gtrastertypegeokey} 541 | * 542 | * {@link RasterTypeKey} 543 | */ 544 | GTRasterTypeGeoKey = 1025, 545 | /** 546 | * ASCII reference to published documentation on the overall configuration of the GeoTIFF file. 547 | * 548 | * @example "NZGD2000 / New Zealand Transverse Mercator 2000" 549 | */ 550 | GTCitationGeoKey = 1026, 551 | 552 | // Geodetic CRS Parameter Keys 553 | /** 554 | * Renamed from GeographicTypeGeoKey in OGC GeoTiff 555 | */ 556 | GeodeticCRSGeoKey = 2048, 557 | /** 558 | * Renamed from GeogCitationGeoKey in OGC GeoTiff 559 | * 560 | * @example "NZTM" 561 | */ 562 | GeodeticCitationGeoKey = 2049, 563 | /** 564 | * Renamed from GeogGeodeticDatumGeoKey in OGC GeoTiff 565 | */ 566 | GeodeticDatumGeoKey = 2050, 567 | /** 568 | * Renamed from "GeogPrimeMeridianGeoKey" in OGC GeoTiff 569 | */ 570 | PrimeMeridianGeoKey = 2051, 571 | /** 572 | * Linear unit of measure 573 | * @example 9001 // Metre 574 | */ 575 | GeogLinearUnitsGeoKey = 2052, 576 | GeogLinearUnitSizeGeoKey = 2053, 577 | /** 578 | * Angular unit of measure 579 | * 580 | * @example 9102 // Degree 581 | */ 582 | GeogAngularUnitsGeoKey = 2054, 583 | GeogAngularUnitSizeGeoKey = 2055, 584 | /** 585 | * Renamed from "GeogEllipsoidGeoKey" in OGC GeoTiff 586 | */ 587 | EllipsoidGeoKey = 2056, 588 | /** 589 | * Renamed from "GeogSemiMajorAxisGeoKey" in OGC GeoTiff 590 | */ 591 | EllipsoidSemiMajorAxisGeoKey = 2057, 592 | /** 593 | * Renamed from "GeogSemiMinorAxisGeoKey" in OGC GeoTiff 594 | */ 595 | EllipsoidSemiMinorAxisGeoKey = 2058, 596 | /** 597 | * Renamed from "GeogInvFlatteningGeoKey" in OGC GeoTiff 598 | */ 599 | EllipsoidInvFlatteningGeoKey = 2059, 600 | /** 601 | * Renamed from "GeogPrimeMeridianLongGeoKey" in OGC GeoTiff 602 | */ 603 | PrimeMeridianLongitudeGeoKey = 2061, 604 | 605 | GeogTOWGS84GeoKey = 2062, 606 | 607 | // Projected CRS Parameter Keys 608 | GeogAzimuthUnitsGeoKey = 2060, 609 | 610 | /** 611 | * EPSG code of the tiff 612 | * 613 | * Renamed from "ProjectedCSTypeGeoKey" in OGC GeoTiff 614 | * 615 | * @example 616 | * ```typescript 617 | * 2193 // NZTM 618 | * 3857 // WebMercatorQuad 619 | * ``` 620 | */ 621 | ProjectedCRSGeoKey = 3072, 622 | /** 623 | * ASCII reference to published documentation on the Projected Coordinate System 624 | * 625 | * Renamed from "PCSCitationGeoKey" in OGC GeoTiff 626 | * 627 | * @example "UTM Zone 60 N with WGS 84" 628 | */ 629 | ProjectedCitationGeoKey = 3073, 630 | 631 | /** 632 | * Specifies a map projection from the GeoTIFF CRS register or to indicate that the map projection is user-defined. 633 | * 634 | * {@link https://docs.ogc.org/is/19-008r4/19-008r4.html#_map_projection_geokeys} 635 | * 636 | * @example 2193 637 | */ 638 | ProjectionGeoKey = 3074, 639 | ProjMethodGeoKey = 3075, 640 | ProjLinearUnitsGeoKey = 3076, 641 | ProjLinearUnitSizeGeoKey = 3077, 642 | ProjStdParallel1GeoKey = 3078, 643 | ProjStdParallel2GeoKey = 3079, 644 | ProjNatOriginLongGeoKey = 3080, 645 | ProjNatOriginLatGeoKey = 3081, 646 | ProjFalseEastingGeoKey = 3082, 647 | ProjFalseNorthingGeoKey = 3083, 648 | ProjFalseOriginLongGeoKey = 3084, 649 | ProjFalseOriginLatGeoKey = 3085, 650 | ProjFalseOriginEastingGeoKey = 3086, 651 | ProjFalseOriginNorthingGeoKey = 3087, 652 | ProjCenterLongGeoKey = 3088, 653 | ProjCenterLatGeoKey = 3089, 654 | ProjCenterEastingGeoKey = 3090, 655 | ProjCenterNorthingGeoKey = 3091, 656 | ProjScaleAtNatOriginGeoKey = 3092, 657 | ProjScaleAtCenterGeoKey = 3093, 658 | ProjAzimuthAngleGeoKey = 3094, 659 | ProjStraightVertPoleLongGeoKey = 3095, 660 | ProjRectifiedGridAngleGeoKey = 3096, 661 | 662 | // Vertical CRS Parameter Keys (4096-5119) 663 | 664 | /** 665 | * This key is provided to specify the vertical coordinate reference system from the GeoTIFF CRS register or to indicate that the CRS is a user-defined vertical coordinate reference system. The value for VerticalGeoKey should follow the 666 | * 667 | * {@link https://docs.ogc.org/is/19-008r4/19-008r4.html#_requirements_class_verticalgeokey} 668 | * 669 | * @example 4979 670 | */ 671 | VerticalGeoKey = 4096, 672 | /** 673 | * 674 | * @example "Geographic 3D WGS 84, Ellipsoidal height" 675 | */ 676 | VerticalCitationGeoKey = 4097, 677 | /** 678 | * vertical datum for a user-defined vertical coordinate reference system. 679 | */ 680 | VerticalDatumGeoKey = 4098, 681 | /** 682 | * Linear Unit for vertical CRS 683 | * 684 | * @example 9001 685 | */ 686 | VerticalUnitsGeoKey = 4099, 687 | } 688 | 689 | /** 690 | * Define the types for all the geo tiff tags 691 | * 692 | * {@link https://docs.ogc.org/is/19-008r4/19-008r4.html#_summary_of_geokey_ids_and_names} 693 | */ 694 | export interface TiffTagGeoType { 695 | // GeoTIFF Configuration Keys 696 | [TiffTagGeo.GTModelTypeGeoKey]: ModelTypeCode; 697 | [TiffTagGeo.GTRasterTypeGeoKey]: RasterTypeKey; 698 | [TiffTagGeo.GTCitationGeoKey]: string; 699 | 700 | // Geodetic CRS Parameter Keys 701 | [TiffTagGeo.GeodeticCRSGeoKey]: number; 702 | [TiffTagGeo.GeodeticCitationGeoKey]: string; 703 | [TiffTagGeo.GeodeticDatumGeoKey]: number; 704 | [TiffTagGeo.PrimeMeridianGeoKey]: number; 705 | [TiffTagGeo.GeogLinearUnitsGeoKey]: number; 706 | [TiffTagGeo.GeogLinearUnitSizeGeoKey]: number; 707 | [TiffTagGeo.GeogAngularUnitsGeoKey]: number; 708 | [TiffTagGeo.GeogAngularUnitSizeGeoKey]: number; 709 | [TiffTagGeo.EllipsoidGeoKey]: number; 710 | [TiffTagGeo.EllipsoidSemiMajorAxisGeoKey]: number; 711 | [TiffTagGeo.EllipsoidSemiMinorAxisGeoKey]: number; 712 | [TiffTagGeo.EllipsoidInvFlatteningGeoKey]: number; 713 | [TiffTagGeo.GeogAzimuthUnitsGeoKey]: number; 714 | [TiffTagGeo.PrimeMeridianLongitudeGeoKey]: number; 715 | [TiffTagGeo.GeogTOWGS84GeoKey]: number | number[]; 716 | 717 | // Projected CRS Parameter Keys 718 | [TiffTagGeo.ProjectedCRSGeoKey]: number; 719 | [TiffTagGeo.ProjectedCitationGeoKey]: string; 720 | [TiffTagGeo.ProjectionGeoKey]: number; 721 | [TiffTagGeo.ProjMethodGeoKey]: number; 722 | [TiffTagGeo.ProjLinearUnitsGeoKey]: number; 723 | [TiffTagGeo.ProjLinearUnitSizeGeoKey]: number; 724 | [TiffTagGeo.ProjStdParallel1GeoKey]: number; 725 | [TiffTagGeo.ProjStdParallel2GeoKey]: number; 726 | [TiffTagGeo.ProjNatOriginLongGeoKey]: number; 727 | [TiffTagGeo.ProjNatOriginLatGeoKey]: number; 728 | [TiffTagGeo.ProjFalseEastingGeoKey]: number; 729 | [TiffTagGeo.ProjFalseNorthingGeoKey]: number; 730 | [TiffTagGeo.ProjFalseOriginLongGeoKey]: number; 731 | [TiffTagGeo.ProjFalseOriginLatGeoKey]: number; 732 | [TiffTagGeo.ProjFalseOriginEastingGeoKey]: number; 733 | [TiffTagGeo.ProjFalseOriginNorthingGeoKey]: number; 734 | [TiffTagGeo.ProjCenterLongGeoKey]: number; 735 | [TiffTagGeo.ProjCenterLatGeoKey]: number; 736 | [TiffTagGeo.ProjCenterEastingGeoKey]: number; 737 | [TiffTagGeo.ProjCenterNorthingGeoKey]: number; 738 | [TiffTagGeo.ProjScaleAtNatOriginGeoKey]: number; 739 | [TiffTagGeo.ProjScaleAtCenterGeoKey]: number; 740 | [TiffTagGeo.ProjAzimuthAngleGeoKey]: number; 741 | [TiffTagGeo.ProjStraightVertPoleLongGeoKey]: number; 742 | [TiffTagGeo.ProjRectifiedGridAngleGeoKey]: number; 743 | 744 | // Vertical CRS Parameter Keys 745 | [TiffTagGeo.VerticalGeoKey]: number; 746 | [TiffTagGeo.VerticalCitationGeoKey]: string; 747 | [TiffTagGeo.VerticalDatumGeoKey]: number; 748 | [TiffTagGeo.VerticalUnitsGeoKey]: number; 749 | } 750 | 751 | /** 752 | * EPSG Angular Units. exist between [9100, 9199] 753 | * 754 | * Taken from libegotiff 755 | */ 756 | export enum AngularUnit { 757 | Radian = 9101, 758 | Degree = 9102, 759 | ArcMinute = 9103, 760 | ArcDegree = 9104, 761 | Grad = 9105, 762 | Gon = 9106, 763 | Dms = 9107, 764 | } 765 | 766 | /** 767 | * ESPG Liner units exist between [9000, 9099] 768 | * 769 | * Taken from libegotiff 770 | */ 771 | export enum LinearUnit { 772 | Metre = 9001, 773 | Foot = 9002, 774 | FootUsSurvey = 9003, 775 | FootModifiedAmerican = 9004, 776 | FootClarke = 9005, 777 | FootIndian = 9006, 778 | Link = 9007, 779 | LinkBenoit = 9008, 780 | LinkSears = 9009, 781 | ChainBenoit = 9010, 782 | ChainSears = 9011, 783 | YardSears = 9012, 784 | YardIndian = 9013, 785 | Fathom = 9014, 786 | MileInternationalNautical = 9015, 787 | } 788 | 789 | /** 790 | * Convert tiff tag values when being read. 791 | */ 792 | export const TiffTagConvertArray: Partial> = { 793 | [TiffTag.TileByteCounts]: true, 794 | [TiffTag.TileOffsets]: true, 795 | [TiffTag.StripOffsets]: true, 796 | [TiffTag.StripByteCounts]: true, 797 | [TiffTag.BitsPerSample]: true, 798 | [TiffTag.SampleFormat]: true, 799 | [TiffTag.GeoKeyDirectory]: true, 800 | [TiffTag.GeoDoubleParams]: true, 801 | }; 802 | -------------------------------------------------------------------------------- /packages/core/src/const/tiff.tag.value.ts: -------------------------------------------------------------------------------- 1 | export enum TiffTagValueType { 2 | Uint8 = 1, 3 | Ascii = 2, 4 | Uint16 = 3, 5 | Uint32 = 4, 6 | Rational = 5, 7 | Int8 = 6, 8 | Undefined = 7, 9 | Int16 = 8, 10 | Int32 = 9, 11 | SignedRational = 10, 12 | Float32 = 11, 13 | Float64 = 12, 14 | // BigTiff 15 | Uint64 = 16, 16 | Int64 = 17, 17 | Ifd8 = 18, 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/src/const/tiff.version.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tif version number that is stored at the start of a tif file 3 | */ 4 | export enum TiffVersion { 5 | /** 6 | * Big tif's, 7 | * generally uses 64bit numbers for offsets 8 | * @see http://bigtiff.org/ 9 | **/ 10 | BigTiff = 43, 11 | /** 12 | * Original tif 13 | * Uses 32 bit or smaller numbers for offsets and counters 14 | */ 15 | Tiff = 42, 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export { TiffEndian } from './const/tiff.endian.js'; 2 | export { TiffCompressionMimeType as TiffCompression, TiffMimeType } from './const/tiff.mime.js'; 3 | export { TiffTag, TiffTagGeo, TiffTagGeoType, TiffTagType } from './const/tiff.tag.id.js'; 4 | export { TiffTagValueType } from './const/tiff.tag.value.js'; 5 | export { TiffVersion } from './const/tiff.version.js'; 6 | export { Tag, TagInline, TagLazy, TagOffset } from './read/tiff.tag.js'; 7 | export { getTiffTagSize } from './read/tiff.value.reader.js'; 8 | export { Source } from './source.js'; 9 | export { TiffImage } from './tiff.image.js'; 10 | export { Tiff } from './tiff.js'; 11 | export { toHex } from './util/util.hex.js'; 12 | export type { BoundingBox, Point, Size, Vector } from './vector.js'; 13 | 14 | // Tag value constants 15 | export { 16 | AngularUnit, 17 | Compression, 18 | LinearUnit, 19 | ModelTypeCode, 20 | OldSubFileType, 21 | Orientation, 22 | Photometric, 23 | PlanarConfiguration, 24 | RasterTypeKey, 25 | SampleFormat, 26 | SubFileType, 27 | } from './const/tiff.tag.id.js'; 28 | -------------------------------------------------------------------------------- /packages/core/src/read/data.view.offset.ts: -------------------------------------------------------------------------------- 1 | /** Extension to DataView that includes the offset to where in a file the view is from */ 2 | export type DataViewOffset = DataView & { 3 | /** Offset in the source to where this data was read from */ 4 | sourceOffset: number; 5 | }; 6 | 7 | /** Convert the dataview to a dataview with a offset */ 8 | export function toDataViewOffset(d: DataView, offset: number): asserts d is DataViewOffset { 9 | (d as DataViewOffset).sourceOffset = offset; 10 | } 11 | 12 | /** 13 | * Does a DataviewOffset include the absolute bytes of the source file 14 | * 15 | * @param view DataViewOffset to check 16 | * @param targetOffset the absolute offset in the file 17 | * @param count number of bytes to include 18 | */ 19 | export function hasBytes(view: DataViewOffset, targetOffset: number, count: number): boolean { 20 | if (targetOffset < view.sourceOffset) return false; 21 | if (view.sourceOffset + view.byteLength < targetOffset + count) return false; 22 | return true; 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/src/read/tiff.gdal.ts: -------------------------------------------------------------------------------- 1 | import { ByteSize } from '../util/bytes.js'; 2 | 3 | export enum GhostOption { 4 | GdalStructuralMetadataSize = 'GDAL_STRUCTURAL_METADATA_SIZE', 5 | Layout = 'LAYOUT', 6 | BlockOrder = 'BLOCK_ORDER', 7 | BlockLeader = 'BLOCK_LEADER', 8 | BlockTrailer = 'BLOCK_TRAILER', 9 | KnownIncompatibleEdition = 'KNOWN_INCOMPATIBLE_EDITION', 10 | MaskInterleavedWithImagery = 'MASK_INTERLEAVED_WITH_IMAGERY', 11 | } 12 | 13 | export enum GhostOptionTileOrder { 14 | RowMajor = 'ROW_MAJOR', 15 | } 16 | 17 | export enum GhostOptionTileLeader { 18 | uint32 = 'SIZE_AS_UINT4', 19 | } 20 | 21 | /** 22 | * GDAL has made a ghost set of options for Tiff files 23 | * this class represents the optimizations that GDAL has applied 24 | */ 25 | export class TiffGhostOptions { 26 | options: Map = new Map(); 27 | 28 | /** 29 | * Has GDAL optimized this tiff 30 | */ 31 | get isCogOptimized(): boolean { 32 | if (this.isBroken) return false; 33 | return this.options.get(GhostOption.Layout) === 'IFDS_BEFORE_DATA'; 34 | } 35 | 36 | /** 37 | * Has GDAL determined this tiff is now broken 38 | */ 39 | get isBroken(): boolean { 40 | return this.options.get(GhostOption.KnownIncompatibleEdition) === 'YES'; 41 | } 42 | 43 | /** 44 | * Load the ghost options from a source 45 | * @param bytes the ghost header bytes 46 | */ 47 | process(bytes: DataView, offset: number, ghostSize: number): void { 48 | let key = ''; 49 | let value = ''; 50 | let setValue = false; 51 | for (let i = 0; i < ghostSize; i++) { 52 | const charCode = bytes.getUint8(offset + i); 53 | if (charCode === 0) break; 54 | 55 | const char = String.fromCharCode(charCode); 56 | if (char === '\n') { 57 | this.options.set(key.trim(), value.trim()); 58 | key = ''; 59 | value = ''; 60 | setValue = false; 61 | } else if (char === '=') { 62 | setValue = true; 63 | } else { 64 | if (setValue) value += char; 65 | else key += char; 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * If the tile leader is set, how many bytes are allocated to the tile size 72 | */ 73 | get tileLeaderByteSize(): ByteSize | null { 74 | switch (this.options.get(GhostOption.BlockLeader)) { 75 | case GhostOptionTileLeader.uint32: 76 | return ByteSize.UInt32; 77 | default: 78 | return null; 79 | } 80 | } 81 | 82 | get isMaskInterleaved(): boolean { 83 | return this.options.get(GhostOption.MaskInterleavedWithImagery) === 'YES'; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/core/src/read/tiff.ifd.config.ts: -------------------------------------------------------------------------------- 1 | import { TiffVersion } from '../const/tiff.version.js'; 2 | import { ByteSize } from '../util/bytes.js'; 3 | 4 | export const TagTiffConfig: TiffIfdConfig = { 5 | version: TiffVersion.Tiff, 6 | pointer: ByteSize.UInt32, 7 | offset: ByteSize.UInt16, 8 | /** 9 | * Each tag entry is specified as 10 | * UInt16:TagCode 11 | * UInt16:TagType 12 | * UInt32:TagCount 13 | * UInt32:Pointer To Value or value 14 | */ 15 | ifd: ByteSize.UInt16 + ByteSize.UInt16 + 2 * ByteSize.UInt32, 16 | }; 17 | 18 | export const TagTiffBigConfig: TiffIfdConfig = { 19 | version: TiffVersion.BigTiff, 20 | /** Size of most pointers */ 21 | pointer: ByteSize.UInt64, 22 | /** Size of offsets */ 23 | offset: ByteSize.UInt64, 24 | 25 | /** 26 | * Each tag entry is specified as 27 | * UInt16:TagCode 28 | * UInt16:TagType 29 | * UInt64:TagCount 30 | * UInt64:Pointer To Value or value 31 | */ 32 | ifd: ByteSize.UInt16 + ByteSize.UInt16 + 2 * ByteSize.UInt64, 33 | }; 34 | 35 | export interface TiffIfdConfig { 36 | /** Tiff type */ 37 | version: TiffVersion; 38 | /** Number of bytes a pointer uses */ 39 | pointer: number; 40 | /** Number of bytes a offset uses */ 41 | offset: number; 42 | /** Number of bytes the IFD tag contains */ 43 | ifd: number; 44 | } 45 | 46 | export const TiffIfdEntry = { 47 | [TiffVersion.BigTiff]: TagTiffBigConfig, 48 | [TiffVersion.Tiff]: TagTiffConfig, 49 | }; 50 | -------------------------------------------------------------------------------- /packages/core/src/read/tiff.tag.factory.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */ 2 | import { TiffTag, TiffTagConvertArray } from '../const/tiff.tag.id.js'; 3 | import { TiffTagValueType } from '../const/tiff.tag.value.js'; 4 | import { Tiff } from '../tiff.js'; 5 | import { getUint, getUint64 } from '../util/bytes.js'; 6 | import { DataViewOffset, hasBytes } from './data.view.offset.js'; 7 | import { Tag, TagLazy, TagOffset } from './tiff.tag.js'; 8 | import { getTiffTagSize } from './tiff.value.reader.js'; 9 | 10 | function readTagValue( 11 | fieldType: TiffTagValueType, 12 | bytes: DataView, 13 | offset: number, 14 | isLittleEndian: boolean, 15 | ): number | number[] | string | bigint { 16 | switch (fieldType) { 17 | case TiffTagValueType.Ascii: 18 | return String.fromCharCode(bytes.getUint8(offset)); 19 | 20 | case TiffTagValueType.Undefined: 21 | case TiffTagValueType.Uint8: 22 | return bytes.getUint8(offset); 23 | 24 | case TiffTagValueType.Int8: 25 | return bytes.getInt8(offset); 26 | 27 | case TiffTagValueType.Uint16: 28 | return bytes.getUint16(offset, isLittleEndian); 29 | 30 | case TiffTagValueType.Int16: 31 | return bytes.getInt16(offset, isLittleEndian); 32 | 33 | case TiffTagValueType.Uint32: 34 | return bytes.getUint32(offset, isLittleEndian); 35 | 36 | case TiffTagValueType.Int32: 37 | return bytes.getInt32(offset, isLittleEndian); 38 | 39 | case TiffTagValueType.Rational: 40 | return [bytes.getUint32(offset, isLittleEndian), bytes.getUint32(offset + 4, isLittleEndian)]; 41 | 42 | case TiffTagValueType.SignedRational: 43 | return [bytes.getInt32(offset, isLittleEndian), bytes.getInt32(offset + 4, isLittleEndian)]; 44 | 45 | case TiffTagValueType.Float64: 46 | return bytes.getFloat64(offset, isLittleEndian); 47 | 48 | case TiffTagValueType.Float32: 49 | return bytes.getFloat32(offset, isLittleEndian); 50 | 51 | case TiffTagValueType.Uint64: 52 | return getUint64(bytes, offset, isLittleEndian); 53 | default: 54 | throw new Error(`Unknown read type "${fieldType}" "${TiffTagValueType[fieldType]}"`); 55 | } 56 | } 57 | 58 | function readValue( 59 | tiff: Tiff, 60 | tagId: TiffTag | undefined, 61 | bytes: DataView, 62 | offset: number, 63 | type: TiffTagValueType, 64 | count: number, 65 | ): T { 66 | const typeSize = getTiffTagSize(type); 67 | const dataLength = count * typeSize; 68 | 69 | if (count === 1) { 70 | const val = readTagValue(type, bytes, offset, tiff.isLittleEndian) as unknown as T; 71 | // Force some single values to be arrays eg BitsPerSample 72 | // makes it easier to not check for number | number[] 73 | if (tagId && TiffTagConvertArray[tagId]) return [val] as T; 74 | return val; 75 | } 76 | 77 | switch (type) { 78 | case TiffTagValueType.Ascii: 79 | return String.fromCharCode.apply( 80 | null, 81 | new Uint8Array(bytes.buffer, offset, dataLength - 1) as unknown as number[], 82 | ) as unknown as T; 83 | } 84 | 85 | const output = []; 86 | for (let i = 0; i < dataLength; i += typeSize) { 87 | output.push(readTagValue(type, bytes, offset + i, tiff.isLittleEndian)); 88 | } 89 | 90 | return output as unknown as T; 91 | } 92 | 93 | /** 94 | * Determine if all the data for the tiff tag is loaded in and use that to create the specific CogTiffTag 95 | * 96 | * @see {@link Tag} 97 | * 98 | * @param tiff 99 | * @param view Bytes to read from 100 | * @param offset Offset in the dataview to read a tag 101 | */ 102 | export function createTag(tiff: Tiff, view: DataViewOffset, offset: number): Tag { 103 | const tagId = view.getUint16(offset + 0, tiff.isLittleEndian); 104 | 105 | const dataType = view.getUint16(offset + 2, tiff.isLittleEndian) as TiffTagValueType; 106 | const dataCount = getUint(view, offset + 4, tiff.ifdConfig.pointer, tiff.isLittleEndian); 107 | const dataTypeSize = getTiffTagSize(dataType); 108 | const dataLength = dataTypeSize * dataCount; 109 | 110 | // Tag value is inline read the value 111 | if (dataLength <= tiff.ifdConfig.pointer) { 112 | const value = readValue(tiff, tagId, view, offset + 4 + tiff.ifdConfig.pointer, dataType, dataCount); 113 | return { type: 'inline', id: tagId, name: TiffTag[tagId], count: dataCount, value, dataType, tagOffset: offset }; 114 | } 115 | 116 | const dataOffset = getUint(view, offset + 4 + tiff.ifdConfig.pointer, tiff.ifdConfig.pointer, tiff.isLittleEndian); 117 | 118 | switch (tagId) { 119 | case TiffTag.TileOffsets: 120 | case TiffTag.TileByteCounts: 121 | case TiffTag.StripByteCounts: 122 | case TiffTag.StripOffsets: 123 | const tag: TagOffset = { 124 | type: 'offset', 125 | id: tagId, 126 | name: TiffTag[tagId], 127 | count: dataCount, 128 | dataType, 129 | dataOffset, 130 | isLoaded: false, 131 | value: [], 132 | tagOffset: offset, 133 | }; 134 | // Some offsets are quite long and don't need to read them often, so only read the tags we are interested in when we need to 135 | if (tagId === TiffTag.TileOffsets && hasBytes(view, dataOffset, dataLength)) setBytes(tag, view); 136 | return tag; 137 | } 138 | 139 | // If we already have the bytes in the view read them in 140 | if (hasBytes(view, dataOffset, dataLength)) { 141 | const value = readValue(tiff, tagId, view, dataOffset - view.sourceOffset, dataType, dataCount); 142 | return { type: 'inline', id: tagId, name: TiffTag[tagId], count: dataCount, value, dataType, tagOffset: offset }; 143 | } 144 | 145 | return { type: 'lazy', id: tagId, name: TiffTag[tagId], count: dataCount, dataOffset, dataType, tagOffset: offset }; 146 | } 147 | 148 | /** Fetch the value from a {@link TagLazy} tag */ 149 | export async function fetchLazy(tag: TagLazy, tiff: Tiff): Promise { 150 | if (tag.value != null) return tag.value; 151 | const dataTypeSize = getTiffTagSize(tag.dataType); 152 | const dataLength = dataTypeSize * tag.count; 153 | const bytes = await tiff.source.fetch(tag.dataOffset, dataLength); 154 | const view = new DataView(bytes); 155 | tag.value = readValue(tiff, tag.id, view, 0, tag.dataType, tag.count); 156 | return tag.value as T; 157 | } 158 | 159 | /** 160 | * Fetch all the values from a {@link TagOffset} 161 | */ 162 | export async function fetchAllOffsets(tiff: Tiff, tag: TagOffset): Promise { 163 | const dataTypeSize = getTiffTagSize(tag.dataType); 164 | 165 | if (tag.view == null) { 166 | const bytes = await tiff.source.fetch(tag.dataOffset, dataTypeSize * tag.count); 167 | tag.view = new DataView(bytes) as DataViewOffset; 168 | tag.view.sourceOffset = tag.dataOffset; 169 | } 170 | 171 | tag.value = readValue(tiff, tag.id, tag.view, 0, tag.dataType, tag.count); 172 | tag.isLoaded = true; 173 | return tag.value; 174 | } 175 | 176 | export function setBytes(tag: TagOffset, view: DataViewOffset): void { 177 | const dataTypeSize = getTiffTagSize(tag.dataType); 178 | const startBytes = view.byteOffset + tag.dataOffset - view.sourceOffset; 179 | tag.view = new DataView(view.buffer.slice(startBytes, startBytes + dataTypeSize * tag.count)) as DataViewOffset; 180 | tag.view.sourceOffset = tag.dataOffset; 181 | } 182 | 183 | /** Partially fetch the values of a {@link TagOffset} and return the value for the offset */ 184 | export async function getValueAt(tiff: Tiff, tag: TagOffset, index: number): Promise { 185 | if (index > tag.count || index < 0) throw new Error('TagOffset: out of bounds :' + index); 186 | if (tag.value[index] != null) return tag.value[index]; 187 | const dataTypeSize = getTiffTagSize(tag.dataType); 188 | 189 | if (tag.view == null) { 190 | const bytes = await tiff.source.fetch(tag.dataOffset + index * dataTypeSize, dataTypeSize); 191 | const view = new DataView(bytes); 192 | // Skip type conversion to array by using undefined tiff tag id 193 | const value = readValue(tiff, undefined, view, 0, tag.dataType, 1); 194 | if (typeof value !== 'number') throw new Error('Value is not a number'); 195 | tag.value[index] = value; 196 | return value; 197 | } 198 | 199 | // Skip type conversion to array by using undefined tiff tag id 200 | const value = readValue(tiff, undefined, tag.view, index * dataTypeSize, tag.dataType, 1); 201 | if (typeof value !== 'number') throw new Error('Value is not a number'); 202 | tag.value[index] = value; 203 | return value; 204 | } 205 | -------------------------------------------------------------------------------- /packages/core/src/read/tiff.tag.ts: -------------------------------------------------------------------------------- 1 | import { TiffTag } from '../const/tiff.tag.id.js'; 2 | import { TiffTagValueType } from '../const/tiff.tag.value.js'; 3 | import { DataViewOffset } from './data.view.offset.js'; 4 | 5 | /** Tiff tag interfaces */ 6 | export type Tag = TagLazy | TagInline | TagOffset; 7 | 8 | export interface TagBase { 9 | /** Id of the Tag */ 10 | id: TiffTag; 11 | /** Name of the tiff tag */ 12 | name: string; 13 | /** Offset in bytes to where this tag was read from */ 14 | tagOffset: number; 15 | /** Number of values */ 16 | count: number; 17 | /** Tiff Tag Datatype @see {TiffTagValueType} */ 18 | dataType: TiffTagValueType; 19 | } 20 | 21 | /** Tiff tag value is not inline and will be loaded later when requested */ 22 | export interface TagLazy extends TagBase { 23 | type: 'lazy'; 24 | /** Value if loaded undefined otherwise */ 25 | value?: T; 26 | /** Where in the file the value is read from */ 27 | dataOffset: number; 28 | } 29 | 30 | /** Tiff tag that's value is inside the IFD and is already read */ 31 | export interface TagInline extends TagBase { 32 | type: 'inline'; 33 | /** Value of the tag */ 34 | value: T; 35 | } 36 | 37 | /** Tiff tag that is a list of offsets this can be partially read */ 38 | export interface TagOffset extends TagBase { 39 | type: 'offset'; 40 | /** Values of the offsets this is a sparse array unless @see {TagOffset.isLoaded} is true */ 41 | value: number[]; 42 | /** Have all the values been read */ 43 | isLoaded: boolean; 44 | /** Raw buffer of the values for lazy decoding, as reading 100,000s of uint64s can take quite a long time */ 45 | view?: DataViewOffset; 46 | /** Where in the file the value is read from */ 47 | dataOffset: number; 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/src/read/tiff.value.reader.ts: -------------------------------------------------------------------------------- 1 | import { TiffTagValueType } from '../const/tiff.tag.value.js'; 2 | import { ByteSize } from '../util/bytes.js'; 3 | 4 | export function getTiffTagSize(fieldType: TiffTagValueType): ByteSize { 5 | switch (fieldType) { 6 | case TiffTagValueType.Uint8: 7 | case TiffTagValueType.Ascii: 8 | case TiffTagValueType.Int8: 9 | case TiffTagValueType.Undefined: 10 | return 1; 11 | case TiffTagValueType.Uint16: 12 | case TiffTagValueType.Int16: 13 | return 2; 14 | case TiffTagValueType.Uint32: 15 | case TiffTagValueType.Int32: 16 | case TiffTagValueType.Float32: 17 | return 4; 18 | case TiffTagValueType.Rational: 19 | case TiffTagValueType.SignedRational: 20 | case TiffTagValueType.Float64: 21 | case TiffTagValueType.Uint64: 22 | case TiffTagValueType.Int64: 23 | case TiffTagValueType.Ifd8: 24 | return 8; 25 | default: 26 | throw new Error(`Invalid fieldType ${String(fieldType)}`); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/source.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a partial re-implementation of @chunkd/source 3 | * this is defined here so that @cogeotiff/core does not have any external dependencies 4 | */ 5 | export interface Source { 6 | /** Where the source is located */ 7 | url: URL; 8 | 9 | /** Optional metadata about the source including the size in bytes of the file */ 10 | metadata?: { 11 | /** Number of bytes in the file if known */ 12 | size?: number; 13 | }; 14 | 15 | /** Fetch bytes from a source */ 16 | fetch(offset: number, length?: number): Promise; 17 | 18 | /** Optionally close the source, useful for sources that have open connections of file descriptors */ 19 | close?(): Promise; 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/tiff.image.ts: -------------------------------------------------------------------------------- 1 | import { getCompressionMimeType, TiffCompressionMimeType, TiffMimeType } from './const/tiff.mime.js'; 2 | import { 3 | Compression, 4 | ModelTypeCode, 5 | SubFileType, 6 | TiffTag, 7 | TiffTagGeo, 8 | TiffTagGeoType, 9 | TiffTagType, 10 | } from './const/tiff.tag.id.js'; 11 | import { fetchAllOffsets, fetchLazy, getValueAt } from './read/tiff.tag.factory.js'; 12 | import { Tag, TagInline, TagOffset } from './read/tiff.tag.js'; 13 | import { Tiff } from './tiff.js'; 14 | import { getUint } from './util/bytes.js'; 15 | import { BoundingBox, Size } from './vector.js'; 16 | 17 | /** Invalid EPSG code */ 18 | export const InvalidProjectionCode = 32767; 19 | 20 | /** 21 | * Number of tiles used inside this image 22 | */ 23 | export interface TiffImageTileCount { 24 | /** Number of tiles on the x axis */ 25 | x: number; 26 | /** Number of tiles on the y axis */ 27 | y: number; 28 | } 29 | 30 | /** Tags that are commonly accessed for geotiffs */ 31 | export const ImportantTags = new Set([ 32 | TiffTag.Compression, 33 | TiffTag.ImageHeight, 34 | TiffTag.ImageWidth, 35 | TiffTag.ModelPixelScale, 36 | TiffTag.ModelTiePoint, 37 | TiffTag.ModelTransformation, 38 | TiffTag.TileHeight, 39 | TiffTag.TileWidth, 40 | TiffTag.GeoKeyDirectory, 41 | TiffTag.GeoAsciiParams, 42 | TiffTag.GeoDoubleParams, 43 | TiffTag.TileOffsets, 44 | ]); 45 | 46 | /** 47 | * Size of a individual tile 48 | */ 49 | export interface TiffImageTileSize { 50 | /** Tile width (pixels) */ 51 | width: number; 52 | /** Tile height (pixels) */ 53 | height: number; 54 | } 55 | 56 | export class TiffImage { 57 | /** 58 | * Id of the tif image, generally the image index inside the tif 59 | * where 0 is the root image, and every sub image is +1 60 | * 61 | * @example 0, 1, 2 62 | */ 63 | id: number; 64 | /** Reference to the TIFF that owns this image */ 65 | tiff: Tiff; 66 | /** Has loadGeoTiffTags been called */ 67 | isGeoTagsLoaded = false; 68 | /** Sub tags stored in TiffTag.GeoKeyDirectory */ 69 | tagsGeo: Map = new Map(); 70 | /** All IFD tags that have been read for the image */ 71 | tags: Map; 72 | 73 | constructor(tiff: Tiff, id: number, tags: Map) { 74 | this.tiff = tiff; 75 | this.id = id; 76 | this.tags = tags; 77 | } 78 | 79 | /** 80 | * Force loading of important tags if they have not already been loaded 81 | * 82 | * @param loadGeoTags Whether to load the GeoKeyDirectory and unpack it 83 | */ 84 | async init(loadGeoTags = true): Promise { 85 | const requiredTags: Promise[] = [ 86 | this.fetch(TiffTag.Compression), 87 | this.fetch(TiffTag.ImageHeight), 88 | this.fetch(TiffTag.ImageWidth), 89 | this.fetch(TiffTag.ModelPixelScale), 90 | this.fetch(TiffTag.ModelTiePoint), 91 | this.fetch(TiffTag.ModelTransformation), 92 | this.fetch(TiffTag.TileHeight), 93 | this.fetch(TiffTag.TileWidth), 94 | ]; 95 | 96 | if (loadGeoTags) { 97 | requiredTags.push(this.fetch(TiffTag.GeoKeyDirectory)); 98 | requiredTags.push(this.fetch(TiffTag.GeoAsciiParams)); 99 | requiredTags.push(this.fetch(TiffTag.GeoDoubleParams)); 100 | } 101 | 102 | await Promise.all(requiredTags); 103 | if (loadGeoTags) await this.loadGeoTiffTags(); 104 | } 105 | 106 | /** 107 | * Get the value of a TiffTag if it has been loaded, null otherwise. 108 | * 109 | * If the value is not loaded use {@link TiffImage.fetch} to load the value 110 | * Or use {@link TiffImage.has} to check if the tag exists 111 | * 112 | * 113 | * @returns value if loaded, null otherwise 114 | */ 115 | value(tag: T): TiffTagType[T] | null { 116 | const sourceTag = this.tags.get(tag); 117 | if (sourceTag == null) return null; 118 | if (sourceTag.type === 'offset' && sourceTag.isLoaded === false) return null; 119 | // TODO would be good to type check this 120 | return sourceTag.value as TiffTagType[T]; 121 | } 122 | 123 | /** 124 | * Does the tag exist 125 | * 126 | * @example 127 | * ```typescript 128 | * img.has(TiffTag.ImageWidth) // true 129 | * ``` 130 | * 131 | * @param tag Tag to check 132 | * @returns true if the tag exists, false otherwise 133 | */ 134 | has(tag: T): boolean { 135 | return this.tags.has(tag); 136 | } 137 | 138 | /** 139 | * Load a tag. 140 | * 141 | * If it is not currently loaded, fetch the required data for the tag. 142 | * 143 | * @example 144 | * ```typescript 145 | * await img.fetch(TiffTag.ImageWidth) // 512 (px) 146 | * ``` 147 | * 148 | * @param tag tag to fetch 149 | */ 150 | public async fetch(tag: T): Promise { 151 | const sourceTag = this.tags.get(tag); 152 | if (sourceTag == null) return null; 153 | if (sourceTag.type === 'inline') return sourceTag.value as TiffTagType[T]; 154 | if (sourceTag.type === 'lazy') return fetchLazy(sourceTag, this.tiff) as Promise; 155 | if (sourceTag.isLoaded) return sourceTag.value as TiffTagType[T]; 156 | if (sourceTag.type === 'offset') return fetchAllOffsets(this.tiff, sourceTag) as Promise; 157 | throw new Error('Cannot fetch:' + tag); 158 | } 159 | /** 160 | * Get the associated TiffTagGeo 161 | * 162 | * @example 163 | * ```typescript 164 | * image.valueGeo(TiffTagGeo.GTRasterTypeGeoKey) 165 | * ``` 166 | * @throws if {@link loadGeoTiffTags} has not been called 167 | */ 168 | valueGeo(tag: T): TiffTagGeoType[T] | null { 169 | if (this.isGeoTagsLoaded === false) throw new Error('loadGeoTiffTags() has not been called'); 170 | return this.tagsGeo.get(tag) as TiffTagGeoType[T]; 171 | } 172 | 173 | /** 174 | * Load and parse the GDAL_NODATA Tifftag 175 | * 176 | * @throws if the tag is not loaded 177 | * @returns null if the tag does not exist 178 | */ 179 | get noData(): number | null { 180 | const tag = this.tags.get(TiffTag.GdalNoData); 181 | if (tag == null) return null; 182 | if (tag.value) return Number(tag.value); 183 | throw new Error('GdalNoData tag is not loaded'); 184 | } 185 | 186 | /** 187 | * Load and unpack the GeoKeyDirectory 188 | * 189 | * @see {TiffTag.GeoKeyDirectory} 190 | */ 191 | async loadGeoTiffTags(): Promise { 192 | // Already loaded 193 | if (this.isGeoTagsLoaded) return; 194 | const sourceTag = this.tags.get(TiffTag.GeoKeyDirectory); 195 | if (sourceTag == null) { 196 | this.isGeoTagsLoaded = true; 197 | return; 198 | } 199 | if (sourceTag.type === 'lazy' && sourceTag.value == null) { 200 | // Load all the required keys 201 | await Promise.all([ 202 | this.fetch(TiffTag.GeoKeyDirectory), 203 | this.fetch(TiffTag.GeoAsciiParams), 204 | this.fetch(TiffTag.GeoDoubleParams), 205 | ]); 206 | } 207 | this.isGeoTagsLoaded = true; 208 | if (sourceTag.value == null) return; 209 | const geoTags = sourceTag.value as Uint16Array; 210 | if (typeof geoTags === 'number') throw new Error('Invalid geo tags found'); 211 | 212 | for (let i = 4; i <= geoTags[3] * 4; i += 4) { 213 | const key = geoTags[i] as TiffTagGeo; 214 | const locationTagId = geoTags[i + 1]; 215 | 216 | const offset = geoTags[i + 3]; 217 | 218 | if (locationTagId === 0) { 219 | this.tagsGeo.set(key, offset); 220 | continue; 221 | } 222 | 223 | const tag = this.tags.get(locationTagId); 224 | if (tag == null || tag.value == null) continue; 225 | const count = geoTags[i + 2]; 226 | if (typeof tag.value === 'string') { 227 | this.tagsGeo.set(key, tag.value.slice(offset, offset + count - 1).trim()); 228 | } else if (Array.isArray(tag.value)) { 229 | if (count === 1) this.tagsGeo.set(key, tag.value[offset] as string); 230 | else this.tagsGeo.set(key, tag.value.slice(offset, offset + count)); 231 | } else { 232 | throw new Error('Failed to extract GeoTiffTags'); 233 | } 234 | } 235 | } 236 | 237 | /** 238 | * Get the origin point for the image 239 | * 240 | * @returns origin point of the image 241 | */ 242 | get origin(): [number, number, number] { 243 | const tiePoints = this.value(TiffTag.ModelTiePoint); 244 | if (tiePoints != null && tiePoints.length === 6) { 245 | return [tiePoints[3], tiePoints[4], tiePoints[5]]; 246 | } 247 | 248 | const modelTransformation = this.value(TiffTag.ModelTransformation); 249 | if (modelTransformation != null) { 250 | return [modelTransformation[3], modelTransformation[7], modelTransformation[11]]; 251 | } 252 | 253 | // If this is a sub image, use the origin from the top level image 254 | if (this.value(TiffTag.SubFileType) === SubFileType.ReducedImage && this.id !== 0) { 255 | return this.tiff.images[0].origin; 256 | } 257 | 258 | throw new Error('Image does not have a geo transformation.'); 259 | } 260 | 261 | /** Is there enough geo information on this image to figure out where its actually located */ 262 | get isGeoLocated(): boolean { 263 | const isImageLocated = 264 | this.value(TiffTag.ModelPixelScale) != null || this.value(TiffTag.ModelTransformation) != null; 265 | if (isImageLocated) return true; 266 | // If this is a sub image, use the isGeoLocated from the top level image 267 | if (this.isSubImage && this.id !== 0) return this.tiff.images[0].isGeoLocated; 268 | return false; 269 | } 270 | 271 | /** 272 | * Get the resolution of the image 273 | * 274 | * @returns [x,y,z] pixel scale 275 | */ 276 | get resolution(): [number, number, number] { 277 | const modelPixelScale: number[] | null = this.value(TiffTag.ModelPixelScale); 278 | if (modelPixelScale != null) { 279 | return [modelPixelScale[0], -modelPixelScale[1], modelPixelScale[2]]; 280 | } 281 | const modelTransformation: number[] | null = this.value(TiffTag.ModelTransformation); 282 | if (modelTransformation != null) { 283 | return [modelTransformation[0], modelTransformation[5], modelTransformation[10]]; 284 | } 285 | 286 | // If this is a sub image, use the resolution from the top level image 287 | if (this.isSubImage && this.id !== 0) { 288 | const firstImg = this.tiff.images[0]; 289 | const [resX, resY, resZ] = firstImg.resolution; 290 | const firstImgSize = firstImg.size; 291 | const imgSize = this.size; 292 | // scale resolution based on the size difference between the two images 293 | return [(resX * firstImgSize.width) / imgSize.width, (resY * firstImgSize.height) / imgSize.height, resZ]; 294 | } 295 | 296 | throw new Error('Image does not have a geo transformation.'); 297 | } 298 | 299 | /** 300 | * Is this image a reduced size image 301 | * @see {@link TiffTag.SubFileType} 302 | * @returns true if SubFileType is Reduces image, false otherwise 303 | */ 304 | get isSubImage(): boolean { 305 | return this.value(TiffTag.SubFileType) === SubFileType.ReducedImage; 306 | } 307 | 308 | /** 309 | * Bounding box of the image 310 | * 311 | * @returns [minX, minY, maxX, maxY] bounding box 312 | */ 313 | get bbox(): [number, number, number, number] { 314 | const size = this.size; 315 | const origin = this.origin; 316 | const resolution = this.resolution; 317 | 318 | if (origin == null || size == null || resolution == null) { 319 | throw new Error('Unable to calculate bounding box'); 320 | } 321 | 322 | const x1 = origin[0]; 323 | const y1 = origin[1]; 324 | 325 | const x2 = x1 + resolution[0] * size.width; 326 | const y2 = y1 + resolution[1] * size.height; 327 | 328 | return [Math.min(x1, x2), Math.min(y1, y2), Math.max(x1, x2), Math.max(y1, y2)]; 329 | } 330 | 331 | /** 332 | * Get the compression used by the tile 333 | * 334 | * @see {@link TiffCompressionMimeType} 335 | * 336 | * @returns Compression type eg webp 337 | */ 338 | get compression(): TiffMimeType | null { 339 | const compression = this.value(TiffTag.Compression); 340 | if (compression == null) return null; 341 | return TiffCompressionMimeType[compression]; 342 | } 343 | 344 | /** 345 | * Attempt to read the EPSG Code from TiffGeoTags 346 | * 347 | * looks at TiffTagGeo.ProjectionGeoKey, TiffTagGeo.ProjectedCRSGeoKey and TiffTagGeo.GeodeticCRSGeoKey 348 | * 349 | * @returns EPSG Code if it exists and is not user defined. 350 | */ 351 | get epsg(): number | null { 352 | const proj = this.valueGeo(TiffTagGeo.ProjectionGeoKey); 353 | if (proj != null && proj !== InvalidProjectionCode) return proj; 354 | 355 | let projection: number | null = null; 356 | switch (this.valueGeo(TiffTagGeo.GTModelTypeGeoKey)) { 357 | case ModelTypeCode.Unknown: 358 | return null; 359 | case ModelTypeCode.Projected: 360 | projection = this.valueGeo(TiffTagGeo.ProjectedCRSGeoKey); 361 | break; 362 | case ModelTypeCode.Geographic: 363 | projection = this.valueGeo(TiffTagGeo.GeodeticCRSGeoKey); 364 | break; 365 | case ModelTypeCode.Geocentric: 366 | projection = this.valueGeo(TiffTagGeo.GeodeticCRSGeoKey); 367 | break; 368 | case ModelTypeCode.UserDefined: 369 | return null; 370 | } 371 | if (projection === InvalidProjectionCode) return null; 372 | return projection; 373 | } 374 | 375 | /** 376 | * Get the size of the image 377 | * 378 | * @returns Size in pixels 379 | */ 380 | get size(): Size { 381 | const width = this.value(TiffTag.ImageWidth); 382 | const height = this.value(TiffTag.ImageHeight); 383 | if (width == null || height == null) throw new Error('Tiff has no height or width'); 384 | 385 | return { width, height }; 386 | } 387 | 388 | /** 389 | * Determine if this image is tiled 390 | */ 391 | public isTiled(): boolean { 392 | return this.value(TiffTag.TileWidth) !== null; 393 | } 394 | 395 | /** 396 | * Get size of individual tiles 397 | */ 398 | get tileSize(): TiffImageTileSize { 399 | const width = this.value(TiffTag.TileWidth); 400 | const height = this.value(TiffTag.TileHeight); 401 | if (width == null || height == null) throw new Error('Tiff is not tiled'); 402 | return { width, height }; 403 | } 404 | 405 | /** 406 | * Number of tiles used to create this image 407 | */ 408 | get tileCount(): TiffImageTileCount { 409 | const size = this.size; 410 | const tileSize = this.tileSize; 411 | const x = Math.ceil(size.width / tileSize.width); 412 | const y = Math.ceil(size.height / tileSize.height); 413 | return { x, y }; 414 | } 415 | 416 | /** 417 | * Get the pointer to where the tiles start in the Tiff file 418 | * 419 | * @remarks Used to read tiled tiffs 420 | * 421 | * @returns file offset to where the tiffs are stored 422 | */ 423 | get tileOffset(): TagOffset | TagInline { 424 | const tileOffset = this.tags.get(TiffTag.TileOffsets) as TagOffset; 425 | if (tileOffset == null) throw new Error('No tile offsets found'); 426 | return tileOffset; 427 | } 428 | 429 | /** 430 | * Get the number of strip's inside this tiff 431 | * 432 | * @remarks Used to read striped tiffs 433 | * 434 | * @returns number of strips present 435 | */ 436 | get stripCount(): number { 437 | return this.tags.get(TiffTag.StripByteCounts)?.count ?? 0; 438 | } 439 | 440 | // Clamp the bounds of the output image to the size of the image, as sometimes the edge tiles are not full tiles 441 | getTileBounds(x: number, y: number): BoundingBox { 442 | const { size, tileSize } = this; 443 | const top = y * tileSize.height; 444 | const left = x * tileSize.width; 445 | const width = left + tileSize.width >= size.width ? size.width - left : tileSize.width; 446 | const height = top + tileSize.height >= size.height ? size.height - top : tileSize.height; 447 | return { x: left, y: top, width, height }; 448 | } 449 | 450 | /** 451 | * Read a strip into a ArrayBuffer 452 | * 453 | * Image has to be striped see {@link stripCount} 454 | * 455 | * @param index Strip index to read 456 | */ 457 | async getStrip(index: number): Promise<{ mimeType: TiffMimeType; bytes: ArrayBuffer } | null> { 458 | if (this.isTiled()) throw new Error('Cannot read stripes, tiff is tiled: ' + index); 459 | 460 | const byteCounts = this.tags.get(TiffTag.StripByteCounts) as TagOffset; 461 | const offsets = this.tags.get(TiffTag.StripOffsets) as TagOffset; 462 | 463 | if (index >= byteCounts.count) throw new Error('Cannot read strip, index out of bounds'); 464 | 465 | const [byteCount, offset] = await Promise.all([ 466 | getOffset(this.tiff, offsets, index), 467 | getOffset(this.tiff, byteCounts, index), 468 | ]); 469 | return this.getBytes(byteCount, offset); 470 | } 471 | 472 | /** The jpeg header is stored in the IFD, read the JPEG header and adjust the byte array to include it */ 473 | getJpegHeader(bytes: ArrayBuffer): ArrayBuffer { 474 | // Both the JPEGTable and the Bytes with have the start of image and end of image markers 475 | // StartOfImage 0xffd8 EndOfImage 0xffd9 476 | const tables = this.value(TiffTag.JpegTables); 477 | if (tables == null) throw new Error('Unable to find Jpeg header'); 478 | 479 | // Remove EndOfImage marker 480 | const tableData = tables.slice(0, tables.length - 2); 481 | const actualBytes = new Uint8Array(bytes.byteLength + tableData.length - 2); 482 | actualBytes.set(tableData, 0); 483 | actualBytes.set(new Uint8Array(bytes).slice(2), tableData.length); 484 | return actualBytes; 485 | } 486 | 487 | /** Read image bytes at the given offset */ 488 | async getBytes( 489 | offset: number, 490 | byteCount: number, 491 | ): Promise<{ mimeType: TiffMimeType; bytes: ArrayBuffer; compression: Compression } | null> { 492 | if (byteCount === 0) return null; 493 | 494 | const bytes = await this.tiff.source.fetch(offset, byteCount); 495 | if (bytes.byteLength < byteCount) { 496 | throw new Error(`Failed to fetch bytes from offset:${offset} wanted:${byteCount} got:${bytes.byteLength}`); 497 | } 498 | 499 | let compression = this.value(TiffTag.Compression); 500 | if (compression == null) compression = Compression.None; // No compression found default ?? 501 | const mimeType = getCompressionMimeType(compression) ?? TiffMimeType.None; 502 | 503 | if (compression === Compression.Jpeg) return { mimeType, bytes: this.getJpegHeader(bytes), compression }; 504 | return { mimeType, bytes, compression }; 505 | } 506 | 507 | /** 508 | * Load a tile into a ArrayBuffer 509 | * 510 | * if the tile compression is JPEG, This will also apply the JPEG compression tables to the resulting ArrayBuffer see {@link getJpegHeader} 511 | * 512 | * @param x Tile x offset 513 | * @param y Tile y offset 514 | */ 515 | async getTile( 516 | x: number, 517 | y: number, 518 | ): Promise<{ mimeType: TiffMimeType; bytes: ArrayBuffer; compression: Compression } | null> { 519 | const size = this.size; 520 | const tiles = this.tileSize; 521 | 522 | if (tiles == null) throw new Error('Tiff is not tiled'); 523 | 524 | // TODO support GhostOptionTileOrder 525 | const nyTiles = Math.ceil(size.height / tiles.height); 526 | const nxTiles = Math.ceil(size.width / tiles.width); 527 | 528 | if (x >= nxTiles || y >= nyTiles) { 529 | throw new Error(`Tile index is outside of range x:${x} >= ${nxTiles} or y:${y} >= ${nyTiles}`); 530 | } 531 | 532 | const idx = y * nxTiles + x; 533 | const totalTiles = nxTiles * nyTiles; 534 | if (idx >= totalTiles) throw new Error(`Tile index is outside of tile range: ${idx} >= ${totalTiles}`); 535 | 536 | const { offset, imageSize } = await this.getTileSize(idx); 537 | 538 | return this.getBytes(offset, imageSize); 539 | } 540 | 541 | /** 542 | * Does this tile exist in the tiff and does it actually have a value 543 | * 544 | * Sparse tiffs can have a lot of empty tiles, they set the tile size to `0 bytes` when the tile is empty 545 | * this checks the tile byte size to validate if it actually has any data. 546 | * 547 | * @param x Tile x offset 548 | * @param y Tile y offset 549 | * 550 | * @returns if the tile exists and has data 551 | */ 552 | async hasTile(x: number, y: number): Promise { 553 | const tiles = this.tileSize; 554 | const size = this.size; 555 | 556 | if (tiles == null) throw new Error('Tiff is not tiled'); 557 | 558 | // TODO support GhostOptionTileOrder 559 | const nyTiles = Math.ceil(size.height / tiles.height); 560 | const nxTiles = Math.ceil(size.width / tiles.width); 561 | if (x >= nxTiles || y >= nyTiles) return false; 562 | const idx = y * nxTiles + x; 563 | const ret = await this.getTileSize(idx); 564 | return ret.offset > 0; 565 | } 566 | 567 | /** 568 | * Load the offset and byteCount of a tile 569 | * @param index index in the tile array 570 | * @returns Offset and byteCount for the tile 571 | */ 572 | async getTileSize(index: number): Promise<{ offset: number; imageSize: number }> { 573 | // GDAL optimizes tiles by storing the size of the tile in 574 | // the few bytes leading up to the tile 575 | const leaderBytes = this.tiff.options?.tileLeaderByteSize; 576 | if (leaderBytes) { 577 | const offset = await getOffset(this.tiff, this.tileOffset, index); 578 | // Sparse tiff no data found 579 | if (offset === 0) return { offset: 0, imageSize: 0 }; 580 | 581 | // This fetch will generally load in the bytes needed for the image too 582 | // provided the image size is less than the size of a chunk 583 | const bytes = await this.tiff.source.fetch(offset - leaderBytes, leaderBytes); 584 | return { offset, imageSize: getUint(new DataView(bytes), 0, leaderBytes, this.tiff.isLittleEndian) }; 585 | } 586 | 587 | const byteCounts = this.tags.get(TiffTag.TileByteCounts) as TagOffset; 588 | if (byteCounts == null) throw new Error('No tile byte counts found'); 589 | const [offset, imageSize] = await Promise.all([ 590 | getOffset(this.tiff, this.tileOffset, index), 591 | getOffset(this.tiff, byteCounts, index), 592 | ]); 593 | return { offset, imageSize }; 594 | } 595 | } 596 | 597 | function getOffset(tiff: Tiff, x: TagOffset | TagInline, index: number): number | Promise { 598 | if (index < 0) { 599 | throw new Error(`Tiff: ${tiff.source.url.href} out of bounds ${TiffTag[x.id]} index:${index} total:${x.count}`); 600 | } 601 | // Sparse tiffs may not have the full tileWidth * tileHeight in their offset arrays 602 | if (index >= x.count) return 0; 603 | if (x.type === 'inline') return x.value[index]; 604 | return getValueAt(tiff, x, index); 605 | } 606 | -------------------------------------------------------------------------------- /packages/core/src/tiff.ts: -------------------------------------------------------------------------------- 1 | import { TiffEndian } from './const/tiff.endian.js'; 2 | import { TiffTag } from './const/tiff.tag.id.js'; 3 | import { TiffVersion } from './const/tiff.version.js'; 4 | import { Tag } from './index.js'; 5 | import { DataViewOffset, hasBytes } from './read/data.view.offset.js'; 6 | import { TiffGhostOptions } from './read/tiff.gdal.js'; 7 | import { TagTiffBigConfig, TagTiffConfig, TiffIfdConfig } from './read/tiff.ifd.config.js'; 8 | import { createTag } from './read/tiff.tag.factory.js'; 9 | import { Source } from './source.js'; 10 | import { TiffImage } from './tiff.image.js'; 11 | import { getUint } from './util/bytes.js'; 12 | import { toHex } from './util/util.hex.js'; 13 | 14 | export class Tiff { 15 | /** Read 16KB blocks at a time */ 16 | static DefaultReadSize = 16 * 1024; 17 | /** Read 16KB blocks at a time */ 18 | defaultReadSize = Tiff.DefaultReadSize; 19 | /** Where this cog is fetching its data from */ 20 | source: Source; 21 | /** Big or small Tiff */ 22 | version = TiffVersion.Tiff; 23 | /** List of images, o is the base image */ 24 | images: TiffImage[] = []; 25 | /** Ghost header options */ 26 | options?: TiffGhostOptions; 27 | /** Configuration for the size of the IFD */ 28 | ifdConfig: TiffIfdConfig = TagTiffConfig; 29 | /** Is the tiff being read is little Endian */ 30 | isLittleEndian = false; 31 | /** Has init() been called */ 32 | isInitialized = false; 33 | 34 | private _initPromise?: Promise; 35 | constructor(source: Source) { 36 | this.source = source; 37 | } 38 | 39 | /** Create a tiff and initialize it by reading the tiff headers */ 40 | static create(source: Source): Promise { 41 | return new Tiff(source).init(); 42 | } 43 | 44 | /** 45 | * Initialize the tiff loading in the header and all image headers 46 | */ 47 | init(): Promise { 48 | if (this._initPromise) return this._initPromise; 49 | this._initPromise = this.readHeader(); 50 | return this._initPromise; 51 | } 52 | 53 | /** 54 | * Find a image which has a resolution similar to the provided resolution 55 | * 56 | * @param resolution resolution to find 57 | */ 58 | getImageByResolution(resolution: number): TiffImage { 59 | const firstImage = this.images[0]; 60 | const firstImageSize = firstImage.size; 61 | const [refX] = firstImage.resolution; 62 | 63 | const resolutionBaseX = refX * firstImageSize.width; 64 | // const resolutionBaseY = refY * firstImageSize.height; 65 | for (let i = this.images.length - 1; i > 0; i--) { 66 | const img = this.images[i]; 67 | const imgSize = img.size; 68 | 69 | const imgResolutionX = resolutionBaseX / imgSize.width; 70 | // TODO do we care about y resolution 71 | // const imgResolutionY = resolutionBaseY / imgSize.height; 72 | 73 | if (imgResolutionX - resolution <= 0.01) return img; 74 | } 75 | return firstImage; 76 | } 77 | 78 | /** Read the Starting header and all Image headers from the source */ 79 | private async readHeader(): Promise { 80 | if (this.isInitialized) return this; 81 | // limit the read to the size of the file if it is known, for small tiffs 82 | const bytes = new DataView( 83 | await this.source.fetch(0, getMaxLength(this.source, 0, this.defaultReadSize)), 84 | ) as DataViewOffset; 85 | bytes.sourceOffset = 0; 86 | 87 | let offset = 0; 88 | const endian = bytes.getUint16(offset, this.isLittleEndian) as TiffEndian; 89 | offset += 2; 90 | 91 | this.isLittleEndian = endian === TiffEndian.Little; 92 | if (!this.isLittleEndian) throw new Error('Only little endian is supported'); 93 | this.version = bytes.getUint16(offset, this.isLittleEndian); 94 | offset += 2; 95 | 96 | let nextOffsetIfd: number; 97 | if (this.version === TiffVersion.BigTiff) { 98 | this.ifdConfig = TagTiffBigConfig; 99 | const pointerSize = bytes.getUint16(offset, this.isLittleEndian); 100 | offset += 2; 101 | if (pointerSize !== 8) throw new Error('Only 8byte pointers are supported'); 102 | const zeros = bytes.getUint16(offset, this.isLittleEndian); 103 | offset += 2; 104 | if (zeros !== 0) throw new Error('Invalid big tiff header'); 105 | nextOffsetIfd = getUint(bytes, offset, this.ifdConfig.pointer, this.isLittleEndian); 106 | offset += this.ifdConfig.pointer; 107 | } else if (this.version === TiffVersion.Tiff) { 108 | nextOffsetIfd = getUint(bytes, offset, this.ifdConfig.pointer, this.isLittleEndian); 109 | offset += this.ifdConfig.pointer; 110 | } else { 111 | throw new Error(`Only tiff supported version:${String(this.version)}`); 112 | } 113 | 114 | const ghostSize = nextOffsetIfd - offset; 115 | // GDAL now stores metadata between the IFD inside a ghost storage area 116 | if (ghostSize > 0 && ghostSize < 16 * 1024) { 117 | this.options = new TiffGhostOptions(); 118 | this.options.process(bytes, offset, ghostSize); 119 | } 120 | 121 | while (nextOffsetIfd !== 0) { 122 | let lastView = bytes; 123 | 124 | // Ensure at least 1KB near at the IFD offset is ready for reading 125 | // TODO is 1KB enough, most IFD entries are in the order of 100-300 bytes 126 | if (!hasBytes(lastView, nextOffsetIfd, 1024)) { 127 | const bytes = await this.source.fetch( 128 | nextOffsetIfd, 129 | getMaxLength(this.source, nextOffsetIfd, this.defaultReadSize), 130 | ); 131 | lastView = new DataView(bytes) as DataViewOffset; 132 | lastView.sourceOffset = nextOffsetIfd; 133 | } 134 | nextOffsetIfd = this.readIfd(nextOffsetIfd, lastView); 135 | } 136 | 137 | await Promise.all(this.images.map((i) => i.init())); 138 | this.isInitialized = true; 139 | return this; 140 | } 141 | 142 | /** 143 | * Read a IFD at a the provided offset 144 | * 145 | * @param offset file offset to read the header from 146 | * @param view offset that contains the bytes for the header 147 | */ 148 | private readIfd(offset: number, view: DataViewOffset): number { 149 | const viewOffset = offset - view.sourceOffset; 150 | const tagCount = getUint(view, viewOffset, this.ifdConfig.offset, this.isLittleEndian); 151 | 152 | const tags: Map = new Map(); 153 | 154 | // We now know how many bytes we need so ensure the ifd bytes are all read 155 | const ifdBytes = tagCount * this.ifdConfig.ifd; 156 | if (!hasBytes(view, offset, ifdBytes)) { 157 | throw new Error('IFD out of range @ ' + toHex(offset) + ' IFD' + this.images.length); 158 | } 159 | 160 | const ifdSize = this.ifdConfig.ifd; 161 | const startOffset = viewOffset + this.ifdConfig.offset; 162 | for (let i = 0; i < tagCount; i++) { 163 | const tag = createTag(this, view, startOffset + i * ifdSize); 164 | tags.set(tag.id, tag); 165 | } 166 | 167 | this.images.push(new TiffImage(this, this.images.length, tags)); 168 | return getUint(view, startOffset + tagCount * ifdSize, this.ifdConfig.pointer, this.isLittleEndian); 169 | } 170 | } 171 | 172 | function getMaxLength(source: Source, offset: number, length: number): number { 173 | // max length is unknown, roll the dice and hope the chunk exists 174 | if (source.metadata?.size == null) return length; 175 | const size = source.metadata.size; 176 | 177 | // Read was going to happen past the end of the file limit it to the end of the file 178 | if (offset + length > size) return size - offset; 179 | return length; 180 | } 181 | -------------------------------------------------------------------------------- /packages/core/src/util/bytes.ts: -------------------------------------------------------------------------------- 1 | export enum ByteSizeFloat { 2 | Double = 8, 3 | Float32 = 4, 4 | } 5 | 6 | export enum ByteSize { 7 | UInt64 = 8, 8 | UInt32 = 4, 9 | UInt16 = 2, 10 | UInt8 = 1, 11 | } 12 | 13 | /** Shifting `<< 32` does not work in javascript */ 14 | const POW_32 = 2 ** 32; 15 | /** 16 | * Read a uint64 at the offset 17 | * 18 | * This is not precise for large numbers 19 | * @see {DataView.getBigUint64} 20 | * @param offset offset to read 21 | */ 22 | export function getUint64(view: DataView, offset: number, isLittleEndian: boolean): number { 23 | // split 64-bit number into two 32-bit (4-byte) parts 24 | const left = view.getUint32(offset, isLittleEndian); 25 | const right = view.getUint32(offset + 4, isLittleEndian); 26 | 27 | // combine the two 32-bit values 28 | const combined = isLittleEndian ? left + POW_32 * right : POW_32 * left + right; 29 | 30 | if (!Number.isSafeInteger(combined)) { 31 | throw new Error(combined + ' exceeds MAX_SAFE_INTEGER. Precision may is lost'); 32 | } 33 | 34 | return combined; 35 | } 36 | 37 | export function getUint(view: DataView, offset: number, bs: ByteSize, isLittleEndian: boolean): number { 38 | switch (bs) { 39 | case ByteSize.UInt8: 40 | return view.getUint8(offset); 41 | case ByteSize.UInt16: 42 | return view.getUint16(offset, isLittleEndian); 43 | case ByteSize.UInt32: 44 | return view.getUint32(offset, isLittleEndian); 45 | case ByteSize.UInt64: 46 | return getUint64(view, offset, isLittleEndian); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/src/util/util.hex.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert a number to a formatted hex string 3 | * 4 | * @param num number to convert 5 | * @param padding number of 0's to pad the digit with 6 | * @param prefix should a `0x` be prefixed to the string 7 | * 8 | * @returns hex string eg 0x0015 9 | **/ 10 | export function toHex(num: number, padding = 4, prefix = true): string { 11 | const hex = num.toString(16).padStart(padding, '0'); 12 | if (prefix) return '0x' + hex; 13 | return hex; 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/vector.ts: -------------------------------------------------------------------------------- 1 | export interface Size { 2 | width: number; 3 | height: number; 4 | } 5 | 6 | export interface Point { 7 | x: number; 8 | y: number; 9 | } 10 | 11 | export interface BoundingBox extends Point, Size {} 12 | export interface Vector extends Point { 13 | z: number; 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./build", 6 | "lib": ["es2018", "dom"] 7 | }, 8 | "include": ["src"], 9 | "references": [] 10 | } 11 | -------------------------------------------------------------------------------- /packages/examples/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /packages/examples/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### Dependencies 4 | 5 | * The following workspace dependencies were updated 6 | * dependencies 7 | * @cogeotiff/core bumped from ^8.0.1 to ^8.0.2 8 | 9 | ### Dependencies 10 | 11 | * The following workspace dependencies were updated 12 | * dependencies 13 | * @cogeotiff/core bumped from ^8.0.2 to ^8.1.0 14 | 15 | ### Dependencies 16 | 17 | * The following workspace dependencies were updated 18 | * dependencies 19 | * @cogeotiff/core bumped from ^8.1.0 to ^8.1.1 20 | 21 | ### Dependencies 22 | 23 | * The following workspace dependencies were updated 24 | * dependencies 25 | * @cogeotiff/core bumped from ^9.0.0 to ^9.0.1 26 | 27 | ### Dependencies 28 | 29 | * The following workspace dependencies were updated 30 | * dependencies 31 | * @cogeotiff/core bumped from ^9.0.1 to ^9.0.2 32 | 33 | ### Dependencies 34 | 35 | * The following workspace dependencies were updated 36 | * dependencies 37 | * @cogeotiff/core bumped from ^9.0.2 to ^9.0.3 38 | 39 | ## [9.0.0](https://github.com/blacha/cogeotiff/compare/examples-v8.0.4...examples-v9.0.0) (2023-12-11) 40 | 41 | 42 | ### ⚠ BREAKING CHANGES 43 | 44 | * rename all type from CogTiff to just Tiff ([#1227](https://github.com/blacha/cogeotiff/issues/1227)) 45 | 46 | ### Features 47 | 48 | * rename all type from CogTiff to just Tiff ([#1227](https://github.com/blacha/cogeotiff/issues/1227)) ([872263b](https://github.com/blacha/cogeotiff/commit/872263b11f1ab06853cb872de54a9d9dd745b647)) 49 | 50 | 51 | ### Dependencies 52 | 53 | * The following workspace dependencies were updated 54 | * dependencies 55 | * @cogeotiff/core bumped from ^8.1.1 to ^9.0.0 56 | 57 | ## [8.0.1](https://github.com/blacha/cogeotiff/compare/examples-v8.0.0...examples-v8.0.1) (2023-08-05) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * broken changelog ordering ([31f8c8a](https://github.com/blacha/cogeotiff/commit/31f8c8ac5e2770427ed2dc0f5c7c34330c6cb0eb)) 63 | 64 | 65 | ### Dependencies 66 | 67 | * The following workspace dependencies were updated 68 | * dependencies 69 | * @cogeotiff/core bumped from ^8.0.0 to ^8.0.1 70 | -------------------------------------------------------------------------------- /packages/examples/README.md: -------------------------------------------------------------------------------- 1 | # @cogeotiff/exmples 2 | 3 | Examples of using @cogeotiff to work with tiffs. 4 | 5 | Examples: 6 | 7 | - [example.single.tile.ts](./src/browser/example.single.tile.ts) - Load a specific tile from a tiff and render it as a image. 8 | 9 | 10 | # Usage 11 | 12 | Bundle the source code using `esbuild` into `dist/index.js` 13 | ``` 14 | yarn bundle 15 | ``` 16 | 17 | Serve the `index.html` and the `dist/` folder using your fav http server, 18 | 19 | ``` 20 | npm install -g serve 21 | serve . 22 | ``` -------------------------------------------------------------------------------- /packages/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example @cogeotiff app 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cogeotiff/examples", 3 | "private": true, 4 | "version": "9.0.3", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/blacha/cogeotiff.git", 8 | "directory": "packages/examples" 9 | }, 10 | "type": "module", 11 | "engines": { 12 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 13 | }, 14 | "main": "./build/index.js", 15 | "types": "./build/index.d.ts", 16 | "author": "Blayne Chard", 17 | "license": "MIT", 18 | "scripts": { 19 | "test": "node --test", 20 | "bundle": "esbuild src/browser/index.ts --bundle --outfile=dist/index.js" 21 | }, 22 | "dependencies": { 23 | "@chunkd/source-http": "^11.0.1", 24 | "@cogeotiff/core": "^9.0.3", 25 | "esbuild": "^0.19.2" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^20.0.0" 29 | }, 30 | "publishConfig": { 31 | "access": "restricted" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/examples/src/browser/example.single.tile.ts: -------------------------------------------------------------------------------- 1 | import { Tiff } from '@cogeotiff/core'; 2 | 3 | /** Loads a single tile from a COG and renders it as a element */ 4 | export async function loadSingleTile(tiff: Tiff): Promise { 5 | const startTime = performance.now(); 6 | const img = tiff.images[tiff.images.length - 1]; 7 | const tile = await img.getTile(0, 0); 8 | if (tile == null) throw new Error('Failed to load tile from tiff'); 9 | 10 | const imgEl = document.createElement('img'); 11 | imgEl.src = URL.createObjectURL(new Blob([tile.bytes], { type: tile.mimeType })); 12 | 13 | const divEl = document.createElement('div'); 14 | const titleEl = document.createElement('div'); 15 | titleEl.innerText = 'Single Tile Example'; 16 | divEl.appendChild(titleEl); 17 | divEl.appendChild(imgEl); 18 | 19 | const duration = performance.now() - startTime; 20 | console.log(`Single Tile example rendered duration: ${Number(duration.toFixed(4))} ms`); 21 | 22 | return divEl; 23 | } 24 | -------------------------------------------------------------------------------- /packages/examples/src/browser/index.ts: -------------------------------------------------------------------------------- 1 | import { SourceCache, SourceChunk } from '@chunkd/middleware'; 2 | import { SourceCallback, SourceMiddleware, SourceRequest, SourceView } from '@chunkd/source'; 3 | import { SourceHttp } from '@chunkd/source-http'; 4 | import { Tiff } from '@cogeotiff/core'; 5 | 6 | import { loadSingleTile } from './example.single.tile.js'; 7 | 8 | // Cache all requests to cogs 9 | const cache = new SourceCache({ size: 16 * 1024 * 1024 }); // 16MB Cache 10 | const chunk = new SourceChunk({ size: 16 * 1024 }); // Chunk requests into 16KB fetches 11 | const fetchLog: SourceMiddleware = { 12 | name: 'fetch:log', 13 | fetch(req: SourceRequest, next: SourceCallback) { 14 | console.log('Tiff:fetch', { href: req.source.url.href, offset: req.offset, length: req.length }); 15 | return next(req); 16 | }, 17 | }; 18 | 19 | async function loadTiff(): Promise { 20 | const tiffSource = new SourceView(new SourceHttp('https://blayne.chard.com/world.webp.google.cog.tiff'), [ 21 | chunk, 22 | cache, 23 | fetchLog, 24 | ]); 25 | const tiff = await Tiff.create(tiffSource); 26 | 27 | const mainEl = document.createElement('div'); 28 | console.log('Loaded: ', tiff.source.url, '\nImages:', tiff.images.length); 29 | document.body.appendChild(mainEl); 30 | 31 | const nodes = await Promise.all([loadSingleTile(tiff)]); 32 | for (const n of nodes) mainEl.appendChild(n); 33 | } 34 | document.addEventListener('DOMContentLoaded', () => { 35 | void loadTiff(); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./build", 6 | "lib": ["es2018", "dom"] 7 | }, 8 | "include": ["src"], 9 | "references": [{ "path": "../core" }] 10 | } 11 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bootstrap-sha": "9fb54d028178cf770e6cf47599569956cb51374d", 3 | "plugins": ["node-workspace"], 4 | "packages": { 5 | "packages/core": {}, 6 | "packages/cli": {}, 7 | "packages/examples": {} 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@linzjs/style/tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["ES2020", "DOM"], 5 | "outDir": "build", 6 | "noUncheckedIndexedAccess": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./packages/core" }, { "path": "./packages/cli" }] 4 | } 5 | --------------------------------------------------------------------------------