├── python
├── docs
│ ├── assets
│ ├── index.md
│ ├── api
│ │ ├── store
│ │ │ ├── http.md
│ │ │ ├── local.md
│ │ │ ├── memory.md
│ │ │ ├── index.md
│ │ │ ├── config.md
│ │ │ ├── aws.md
│ │ │ ├── gcs.md
│ │ │ └── azure.md
│ │ ├── thread-pool.md
│ │ ├── decoder.md
│ │ ├── tile.md
│ │ ├── ifd.md
│ │ ├── geo.md
│ │ └── tiff.md
│ └── overrides
│ │ ├── main.html
│ │ └── stylesheets
│ │ └── extra.css
├── tests
│ ├── __init__.py
│ ├── test_exceptions.py
│ ├── test_cog.py
│ └── test_ifd.py
├── python
│ └── async_tiff
│ │ ├── py.typed
│ │ ├── store
│ │ ├── _thread_pool.pyi
│ │ ├── __init__.py
│ │ ├── _async_tiff.pyi
│ │ ├── _decoder.pyi
│ │ ├── _tile.pyi
│ │ ├── enums.py
│ │ ├── _tiff.pyi
│ │ ├── _geo.pyi
│ │ └── _ifd.pyi
├── assets
│ ├── logo_no_text.png
│ └── naip-example.jpg
├── DEVELOP.md
├── pyproject.toml
├── src
│ ├── error.rs
│ ├── thread_pool.rs
│ ├── lib.rs
│ ├── value.rs
│ ├── tile.rs
│ ├── decoder.rs
│ ├── tiff.rs
│ ├── reader.rs
│ └── enums.rs
├── Cargo.toml
├── README.md
├── CHANGELOG.md
├── .gitignore
└── mkdocs.yml
├── tests
├── mod.rs
├── image_tiff
│ ├── mod.rs
│ ├── images
│ │ ├── int16.tif
│ │ ├── int8.tif
│ │ ├── geo-5b.tif
│ │ ├── int16_rgb.tif
│ │ ├── int16_zstd.tif
│ │ ├── int8_rgb.tif
│ │ ├── rgb-3c-8b.ppm
│ │ ├── rgb-3c-8b.tiff
│ │ ├── cmyk-3c-16b.tiff
│ │ ├── cmyk-3c-8b.tiff
│ │ ├── random-fp16.pgm
│ │ ├── random-fp16.tiff
│ │ ├── rgb-3c-16b.tiff
│ │ ├── tiled-rgb-u8.tif
│ │ ├── white-fp16.tiff
│ │ ├── 12bit.cropped.tiff
│ │ ├── bigtiff
│ │ │ ├── BigTIFF.tif
│ │ │ ├── BigTIFFLong.tif
│ │ │ └── BigTIFFMotorola.tif
│ │ ├── issue_69_lzw.tiff
│ │ ├── logluv-3c-16b.tiff
│ │ ├── palette-1c-1b.tiff
│ │ ├── palette-1c-4b.tiff
│ │ ├── palette-1c-8b.tiff
│ │ ├── planar-rgb-u8.tif
│ │ ├── quad-tile.jpg.tiff
│ │ ├── tiled-cmyk-i8.tif
│ │ ├── tiled-gray-i1.tif
│ │ ├── gradient-1c-32b.tiff
│ │ ├── gradient-1c-64b.tiff
│ │ ├── gradient-3c-32b.tiff
│ │ ├── gradient-3c-64b.tiff
│ │ ├── minisblack-1c-8b.pgm
│ │ ├── minisblack-1c-8b.tiff
│ │ ├── miniswhite-1c-1b.pbm
│ │ ├── miniswhite-1c-1b.tiff
│ │ ├── quad-lzw-compat.tiff
│ │ ├── tiled-jpeg-rgb-u8.tif
│ │ ├── tiled-jpeg-ycbcr.tif
│ │ ├── tiled-rect-rgb-u8.tif
│ │ ├── white-fp16-pred2.tiff
│ │ ├── white-fp16-pred3.tiff
│ │ ├── 12bit.cropped.rgb.tiff
│ │ ├── cmyk-3c-32b-float.tiff
│ │ ├── issue_69_packbits.tiff
│ │ ├── minisblack-1c-16b.tiff
│ │ ├── minisblack-1c-i16b.tiff
│ │ ├── minisblack-1c-i8b.tiff
│ │ ├── no_rows_per_strip.tiff
│ │ ├── predictor-3-gray-f32.tif
│ │ ├── predictor-3-rgb-f32.tif
│ │ ├── random-fp16-pred2.tiff
│ │ ├── random-fp16-pred3.tiff
│ │ ├── single-black-fp16.tiff
│ │ ├── gradient-1c-32b-float.tiff
│ │ ├── gradient-1c-64b-float.tiff
│ │ ├── gradient-3c-32b-float.tiff
│ │ ├── tiled-oversize-gray-i8.tif
│ │ ├── minisblack-2c-8b-alpha.tiff
│ │ ├── README.txt
│ │ └── COPYRIGHT
│ ├── decodedata-rgb-3c-8b.tiff
│ ├── README.md
│ ├── util.rs
│ ├── decode_bigtiff_images.rs
│ ├── decode_geotiff_images.rs
│ ├── decode_fp16_images.rs
│ └── predict.rs
├── images
│ ├── geogtowgs_subset_USGS_13_s14w171.tif
│ └── readme.md
├── geo.rs
└── ome_tiff.rs
├── .gitmodules
├── CHANGELOG.md
├── src
├── geo
│ ├── mod.rs
│ ├── partial_reads.rs
│ ├── affine.rs
│ └── geo_key_directory.rs
├── lib.rs
├── metadata
│ ├── mod.rs
│ ├── fetch.rs
│ └── cache.rs
├── tiff.rs
├── tile.rs
├── decoder.rs
├── tags.rs
├── reader.rs
└── tag_value.rs
├── .github
└── workflows
│ ├── conventional-commits.yml
│ ├── rust-release.yml
│ ├── test.yml
│ ├── lint.yml
│ ├── test-python.yml
│ ├── python-docs-publish.yml
│ └── python-wheels.yml
├── show_image.py
├── .gitignore
├── LICENSE
├── README.md
└── Cargo.toml
/python/docs/assets:
--------------------------------------------------------------------------------
1 | ../assets
--------------------------------------------------------------------------------
/python/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/python/docs/index.md:
--------------------------------------------------------------------------------
1 | ../README.md
--------------------------------------------------------------------------------
/python/python/async_tiff/py.typed:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/mod.rs:
--------------------------------------------------------------------------------
1 | mod geo;
2 | mod image_tiff;
3 | mod ome_tiff;
4 |
--------------------------------------------------------------------------------
/python/python/async_tiff/store:
--------------------------------------------------------------------------------
1 | ../../_obstore/obstore/python/obstore/_store
--------------------------------------------------------------------------------
/python/docs/api/store/http.md:
--------------------------------------------------------------------------------
1 | # HTTP
2 |
3 | ::: async_tiff.store.HTTPStore
4 |
--------------------------------------------------------------------------------
/python/docs/api/store/local.md:
--------------------------------------------------------------------------------
1 | # Local
2 |
3 | ::: async_tiff.store.LocalStore
4 |
--------------------------------------------------------------------------------
/python/docs/api/thread-pool.md:
--------------------------------------------------------------------------------
1 | # Thread Pool
2 |
3 | ::: async_tiff.ThreadPool
4 |
--------------------------------------------------------------------------------
/python/docs/api/store/memory.md:
--------------------------------------------------------------------------------
1 | # Memory
2 |
3 | ::: async_tiff.store.MemoryStore
4 |
--------------------------------------------------------------------------------
/python/docs/api/decoder.md:
--------------------------------------------------------------------------------
1 | # Decoder
2 |
3 | ::: async_tiff.Decoder
4 | ::: async_tiff.DecoderRegistry
5 |
--------------------------------------------------------------------------------
/python/assets/logo_no_text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/python/assets/logo_no_text.png
--------------------------------------------------------------------------------
/python/assets/naip-example.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/python/assets/naip-example.jpg
--------------------------------------------------------------------------------
/python/docs/api/tile.md:
--------------------------------------------------------------------------------
1 | # Tile
2 |
3 | ::: async_tiff.Tile
4 | options:
5 | show_if_no_docstring: true
6 |
--------------------------------------------------------------------------------
/tests/image_tiff/mod.rs:
--------------------------------------------------------------------------------
1 | mod decode_bigtiff_images;
2 | mod decode_geotiff_images;
3 | mod decode_images;
4 | mod util;
5 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "python/obstore"]
2 | path = python/_obstore
3 | url = https://github.com/developmentseed/obstore
4 |
--------------------------------------------------------------------------------
/python/docs/api/store/index.md:
--------------------------------------------------------------------------------
1 | # ObjectStore
2 |
3 | ::: async_tiff.store.from_url
4 | ::: async_tiff.store.ObjectStore
5 |
--------------------------------------------------------------------------------
/tests/image_tiff/images/int16.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/int16.tif
--------------------------------------------------------------------------------
/tests/image_tiff/images/int8.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/int8.tif
--------------------------------------------------------------------------------
/python/docs/api/ifd.md:
--------------------------------------------------------------------------------
1 | # IFD
2 |
3 | ::: async_tiff.ImageFileDirectory
4 | options:
5 | show_if_no_docstring: true
6 |
--------------------------------------------------------------------------------
/tests/image_tiff/images/geo-5b.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/geo-5b.tif
--------------------------------------------------------------------------------
/tests/image_tiff/images/int16_rgb.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/int16_rgb.tif
--------------------------------------------------------------------------------
/tests/image_tiff/images/int16_zstd.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/int16_zstd.tif
--------------------------------------------------------------------------------
/tests/image_tiff/images/int8_rgb.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/int8_rgb.tif
--------------------------------------------------------------------------------
/tests/image_tiff/images/rgb-3c-8b.ppm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/rgb-3c-8b.ppm
--------------------------------------------------------------------------------
/tests/image_tiff/images/rgb-3c-8b.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/rgb-3c-8b.tiff
--------------------------------------------------------------------------------
/python/docs/api/geo.md:
--------------------------------------------------------------------------------
1 | # Geospatial tags
2 |
3 | ::: async_tiff.GeoKeyDirectory
4 | options:
5 | show_if_no_docstring: true
6 |
--------------------------------------------------------------------------------
/tests/image_tiff/images/cmyk-3c-16b.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/cmyk-3c-16b.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/cmyk-3c-8b.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/cmyk-3c-8b.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/random-fp16.pgm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/random-fp16.pgm
--------------------------------------------------------------------------------
/tests/image_tiff/images/random-fp16.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/random-fp16.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/rgb-3c-16b.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/rgb-3c-16b.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/tiled-rgb-u8.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/tiled-rgb-u8.tif
--------------------------------------------------------------------------------
/tests/image_tiff/images/white-fp16.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/white-fp16.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/decodedata-rgb-3c-8b.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/decodedata-rgb-3c-8b.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/12bit.cropped.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/12bit.cropped.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/bigtiff/BigTIFF.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/bigtiff/BigTIFF.tif
--------------------------------------------------------------------------------
/tests/image_tiff/images/issue_69_lzw.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/issue_69_lzw.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/logluv-3c-16b.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/logluv-3c-16b.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/palette-1c-1b.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/palette-1c-1b.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/palette-1c-4b.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/palette-1c-4b.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/palette-1c-8b.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/palette-1c-8b.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/planar-rgb-u8.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/planar-rgb-u8.tif
--------------------------------------------------------------------------------
/tests/image_tiff/images/quad-tile.jpg.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/quad-tile.jpg.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/tiled-cmyk-i8.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/tiled-cmyk-i8.tif
--------------------------------------------------------------------------------
/tests/image_tiff/images/tiled-gray-i1.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/tiled-gray-i1.tif
--------------------------------------------------------------------------------
/python/docs/api/tiff.md:
--------------------------------------------------------------------------------
1 | # TIFF
2 |
3 | ::: async_tiff.TIFF
4 | options:
5 | show_if_no_docstring: true
6 | ::: async_tiff.ObspecInput
7 |
--------------------------------------------------------------------------------
/tests/image_tiff/images/gradient-1c-32b.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/gradient-1c-32b.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/gradient-1c-64b.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/gradient-1c-64b.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/gradient-3c-32b.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/gradient-3c-32b.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/gradient-3c-64b.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/gradient-3c-64b.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/minisblack-1c-8b.pgm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/minisblack-1c-8b.pgm
--------------------------------------------------------------------------------
/tests/image_tiff/images/minisblack-1c-8b.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/minisblack-1c-8b.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/miniswhite-1c-1b.pbm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/miniswhite-1c-1b.pbm
--------------------------------------------------------------------------------
/tests/image_tiff/images/miniswhite-1c-1b.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/miniswhite-1c-1b.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/quad-lzw-compat.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/quad-lzw-compat.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/tiled-jpeg-rgb-u8.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/tiled-jpeg-rgb-u8.tif
--------------------------------------------------------------------------------
/tests/image_tiff/images/tiled-jpeg-ycbcr.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/tiled-jpeg-ycbcr.tif
--------------------------------------------------------------------------------
/tests/image_tiff/images/tiled-rect-rgb-u8.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/tiled-rect-rgb-u8.tif
--------------------------------------------------------------------------------
/tests/image_tiff/images/white-fp16-pred2.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/white-fp16-pred2.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/white-fp16-pred3.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/white-fp16-pred3.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/12bit.cropped.rgb.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/12bit.cropped.rgb.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/bigtiff/BigTIFFLong.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/bigtiff/BigTIFFLong.tif
--------------------------------------------------------------------------------
/tests/image_tiff/images/cmyk-3c-32b-float.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/cmyk-3c-32b-float.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/issue_69_packbits.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/issue_69_packbits.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/minisblack-1c-16b.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/minisblack-1c-16b.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/minisblack-1c-i16b.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/minisblack-1c-i16b.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/minisblack-1c-i8b.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/minisblack-1c-i8b.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/no_rows_per_strip.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/no_rows_per_strip.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/predictor-3-gray-f32.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/predictor-3-gray-f32.tif
--------------------------------------------------------------------------------
/tests/image_tiff/images/predictor-3-rgb-f32.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/predictor-3-rgb-f32.tif
--------------------------------------------------------------------------------
/tests/image_tiff/images/random-fp16-pred2.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/random-fp16-pred2.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/random-fp16-pred3.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/random-fp16-pred3.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/single-black-fp16.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/single-black-fp16.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/gradient-1c-32b-float.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/gradient-1c-32b-float.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/gradient-1c-64b-float.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/gradient-1c-64b-float.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/gradient-3c-32b-float.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/gradient-3c-32b-float.tiff
--------------------------------------------------------------------------------
/tests/image_tiff/images/tiled-oversize-gray-i8.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/tiled-oversize-gray-i8.tif
--------------------------------------------------------------------------------
/tests/images/geogtowgs_subset_USGS_13_s14w171.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/images/geogtowgs_subset_USGS_13_s14w171.tif
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [0.1.0] - 2025-03-14
4 |
5 | - Initial release.
6 | - Includes support for reading metadata out of TIFF files in an async way.
7 |
--------------------------------------------------------------------------------
/tests/image_tiff/images/bigtiff/BigTIFFMotorola.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/bigtiff/BigTIFFMotorola.tif
--------------------------------------------------------------------------------
/tests/image_tiff/images/minisblack-2c-8b-alpha.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/async-tiff/HEAD/tests/image_tiff/images/minisblack-2c-8b-alpha.tiff
--------------------------------------------------------------------------------
/python/docs/api/store/config.md:
--------------------------------------------------------------------------------
1 | # Configuration
2 |
3 | ::: async_tiff.store.ClientConfig
4 | ::: async_tiff.store.BackoffConfig
5 | ::: async_tiff.store.RetryConfig
6 |
--------------------------------------------------------------------------------
/tests/image_tiff/README.md:
--------------------------------------------------------------------------------
1 | Tests that have been vendored from image-tiff
2 |
3 | https://github.com/image-rs/image-tiff/tree/3bfb43e83e31b0da476832067ada68a82b378b7b
4 |
--------------------------------------------------------------------------------
/python/DEVELOP.md:
--------------------------------------------------------------------------------
1 | ```
2 | uv sync --no-install-package async-tiff
3 | uv run --no-project maturin develop --uv
4 | uv run --no-project mkdocs serve
5 | uv run --no-project pytest tests --verbose
6 | ```
7 |
--------------------------------------------------------------------------------
/python/python/async_tiff/_thread_pool.pyi:
--------------------------------------------------------------------------------
1 | class ThreadPool:
2 | """A Rust-managed thread pool."""
3 | def __init__(self, num_threads: int) -> None:
4 | """Construct a new ThreadPool with the given number of threads."""
5 |
--------------------------------------------------------------------------------
/python/docs/api/store/aws.md:
--------------------------------------------------------------------------------
1 | # AWS S3
2 |
3 | ::: async_tiff.store.S3Store
4 | ::: async_tiff.store.S3Config
5 | options:
6 | show_if_no_docstring: true
7 | ::: async_tiff.store.S3Credential
8 | ::: async_tiff.store.S3CredentialProvider
9 |
--------------------------------------------------------------------------------
/python/python/async_tiff/__init__.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING
2 |
3 | from ._async_tiff import *
4 | from ._async_tiff import ___version
5 |
6 | if TYPE_CHECKING:
7 | from . import store
8 |
9 | __version__: str = ___version()
10 |
--------------------------------------------------------------------------------
/python/docs/api/store/gcs.md:
--------------------------------------------------------------------------------
1 | # Google Cloud Storage
2 |
3 | ::: async_tiff.store.GCSStore
4 | ::: async_tiff.store.GCSConfig
5 | options:
6 | show_if_no_docstring: true
7 | ::: async_tiff.store.GCSCredential
8 | ::: async_tiff.store.GCSCredentialProvider
9 |
--------------------------------------------------------------------------------
/src/geo/mod.rs:
--------------------------------------------------------------------------------
1 | //! Support for GeoTIFF files.
2 |
3 | mod affine;
4 | mod geo_key_directory;
5 | mod partial_reads;
6 |
7 | pub use affine::AffineTransform;
8 | pub use geo_key_directory::GeoKeyDirectory;
9 | pub(crate) use geo_key_directory::GeoKeyTag;
10 |
--------------------------------------------------------------------------------
/python/docs/api/store/azure.md:
--------------------------------------------------------------------------------
1 | # Microsoft Azure
2 |
3 | ::: async_tiff.store.AzureStore
4 | ::: async_tiff.store.AzureAccessKey
5 | ::: async_tiff.store.AzureConfig
6 | options:
7 | show_if_no_docstring: true
8 | ::: async_tiff.store.AzureSASToken
9 | ::: async_tiff.store.AzureBearerToken
10 | ::: async_tiff.store.AzureCredential
11 | ::: async_tiff.store.AzureCredentialProvider
12 |
--------------------------------------------------------------------------------
/python/python/async_tiff/_async_tiff.pyi:
--------------------------------------------------------------------------------
1 | from ._decoder import Decoder as Decoder
2 | from ._decoder import DecoderRegistry as DecoderRegistry
3 | from ._geo import GeoKeyDirectory as GeoKeyDirectory
4 | from ._ifd import ImageFileDirectory as ImageFileDirectory
5 | from ._thread_pool import ThreadPool as ThreadPool
6 | from ._tiff import ObspecInput as ObspecInput
7 | from ._tiff import TIFF as TIFF
8 | from ._tile import Tile as Tile
9 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![doc = include_str!("../README.md")]
2 | #![warn(missing_docs)]
3 |
4 | pub mod reader;
5 | // TODO: maybe rename this mod
6 | pub mod decoder;
7 | pub mod error;
8 | pub mod geo;
9 | mod ifd;
10 | pub mod metadata;
11 | pub mod predictor;
12 | mod tag_value;
13 | pub mod tags;
14 | mod tiff;
15 | mod tile;
16 |
17 | pub use ifd::ImageFileDirectory;
18 | pub use tag_value::TagValue;
19 | pub use tiff::TIFF;
20 | pub use tile::Tile;
21 |
--------------------------------------------------------------------------------
/.github/workflows/conventional-commits.yml:
--------------------------------------------------------------------------------
1 | name: PR Conventional Commit Validation
2 |
3 | on:
4 | pull_request:
5 | types: [opened, synchronize, reopened, edited]
6 |
7 | jobs:
8 | validate-pr-title:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: PR Conventional Commit Validation
12 | uses: ytanikin/pr-conventional-commits@1.4.0
13 | with:
14 | task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]'
15 |
--------------------------------------------------------------------------------
/python/docs/overrides/main.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block content %}
4 | {% if page.nb_url %}
5 |
6 | {% include ".icons/material/download.svg" %}
7 |
8 | {% endif %}
9 |
10 | {{ super() }}
11 | {% endblock content %}
12 |
13 | {% block outdated %}
14 | You're not viewing the latest version.
15 |
16 | Click here to go to latest.
17 |
18 | {% endblock %}
19 |
--------------------------------------------------------------------------------
/.github/workflows/rust-release.yml:
--------------------------------------------------------------------------------
1 | name: Publish to crates.io
2 | on:
3 | push:
4 | # Triggers when pushing tags starting with 'rust-v'
5 | tags: ["rust-v*"]
6 |
7 | jobs:
8 | publish:
9 | runs-on: ubuntu-latest
10 | environment: rust-release
11 | permissions:
12 | id-token: write # Required for OIDC token exchange
13 | steps:
14 | - uses: actions/checkout@v5
15 | - uses: rust-lang/crates-io-auth-action@v1
16 | id: auth
17 | - run: cargo publish
18 | env:
19 | CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
20 |
--------------------------------------------------------------------------------
/show_image.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from rasterio.plot import show, reshape_as_raster
3 |
4 | with open("img.buf", "rb") as f:
5 | buffer = f.read()
6 |
7 | arr = np.frombuffer(buffer, np.uint8)
8 | arr = arr.reshape(512, 512, 3)
9 | arr = reshape_as_raster(arr)
10 | show(arr, adjust=True)
11 |
12 | ###########
13 |
14 | with open("img_from_tiff.buf", "rb") as f:
15 | buffer = f.read()
16 |
17 | arr = np.frombuffer(buffer, np.uint8)
18 | arr.reshape()
19 |
20 | # We first need to reshape into "image" axes, then we need to reshape as "raster" axes.
21 | arr = arr.reshape(12410, 9680, 3)
22 | arr2 = reshape_as_raster(arr)
23 | show(arr2, adjust=True)
24 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Rust
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | test:
11 | name: Test
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 |
16 | - name: Install Rust
17 | uses: dtolnay/rust-toolchain@stable
18 |
19 | - uses: Swatinem/rust-cache@v2
20 |
21 | - run: cargo install cargo-all-features
22 |
23 | - name: Check all combinations of features can build
24 | run: cargo check-all-features
25 |
26 | - name: "cargo test"
27 | run: |
28 | cargo test --all
29 | cargo test --all --all-features
30 |
--------------------------------------------------------------------------------
/python/tests/test_exceptions.py:
--------------------------------------------------------------------------------
1 | """
2 | Unit tests to ensure that proper errors are raised instead of a panic.
3 | """
4 |
5 | import pytest
6 | from async_tiff.store import HTTPStore
7 |
8 | from async_tiff import TIFF
9 |
10 |
11 | async def test_raise_typeerror_fetch_tile_striped_tiff():
12 | """
13 | Ensure that a TypeError is raised when trying to fetch a tile from a striped TIFF.
14 | """
15 | store = HTTPStore(url="https://github.com/")
16 | path = "OSGeo/gdal/raw/refs/tags/v3.11.0/autotest/gdrivers/data/gtiff/int8.tif"
17 |
18 | tiff = await TIFF.open(path=path, store=store)
19 | assert len(tiff.ifds) >= 1
20 |
21 | with pytest.raises(TypeError):
22 | await tiff.fetch_tile(0, 0, 0)
23 |
--------------------------------------------------------------------------------
/src/geo/partial_reads.rs:
--------------------------------------------------------------------------------
1 | #[allow(dead_code)]
2 | struct TileMetadata {
3 | /// top left corner of the partial read
4 | tlx: f64,
5 | tly: f64,
6 | /// width and height of the partial read (# of pixels)
7 | width: usize,
8 | height: usize,
9 | /// width and height of each block (# of pixels)
10 | tile_width: usize,
11 | tile_height: usize,
12 | /// range of internal x/y blocks which intersect the partial read
13 | xmin: usize,
14 | ymin: usize,
15 | xmax: usize,
16 | ymax: usize,
17 | /// expected number of bands
18 | bands: usize,
19 | /// numpy data type
20 | // dtype: np.dtype,
21 | /// overview level (where 0 is source)
22 | ovr_level: usize,
23 | }
24 |
--------------------------------------------------------------------------------
/tests/image_tiff/images/README.txt:
--------------------------------------------------------------------------------
1 | The test files in this directory mostly have dimension 157x151.
2 | The naming convention is
3 |
4 | photometric-channels-bits.tiff
5 |
6 | minisblack-1c-16b.tiff
7 | minisblack-1c-8b.tiff
8 | miniswhite-1c-1b.tiff
9 | palette-1c-1b.tiff
10 | palette-1c-4b.tiff
11 | palette-1c-8b.tiff
12 | rgb-3c-16b.tiff
13 | rgb-3c-8b.tiff
14 |
15 | logluv-3c-16b.tiff: logluv compression/photometric interp
16 | minisblack-2c-8b-alpha.tiff: grey+alpha
17 |
18 | BMP files (anchient BMPv2 since v3 does not work):
19 |
20 | palette-1c-8b.bmp
21 | rgb-3c-8b.bmp
22 |
23 | GIF files (anchient GIF '87 since modern GIF does not work):
24 | palette-1c-8b.gif
25 |
26 | PNM files:
27 | minisblack-1c-8b.pgm
28 | miniswhite-1c-1b.pbm
29 | rgb-3c-8b.ppm
30 |
--------------------------------------------------------------------------------
/tests/image_tiff/util.rs:
--------------------------------------------------------------------------------
1 | use std::env::current_dir;
2 | use std::sync::Arc;
3 |
4 | use async_tiff::metadata::TiffMetadataReader;
5 | use async_tiff::reader::{AsyncFileReader, ObjectReader};
6 | use async_tiff::TIFF;
7 | use object_store::local::LocalFileSystem;
8 |
9 | const TEST_IMAGE_DIR: &str = "tests/image_tiff/images/";
10 |
11 | pub(crate) async fn open_tiff(filename: &str) -> TIFF {
12 | let store = Arc::new(LocalFileSystem::new_with_prefix(current_dir().unwrap()).unwrap());
13 | let path = format!("{TEST_IMAGE_DIR}/{filename}");
14 | let reader = Arc::new(ObjectReader::new(store.clone(), path.as_str().into()))
15 | as Arc;
16 | let mut metadata_reader = TiffMetadataReader::try_open(&reader).await.unwrap();
17 | metadata_reader.read(&reader).await.unwrap()
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode
3 | *.tif
4 | *.buf
5 |
6 | # Generated by Cargo
7 | # will have compiled files and executables
8 | debug/
9 | target/
10 |
11 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
12 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
13 | Cargo.lock
14 |
15 | # These are backup files generated by rustfmt
16 | **/*.rs.bk
17 |
18 | # MSVC Windows builds of rustc generate these, which store debugging information
19 | *.pdb
20 |
21 | # RustRover
22 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
23 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
24 | # and can be added to the global gitignore or merged into this file. For a more nuclear
25 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
26 | #.idea/
27 |
28 | # Added by cargo
29 |
30 | /target
31 |
--------------------------------------------------------------------------------
/tests/geo.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 | use std::sync::Arc;
3 |
4 | use async_tiff::metadata::cache::ReadaheadMetadataCache;
5 | use async_tiff::metadata::TiffMetadataReader;
6 | use async_tiff::reader::{AsyncFileReader, ObjectReader};
7 | use object_store::local::LocalFileSystem;
8 |
9 | #[tokio::test]
10 | async fn test_parse_file_with_unknown_geokey() {
11 | let folder = env::current_dir().unwrap();
12 | let path = object_store::path::Path::parse("tests/images/geogtowgs_subset_USGS_13_s14w171.tif")
13 | .unwrap();
14 | let store = Arc::new(LocalFileSystem::new_with_prefix(folder).unwrap());
15 | let reader = Arc::new(ObjectReader::new(store, path)) as Arc;
16 | let prefetch_reader = ReadaheadMetadataCache::new(reader.clone());
17 | let mut metadata_reader = TiffMetadataReader::try_open(&prefetch_reader)
18 | .await
19 | .unwrap();
20 | let _ = metadata_reader
21 | .read_all_ifds(&prefetch_reader)
22 | .await
23 | .unwrap();
24 | }
25 |
--------------------------------------------------------------------------------
/python/docs/overrides/stylesheets/extra.css:
--------------------------------------------------------------------------------
1 | :root,
2 | [data-md-color-scheme="default"] {
3 | /* --md-heading-font: "Oswald"; */
4 | --md-primary-fg-color: #cf3f02;
5 | --md-default-fg-color: #443f3f;
6 | --boxShadowD: 0px 12px 24px 0px rgba(68, 63, 63, 0.08),
7 | 0px 0px 4px 0px rgba(68, 63, 63, 0.08);
8 | }
9 | body {
10 | margin: 0;
11 | padding: 0;
12 | /* font-size: 16px; */
13 | }
14 | h1,
15 | h2,
16 | h3,
17 | h4,
18 | h5,
19 | h6 {
20 | font-family: var(--md-heading-font);
21 | font-weight: bold;
22 | }
23 | .md-typeset h1,
24 | .md-typeset h2 {
25 | font-weight: normal;
26 | color: var(--md-default-fg-color);
27 | }
28 | .md-typeset h3,
29 | .md-typeset h4 {
30 | font-weight: bold;
31 | color: var(--md-default-fg-color);
32 | }
33 | .md-button,
34 | .md-typeset .md-button {
35 | font-family: var(--md-heading-font);
36 | }
37 | .md-content .supheading {
38 | font-family: var(--md-heading-font);
39 | text-transform: uppercase;
40 | color: var(--md-primary-fg-color);
41 | font-size: 0.75rem;
42 | font-weight: bold;
43 | }
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Development Seed
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 |
--------------------------------------------------------------------------------
/tests/images/readme.md:
--------------------------------------------------------------------------------
1 | `geogtowgs_subset_USGS_13_s14w171.tif` was created from "s3://prd-tnm/StagedProducts/Elevation/13/TIFF/current/s14w171/USGS_13_s14w171.tif" using these commands:
2 |
3 | ```bash
4 | gdal_translate USGS_13_s14w171.tif tiny.tif -srcwin 0 0 1 1 -co COMPRESS=DEFLATE
5 | listgeo USGS_13_s14w171.tif > metadata.txt # Then modify to remove information related to spatial extent
6 | cp tiny.tif geogtowgs_subset_USGS_13_s14w171.tif
7 | geotifcp -g metadata.txt tiny.tif geogtowgs_subset_USGS_13_s14w171.tif
8 | listgeo geogtowgs_subset_USGS_13_s14w171.tif
9 | ```
10 |
11 | and this workspace definition:
12 |
13 | ```toml
14 | [project]
15 | name = "gdal-workspace"
16 | version = "0.1.0"
17 | description = "workspace for using gdal via pixi"
18 | readme = "README.md"
19 | requires-python = ">=3.12"
20 | dependencies = []
21 |
22 | [tool.pixi.workspace]
23 | channels = ["conda-forge"]
24 | platforms = ["osx-arm64"]
25 |
26 | [tool.pixi.pypi-dependencies]
27 | gdal-workspace = { path = ".", editable = true }
28 |
29 | [tool.pixi.tasks]
30 |
31 | [tool.pixi.dependencies]
32 | gdal = ">=3.11.5,<4"
33 | libgdal = ">=3.11.5,<4"
34 | geotiff = ">=1.7.4,<2"
35 | ```
--------------------------------------------------------------------------------
/python/python/async_tiff/_decoder.pyi:
--------------------------------------------------------------------------------
1 | from typing import Protocol
2 | from collections.abc import Buffer
3 |
4 | from .enums import CompressionMethod
5 |
6 | class Decoder(Protocol):
7 | """A custom Python-provided decompression algorithm."""
8 | # In the future, we could pass in photometric interpretation and jpeg tables as
9 | # well.
10 | @staticmethod
11 | def __call__(buffer: Buffer) -> Buffer:
12 | """A callback to decode compressed data."""
13 |
14 | class DecoderRegistry:
15 | """A registry holding multiple decoder methods."""
16 | def __init__(
17 | self, custom_decoders: dict[CompressionMethod | int, Decoder] | None = None
18 | ) -> None:
19 | """Construct a new decoder registry.
20 |
21 | By default, pure-Rust decoders will be used for any recognized and supported
22 | compression types. Only the supplied decoders will override Rust-native
23 | decoders.
24 |
25 | Args:
26 | custom_decoders: any custom decoder methods to use. This will be applied
27 | _after_ (and override) any default provided Rust decoders. Defaults to
28 | None.
29 | """
30 |
--------------------------------------------------------------------------------
/tests/image_tiff/decode_bigtiff_images.rs:
--------------------------------------------------------------------------------
1 | extern crate tiff;
2 |
3 | use async_tiff::tags::PhotometricInterpretation;
4 |
5 | use crate::image_tiff::util::open_tiff;
6 |
7 | #[tokio::test]
8 | async fn test_big_tiff() {
9 | let filenames = [
10 | "bigtiff/BigTIFF.tif",
11 | "bigtiff/BigTIFFMotorola.tif",
12 | "bigtiff/BigTIFFLong.tif",
13 | ];
14 | for filename in filenames.iter() {
15 | let tiff = open_tiff(filename).await;
16 | let ifd = &tiff.ifds()[0];
17 | assert_eq!(ifd.image_height(), 64);
18 | assert_eq!(ifd.image_width(), 64);
19 | assert_eq!(
20 | ifd.photometric_interpretation(),
21 | PhotometricInterpretation::RGB
22 | );
23 | assert!(ifd.bits_per_sample().iter().all(|x| *x == 8));
24 | assert_eq!(
25 | ifd.strip_offsets().expect("Cannot get StripOffsets"),
26 | vec![16]
27 | );
28 | assert_eq!(ifd.rows_per_strip().expect("Cannot get RowsPerStrip"), 64);
29 | assert_eq!(
30 | ifd.strip_byte_counts().expect("Cannot get StripByteCounts"),
31 | vec![12288]
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # async-tiff
2 |
3 | An async, low-level [TIFF](https://en.wikipedia.org/wiki/TIFF) reader for Rust and Python.
4 |
5 | [**Rust documentation**](https://docs.rs/async-tiff/)
6 | [**Python documentation**](https://developmentseed.org/async-tiff/latest/)
7 |
8 | ## Features
9 |
10 | - Support for tiled TIFF images.
11 | - Read directly from object storage providers, via the `object_store` crate.
12 | - Support for user-defined decompression algorithms.
13 | - Tile request merging and concurrency.
14 |
15 | ## Background
16 |
17 | The existing [`tiff` crate](https://crates.io/crates/tiff) is great, but only supports synchronous reading of TIFF files. Furthermore, due to low maintenance bandwidth it is not designed for extensibility (see [#250](https://github.com/image-rs/image-tiff/issues/250)).
18 |
19 | It additionally exposes geospatial-specific TIFF tag metadata.
20 |
21 | ### Tests
22 |
23 | Download the following file for use in the tests.
24 |
25 | ```shell
26 | aws s3 cp s3://naip-visualization/ny/2022/60cm/rgb/40073/m_4007307_sw_18_060_20220803.tif ./ --request-payer
27 | aws s3 cp s3://prd-tnm/StagedProducts/Elevation/13/TIFF/current/s14w171/USGS_13_s14w171.tif ./ --no-sign-request --region us-west-2
28 | ```
29 |
--------------------------------------------------------------------------------
/tests/image_tiff/images/COPYRIGHT:
--------------------------------------------------------------------------------
1 | Copyright (c) 1988-1997 Sam Leffler
2 | Copyright (c) 1991-1997 Silicon Graphics, Inc.
3 |
4 | Permission to use, copy, modify, distribute, and sell this software and
5 | its documentation for any purpose is hereby granted without fee, provided
6 | that (i) the above copyright notices and this permission notice appear in
7 | all copies of the software and related documentation, and (ii) the names of
8 | Sam Leffler and Silicon Graphics may not be used in any advertising or
9 | publicity relating to the software without the specific, prior written
10 | permission of Sam Leffler and Silicon Graphics.
11 |
12 | THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND,
13 | EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY
14 | WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
15 |
16 | IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR
17 | ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND,
18 | OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
19 | WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF
20 | LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
21 | OF THIS SOFTWARE.
22 |
--------------------------------------------------------------------------------
/python/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["maturin>=1.4.0,<2.0"]
3 | build-backend = "maturin"
4 |
5 | [project]
6 | name = "async-tiff"
7 | requires-python = ">=3.10"
8 | dependencies = ["obspec>=0.1.0-beta.3"]
9 | dynamic = ["version"]
10 | classifiers = [
11 | "Programming Language :: Rust",
12 | "Programming Language :: Python :: Implementation :: CPython",
13 | "Programming Language :: Python :: Implementation :: PyPy",
14 | ]
15 |
16 | [tool.maturin]
17 | features = ["pyo3/extension-module"]
18 | module-name = "async_tiff._async_tiff"
19 | python-source = "python"
20 |
21 | [tool.uv]
22 | dev-dependencies = [
23 | # To enable following symlinks for pyi files
24 | "griffe>=1.6.0",
25 | "griffe-inherited-docstrings>=1.0.1",
26 | "ipykernel>=6.29.5",
27 | "maturin>=1.7.4",
28 | "mike>=2.1.3",
29 | "mkdocs-material[imaging]>=9.6.3",
30 | "mkdocs>=1.6.1",
31 | "mkdocstrings-python>=1.13.0",
32 | "mkdocstrings>=0.27.0",
33 | "numpy>=1",
34 | "obstore>=0.5.1",
35 | "pytest-asyncio>=0.26.0",
36 | "pytest>=8.3.3",
37 | "ruff>=0.8.4",
38 | ]
39 |
40 | [tool.pytest.ini_options]
41 | addopts = "--color=yes"
42 | asyncio_default_fixture_loop_scope = "function"
43 | asyncio_mode = "auto"
44 |
--------------------------------------------------------------------------------
/python/python/async_tiff/_tile.pyi:
--------------------------------------------------------------------------------
1 | from collections.abc import Buffer
2 |
3 | from .enums import CompressionMethod
4 | from ._decoder import DecoderRegistry
5 | from ._thread_pool import ThreadPool
6 |
7 | class Tile:
8 | """A representation of a TIFF image tile."""
9 | @property
10 | def x(self) -> int:
11 | """The column index this tile represents."""
12 | @property
13 | def y(self) -> int:
14 | """The row index this tile represents."""
15 | @property
16 | def compressed_bytes(self) -> Buffer:
17 | """The compressed bytes underlying this tile."""
18 | @property
19 | def compression_method(self) -> CompressionMethod | int:
20 | """The compression method used by this tile."""
21 | async def decode_async(
22 | self,
23 | *,
24 | decoder_registry: DecoderRegistry | None = None,
25 | pool: ThreadPool | None = None,
26 | ) -> Buffer:
27 | """Decode this tile's data.
28 |
29 | Keyword Args:
30 | decoder_registry: the decoders to use for decompression. Defaults to None.
31 | pool: the thread pool on which to run decompression. Defaults to None.
32 |
33 | Returns:
34 | Decoded tile data as a buffer.
35 | """
36 |
--------------------------------------------------------------------------------
/python/python/async_tiff/enums.py:
--------------------------------------------------------------------------------
1 | from enum import IntEnum
2 |
3 |
4 | class CompressionMethod(IntEnum):
5 | """
6 | See [TIFF compression
7 | tags](https://www.awaresystems.be/imaging/tiff/tifftags/compression.html) for
8 | reference.
9 | """
10 |
11 | Uncompressed = 1
12 | Huffman = 2
13 | Fax3 = 3
14 | Fax4 = 4
15 | LZW = 5
16 | JPEG = 6
17 | # // "Extended JPEG" or "new JPEG" style
18 | ModernJPEG = 7
19 | Deflate = 8
20 | OldDeflate = 0x80B2
21 | PackBits = 0x8005
22 |
23 |
24 | class Endianness(IntEnum):
25 | LittleEndian = 0
26 | BigEndian = 1
27 |
28 |
29 | class PhotometricInterpretation(IntEnum):
30 | WhiteIsZero = 0
31 | BlackIsZero = 1
32 | RGB = 2
33 | RGBPalette = 3
34 | TransparencyMask = 4
35 | CMYK = 5
36 | YCbCr = 6
37 | CIELab = 8
38 |
39 |
40 | class PlanarConfiguration(IntEnum):
41 | Chunky = 1
42 | Planar = 2
43 |
44 |
45 | class Predictor(IntEnum):
46 | Unknown = 1
47 | Horizontal = 2
48 | FloatingPoint = 3
49 |
50 |
51 | class ResolutionUnit(IntEnum):
52 | Unknown = 1
53 | Inch = 2
54 | Centimeter = 3
55 |
56 |
57 | class SampleFormat(IntEnum):
58 | Uint = 1
59 | Int = 2
60 | IEEEFP = 3
61 | Void = 4
62 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | push:
5 | branches: ["main"]
6 | pull_request:
7 | types: [opened, reopened, synchronize]
8 | branches: ["main"]
9 |
10 | permissions: {}
11 |
12 | # Make sure CI fails on all warnings, including Clippy lints
13 | env:
14 | CARGO_TERM_COLOR: always
15 | RUSTFLAGS: "-Dwarnings"
16 |
17 | jobs:
18 | clippy_check:
19 | runs-on: ubuntu-latest
20 |
21 | steps:
22 | - name: Checkout repository
23 | uses: actions/checkout@v6
24 | with:
25 | persist-credentials: false
26 |
27 | - name: Run Clippy
28 | run: |
29 | cargo clippy --all-targets --all-features
30 | cargo clippy --all-targets --all-features --manifest-path python/Cargo.toml
31 |
32 | format_check:
33 | runs-on: ubuntu-latest
34 |
35 | steps:
36 | - name: Checkout repository
37 | uses: actions/checkout@v6
38 | with:
39 | persist-credentials: false
40 |
41 | - name: Run Rustfmt
42 | run: |
43 | cargo fmt --all --check -- --config imports_granularity=Module,group_imports=StdExternalCrate
44 | cargo fmt --all --check --manifest-path python/Cargo.toml -- --config imports_granularity=Module,group_imports=StdExternalCrate
45 |
--------------------------------------------------------------------------------
/tests/image_tiff/decode_geotiff_images.rs:
--------------------------------------------------------------------------------
1 | extern crate tiff;
2 |
3 | use crate::image_tiff::util::open_tiff;
4 |
5 | #[tokio::test]
6 | async fn test_geo_tiff() {
7 | let filenames = ["geo-5b.tif"];
8 | for filename in filenames.iter() {
9 | let tiff = open_tiff(filename).await;
10 | let ifd = &tiff.ifds()[0];
11 | dbg!(&ifd);
12 | assert_eq!(ifd.image_height(), 10);
13 | assert_eq!(ifd.image_width(), 10);
14 | assert_eq!(ifd.bits_per_sample(), vec![16; 5]);
15 | assert_eq!(
16 | ifd.strip_offsets().expect("Cannot get StripOffsets"),
17 | vec![418]
18 | );
19 | assert_eq!(ifd.rows_per_strip().expect("Cannot get RowsPerStrip"), 10);
20 | assert_eq!(
21 | ifd.strip_byte_counts().expect("Cannot get StripByteCounts"),
22 | vec![1000]
23 | );
24 | assert_eq!(
25 | ifd.model_pixel_scale().expect("Cannot get pixel scale"),
26 | vec![60.0, 60.0, 0.0]
27 | );
28 |
29 | // We don't currently support reading strip images
30 | // let DecodingResult::I16(data) = decoder.read_image().unwrap() else {
31 | // panic!("Cannot read band data")
32 | // };
33 | // assert_eq!(data.len(), 500);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "async-tiff"
3 | version = "0.1.0"
4 | edition = "2021"
5 | authors = ["Kyle Barron "]
6 | license = "MIT OR Apache-2.0"
7 | repository = "https://github.com/developmentseed/async-tiff"
8 | description = "Low-level asynchronous TIFF reader."
9 | readme = "README.md"
10 |
11 | [dependencies]
12 | async-trait = "0.1.89"
13 | byteorder = "1"
14 | bytes = "1.7.0"
15 | flate2 = "1.0.20"
16 | futures = "0.3.31"
17 | jpeg = { package = "jpeg-decoder", version = "0.3.0", default-features = false }
18 | num_enum = "0.7.3"
19 | object_store = { version = "0.12", optional = true }
20 | reqwest = { version = "0.12", default-features = false, optional = true }
21 | thiserror = "1"
22 | tokio = { version = "1.43.0", default-features = false, features = ["sync"] }
23 | weezl = "0.1.0"
24 | zstd = "0.13"
25 |
26 | [dev-dependencies]
27 | object_store = { version = "0.12", features = ["http"] }
28 | tiff = "0.9.1"
29 | tokio = { version = "1.9", features = [
30 | "macros",
31 | "fs",
32 | "rt-multi-thread",
33 | "io-util",
34 | ] }
35 | tokio-test = "0.4.4"
36 |
37 | [features]
38 | default = ["object_store", "reqwest"]
39 | tokio = ["tokio/io-util"]
40 | reqwest = ["dep:reqwest"]
41 | object_store = ["dep:object_store"]
42 |
43 | [package.metadata.cargo-all-features]
44 |
--------------------------------------------------------------------------------
/python/src/error.rs:
--------------------------------------------------------------------------------
1 | use async_tiff::error::AsyncTiffError;
2 | use pyo3::create_exception;
3 | use pyo3::exceptions::PyFileNotFoundError;
4 | use pyo3::prelude::*;
5 |
6 | create_exception!(
7 | async_tiff,
8 | AsyncTiffException,
9 | pyo3::exceptions::PyException,
10 | "A general error from the underlying Rust async-tiff library."
11 | );
12 |
13 | #[allow(missing_docs)]
14 | pub enum PyAsyncTiffError {
15 | AsyncTiffError(async_tiff::error::AsyncTiffError),
16 | }
17 |
18 | impl From for PyAsyncTiffError {
19 | fn from(value: AsyncTiffError) -> Self {
20 | Self::AsyncTiffError(value)
21 | }
22 | }
23 |
24 | impl From for PyErr {
25 | fn from(error: PyAsyncTiffError) -> Self {
26 | match error {
27 | PyAsyncTiffError::AsyncTiffError(err) => match err {
28 | AsyncTiffError::ObjectStore(err) => match err {
29 | object_store::Error::NotFound { path: _, source: _ } => {
30 | PyFileNotFoundError::new_err(err.to_string())
31 | }
32 | _ => AsyncTiffException::new_err(err.to_string()),
33 | },
34 | _ => AsyncTiffException::new_err(err.to_string()),
35 | },
36 | }
37 | }
38 | }
39 |
40 | pub type PyAsyncTiffResult = std::result::Result;
41 |
--------------------------------------------------------------------------------
/python/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "py-async-tiff"
3 | version = "0.3.0"
4 | authors = ["Kyle Barron "]
5 | edition = "2021"
6 | # description = "Fast, memory-efficient 2D spatial indexes for Python."
7 | readme = "README.md"
8 | repository = "https://github.com/developmentseed/async-tiff"
9 | license = "MIT OR Apache-2.0"
10 | keywords = ["python", "geospatial"]
11 | categories = ["science::geo"]
12 | rust-version = "1.75"
13 |
14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
15 | [lib]
16 | name = "_async_tiff"
17 | crate-type = ["cdylib"]
18 |
19 | [features]
20 | abi3-py311 = ["pyo3/abi3-py311"]
21 | extension-module = ["pyo3/extension-module"]
22 |
23 | [dependencies]
24 | async-tiff = { path = "../" }
25 | async-trait = "0.1.89"
26 | bytes = "1.10.1"
27 | futures = "0.3.31"
28 | object_store = "0.12"
29 | pyo3 = { version = "0.27.0", features = ["macros"] }
30 | pyo3-async-runtimes = "0.27"
31 | pyo3-bytes = "0.5"
32 | pyo3-object_store = "0.7.0"
33 | rayon = "1.11.0"
34 | tokio-rayon = "2.1.0"
35 | thiserror = "1"
36 |
37 | # We opt-in to using rustls as the TLS provider for reqwest, which is the HTTP
38 | # library used by object_store.
39 | # https://github.com/seanmonstar/reqwest/issues/2025
40 | reqwest = { version = "*", default-features = false, features = [
41 | "rustls-tls-native-roots",
42 | ] }
43 |
44 | [profile.release]
45 | lto = true
46 | codegen-units = 1
47 |
--------------------------------------------------------------------------------
/python/tests/test_cog.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from async_tiff import TIFF, enums
4 | from async_tiff.store import LocalStore, S3Store
5 |
6 |
7 | async def test_cog_s3():
8 | """
9 | Ensure that TIFF.open can open a Sentinel-2 Cloud-Optimized GeoTIFF file from an
10 | s3 bucket, read IFDs and GeoKeyDirectory metadata.
11 | """
12 | path = "sentinel-s2-l2a-cogs/12/S/UF/2022/6/S2B_12SUF_20220609_0_L2A/B04.tif"
13 | store = S3Store("sentinel-cogs", region="us-west-2", skip_signature=True)
14 | tiff = await TIFF.open(path=path, store=store)
15 |
16 | assert tiff.endianness == enums.Endianness.LittleEndian
17 |
18 | ifds = tiff.ifds
19 | assert len(ifds) == 5
20 |
21 | ifd = ifds[0]
22 | assert ifd.compression == enums.CompressionMethod.Deflate
23 | assert ifd.tile_height == 1024
24 | assert ifd.tile_width == 1024
25 | assert ifd.photometric_interpretation == enums.PhotometricInterpretation.BlackIsZero
26 |
27 | gkd = ifd.geo_key_directory
28 | assert gkd is not None, "GeoKeyDirectory should exist"
29 | assert gkd.citation == "WGS 84 / UTM zone 12N"
30 | assert gkd.projected_type == 32612
31 |
32 |
33 | async def test_cog_missing_file():
34 | """
35 | Ensure that a FileNotFoundError is raised when passing in a missing file.
36 | """
37 | store = LocalStore()
38 | with pytest.raises(FileNotFoundError):
39 | await TIFF.open(path="imaginary_file.tif", store=store)
40 |
--------------------------------------------------------------------------------
/python/src/thread_pool.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | use pyo3::exceptions::PyValueError;
4 | use pyo3::prelude::*;
5 | use pyo3::sync::PyOnceLock;
6 | use rayon::{ThreadPool, ThreadPoolBuilder};
7 |
8 | static DEFAULT_POOL: PyOnceLock> = PyOnceLock::new();
9 |
10 | pub fn get_default_pool(py: Python<'_>) -> PyResult> {
11 | let runtime = DEFAULT_POOL.get_or_try_init(py, || {
12 | let pool = ThreadPoolBuilder::new().build().map_err(|err| {
13 | PyValueError::new_err(format!("Could not create rayon threadpool. {err}"))
14 | })?;
15 | Ok::<_, PyErr>(Arc::new(pool))
16 | })?;
17 | Ok(runtime.clone())
18 | }
19 |
20 | #[pyclass(name = "ThreadPool", frozen, module = "async_tiff")]
21 | pub(crate) struct PyThreadPool(Arc);
22 |
23 | #[pymethods]
24 | impl PyThreadPool {
25 | #[new]
26 | fn new(num_threads: usize) -> PyResult {
27 | let pool = ThreadPoolBuilder::new()
28 | .num_threads(num_threads)
29 | .build()
30 | .map_err(|err| {
31 | PyValueError::new_err(format!("Could not create rayon threadpool. {err}"))
32 | })?;
33 | Ok(Self(Arc::new(pool)))
34 | }
35 | }
36 |
37 | impl PyThreadPool {
38 | pub(crate) fn inner(&self) -> &Arc {
39 | &self.0
40 | }
41 | }
42 |
43 | impl AsRef for PyThreadPool {
44 | fn as_ref(&self) -> &ThreadPool {
45 | &self.0
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/geo/affine.rs:
--------------------------------------------------------------------------------
1 | use crate::ImageFileDirectory;
2 |
3 | /// Affine transformation values.
4 | #[derive(Debug)]
5 | pub struct AffineTransform(f64, f64, f64, f64, f64, f64);
6 |
7 | impl AffineTransform {
8 | /// Construct a new Affine Transform
9 | pub fn new(a: f64, b: f64, xoff: f64, d: f64, e: f64, yoff: f64) -> Self {
10 | Self(a, b, xoff, d, e, yoff)
11 | }
12 |
13 | /// a
14 | pub fn a(&self) -> f64 {
15 | self.0
16 | }
17 |
18 | /// b
19 | pub fn b(&self) -> f64 {
20 | self.1
21 | }
22 |
23 | /// c
24 | pub fn c(&self) -> f64 {
25 | self.2
26 | }
27 |
28 | /// d
29 | pub fn d(&self) -> f64 {
30 | self.3
31 | }
32 |
33 | /// e
34 | pub fn e(&self) -> f64 {
35 | self.4
36 | }
37 |
38 | /// f
39 | pub fn f(&self) -> f64 {
40 | self.5
41 | }
42 |
43 | /// Construct a new Affine Transform from the IFD
44 | pub fn from_ifd(ifd: &ImageFileDirectory) -> Option {
45 | if let (Some(model_pixel_scale), Some(model_tiepoint)) =
46 | (&ifd.model_pixel_scale, &ifd.model_tiepoint)
47 | {
48 | Some(Self::new(
49 | model_pixel_scale[0],
50 | 0.0,
51 | model_tiepoint[3],
52 | 0.0,
53 | -model_pixel_scale[1],
54 | model_tiepoint[4],
55 | ))
56 | } else {
57 | None
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/.github/workflows/test-python.yml:
--------------------------------------------------------------------------------
1 | name: Test Python
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | test:
11 | name: Test
12 | runs-on: ubuntu-latest
13 | defaults:
14 | run:
15 | working-directory: python
16 | steps:
17 | - uses: actions/checkout@v3
18 |
19 | - name: Install Rust
20 | uses: dtolnay/rust-toolchain@stable
21 |
22 | - uses: Swatinem/rust-cache@v2
23 |
24 | - name: "cargo check"
25 | run: cargo check --all --all-features
26 |
27 | - name: "cargo test"
28 | run: |
29 | cargo test --all
30 | cargo test --all --all-features
31 |
32 | pytest:
33 | name: Pytest
34 | runs-on: ubuntu-latest
35 | strategy:
36 | matrix:
37 | python-version: ["3.10", "3.11", "3.12", "3.13"]
38 | steps:
39 | - uses: actions/checkout@v4
40 | - name: Install Rust
41 | uses: dtolnay/rust-toolchain@stable
42 |
43 | - uses: Swatinem/rust-cache@v2
44 | - uses: actions/setup-python@v5
45 | with:
46 | python-version: ${{ matrix.python-version }}
47 |
48 | - name: Install a specific version of uv
49 | uses: astral-sh/setup-uv@v6
50 | with:
51 | version: "latest"
52 |
53 | - name: uv sync
54 | working-directory: python
55 | run: uv sync --no-install-package async-tiff
56 |
57 | - name: maturin venv Build
58 | working-directory: python
59 | run: uv run --no-project maturin develop
60 |
61 | - name: Run pytest
62 | working-directory: python
63 | run: uv run --no-project pytest --verbose
64 |
--------------------------------------------------------------------------------
/python/tests/test_ifd.py:
--------------------------------------------------------------------------------
1 | from async_tiff import TIFF
2 | from async_tiff.enums import (
3 | CompressionMethod,
4 | PhotometricInterpretation,
5 | PlanarConfiguration,
6 | SampleFormat,
7 | )
8 | from async_tiff.store import LocalStore
9 | from pathlib import Path
10 |
11 |
12 | FIXTURES_DIR = Path(__file__).parent.parent.parent / "tests" / "images"
13 |
14 |
15 | async def load_tiff(filename: str):
16 | path = FIXTURES_DIR / filename
17 | store = LocalStore()
18 | tiff = await TIFF.open(path=str(path), store=store)
19 | return tiff
20 |
21 |
22 | async def test_ifd_dict():
23 | filename = "geogtowgs_subset_USGS_13_s14w171.tif"
24 | tiff = await load_tiff(filename)
25 | first_ifd = tiff.ifds[0]
26 |
27 | expected_ifd = {
28 | "image_width": 1,
29 | "image_height": 1,
30 | "bits_per_sample": [32],
31 | "compression": CompressionMethod.Deflate,
32 | "photometric_interpretation": PhotometricInterpretation.BlackIsZero,
33 | "samples_per_pixel": 1,
34 | "planar_configuration": PlanarConfiguration.Chunky,
35 | "sample_format": [SampleFormat.IEEEFP],
36 | "other_tags": {},
37 | "strip_offsets": [8],
38 | "rows_per_strip": 1,
39 | "strip_byte_counts": [15],
40 | "geo_key_directory": first_ifd.geo_key_directory,
41 | }
42 | assert dict(first_ifd) == expected_ifd
43 |
44 | gkd = first_ifd.geo_key_directory
45 | assert gkd is not None
46 | expected_gkd = {
47 | "model_type": 2,
48 | "raster_type": 1,
49 | "geographic_type": 4269,
50 | "geog_citation": "NAD83",
51 | "geog_angular_units": 9102,
52 | "geog_semi_major_axis": 6378137.0,
53 | "geog_inv_flattening": 298.257222101004,
54 | }
55 | assert dict(gkd) == expected_gkd
56 |
--------------------------------------------------------------------------------
/python/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![deny(clippy::undocumented_unsafe_blocks)]
2 |
3 | mod decoder;
4 | mod enums;
5 | mod error;
6 | mod geo;
7 | mod ifd;
8 | mod reader;
9 | mod thread_pool;
10 | mod tiff;
11 | mod tile;
12 | mod value;
13 |
14 | use pyo3::prelude::*;
15 |
16 | use crate::decoder::PyDecoderRegistry;
17 | use crate::geo::PyGeoKeyDirectory;
18 | use crate::ifd::PyImageFileDirectory;
19 | use crate::thread_pool::PyThreadPool;
20 | use crate::tiff::PyTIFF;
21 |
22 | const VERSION: &str = env!("CARGO_PKG_VERSION");
23 |
24 | #[pyfunction]
25 | fn ___version() -> &'static str {
26 | VERSION
27 | }
28 |
29 | /// Raise RuntimeWarning for debug builds
30 | #[pyfunction]
31 | fn check_debug_build(_py: Python) -> PyResult<()> {
32 | #[cfg(debug_assertions)]
33 | {
34 | use pyo3::exceptions::PyRuntimeWarning;
35 | use pyo3::intern;
36 | use pyo3::types::PyTuple;
37 |
38 | let warnings_mod = _py.import(intern!(_py, "warnings"))?;
39 | let warning = PyRuntimeWarning::new_err(
40 | "async-tiff has not been compiled in release mode. Performance will be degraded.",
41 | );
42 | let args = PyTuple::new(_py, vec![warning])?;
43 | warnings_mod.call_method1(intern!(_py, "warn"), args)?;
44 | }
45 |
46 | Ok(())
47 | }
48 |
49 | #[pymodule]
50 | fn _async_tiff(py: Python, m: &Bound) -> PyResult<()> {
51 | check_debug_build(py)?;
52 |
53 | m.add_wrapped(wrap_pyfunction!(___version))?;
54 | m.add_class::()?;
55 | m.add_class::()?;
56 | m.add_class::()?;
57 | m.add_class::()?;
58 | m.add_class::()?;
59 |
60 | pyo3_object_store::register_store_module(py, m, "async_tiff", "store")?;
61 | pyo3_object_store::register_exceptions_module(py, m, "async_tiff", "exceptions")?;
62 |
63 | Ok(())
64 | }
65 |
--------------------------------------------------------------------------------
/python/src/value.rs:
--------------------------------------------------------------------------------
1 | use async_tiff::TagValue;
2 | use pyo3::exceptions::PyRuntimeError;
3 | use pyo3::prelude::*;
4 | use pyo3::IntoPyObjectExt;
5 |
6 | pub struct PyValue(TagValue);
7 |
8 | impl<'py> IntoPyObject<'py> for PyValue {
9 | type Target = PyAny;
10 | type Output = Bound<'py, Self::Target>;
11 | type Error = PyErr;
12 |
13 | fn into_pyobject(self, py: Python<'py>) -> Result {
14 | match self.0 {
15 | TagValue::Byte(val) => val.into_bound_py_any(py),
16 | TagValue::Short(val) => val.into_bound_py_any(py),
17 | TagValue::SignedByte(val) => val.into_bound_py_any(py),
18 | TagValue::SignedShort(val) => val.into_bound_py_any(py),
19 | TagValue::Signed(val) => val.into_bound_py_any(py),
20 | TagValue::SignedBig(val) => val.into_bound_py_any(py),
21 | TagValue::Unsigned(val) => val.into_bound_py_any(py),
22 | TagValue::UnsignedBig(val) => val.into_bound_py_any(py),
23 | TagValue::Float(val) => val.into_bound_py_any(py),
24 | TagValue::Double(val) => val.into_bound_py_any(py),
25 | TagValue::List(val) => val
26 | .into_iter()
27 | .map(|v| PyValue(v).into_bound_py_any(py))
28 | .collect::>>()?
29 | .into_bound_py_any(py),
30 | TagValue::Rational(num, denom) => (num, denom).into_bound_py_any(py),
31 | TagValue::RationalBig(num, denom) => (num, denom).into_bound_py_any(py),
32 | TagValue::SRational(num, denom) => (num, denom).into_bound_py_any(py),
33 | TagValue::SRationalBig(num, denom) => (num, denom).into_bound_py_any(py),
34 | TagValue::Ascii(val) => val.into_bound_py_any(py),
35 | TagValue::Ifd(_val) => Err(PyRuntimeError::new_err("Unsupported value type 'Ifd'")),
36 | TagValue::IfdBig(_val) => {
37 | Err(PyRuntimeError::new_err("Unsupported value type 'IfdBig'"))
38 | }
39 | v => Err(PyRuntimeError::new_err(format!(
40 | "Unknown value type: {v:?}"
41 | ))),
42 | }
43 | }
44 | }
45 |
46 | impl From for PyValue {
47 | fn from(value: TagValue) -> Self {
48 | Self(value)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/python/python/async_tiff/_tiff.pyi:
--------------------------------------------------------------------------------
1 | from typing import Protocol
2 |
3 | # Fix exports
4 | from obspec._get import GetRangeAsync, GetRangesAsync
5 |
6 | from ._ifd import ImageFileDirectory
7 | from ._tile import Tile
8 | from .enums import Endianness
9 | from .store import ObjectStore
10 |
11 | class ObspecInput(GetRangeAsync, GetRangesAsync, Protocol):
12 | """Supported obspec input to reader."""
13 |
14 | class TIFF:
15 | @classmethod
16 | async def open(
17 | cls,
18 | path: str,
19 | *,
20 | store: ObjectStore | ObspecInput,
21 | prefetch: int = 32768,
22 | multiplier: int | float = 2.0,
23 | ) -> TIFF:
24 | """Open a new TIFF.
25 |
26 | Args:
27 | path: The path within the store to read from.
28 | store: The backend to use for data fetching.
29 | prefetch: The number of initial bytes to read up front.
30 | multiplier: The multiplier to use for readahead size growth. Must be
31 | greater than 1.0. For example, for a value of `2.0`, the first metadata
32 | read will be of size `prefetch`, and then the next read will be of size
33 | `prefetch * 2`.
34 |
35 | Returns:
36 | A TIFF instance.
37 | """
38 |
39 | @property
40 | def endianness(self) -> Endianness:
41 | """The endianness of this TIFF file."""
42 | @property
43 | def ifds(self) -> list[ImageFileDirectory]:
44 | """Access the underlying IFDs of this TIFF.
45 |
46 | Each ImageFileDirectory (IFD) represents one of the internal "sub images" of
47 | this file.
48 | """
49 | async def fetch_tile(self, x: int, y: int, z: int) -> Tile:
50 | """Fetch a single tile.
51 |
52 | Args:
53 | x: The column index within the ifd to read from.
54 | y: The row index within the ifd to read from.
55 | z: The IFD index to read from.
56 |
57 | Returns:
58 | Tile response.
59 | """
60 | async def fetch_tiles(self, x: list[int], y: list[int], z: int) -> list[Tile]:
61 | """Fetch multiple tiles concurrently.
62 |
63 | Args:
64 | x: The column indexes within the ifd to read from.
65 | y: The row indexes within the ifd to read from.
66 | z: The IFD index to read from.
67 |
68 | Returns:
69 | Tile responses.
70 | """
71 |
--------------------------------------------------------------------------------
/.github/workflows/python-docs-publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish Python docs
2 |
3 | # Only run on new tags starting with `py-v`
4 | on:
5 | push:
6 | tags:
7 | - "py-v*"
8 | workflow_dispatch:
9 |
10 | # https://stackoverflow.com/a/77412363
11 | permissions:
12 | contents: write
13 | pages: write
14 |
15 | jobs:
16 | build:
17 | name: Deploy Python docs
18 | runs-on: ubuntu-latest
19 | defaults:
20 | run:
21 | working-directory: python
22 | steps:
23 | - uses: actions/checkout@v4
24 | # We need to additionally fetch the gh-pages branch for mike deploy
25 | with:
26 | fetch-depth: 0
27 | submodules: "true"
28 |
29 | - name: Install Rust
30 | uses: dtolnay/rust-toolchain@stable
31 |
32 | - uses: Swatinem/rust-cache@v2
33 |
34 | - name: Install a specific version of uv
35 | uses: astral-sh/setup-uv@v5
36 | with:
37 | enable-cache: true
38 | version: "0.5.x"
39 |
40 | - name: Set up Python 3.11
41 | run: uv python install 3.11
42 |
43 | - name: Install dependencies
44 | run: uv sync --no-install-package async-tiff
45 |
46 | - name: Build python packages
47 | run: |
48 | uv run --no-project maturin develop
49 |
50 | - name: Deploy docs
51 | env:
52 | GIT_COMMITTER_NAME: CI
53 | GIT_COMMITTER_EMAIL: ci-bot@example.com
54 | run: |
55 | # Get most recent git tag
56 | # https://stackoverflow.com/a/7261049
57 | # https://stackoverflow.com/a/3867811
58 | # We don't use {{github.ref_name}} because if triggered manually, it
59 | # will be a branch name instead of a tag version.
60 | # Then remove `py-` from the tag
61 | VERSION=$(git describe --tags --match="py-*" --abbrev=0 | cut -c 4-)
62 |
63 | # Only push publish docs as latest version if no letters in git tag
64 | # after the first character
65 | # (usually the git tag will have v as the first character)
66 | # Note the `cut` index is 1-ordered
67 | if echo $VERSION | cut -c 2- | grep -q "[A-Za-z]"; then
68 | echo "Is beta version"
69 | # For beta versions publish but don't set as latest
70 | uv run --no-project mike deploy $VERSION --update-aliases --push
71 | else
72 | echo "Is NOT beta version"
73 | uv run --no-project mike deploy $VERSION latest --update-aliases --push
74 | fi
75 |
--------------------------------------------------------------------------------
/python/README.md:
--------------------------------------------------------------------------------
1 | # async-tiff
2 |
3 | [![PyPI][pypi_badge]][pypi_link]
4 |
5 | [pypi_badge]: https://badge.fury.io/py/async-tiff.svg
6 | [pypi_link]: https://pypi.org/project/async-tiff/
7 |
8 | Fast, low-level async TIFF and GeoTIFF reader for Python.
9 |
10 | This documentation is for the Python bindings. [Refer here for the Rust crate documentation](https://docs.rs/async-tiff).
11 |
12 | ## Examples
13 |
14 | ### Reading NAIP
15 |
16 | ```py
17 | from async_tiff import TIFF
18 | from async_tiff.store import S3Store
19 |
20 | # You'll also need to provide credentials to access a requester pays bucket
21 | store = S3Store("naip-visualization", region="us-west-2", request_payer=True)
22 | path = "ny/2022/60cm/rgb/40073/m_4007307_sw_18_060_20220803.tif"
23 |
24 | tiff = await TIFF.open(path, store=store)
25 | primary_ifd = tiff.ifds[0]
26 |
27 | primary_ifd.geo_key_directory.citation
28 | # 'NAD83 / UTM zone 18N'
29 |
30 | primary_ifd.geo_key_directory.projected_type
31 | # 26918
32 | # (EPSG code)
33 |
34 | primary_ifd.sample_format
35 | # [, , ]
36 |
37 | primary_ifd.bits_per_sample
38 | # [8, 8, 8]
39 |
40 | tile = await tiff.fetch_tile(0, 0, 4)
41 | decoded_bytes = await tile.decode_async()
42 |
43 | # Use rasterio and matplotlib for visualization
44 | import numpy as np
45 | from rasterio.plot import reshape_as_raster, show
46 |
47 | # Wrap the rust buffer into a numpy array
48 | arr = np.frombuffer(decoded_bytes, np.uint8)
49 |
50 | # We first need to reshape the array into the *existing* "image" axes
51 | arr = arr.reshape(512, 512, 3)
52 |
53 | # Then we need to reshape the "image" axes into "raster" axes
54 | # https://rasterio.readthedocs.io/en/stable/topics/image_processing.html
55 | arr = reshape_as_raster(arr)
56 | show(arr, adjust=True)
57 | ```
58 |
59 | 
60 |
61 |
62 | ### Reading Sentinel 2 L2A
63 |
64 | ```py
65 | from async_tiff import TIFF
66 | from async_tiff.store import S3Store
67 |
68 | store = S3Store("sentinel-cogs", region="us-west-2", skip_signature=True)
69 | path = "sentinel-s2-l2a-cogs/12/S/UF/2022/6/S2B_12SUF_20220609_0_L2A/B04.tif"
70 |
71 | tiff = await TIFF.open(path, store=store)
72 | primary_ifd = tiff.ifds[0]
73 | # Text readable citation
74 | primary_ifd.geo_key_directory.citation
75 | # EPSG code
76 | primary_ifd.geo_key_directory.projected_type
77 |
78 | primary_ifd.sample_format[0]
79 | #
80 | primary_ifd.bits_per_sample[0]
81 | # 16
82 |
83 | tile = await tiff.fetch_tile(0, 0, 0)
84 | decoded_bytes = await tile.decode_async()
85 | ```
86 |
87 |
--------------------------------------------------------------------------------
/python/src/tile.rs:
--------------------------------------------------------------------------------
1 | use async_tiff::Tile;
2 | use pyo3::exceptions::PyValueError;
3 | use pyo3::prelude::*;
4 | use pyo3_async_runtimes::tokio::future_into_py;
5 | use pyo3_bytes::PyBytes;
6 | use tokio_rayon::AsyncThreadPool;
7 |
8 | use crate::decoder::get_default_decoder_registry;
9 | use crate::enums::PyCompressionMethod;
10 | use crate::thread_pool::{get_default_pool, PyThreadPool};
11 | use crate::PyDecoderRegistry;
12 |
13 | #[pyclass(name = "Tile")]
14 | pub(crate) struct PyTile(Option);
15 |
16 | impl PyTile {
17 | pub(crate) fn new(tile: Tile) -> Self {
18 | Self(Some(tile))
19 | }
20 | }
21 |
22 | #[pymethods]
23 | impl PyTile {
24 | #[getter]
25 | fn x(&self) -> PyResult {
26 | self.0
27 | .as_ref()
28 | .ok_or(PyValueError::new_err("Tile has been consumed"))
29 | .map(|t| t.x())
30 | }
31 |
32 | #[getter]
33 | fn y(&self) -> PyResult {
34 | self.0
35 | .as_ref()
36 | .ok_or(PyValueError::new_err("Tile has been consumed"))
37 | .map(|t| t.y())
38 | }
39 |
40 | #[getter]
41 | fn compressed_bytes(&self) -> PyResult {
42 | let tile = self
43 | .0
44 | .as_ref()
45 | .ok_or(PyValueError::new_err("Tile has been consumed"))?;
46 | Ok(tile.compressed_bytes().clone().into())
47 | }
48 |
49 | #[getter]
50 | fn compression_method(&self) -> PyResult {
51 | self.0
52 | .as_ref()
53 | .ok_or(PyValueError::new_err("Tile has been consumed"))
54 | .map(|t| t.compression_method().into())
55 | }
56 |
57 | #[pyo3(signature = (*, decoder_registry=None, pool=None))]
58 | fn decode_async<'py>(
59 | &mut self,
60 | py: Python<'py>,
61 | decoder_registry: Option<&PyDecoderRegistry>,
62 | pool: Option<&PyThreadPool>,
63 | ) -> PyResult> {
64 | let decoder_registry = decoder_registry
65 | .map(|r| r.inner().clone())
66 | .unwrap_or_else(|| get_default_decoder_registry(py));
67 | let pool = pool
68 | .map(|p| Ok(p.inner().clone()))
69 | .unwrap_or_else(|| get_default_pool(py))?;
70 | let tile = self.0.take().unwrap();
71 |
72 | future_into_py(py, async move {
73 | let decoded_bytes = pool
74 | .spawn_async(move || tile.decode(&decoder_registry))
75 | .await
76 | .unwrap();
77 | Ok(PyBytes::new(decoded_bytes))
78 | })
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/metadata/mod.rs:
--------------------------------------------------------------------------------
1 | //! API for reading metadata out of a TIFF file.
2 | //!
3 | //! ### Reading all TIFF metadata
4 | //!
5 | //! We can use [`TiffMetadataReader::read_all_ifds`] to read all IFDs up front:
6 | //!
7 | //! ```
8 | //! # tokio_test::block_on(async {
9 | //! use std::env::current_dir;
10 | //! use std::sync::Arc;
11 | //!
12 | //! use object_store::local::LocalFileSystem;
13 | //!
14 | //! use async_tiff::metadata::TiffMetadataReader;
15 | //! use async_tiff::metadata::cache::ReadaheadMetadataCache;
16 | //! use async_tiff::reader::ObjectReader;
17 | //!
18 | //! // Create new Arc
19 | //! let store = Arc::new(LocalFileSystem::new_with_prefix(current_dir().unwrap()).unwrap());
20 | //!
21 | //! // Create new ObjectReader to map the ObjectStore to the AsyncFileReader trait
22 | //! let reader = ObjectReader::new(
23 | //! store,
24 | //! "tests/image_tiff/images/tiled-jpeg-rgb-u8.tif".into(),
25 | //! );
26 | //!
27 | //! // Use ReadaheadMetadataCache to ensure that a given number of bytes at the start of the
28 | //! // file are prefetched, and to ensure that any additional fetches are made in larger chunks.
29 | //! //
30 | //! // The `ReadaheadMetadataCache` or a similar caching layer should **always** be used to ensure
31 | //! // that the underlying small read calls that the TiffMetadataReader makes don't translate to
32 | //! // individual tiny network fetches.
33 | //! let cached_reader = ReadaheadMetadataCache::new(reader.clone());
34 | //!
35 | //! // Create a TiffMetadataReader wrapping some MetadataFetch
36 | //! let mut metadata_reader = TiffMetadataReader::try_open(&cached_reader)
37 | //! .await
38 | //! .unwrap();
39 | //!
40 | //! // Read all IFDs out of the source.
41 | //! let ifds = metadata_reader
42 | //! .read_all_ifds(&cached_reader)
43 | //! .await
44 | //! .unwrap();
45 | //! # })
46 | //! ```
47 | //!
48 | //!
49 | //! ### Caching/prefetching/buffering
50 | //!
51 | //! The underlying [`ImageFileDirectoryReader`] used to read tags out of the TIFF file reads each
52 | //! tag individually. This means that it will make many small byte range requests to the
53 | //! [`MetadataFetch`] implementation.
54 | //!
55 | //! Thus, it is **imperative to always supply some sort of caching, prefetching, or buffering**
56 | //! middleware when reading metadata. [`ReadaheadMetadataCache`] is an example of this, which
57 | //! fetches the first `N` bytes out of a file, and then multiplies the size of any subsequent
58 | //! fetches by a given `multiplier`.
59 |
60 | pub mod cache;
61 | mod fetch;
62 | mod reader;
63 |
64 | pub use fetch::MetadataFetch;
65 | pub use reader::{ImageFileDirectoryReader, TiffMetadataReader};
66 |
--------------------------------------------------------------------------------
/src/tiff.rs:
--------------------------------------------------------------------------------
1 | use crate::ifd::ImageFileDirectory;
2 | use crate::reader::Endianness;
3 |
4 | /// A TIFF file.
5 | #[derive(Debug, Clone)]
6 | pub struct TIFF {
7 | endianness: Endianness,
8 | ifds: Vec,
9 | }
10 |
11 | impl TIFF {
12 | /// Create a new TIFF from existing IFDs.
13 | pub fn new(ifds: Vec, endianness: Endianness) -> Self {
14 | Self { ifds, endianness }
15 | }
16 |
17 | /// Access the underlying Image File Directories.
18 | pub fn ifds(&self) -> &[ImageFileDirectory] {
19 | &self.ifds
20 | }
21 |
22 | /// Get the endianness of the TIFF file.
23 | pub fn endianness(&self) -> Endianness {
24 | self.endianness
25 | }
26 | }
27 |
28 | #[cfg(test)]
29 | mod test {
30 | use std::io::BufReader;
31 | use std::sync::Arc;
32 |
33 | use object_store::local::LocalFileSystem;
34 | use tiff::decoder::{DecodingResult, Limits};
35 |
36 | use super::*;
37 | use crate::metadata::cache::ReadaheadMetadataCache;
38 | use crate::metadata::TiffMetadataReader;
39 | use crate::reader::{AsyncFileReader, ObjectReader};
40 |
41 | #[ignore = "local file"]
42 | #[tokio::test]
43 | async fn tmp() {
44 | let folder = "/Users/kyle/github/developmentseed/async-tiff/";
45 | let path = object_store::path::Path::parse("m_4007307_sw_18_060_20220803.tif").unwrap();
46 | let store = Arc::new(LocalFileSystem::new_with_prefix(folder).unwrap());
47 | let reader = Arc::new(ObjectReader::new(store, path)) as Arc;
48 | let cached_reader = ReadaheadMetadataCache::new(reader.clone());
49 | let mut metadata_reader = TiffMetadataReader::try_open(&cached_reader).await.unwrap();
50 | let ifds = metadata_reader.read_all_ifds(&cached_reader).await.unwrap();
51 | let tiff = TIFF::new(ifds, metadata_reader.endianness());
52 |
53 | let ifd = &tiff.ifds[1];
54 | let tile = ifd.fetch_tile(0, 0, reader.as_ref()).await.unwrap();
55 | let tile = tile.decode(&Default::default()).unwrap();
56 | std::fs::write("img.buf", tile).unwrap();
57 | }
58 |
59 | #[ignore = "local file"]
60 | #[test]
61 | fn tmp_tiff_example() {
62 | let path = "/Users/kyle/github/developmentseed/async-tiff/m_4007307_sw_18_060_20220803.tif";
63 | let reader = std::fs::File::open(path).unwrap();
64 | let mut decoder = tiff::decoder::Decoder::new(BufReader::new(reader))
65 | .unwrap()
66 | .with_limits(Limits::unlimited());
67 | let result = decoder.read_image().unwrap();
68 | match result {
69 | DecodingResult::U8(content) => std::fs::write("img_from_tiff.buf", content).unwrap(),
70 | _ => todo!(),
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/python/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [0.3.0] - 2025-12-12
4 |
5 | ### What's Changed
6 |
7 | - feat: Exponential read-ahead cache by @kylebarron in https://github.com/developmentseed/async-tiff/pull/140
8 | - feat(python): implement Mapping protocol for IFD and GeoKeyDirectory by @kylebarron in https://github.com/developmentseed/async-tiff/pull/148
9 | - feat: Include Endianness as property of TIFF struct by @kylebarron in https://github.com/developmentseed/async-tiff/pull/149
10 | - fix: Handle non utf-8 characters in OME-XML by @weiji14 in https://github.com/developmentseed/async-tiff/pull/141
11 | - feat: Add ZSTD Decoder by @nivdee in https://github.com/developmentseed/async-tiff/pull/157
12 | - refactor: Use `pyclass(get_all)` for cleaner code by @kylebarron in https://github.com/developmentseed/async-tiff/pull/158
13 | - fix: Skip unknown GeoTag keys by @kylebarron in https://github.com/developmentseed/async-tiff/pull/134
14 | - ci: Deprecate Python 3.9, add testing on Python 3.13 by @kylebarron in https://github.com/developmentseed/async-tiff/pull/129
15 |
16 | ### New Contributors
17 |
18 | - @alukach made their first contribution in https://github.com/developmentseed/async-tiff/pull/138
19 | - @nivdee made their first contribution in https://github.com/developmentseed/async-tiff/pull/157
20 |
21 | **Full Changelog**: https://github.com/developmentseed/async-tiff/compare/py-v0.2.0...py-v0.3.0
22 |
23 | ## [0.2.0] - 2025-10-23
24 |
25 | ### What's Changed
26 |
27 | - Enable pytest-asyncio tests in CI by @weiji14 in https://github.com/developmentseed/async-tiff/pull/92
28 | - Raise FileNotFoundError instead of panic when opening missing files by @weiji14 in https://github.com/developmentseed/async-tiff/pull/93
29 | - Raise TypeError instead of panic on doing fetch_tile from striped TIFFs by @weiji14 in https://github.com/developmentseed/async-tiff/pull/99
30 | - Test opening single-channel OME-TIFF file by @weiji14 in https://github.com/developmentseed/async-tiff/pull/102
31 | - Remove broken symlink when building windows wheels by @maxrjones in https://github.com/developmentseed/async-tiff/pull/120
32 | - chore!: Bump minimum Python version to 3.10 by @kylebarron in https://github.com/developmentseed/async-tiff/pull/122
33 | - chore: Bump pyo3 to 0.26 by @kylebarron in https://github.com/developmentseed/async-tiff/pull/121
34 | - ci: Build abi3 wheels where possible by @kylebarron in https://github.com/developmentseed/async-tiff/pull/123
35 | - chore: Bump \_obstore submodule for latest store creation types #125
36 |
37 | ### New Contributors
38 |
39 | - @feefladder made their first contribution in https://github.com/developmentseed/async-tiff/pull/71
40 | - @weiji14 made their first contribution in https://github.com/developmentseed/async-tiff/pull/92
41 |
42 | **Full Changelog**: https://github.com/developmentseed/async-tiff/compare/py-v0.1.0...py-v0.1.1
43 |
44 | ## [0.1.0] - 2025-03-18
45 |
46 | - Initial release.
47 |
--------------------------------------------------------------------------------
/python/src/decoder.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 | use std::sync::Arc;
3 |
4 | use async_tiff::decoder::{Decoder, DecoderRegistry};
5 | use async_tiff::error::{AsyncTiffError, AsyncTiffResult};
6 | use async_tiff::tags::PhotometricInterpretation;
7 | use bytes::Bytes;
8 | use pyo3::exceptions::PyTypeError;
9 | use pyo3::intern;
10 | use pyo3::prelude::*;
11 | use pyo3::sync::PyOnceLock;
12 | use pyo3::types::{PyDict, PyTuple};
13 | use pyo3_bytes::PyBytes;
14 |
15 | use crate::enums::PyCompressionMethod;
16 |
17 | static DEFAULT_DECODER_REGISTRY: PyOnceLock> = PyOnceLock::new();
18 |
19 | pub fn get_default_decoder_registry(py: Python<'_>) -> Arc {
20 | let registry =
21 | DEFAULT_DECODER_REGISTRY.get_or_init(py, || Arc::new(DecoderRegistry::default()));
22 | registry.clone()
23 | }
24 |
25 | #[pyclass(name = "DecoderRegistry", frozen)]
26 | #[derive(Debug, Default)]
27 | pub(crate) struct PyDecoderRegistry(Arc);
28 |
29 | #[pymethods]
30 | impl PyDecoderRegistry {
31 | #[new]
32 | #[pyo3(signature = (custom_decoders = None))]
33 | pub(crate) fn new(custom_decoders: Option>) -> Self {
34 | let mut decoder_registry = DecoderRegistry::default();
35 | if let Some(custom_decoders) = custom_decoders {
36 | for (compression, decoder) in custom_decoders.into_iter() {
37 | decoder_registry
38 | .as_mut()
39 | .insert(compression.into(), Box::new(decoder));
40 | }
41 | }
42 | Self(Arc::new(decoder_registry))
43 | }
44 | }
45 | impl PyDecoderRegistry {
46 | pub(crate) fn inner(&self) -> &Arc {
47 | &self.0
48 | }
49 | }
50 |
51 | #[derive(Debug)]
52 | pub(crate) struct PyDecoder(Py);
53 |
54 | impl PyDecoder {
55 | fn call(&self, py: Python, buffer: Bytes) -> PyResult {
56 | let kwargs = PyDict::new(py);
57 | kwargs.set_item(intern!(py, "buffer"), PyBytes::new(buffer))?;
58 | let result = self.0.call(py, PyTuple::empty(py), Some(&kwargs))?;
59 | result.extract(py)
60 | }
61 | }
62 |
63 | impl<'py> FromPyObject<'_, 'py> for PyDecoder {
64 | type Error = PyErr;
65 |
66 | fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result {
67 | if !obj.hasattr(intern!(obj.py(), "__call__"))? {
68 | return Err(PyTypeError::new_err(
69 | "Expected callable object for custom decoder.",
70 | ));
71 | }
72 | Ok(Self(obj.as_unbound().clone_ref(obj.py())))
73 | }
74 | }
75 |
76 | impl Decoder for PyDecoder {
77 | fn decode_tile(
78 | &self,
79 | buffer: Bytes,
80 | _photometric_interpretation: PhotometricInterpretation,
81 | _jpeg_tables: Option<&[u8]>,
82 | ) -> AsyncTiffResult {
83 | let decoded_buffer = Python::attach(|py| self.call(py, buffer))
84 | .map_err(|err| AsyncTiffError::General(err.to_string()))?;
85 | Ok(decoded_buffer.into_inner())
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/tile.rs:
--------------------------------------------------------------------------------
1 | use bytes::Bytes;
2 |
3 | use crate::decoder::DecoderRegistry;
4 | use crate::error::{AsyncTiffResult, TiffError, TiffUnsupportedError};
5 | use crate::predictor::{fix_endianness, unpredict_float, unpredict_hdiff, PredictorInfo};
6 | use crate::tags::{CompressionMethod, PhotometricInterpretation, Predictor};
7 |
8 | /// A TIFF Tile response.
9 | ///
10 | /// This contains the required information to decode the tile. Decoding is separated from fetching
11 | /// so that sync and async operations can be separated and non-blocking.
12 | ///
13 | /// This is returned by `fetch_tile`.
14 | ///
15 | /// A strip of a stripped tiff is an image-width, rows-per-strip tile.
16 | #[derive(Debug)]
17 | pub struct Tile {
18 | pub(crate) x: usize,
19 | pub(crate) y: usize,
20 | pub(crate) predictor: Predictor,
21 | pub(crate) predictor_info: PredictorInfo,
22 | pub(crate) compressed_bytes: Bytes,
23 | pub(crate) compression_method: CompressionMethod,
24 | pub(crate) photometric_interpretation: PhotometricInterpretation,
25 | pub(crate) jpeg_tables: Option,
26 | }
27 |
28 | impl Tile {
29 | /// The column index of this tile.
30 | pub fn x(&self) -> usize {
31 | self.x
32 | }
33 |
34 | /// The row index of this tile.
35 | pub fn y(&self) -> usize {
36 | self.y
37 | }
38 |
39 | /// Access the compressed bytes underlying this tile.
40 | ///
41 | /// Note that [`Bytes`] is reference-counted, so it is very cheap to clone if needed.
42 | pub fn compressed_bytes(&self) -> &Bytes {
43 | &self.compressed_bytes
44 | }
45 |
46 | /// Access the compression tag representing this tile.
47 | pub fn compression_method(&self) -> CompressionMethod {
48 | self.compression_method
49 | }
50 |
51 | /// Access the photometric interpretation tag representing this tile.
52 | pub fn photometric_interpretation(&self) -> PhotometricInterpretation {
53 | self.photometric_interpretation
54 | }
55 |
56 | /// Access the JPEG Tables, if any, from the IFD producing this tile.
57 | ///
58 | /// Note that [`Bytes`] is reference-counted, so it is very cheap to clone if needed.
59 | pub fn jpeg_tables(&self) -> Option<&Bytes> {
60 | self.jpeg_tables.as_ref()
61 | }
62 |
63 | /// Decode this tile.
64 | ///
65 | /// Decoding is separate from fetching so that sync and async operations do not block the same
66 | /// runtime.
67 | pub fn decode(self, decoder_registry: &DecoderRegistry) -> AsyncTiffResult {
68 | let decoder = decoder_registry
69 | .as_ref()
70 | .get(&self.compression_method)
71 | .ok_or(TiffError::UnsupportedError(
72 | TiffUnsupportedError::UnsupportedCompressionMethod(self.compression_method),
73 | ))?;
74 |
75 | let decoded_tile = decoder.decode_tile(
76 | self.compressed_bytes.clone(),
77 | self.photometric_interpretation,
78 | self.jpeg_tables.as_deref(),
79 | )?;
80 |
81 | match self.predictor {
82 | Predictor::None => Ok(fix_endianness(
83 | decoded_tile,
84 | self.predictor_info.endianness(),
85 | self.predictor_info.bits_per_sample(),
86 | )),
87 | Predictor::Horizontal => {
88 | unpredict_hdiff(decoded_tile, &self.predictor_info, self.x as _)
89 | }
90 | Predictor::FloatingPoint => {
91 | unpredict_float(decoded_tile, &self.predictor_info, self.x as _, self.y as _)
92 | }
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/python/.gitignore:
--------------------------------------------------------------------------------
1 | *.whl
2 |
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | *.py[cod]
6 | *$py.class
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | wheels/
25 | share/python-wheels/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 | MANIFEST
30 |
31 | # PyInstaller
32 | # Usually these files are written by a python script from a template
33 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
34 | *.manifest
35 | *.spec
36 |
37 | # Installer logs
38 | pip-log.txt
39 | pip-delete-this-directory.txt
40 |
41 | # Unit test / coverage reports
42 | htmlcov/
43 | .tox/
44 | .nox/
45 | .coverage
46 | .coverage.*
47 | .cache
48 | nosetests.xml
49 | coverage.xml
50 | *.cover
51 | *.py,cover
52 | .hypothesis/
53 | .pytest_cache/
54 | cover/
55 |
56 | # Translations
57 | *.mo
58 | *.pot
59 |
60 | # Django stuff:
61 | *.log
62 | local_settings.py
63 | db.sqlite3
64 | db.sqlite3-journal
65 |
66 | # Flask stuff:
67 | instance/
68 | .webassets-cache
69 |
70 | # Scrapy stuff:
71 | .scrapy
72 |
73 | # Sphinx documentation
74 | docs/_build/
75 |
76 | # PyBuilder
77 | .pybuilder/
78 | target/
79 |
80 | # Jupyter Notebook
81 | .ipynb_checkpoints
82 |
83 | # IPython
84 | profile_default/
85 | ipython_config.py
86 |
87 | # pyenv
88 | # For a library or package, you might want to ignore these files since the code is
89 | # intended to run in multiple environments; otherwise, check them in:
90 | # .python-version
91 |
92 | # pipenv
93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
96 | # install all needed dependencies.
97 | #Pipfile.lock
98 |
99 | # poetry
100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
101 | # This is especially recommended for binary packages to ensure reproducibility, and is more
102 | # commonly ignored for libraries.
103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
104 | #poetry.lock
105 |
106 | # pdm
107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
108 | #pdm.lock
109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
110 | # in version control.
111 | # https://pdm.fming.dev/#use-with-ide
112 | .pdm.toml
113 |
114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
115 | __pypackages__/
116 |
117 | # Celery stuff
118 | celerybeat-schedule
119 | celerybeat.pid
120 |
121 | # SageMath parsed files
122 | *.sage.py
123 |
124 | # Environments
125 | .env
126 | .venv
127 | env/
128 | venv/
129 | ENV/
130 | env.bak/
131 | venv.bak/
132 |
133 | # Spyder project settings
134 | .spyderproject
135 | .spyproject
136 |
137 | # Rope project settings
138 | .ropeproject
139 |
140 | # mkdocs documentation
141 | /site
142 |
143 | # mypy
144 | .mypy_cache/
145 | .dmypy.json
146 | dmypy.json
147 |
148 | # Pyre type checker
149 | .pyre/
150 |
151 | # pytype static type analyzer
152 | .pytype/
153 |
154 | # Cython debug symbols
155 | cython_debug/
156 |
157 | # PyCharm
158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
160 | # and can be added to the global gitignore or merged into this file. For a more nuclear
161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
162 | #.idea/
163 |
--------------------------------------------------------------------------------
/python/python/async_tiff/_geo.pyi:
--------------------------------------------------------------------------------
1 | from collections.abc import Iterable
2 | from typing import Any
3 |
4 | class GeoKeyDirectory:
5 | def keys(self) -> list[str]:
6 | """A list of string keys representing the GeoKey fields."""
7 | def __eq__(self, value: object) -> bool: ...
8 | def __iter__(self) -> Iterable[str]:
9 | """An iterable of string keys representing the GeoKey fields."""
10 | def __getitem__(self, key: str) -> Any:
11 | """Access GeoKey fields by string key."""
12 |
13 | @property
14 | def model_type(self) -> int | None: ...
15 | @property
16 | def raster_type(self) -> int | None: ...
17 | @property
18 | def citation(self) -> str | None: ...
19 | @property
20 | def geographic_type(self) -> int | None: ...
21 | @property
22 | def geog_citation(self) -> str | None: ...
23 | @property
24 | def geog_geodetic_datum(self) -> int | None: ...
25 | @property
26 | def geog_prime_meridian(self) -> int | None: ...
27 | @property
28 | def geog_linear_units(self) -> int | None: ...
29 | @property
30 | def geog_linear_unit_size(self) -> float | None: ...
31 | @property
32 | def geog_angular_units(self) -> int | None: ...
33 | @property
34 | def geog_angular_unit_size(self) -> float | None: ...
35 | @property
36 | def geog_ellipsoid(self) -> int | None: ...
37 | @property
38 | def geog_semi_major_axis(self) -> float | None: ...
39 | @property
40 | def geog_semi_minor_axis(self) -> float | None: ...
41 | @property
42 | def geog_inv_flattening(self) -> float | None: ...
43 | @property
44 | def geog_azimuth_units(self) -> int | None: ...
45 | @property
46 | def geog_prime_meridian_long(self) -> float | None: ...
47 | @property
48 | def projected_type(self) -> int | None: ...
49 | @property
50 | def proj_citation(self) -> str | None: ...
51 | @property
52 | def projection(self) -> int | None: ...
53 | @property
54 | def proj_coord_trans(self) -> int | None: ...
55 | @property
56 | def proj_linear_units(self) -> int | None: ...
57 | @property
58 | def proj_linear_unit_size(self) -> float | None: ...
59 | @property
60 | def proj_std_parallel1(self) -> float | None: ...
61 | @property
62 | def proj_std_parallel2(self) -> float | None: ...
63 | @property
64 | def proj_nat_origin_long(self) -> float | None: ...
65 | @property
66 | def proj_nat_origin_lat(self) -> float | None: ...
67 | @property
68 | def proj_false_easting(self) -> float | None: ...
69 | @property
70 | def proj_false_northing(self) -> float | None: ...
71 | @property
72 | def proj_false_origin_long(self) -> float | None: ...
73 | @property
74 | def proj_false_origin_lat(self) -> float | None: ...
75 | @property
76 | def proj_false_origin_easting(self) -> float | None: ...
77 | @property
78 | def proj_false_origin_northing(self) -> float | None: ...
79 | @property
80 | def proj_center_long(self) -> float | None: ...
81 | @property
82 | def proj_center_lat(self) -> float | None: ...
83 | @property
84 | def proj_center_easting(self) -> float | None: ...
85 | @property
86 | def proj_center_northing(self) -> float | None: ...
87 | @property
88 | def proj_scale_at_nat_origin(self) -> float | None: ...
89 | @property
90 | def proj_scale_at_center(self) -> float | None: ...
91 | @property
92 | def proj_azimuth_angle(self) -> float | None: ...
93 | @property
94 | def proj_straight_vert_pole_long(self) -> float | None: ...
95 | @property
96 | def vertical(self) -> int | None: ...
97 | @property
98 | def vertical_citation(self) -> str | None: ...
99 | @property
100 | def vertical_datum(self) -> int | None: ...
101 | @property
102 | def vertical_units(self) -> int | None: ...
103 |
--------------------------------------------------------------------------------
/python/src/tiff.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | use async_tiff::metadata::cache::ReadaheadMetadataCache;
4 | use async_tiff::metadata::TiffMetadataReader;
5 | use async_tiff::reader::AsyncFileReader;
6 | use async_tiff::TIFF;
7 | use pyo3::exceptions::{PyIndexError, PyTypeError};
8 | use pyo3::prelude::*;
9 | use pyo3::types::PyType;
10 | use pyo3_async_runtimes::tokio::future_into_py;
11 |
12 | use crate::enums::PyEndianness;
13 | use crate::error::PyAsyncTiffResult;
14 | use crate::reader::StoreInput;
15 | use crate::tile::PyTile;
16 | use crate::PyImageFileDirectory;
17 |
18 | #[pyclass(name = "TIFF", frozen)]
19 | pub(crate) struct PyTIFF {
20 | tiff: TIFF,
21 | reader: Arc,
22 | }
23 |
24 | async fn open(
25 | reader: Arc,
26 | prefetch: u64,
27 | multiplier: f64,
28 | ) -> PyAsyncTiffResult {
29 | let metadata_fetch = ReadaheadMetadataCache::new(reader.clone())
30 | .with_initial_size(prefetch)
31 | .with_multiplier(multiplier);
32 | let mut metadata_reader = TiffMetadataReader::try_open(&metadata_fetch).await?;
33 | let tiff = metadata_reader.read(&metadata_fetch).await?;
34 | Ok(PyTIFF { tiff, reader })
35 | }
36 |
37 | #[pymethods]
38 | impl PyTIFF {
39 | #[classmethod]
40 | #[pyo3(signature = (path, *, store, prefetch=32768, multiplier=2.0))]
41 | fn open<'py>(
42 | _cls: &'py Bound,
43 | py: Python<'py>,
44 | path: String,
45 | store: StoreInput,
46 | prefetch: u64,
47 | multiplier: f64,
48 | ) -> PyResult> {
49 | let reader = store.into_async_file_reader(path);
50 |
51 | let cog_reader =
52 | future_into_py(
53 | py,
54 | async move { Ok(open(reader, prefetch, multiplier).await?) },
55 | )?;
56 | Ok(cog_reader)
57 | }
58 |
59 | #[getter]
60 | fn endianness(&self) -> PyEndianness {
61 | self.tiff.endianness().into()
62 | }
63 |
64 | #[getter]
65 | fn ifds(&self) -> Vec {
66 | self.tiff
67 | .ifds()
68 | .iter()
69 | .map(|ifd| ifd.clone().into())
70 | .collect()
71 | }
72 |
73 | fn fetch_tile<'py>(
74 | &'py self,
75 | py: Python<'py>,
76 | x: usize,
77 | y: usize,
78 | z: usize,
79 | ) -> PyResult> {
80 | let reader = self.reader.clone();
81 | let ifd = self
82 | .tiff
83 | .ifds()
84 | .as_ref()
85 | .get(z)
86 | .ok_or_else(|| PyIndexError::new_err(format!("No IFD found for z={z}")))?
87 | // TODO: avoid this clone; add Arc to underlying rust code?
88 | .clone();
89 | future_into_py(py, async move {
90 | let tile = ifd
91 | .fetch_tile(x, y, reader.as_ref())
92 | .await
93 | .map_err(|err| PyTypeError::new_err(err.to_string()))?;
94 |
95 | Ok(PyTile::new(tile))
96 | })
97 | }
98 |
99 | fn fetch_tiles<'py>(
100 | &'py self,
101 | py: Python<'py>,
102 | x: Vec,
103 | y: Vec,
104 | z: usize,
105 | ) -> PyResult> {
106 | let reader = self.reader.clone();
107 | let ifd = self
108 | .tiff
109 | .ifds()
110 | .as_ref()
111 | .get(z)
112 | .ok_or_else(|| PyIndexError::new_err(format!("No IFD found for z={z}")))?
113 | // TODO: avoid this clone; add Arc to underlying rust code?
114 | .clone();
115 | future_into_py(py, async move {
116 | let tiles = ifd
117 | .fetch_tiles(&x, &y, reader.as_ref())
118 | .await
119 | .map_err(|err| PyTypeError::new_err(err.to_string()))?;
120 | let py_tiles = tiles.into_iter().map(PyTile::new).collect::>();
121 | Ok(py_tiles)
122 | })
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/python/python/async_tiff/_ifd.pyi:
--------------------------------------------------------------------------------
1 | from collections.abc import Iterable
2 | from typing import Any
3 | from .enums import (
4 | CompressionMethod,
5 | PhotometricInterpretation,
6 | PlanarConfiguration,
7 | Predictor,
8 | ResolutionUnit,
9 | SampleFormat,
10 | )
11 | from ._geo import GeoKeyDirectory
12 |
13 | Value = int | float | str | tuple[int, int] | list[Value]
14 |
15 | class ImageFileDirectory:
16 | def keys(self) -> list[str]:
17 | """A list of string keys representing the IFD fields."""
18 | def __eq__(self, value: object) -> bool: ...
19 | def __iter__(self) -> Iterable[str]:
20 | """An iterable of string keys representing the IFD fields."""
21 | def __getitem__(self, key: str) -> Any:
22 | """Access IFD fields by string key."""
23 |
24 | @property
25 | def new_subfile_type(self) -> int | None: ...
26 | @property
27 | def image_width(self) -> int:
28 | """The number of columns in the image, i.e., the number of pixels per row."""
29 |
30 | @property
31 | def image_height(self) -> int:
32 | """The number of rows of pixels in the image."""
33 |
34 | @property
35 | def bits_per_sample(self) -> list[int]: ...
36 | @property
37 | def compression(self) -> CompressionMethod | int:
38 | """Access the compression tag.
39 |
40 | An `int` will be returned if the compression is not one of the values in
41 | `CompressionMethod`.
42 | """
43 | @property
44 | def photometric_interpretation(self) -> PhotometricInterpretation: ...
45 | @property
46 | def document_name(self) -> str | None: ...
47 | @property
48 | def image_description(self) -> str | None: ...
49 | @property
50 | def strip_offsets(self) -> list[int] | None: ...
51 | @property
52 | def orientation(self) -> int | None: ...
53 | @property
54 | def samples_per_pixel(self) -> int:
55 | """
56 | The number of components per pixel.
57 |
58 | SamplesPerPixel is usually 1 for bilevel, grayscale, and palette-color images.
59 | SamplesPerPixel is usually 3 for RGB images. If this value is higher,
60 | ExtraSamples should give an indication of the meaning of the additional
61 | channels.
62 | """
63 |
64 | @property
65 | def rows_per_strip(self) -> int | None: ...
66 | @property
67 | def strip_byte_counts(self) -> int | None: ...
68 | @property
69 | def min_sample_value(self) -> int | None: ...
70 | @property
71 | def max_sample_value(self) -> int | None: ...
72 | @property
73 | def x_resolution(self) -> float | None:
74 | """The number of pixels per ResolutionUnit in the ImageWidth direction."""
75 |
76 | @property
77 | def y_resolution(self) -> float | None:
78 | """The number of pixels per ResolutionUnit in the ImageLength direction."""
79 |
80 | @property
81 | def planar_configuration(self) -> PlanarConfiguration: ...
82 | @property
83 | def resolution_unit(self) -> ResolutionUnit | None: ...
84 | @property
85 | def software(self) -> str | None: ...
86 | @property
87 | def date_time(self) -> str | None: ...
88 | @property
89 | def artist(self) -> str | None: ...
90 | @property
91 | def host_computer(self) -> str | None: ...
92 | @property
93 | def predictor(self) -> Predictor | None: ...
94 | @property
95 | def tile_width(self) -> int | None: ...
96 | @property
97 | def tile_height(self) -> int | None: ...
98 | @property
99 | def tile_offsets(self) -> list[int] | None: ...
100 | @property
101 | def tile_byte_counts(self) -> list[int] | None: ...
102 | @property
103 | def extra_samples(self) -> list[int] | None: ...
104 | @property
105 | def sample_format(self) -> list[SampleFormat]: ...
106 | @property
107 | def jpeg_tables(self) -> bytes | None: ...
108 | @property
109 | def copyright(self) -> str | None: ...
110 | @property
111 | def geo_key_directory(self) -> GeoKeyDirectory | None: ...
112 | @property
113 | def model_pixel_scale(self) -> list[float] | None: ...
114 | @property
115 | def model_tiepoint(self) -> list[float] | None: ...
116 | @property
117 | def other_tags(self) -> dict[int, Value]: ...
118 |
--------------------------------------------------------------------------------
/src/metadata/fetch.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::Debug;
2 | use std::ops::Range;
3 |
4 | use async_trait::async_trait;
5 | use bytes::Bytes;
6 |
7 | use crate::error::AsyncTiffResult;
8 | use crate::reader::{AsyncFileReader, EndianAwareReader, Endianness};
9 |
10 | /// A data source that can be used with [`TiffMetadataReader`] and [`ImageFileDirectoryReader`] to
11 | /// load [`ImageFileDirectory`]s.
12 | ///
13 | /// Note that implementation is provided for [`AsyncFileReader`].
14 | #[async_trait]
15 | pub trait MetadataFetch: Debug + Send + Sync + 'static {
16 | /// Return a future that fetches the specified range of bytes asynchronously
17 | ///
18 | /// Note the returned type is a boxed future, often created by
19 | /// [futures::FutureExt::boxed]. See the trait documentation for an example.
20 | async fn fetch(&self, range: Range) -> AsyncTiffResult;
21 | }
22 |
23 | #[async_trait]
24 | impl MetadataFetch for T {
25 | async fn fetch(&self, range: Range) -> AsyncTiffResult {
26 | self.get_bytes(range).await
27 | }
28 | }
29 |
30 | pub(crate) struct MetadataCursor<'a, F: MetadataFetch> {
31 | fetch: &'a F,
32 | offset: u64,
33 | endianness: Endianness,
34 | }
35 |
36 | impl<'a, F: MetadataFetch> MetadataCursor<'a, F> {
37 | pub fn new(fetch: &'a F, endianness: Endianness) -> Self {
38 | Self {
39 | fetch,
40 | offset: 0,
41 | endianness,
42 | }
43 | }
44 |
45 | pub fn new_with_offset(fetch: &'a F, endianness: Endianness, offset: u64) -> Self {
46 | Self {
47 | fetch,
48 | offset,
49 | endianness,
50 | }
51 | }
52 |
53 | pub fn with_offset(mut self, offset: u64) -> Self {
54 | self.offset = offset;
55 | self
56 | }
57 |
58 | pub fn seek(&mut self, offset: u64) {
59 | self.offset = offset;
60 | }
61 |
62 | /// Advance cursor position by a set amount
63 | pub(crate) fn advance(&mut self, amount: u64) {
64 | self.offset += amount;
65 | }
66 |
67 | /// Read the given number of bytes, advancing the internal cursor state by the same amount.
68 | pub(crate) async fn read(&mut self, length: u64) -> AsyncTiffResult {
69 | let range = self.offset as _..(self.offset + length) as _;
70 | self.offset += length;
71 | let bytes = self.fetch.fetch(range).await?;
72 | Ok(EndianAwareReader::new(bytes, self.endianness))
73 | }
74 |
75 | /// Read a u8 from the cursor, advancing the internal state by 1 byte.
76 | pub(crate) async fn read_u8(&mut self) -> AsyncTiffResult {
77 | self.read(1).await?.read_u8()
78 | }
79 |
80 | /// Read a i8 from the cursor, advancing the internal state by 1 byte.
81 | pub(crate) async fn read_i8(&mut self) -> AsyncTiffResult {
82 | self.read(1).await?.read_i8()
83 | }
84 |
85 | /// Read a u16 from the cursor, advancing the internal state by 2 bytes.
86 | pub(crate) async fn read_u16(&mut self) -> AsyncTiffResult {
87 | self.read(2).await?.read_u16()
88 | }
89 |
90 | /// Read a i16 from the cursor, advancing the internal state by 2 bytes.
91 | pub(crate) async fn read_i16(&mut self) -> AsyncTiffResult {
92 | self.read(2).await?.read_i16()
93 | }
94 |
95 | /// Read a u32 from the cursor, advancing the internal state by 4 bytes.
96 | pub(crate) async fn read_u32(&mut self) -> AsyncTiffResult {
97 | self.read(4).await?.read_u32()
98 | }
99 |
100 | /// Read a i32 from the cursor, advancing the internal state by 4 bytes.
101 | pub(crate) async fn read_i32(&mut self) -> AsyncTiffResult {
102 | self.read(4).await?.read_i32()
103 | }
104 |
105 | /// Read a u64 from the cursor, advancing the internal state by 8 bytes.
106 | pub(crate) async fn read_u64(&mut self) -> AsyncTiffResult {
107 | self.read(8).await?.read_u64()
108 | }
109 |
110 | /// Read a i64 from the cursor, advancing the internal state by 8 bytes.
111 | pub(crate) async fn read_i64(&mut self) -> AsyncTiffResult {
112 | self.read(8).await?.read_i64()
113 | }
114 |
115 | pub(crate) async fn read_f32(&mut self) -> AsyncTiffResult {
116 | self.read(4).await?.read_f32()
117 | }
118 |
119 | pub(crate) async fn read_f64(&mut self) -> AsyncTiffResult {
120 | self.read(8).await?.read_f64()
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/tests/ome_tiff.rs:
--------------------------------------------------------------------------------
1 | //! Integration tests on OME-TIFF files.
2 |
3 | use std::sync::Arc;
4 |
5 | use async_tiff::metadata::cache::ReadaheadMetadataCache;
6 | use async_tiff::metadata::TiffMetadataReader;
7 | use async_tiff::reader::{AsyncFileReader, ObjectReader};
8 | use async_tiff::tags::PhotometricInterpretation;
9 | use async_tiff::TIFF;
10 | use reqwest::Url;
11 |
12 | async fn open_remote_tiff(url: &str) -> TIFF {
13 | let parsed_url = Url::parse(url).expect("failed parsing url");
14 | let (store, path) = object_store::parse_url(&parsed_url).unwrap();
15 |
16 | let reader = Arc::new(ObjectReader::new(Arc::new(store), path)) as Arc;
17 | let cached_reader = ReadaheadMetadataCache::new(reader.clone());
18 | let mut metadata_reader = TiffMetadataReader::try_open(&cached_reader).await.unwrap();
19 | metadata_reader.read(&cached_reader).await.unwrap()
20 | }
21 |
22 | #[tokio::test]
23 | async fn test_ome_tiff_single_channel() {
24 | let tiff = open_remote_tiff("https://cildata.crbs.ucsd.edu/media/images/40613/40613.tif").await;
25 |
26 | assert_eq!(tiff.ifds().len(), 3);
27 | let ifd = &tiff.ifds()[0];
28 |
29 | assert_eq!(
30 | ifd.photometric_interpretation(),
31 | PhotometricInterpretation::BlackIsZero
32 | );
33 | assert_eq!(
34 | ifd.image_description(),
35 | Some(
36 | r##"
37 |
38 |
39 |
40 |
41 | 2012-03-25 21:26:29.0
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | 3af39f55-0ac0-431a-bc60-8f9c3e782b85
50 |
51 |
52 |
53 |
54 | "##
55 | )
56 | );
57 |
58 | assert!(ifd.bits_per_sample().iter().all(|x| *x == 8));
59 | assert_eq!(ifd.software(), Some("LOCI Bio-Formats"));
60 | }
61 |
--------------------------------------------------------------------------------
/python/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: async-tiff
2 | repo_name: developmentseed/async-tiff
3 | repo_url: https://github.com/developmentseed/async-tiff
4 | site_description: A fast, low-level async TIFF reader powered by Rust.
5 | site_author: Development Seed
6 | # Note: trailing slash recommended with mike:
7 | # https://squidfunk.github.io/mkdocs-material/setup/setting-up-versioning/#publishing-a-new-version
8 | site_url: https://developmentseed.org/async-tiff/
9 | docs_dir: docs
10 |
11 | extra:
12 | social:
13 | - icon: "fontawesome/brands/github"
14 | link: "https://github.com/developmentseed"
15 | - icon: "fontawesome/brands/twitter"
16 | link: "https://twitter.com/developmentseed"
17 | - icon: "fontawesome/brands/linkedin"
18 | link: "https://www.linkedin.com/company/development-seed"
19 | version:
20 | alias: true
21 | provider: mike
22 |
23 | nav:
24 | - "index.md"
25 | - API Reference:
26 | - api/tiff.md
27 | - api/ifd.md
28 | - api/tile.md
29 | - api/geo.md
30 | - api/decoder.md
31 | - api/thread-pool.md
32 | - async-tiff.store:
33 | - api/store/index.md
34 | - api/store/aws.md
35 | - api/store/gcs.md
36 | - api/store/azure.md
37 | - api/store/http.md
38 | - api/store/local.md
39 | - api/store/memory.md
40 | - api/store/config.md
41 | # - API Reference:
42 | # - api/rtree.md
43 | # - api/kdtree.md
44 | # - Changelog: CHANGELOG.md
45 |
46 | watch:
47 | - python
48 | - docs
49 |
50 | theme:
51 | language: en
52 | name: material
53 | custom_dir: docs/overrides
54 | logo: assets/logo_no_text.png
55 | palette:
56 | # Palette toggle for automatic mode
57 | - media: "(prefers-color-scheme)"
58 | toggle:
59 | icon: material/brightness-auto
60 | name: Switch to light mode
61 |
62 | # Palette toggle for light mode
63 | - media: "(prefers-color-scheme: light)"
64 | primary: default
65 | accent: deep orange
66 | toggle:
67 | icon: material/brightness-7
68 | name: Switch to dark mode
69 |
70 | # Palette toggle for dark mode
71 | - media: "(prefers-color-scheme: dark)"
72 | scheme: slate
73 | primary: default
74 | accent: deep orange
75 | toggle:
76 | icon: material/brightness-4
77 | name: Switch to system preference
78 |
79 | font:
80 | text: Roboto
81 | code: Roboto Mono
82 |
83 | features:
84 | - content.code.annotate
85 | - content.code.copy
86 | - navigation.indexes
87 | - navigation.instant
88 | - navigation.tracking
89 | - search.suggest
90 | - search.share
91 |
92 | extra_css:
93 | - overrides/stylesheets/extra.css
94 |
95 | plugins:
96 | - search
97 | # - social
98 | - mike:
99 | alias_type: "copy"
100 | canonical_version: "latest"
101 | - mkdocstrings:
102 | enable_inventory: true
103 | handlers:
104 | python:
105 | paths: [python]
106 | options:
107 | # We set allow_inspection: false to ensure that all docstrings come
108 | # from the pyi files, not the Rust-facing doc comments.
109 | allow_inspection: false
110 | docstring_section_style: list
111 | docstring_style: google
112 | line_length: 80
113 | separate_signature: true
114 | show_root_heading: true
115 | show_signature_annotations: true
116 | show_source: false
117 | show_symbol_type_toc: true
118 | signature_crossrefs: true
119 | extensions:
120 | - griffe_inherited_docstrings
121 |
122 | import:
123 | - https://docs.python.org/3/objects.inv
124 | - https://developmentseed.org/obstore/latest/objects.inv
125 |
126 | # https://github.com/developmentseed/titiler/blob/50934c929cca2fa8d3c408d239015f8da429c6a8/docs/mkdocs.yml#L115-L140
127 | markdown_extensions:
128 | - admonition
129 | - attr_list
130 | - codehilite:
131 | guess_lang: false
132 | - def_list
133 | - footnotes
134 | - md_in_html
135 | - pymdownx.arithmatex
136 | - pymdownx.betterem
137 | - pymdownx.caret:
138 | insert: false
139 | - pymdownx.details
140 | - pymdownx.emoji:
141 | emoji_index: !!python/name:material.extensions.emoji.twemoji
142 | emoji_generator: !!python/name:material.extensions.emoji.to_svg
143 | - pymdownx.escapeall:
144 | hardbreak: true
145 | nbsp: true
146 | - pymdownx.magiclink:
147 | hide_protocol: true
148 | repo_url_shortener: true
149 | - pymdownx.smartsymbols
150 | - pymdownx.superfences
151 | - pymdownx.tasklist:
152 | custom_checkbox: true
153 | - pymdownx.tilde
154 | - toc:
155 | permalink: true
156 |
--------------------------------------------------------------------------------
/python/src/reader.rs:
--------------------------------------------------------------------------------
1 | use std::ops::Range;
2 | use std::sync::Arc;
3 |
4 | use async_tiff::error::{AsyncTiffError, AsyncTiffResult};
5 | use async_tiff::reader::{AsyncFileReader, ObjectReader};
6 | use async_trait::async_trait;
7 | use bytes::Bytes;
8 | use pyo3::exceptions::PyTypeError;
9 | use pyo3::intern;
10 | use pyo3::prelude::*;
11 | use pyo3::types::PyDict;
12 | use pyo3_async_runtimes::tokio::into_future;
13 | use pyo3_bytes::PyBytes;
14 | use pyo3_object_store::PyObjectStore;
15 |
16 | #[derive(FromPyObject)]
17 | pub(crate) enum StoreInput {
18 | ObjectStore(PyObjectStore),
19 | ObspecBackend(ObspecBackend),
20 | }
21 |
22 | impl StoreInput {
23 | pub(crate) fn into_async_file_reader(self, path: String) -> Arc {
24 | match self {
25 | Self::ObjectStore(store) => {
26 | Arc::new(ObjectReader::new(store.into_inner(), path.into()))
27 | }
28 | Self::ObspecBackend(backend) => Arc::new(ObspecReader { backend, path }),
29 | }
30 | }
31 | }
32 |
33 | /// A Python backend for making requests that conforms to the GetRangeAsync and GetRangesAsync
34 | /// protocols defined by obspec.
35 | /// https://developmentseed.org/obspec/latest/api/get/#obspec.GetRangeAsync
36 | /// https://developmentseed.org/obspec/latest/api/get/#obspec.GetRangesAsync
37 | #[derive(Debug)]
38 | pub(crate) struct ObspecBackend(Py);
39 |
40 | impl ObspecBackend {
41 | async fn get_range(&self, path: &str, range: Range) -> PyResult {
42 | let future = Python::attach(|py| {
43 | let kwargs = PyDict::new(py);
44 | kwargs.set_item(intern!(py, "path"), path)?;
45 | kwargs.set_item(intern!(py, "start"), range.start)?;
46 | kwargs.set_item(intern!(py, "end"), range.end)?;
47 |
48 | let coroutine = self
49 | .0
50 | .call_method(py, intern!(py, "get_range"), (), Some(&kwargs))?;
51 | into_future(coroutine.bind(py).clone())
52 | })?;
53 | let result = future.await?;
54 | Python::attach(|py| result.extract(py))
55 | }
56 |
57 | async fn get_ranges(&self, path: &str, ranges: &[Range]) -> PyResult> {
58 | let starts = ranges.iter().map(|r| r.start).collect::>();
59 | let ends = ranges.iter().map(|r| r.end).collect::>();
60 |
61 | let future = Python::attach(|py| {
62 | let kwargs = PyDict::new(py);
63 | kwargs.set_item(intern!(py, "path"), path)?;
64 | kwargs.set_item(intern!(py, "starts"), starts)?;
65 | kwargs.set_item(intern!(py, "ends"), ends)?;
66 |
67 | let coroutine = self
68 | .0
69 | .call_method(py, intern!(py, "get_range"), (), Some(&kwargs))?;
70 | into_future(coroutine.bind(py).clone())
71 | })?;
72 | let result = future.await?;
73 | Python::attach(|py| result.extract(py))
74 | }
75 |
76 | async fn get_range_wrapper(&self, path: &str, range: Range) -> AsyncTiffResult {
77 | let result = self
78 | .get_range(path, range)
79 | .await
80 | .map_err(|err| AsyncTiffError::External(Box::new(err)))?;
81 | Ok(result.into_inner())
82 | }
83 |
84 | async fn get_ranges_wrapper(
85 | &self,
86 | path: &str,
87 | ranges: Vec>,
88 | ) -> AsyncTiffResult> {
89 | let result = self
90 | .get_ranges(path, &ranges)
91 | .await
92 | .map_err(|err| AsyncTiffError::External(Box::new(err)))?;
93 | Ok(result.into_iter().map(|b| b.into_inner()).collect())
94 | }
95 | }
96 |
97 | impl<'py> FromPyObject<'_, 'py> for ObspecBackend {
98 | type Error = PyErr;
99 |
100 | fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result {
101 | let py = obj.py();
102 | if obj.hasattr(intern!(py, "get_range_async"))?
103 | && obj.hasattr(intern!(py, "get_ranges_async"))?
104 | {
105 | Ok(Self(obj.as_unbound().clone_ref(py)))
106 | } else {
107 | Err(PyTypeError::new_err("Expected obspec-compatible class with `get_range_async` and `get_ranges_async` method."))
108 | }
109 | }
110 | }
111 |
112 | #[derive(Debug)]
113 | struct ObspecReader {
114 | backend: ObspecBackend,
115 | path: String,
116 | }
117 |
118 | #[async_trait]
119 | impl AsyncFileReader for ObspecReader {
120 | async fn get_bytes(&self, range: Range) -> AsyncTiffResult {
121 | self.backend.get_range_wrapper(&self.path, range).await
122 | }
123 |
124 | async fn get_byte_ranges(&self, ranges: Vec>) -> AsyncTiffResult> {
125 | self.backend.get_ranges_wrapper(&self.path, ranges).await
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/python/src/enums.rs:
--------------------------------------------------------------------------------
1 | use async_tiff::reader::Endianness;
2 | use async_tiff::tags::{
3 | CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, ResolutionUnit,
4 | SampleFormat,
5 | };
6 | use pyo3::prelude::*;
7 | use pyo3::types::{PyString, PyTuple};
8 | use pyo3::{intern, IntoPyObjectExt};
9 |
10 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11 | pub(crate) struct PyCompressionMethod(CompressionMethod);
12 |
13 | impl From for PyCompressionMethod {
14 | fn from(value: CompressionMethod) -> Self {
15 | Self(value)
16 | }
17 | }
18 |
19 | impl From for CompressionMethod {
20 | fn from(value: PyCompressionMethod) -> Self {
21 | value.0
22 | }
23 | }
24 |
25 | impl<'py> FromPyObject<'_, 'py> for PyCompressionMethod {
26 | type Error = PyErr;
27 |
28 | fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result {
29 | Ok(Self(CompressionMethod::from_u16_exhaustive(obj.extract()?)))
30 | }
31 | }
32 |
33 | impl<'py> IntoPyObject<'py> for PyCompressionMethod {
34 | type Target = PyAny;
35 | type Output = Bound<'py, PyAny>;
36 | type Error = PyErr;
37 |
38 | fn into_pyobject(self, py: Python<'py>) -> Result {
39 | to_py_enum_variant(py, intern!(py, "CompressionMethod"), self.0.to_u16())
40 | }
41 | }
42 |
43 | pub(crate) struct PyEndianness(Endianness);
44 |
45 | impl From for PyEndianness {
46 | fn from(value: Endianness) -> Self {
47 | Self(value)
48 | }
49 | }
50 |
51 | impl<'py> IntoPyObject<'py> for PyEndianness {
52 | type Target = PyAny;
53 | type Output = Bound<'py, PyAny>;
54 | type Error = PyErr;
55 |
56 | fn into_pyobject(self, py: Python<'py>) -> Result {
57 | let enums_mod = py.import(intern!(py, "async_tiff.enums"))?;
58 | let endianness_enum = enums_mod.getattr(intern!(py, "Endianness"))?;
59 |
60 | match self.0 {
61 | Endianness::LittleEndian => endianness_enum.getattr("LittleEndian"),
62 | Endianness::BigEndian => endianness_enum.getattr("BigEndian"),
63 | }
64 | }
65 | }
66 |
67 | pub(crate) struct PyPhotometricInterpretation(PhotometricInterpretation);
68 |
69 | impl From for PyPhotometricInterpretation {
70 | fn from(value: PhotometricInterpretation) -> Self {
71 | Self(value)
72 | }
73 | }
74 |
75 | impl<'py> IntoPyObject<'py> for PyPhotometricInterpretation {
76 | type Target = PyAny;
77 | type Output = Bound<'py, PyAny>;
78 | type Error = PyErr;
79 |
80 | fn into_pyobject(self, py: Python<'py>) -> Result {
81 | to_py_enum_variant(
82 | py,
83 | intern!(py, "PhotometricInterpretation"),
84 | self.0.to_u16(),
85 | )
86 | }
87 | }
88 |
89 | pub(crate) struct PyPlanarConfiguration(PlanarConfiguration);
90 |
91 | impl From for PyPlanarConfiguration {
92 | fn from(value: PlanarConfiguration) -> Self {
93 | Self(value)
94 | }
95 | }
96 |
97 | impl<'py> IntoPyObject<'py> for PyPlanarConfiguration {
98 | type Target = PyAny;
99 | type Output = Bound<'py, PyAny>;
100 | type Error = PyErr;
101 |
102 | fn into_pyobject(self, py: Python<'py>) -> Result {
103 | to_py_enum_variant(py, intern!(py, "PlanarConfiguration"), self.0.to_u16())
104 | }
105 | }
106 |
107 | pub(crate) struct PyResolutionUnit(ResolutionUnit);
108 |
109 | impl From for PyResolutionUnit {
110 | fn from(value: ResolutionUnit) -> Self {
111 | Self(value)
112 | }
113 | }
114 |
115 | impl<'py> IntoPyObject<'py> for PyResolutionUnit {
116 | type Target = PyAny;
117 | type Output = Bound<'py, PyAny>;
118 | type Error = PyErr;
119 |
120 | fn into_pyobject(self, py: Python<'py>) -> Result {
121 | to_py_enum_variant(py, intern!(py, "ResolutionUnit"), self.0.to_u16())
122 | }
123 | }
124 |
125 | pub(crate) struct PyPredictor(Predictor);
126 |
127 | impl From for PyPredictor {
128 | fn from(value: Predictor) -> Self {
129 | Self(value)
130 | }
131 | }
132 |
133 | impl<'py> IntoPyObject<'py> for PyPredictor {
134 | type Target = PyAny;
135 | type Output = Bound<'py, PyAny>;
136 | type Error = PyErr;
137 |
138 | fn into_pyobject(self, py: Python<'py>) -> Result {
139 | to_py_enum_variant(py, intern!(py, "Predictor"), self.0.to_u16())
140 | }
141 | }
142 |
143 | pub(crate) struct PySampleFormat(SampleFormat);
144 |
145 | impl From for PySampleFormat {
146 | fn from(value: SampleFormat) -> Self {
147 | Self(value)
148 | }
149 | }
150 |
151 | impl<'py> IntoPyObject<'py> for PySampleFormat {
152 | type Target = PyAny;
153 | type Output = Bound<'py, PyAny>;
154 | type Error = PyErr;
155 |
156 | fn into_pyobject(self, py: Python<'py>) -> Result {
157 | to_py_enum_variant(py, intern!(py, "SampleFormat"), self.0.to_u16())
158 | }
159 | }
160 |
161 | fn to_py_enum_variant<'py>(
162 | py: Python<'py>,
163 | enum_name: &Bound<'py, PyString>,
164 | value: u16,
165 | ) -> PyResult> {
166 | let enums_mod = py.import(intern!(py, "async_tiff.enums"))?;
167 | if let Ok(enum_variant) = enums_mod.call_method1(enum_name, PyTuple::new(py, vec![value])?) {
168 | Ok(enum_variant)
169 | } else {
170 | // If the value is not included in the enum, return the integer itself
171 | value.into_bound_py_any(py)
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/tests/image_tiff/decode_fp16_images.rs:
--------------------------------------------------------------------------------
1 | extern crate tiff;
2 |
3 | use tiff::decoder::{Decoder, DecodingResult};
4 | use tiff::ColorType;
5 |
6 | use std::fs::File;
7 | use std::path::PathBuf;
8 |
9 | const TEST_IMAGE_DIR: &str = "./tests/image_tiff/images/";
10 |
11 | /// Test a basic all white image
12 | #[test]
13 | fn test_white_ieee_fp16() {
14 | let filenames = ["white-fp16.tiff"];
15 |
16 | for filename in filenames.iter() {
17 | let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
18 | let img_file = File::open(path).expect("Cannot find test image!");
19 | let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
20 | assert_eq!(
21 | decoder.dimensions().expect("Cannot get dimensions"),
22 | (256, 256)
23 | );
24 | assert_eq!(
25 | decoder.colortype().expect("Cannot get colortype"),
26 | ColorType::Gray(16)
27 | );
28 | if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
29 | for p in img {
30 | assert!(p == half::f16::from_f32_const(1.0));
31 | }
32 | } else {
33 | panic!("Wrong data type");
34 | }
35 | }
36 | }
37 |
38 | /// Test a single black pixel, to make sure scaling is ok
39 | #[test]
40 | fn test_one_black_pixel_ieee_fp16() {
41 | let filenames = ["single-black-fp16.tiff"];
42 |
43 | for filename in filenames.iter() {
44 | let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
45 | let img_file = File::open(path).expect("Cannot find test image!");
46 | let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
47 | assert_eq!(
48 | decoder.dimensions().expect("Cannot get dimensions"),
49 | (256, 256)
50 | );
51 | assert_eq!(
52 | decoder.colortype().expect("Cannot get colortype"),
53 | ColorType::Gray(16)
54 | );
55 | if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
56 | for (i, p) in img.iter().enumerate() {
57 | if i == 0 {
58 | assert!(p < &half::f16::from_f32_const(0.001));
59 | } else {
60 | assert!(p == &half::f16::from_f32_const(1.0));
61 | }
62 | }
63 | } else {
64 | panic!("Wrong data type");
65 | }
66 | }
67 | }
68 |
69 | /// Test white with horizontal differencing predictor
70 | #[test]
71 | fn test_pattern_horizontal_differencing_ieee_fp16() {
72 | let filenames = ["white-fp16-pred2.tiff"];
73 |
74 | for filename in filenames.iter() {
75 | let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
76 | let img_file = File::open(path).expect("Cannot find test image!");
77 | let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
78 | assert_eq!(
79 | decoder.dimensions().expect("Cannot get dimensions"),
80 | (256, 256)
81 | );
82 | assert_eq!(
83 | decoder.colortype().expect("Cannot get colortype"),
84 | ColorType::Gray(16)
85 | );
86 | if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
87 | // 0, 2, 5, 8, 12, 16, 255 are black
88 | let black = [0, 2, 5, 8, 12, 16, 255];
89 | for (i, p) in img.iter().enumerate() {
90 | if black.contains(&i) {
91 | assert!(p < &half::f16::from_f32_const(0.001));
92 | } else {
93 | assert!(p == &half::f16::from_f32_const(1.0));
94 | }
95 | }
96 | } else {
97 | panic!("Wrong data type");
98 | }
99 | }
100 | }
101 |
102 | /// Test white with floating point predictor
103 | #[test]
104 | fn test_pattern_predictor_ieee_fp16() {
105 | let filenames = ["white-fp16-pred3.tiff"];
106 |
107 | for filename in filenames.iter() {
108 | let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
109 | let img_file = File::open(path).expect("Cannot find test image!");
110 | let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
111 | assert_eq!(
112 | decoder.dimensions().expect("Cannot get dimensions"),
113 | (256, 256)
114 | );
115 | assert_eq!(
116 | decoder.colortype().expect("Cannot get colortype"),
117 | ColorType::Gray(16)
118 | );
119 | if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
120 | // 0, 2, 5, 8, 12, 16, 255 are black
121 | let black = [0, 2, 5, 8, 12, 16, 255];
122 | for (i, p) in img.iter().enumerate() {
123 | if black.contains(&i) {
124 | assert!(p < &half::f16::from_f32_const(0.001));
125 | } else {
126 | assert!(p == &half::f16::from_f32_const(1.0));
127 | }
128 | }
129 | } else {
130 | panic!("Wrong data type");
131 | }
132 | }
133 | }
134 |
135 | /// Test several random images
136 | /// we'rell compare against a pnm file, that scales from 0 (for 0.0) to 65767 (for 1.0)
137 | #[test]
138 | fn test_predictor_ieee_fp16() {
139 | // first parse pnm, skip the first 4 \n
140 | let pnm_path = PathBuf::from(TEST_IMAGE_DIR).join("random-fp16.pgm");
141 | let pnm_bytes = std::fs::read(pnm_path).expect("Failed to read expected PNM file");
142 |
143 | // PGM looks like this:
144 | // ---
145 | // P5
146 | // #Created with GIMP
147 | // 16 16
148 | // 65535
149 | // ...
150 | // ---
151 | // get index of 4th \n
152 | let byte_start = pnm_bytes
153 | .iter()
154 | .enumerate()
155 | .filter(|(_, &v)| v == b'\n')
156 | .map(|(i, _)| i)
157 | .nth(3)
158 | .expect("Must be 4 \\n's");
159 |
160 | let pnm_values: Vec = pnm_bytes[(byte_start + 1)..]
161 | .chunks(2)
162 | .map(|slice| {
163 | let bts = [slice[0], slice[1]];
164 | (u16::from_be_bytes(bts) as f32) / (u16::MAX as f32)
165 | })
166 | .collect();
167 | assert!(pnm_values.len() == 256);
168 |
169 | let filenames = [
170 | "random-fp16-pred2.tiff",
171 | "random-fp16-pred3.tiff",
172 | "random-fp16.tiff",
173 | ];
174 |
175 | for filename in filenames.iter() {
176 | let path = PathBuf::from(TEST_IMAGE_DIR).join(filename);
177 | let img_file = File::open(path).expect("Cannot find test image!");
178 | let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
179 | assert_eq!(
180 | decoder.dimensions().expect("Cannot get dimensions"),
181 | (16, 16)
182 | );
183 | assert_eq!(
184 | decoder.colortype().expect("Cannot get colortype"),
185 | ColorType::Gray(16)
186 | );
187 | if let DecodingResult::F16(img) = decoder.read_image().unwrap() {
188 | for (exp, found) in std::iter::zip(pnm_values.iter(), img.iter()) {
189 | assert!((exp - found.to_f32()).abs() < 0.0001);
190 | }
191 | } else {
192 | panic!("Wrong data type");
193 | }
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/src/decoder.rs:
--------------------------------------------------------------------------------
1 | //! Decoders for different TIFF compression methods.
2 |
3 | use std::collections::HashMap;
4 | use std::fmt::Debug;
5 | use std::io::{Cursor, Read};
6 |
7 | use bytes::Bytes;
8 | use flate2::bufread::ZlibDecoder;
9 |
10 | use crate::error::{AsyncTiffResult, TiffError, TiffUnsupportedError};
11 | use crate::tags::{CompressionMethod, PhotometricInterpretation};
12 |
13 | /// A registry of decoders.
14 | ///
15 | /// This allows end users to register their own decoders, for custom compression methods, or
16 | /// override the default decoder implementations.
17 | #[derive(Debug)]
18 | pub struct DecoderRegistry(HashMap>);
19 |
20 | impl DecoderRegistry {
21 | /// Create a new decoder registry with no decoders registered
22 | pub fn new() -> Self {
23 | Self(HashMap::new())
24 | }
25 | }
26 |
27 | impl AsRef>> for DecoderRegistry {
28 | fn as_ref(&self) -> &HashMap> {
29 | &self.0
30 | }
31 | }
32 |
33 | impl AsMut>> for DecoderRegistry {
34 | fn as_mut(&mut self) -> &mut HashMap> {
35 | &mut self.0
36 | }
37 | }
38 |
39 | impl Default for DecoderRegistry {
40 | fn default() -> Self {
41 | let mut registry = HashMap::with_capacity(6);
42 | registry.insert(CompressionMethod::None, Box::new(UncompressedDecoder) as _);
43 | registry.insert(CompressionMethod::Deflate, Box::new(DeflateDecoder) as _);
44 | registry.insert(CompressionMethod::OldDeflate, Box::new(DeflateDecoder) as _);
45 | registry.insert(CompressionMethod::LZW, Box::new(LZWDecoder) as _);
46 | registry.insert(CompressionMethod::ModernJPEG, Box::new(JPEGDecoder) as _);
47 | registry.insert(CompressionMethod::ZSTD, Box::new(ZstdDecoder) as _);
48 | Self(registry)
49 | }
50 | }
51 |
52 | /// A trait to decode a TIFF tile.
53 | pub trait Decoder: Debug + Send + Sync {
54 | /// Decode a TIFF tile.
55 | fn decode_tile(
56 | &self,
57 | buffer: Bytes,
58 | photometric_interpretation: PhotometricInterpretation,
59 | jpeg_tables: Option<&[u8]>,
60 | ) -> AsyncTiffResult;
61 | }
62 |
63 | /// A decoder for the Deflate compression method.
64 | #[derive(Debug, Clone)]
65 | pub struct DeflateDecoder;
66 |
67 | impl Decoder for DeflateDecoder {
68 | fn decode_tile(
69 | &self,
70 | buffer: Bytes,
71 | _photometric_interpretation: PhotometricInterpretation,
72 | _jpeg_tables: Option<&[u8]>,
73 | ) -> AsyncTiffResult {
74 | let mut decoder = ZlibDecoder::new(Cursor::new(buffer));
75 | let mut buf = Vec::new();
76 | decoder.read_to_end(&mut buf)?;
77 | Ok(buf.into())
78 | }
79 | }
80 |
81 | /// A decoder for the JPEG compression method.
82 | #[derive(Debug, Clone)]
83 | pub struct JPEGDecoder;
84 |
85 | impl Decoder for JPEGDecoder {
86 | fn decode_tile(
87 | &self,
88 | buffer: Bytes,
89 | photometric_interpretation: PhotometricInterpretation,
90 | jpeg_tables: Option<&[u8]>,
91 | ) -> AsyncTiffResult {
92 | decode_modern_jpeg(buffer, photometric_interpretation, jpeg_tables)
93 | }
94 | }
95 |
96 | /// A decoder for the LZW compression method.
97 | #[derive(Debug, Clone)]
98 | pub struct LZWDecoder;
99 |
100 | impl Decoder for LZWDecoder {
101 | fn decode_tile(
102 | &self,
103 | buffer: Bytes,
104 | _photometric_interpretation: PhotometricInterpretation,
105 | _jpeg_tables: Option<&[u8]>,
106 | ) -> AsyncTiffResult {
107 | // https://github.com/image-rs/image-tiff/blob/90ae5b8e54356a35e266fb24e969aafbcb26e990/src/decoder/stream.rs#L147
108 | let mut decoder = weezl::decode::Decoder::with_tiff_size_switch(weezl::BitOrder::Msb, 8);
109 | let decoded = decoder.decode(&buffer).expect("failed to decode LZW data");
110 | Ok(decoded.into())
111 | }
112 | }
113 |
114 | /// A decoder for uncompressed data.
115 | #[derive(Debug, Clone)]
116 | pub struct UncompressedDecoder;
117 |
118 | impl Decoder for UncompressedDecoder {
119 | fn decode_tile(
120 | &self,
121 | buffer: Bytes,
122 | _photometric_interpretation: PhotometricInterpretation,
123 | _jpeg_tables: Option<&[u8]>,
124 | ) -> AsyncTiffResult {
125 | Ok(buffer)
126 | }
127 | }
128 |
129 | /// A decoder for the Zstd compression method.
130 | #[derive(Debug, Clone)]
131 | pub struct ZstdDecoder;
132 |
133 | impl Decoder for ZstdDecoder {
134 | fn decode_tile(
135 | &self,
136 | buffer: Bytes,
137 | _photometric_interpretation: PhotometricInterpretation,
138 | _jpeg_tables: Option<&[u8]>,
139 | ) -> AsyncTiffResult {
140 | let mut decoder = zstd::Decoder::new(Cursor::new(buffer))?;
141 | let mut buf = Vec::new();
142 | decoder.read_to_end(&mut buf)?;
143 | Ok(buf.into())
144 | }
145 | }
146 |
147 | // https://github.com/image-rs/image-tiff/blob/3bfb43e83e31b0da476832067ada68a82b378b7b/src/decoder/image.rs#L389-L450
148 | fn decode_modern_jpeg(
149 | buf: Bytes,
150 | photometric_interpretation: PhotometricInterpretation,
151 | jpeg_tables: Option<&[u8]>,
152 | ) -> AsyncTiffResult {
153 | // Construct new jpeg_reader wrapping a SmartReader.
154 | //
155 | // JPEG compression in TIFF allows saving quantization and/or huffman tables in one central
156 | // location. These `jpeg_tables` are simply prepended to the remaining jpeg image data. Because
157 | // these `jpeg_tables` start with a `SOI` (HEX: `0xFFD8`) or __start of image__ marker which is
158 | // also at the beginning of the remaining JPEG image data and would confuse the JPEG renderer,
159 | // one of these has to be taken off. In this case the first two bytes of the remaining JPEG
160 | // data is removed because it follows `jpeg_tables`. Similary, `jpeg_tables` ends with a `EOI`
161 | // (HEX: `0xFFD9`) or __end of image__ marker, this has to be removed as well (last two bytes
162 | // of `jpeg_tables`).
163 | let reader = Cursor::new(buf);
164 |
165 | let jpeg_reader = match jpeg_tables {
166 | Some(jpeg_tables) => {
167 | let mut reader = reader;
168 | reader.read_exact(&mut [0; 2])?;
169 |
170 | Box::new(Cursor::new(&jpeg_tables[..jpeg_tables.len() - 2]).chain(reader))
171 | as Box
172 | }
173 | None => Box::new(reader),
174 | };
175 |
176 | let mut decoder = jpeg::Decoder::new(jpeg_reader);
177 |
178 | match photometric_interpretation {
179 | PhotometricInterpretation::RGB => decoder.set_color_transform(jpeg::ColorTransform::RGB),
180 | PhotometricInterpretation::WhiteIsZero
181 | | PhotometricInterpretation::BlackIsZero
182 | | PhotometricInterpretation::TransparencyMask => {
183 | decoder.set_color_transform(jpeg::ColorTransform::None)
184 | }
185 | PhotometricInterpretation::CMYK => decoder.set_color_transform(jpeg::ColorTransform::CMYK),
186 | PhotometricInterpretation::YCbCr => {
187 | decoder.set_color_transform(jpeg::ColorTransform::YCbCr)
188 | }
189 | photometric_interpretation => {
190 | return Err(TiffError::UnsupportedError(
191 | TiffUnsupportedError::UnsupportedInterpretation(photometric_interpretation),
192 | )
193 | .into());
194 | }
195 | }
196 |
197 | let data = decoder.decode()?;
198 | Ok(data.into())
199 | }
200 |
--------------------------------------------------------------------------------
/src/tags.rs:
--------------------------------------------------------------------------------
1 | #![allow(clippy::no_effect)]
2 | #![allow(missing_docs)]
3 | #![allow(clippy::upper_case_acronyms)]
4 |
5 | macro_rules! tags {
6 | {
7 | // Permit arbitrary meta items, which include documentation.
8 | $( #[$enum_attr:meta] )*
9 | $vis:vis enum $name:ident($ty:tt) $(unknown($unknown_doc:literal))* {
10 | // Each of the `Name = Val,` permitting documentation.
11 | $($(#[$ident_attr:meta])* $tag:ident = $val:expr,)*
12 | }
13 | } => {
14 | $( #[$enum_attr] )*
15 | #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
16 | #[non_exhaustive]
17 | pub enum $name {
18 | $($(#[$ident_attr])* $tag,)*
19 | $(
20 | #[doc = $unknown_doc]
21 | Unknown($ty),
22 | )*
23 | }
24 |
25 | impl $name {
26 | #[inline(always)]
27 | fn __from_inner_type(n: $ty) -> Result {
28 | match n {
29 | $( $val => Ok($name::$tag), )*
30 | n => Err(n),
31 | }
32 | }
33 |
34 | #[inline(always)]
35 | fn __to_inner_type(&self) -> $ty {
36 | match *self {
37 | $( $name::$tag => $val, )*
38 | $( $name::Unknown(n) => { $unknown_doc; n }, )*
39 | }
40 | }
41 | }
42 |
43 | tags!($name, $ty, $($unknown_doc)*);
44 | };
45 | // For u16 tags, provide direct inherent primitive conversion methods.
46 | ($name:tt, u16, $($unknown_doc:literal)*) => {
47 | impl $name {
48 | /// Construct from a u16 value, returning `None` if the value is not a known tag.
49 | #[inline(always)]
50 | pub fn from_u16(val: u16) -> Option {
51 | Self::__from_inner_type(val).ok()
52 | }
53 |
54 | $(
55 | /// Construct from a u16 value, storing `Unknown` if the value is not a known tag.
56 | #[inline(always)]
57 | pub fn from_u16_exhaustive(val: u16) -> Self {
58 | $unknown_doc;
59 | Self::__from_inner_type(val).unwrap_or_else(|_| $name::Unknown(val))
60 | }
61 | )*
62 |
63 | /// Convert to a u16 value.
64 | #[inline(always)]
65 | pub fn to_u16(self) -> u16 {
66 | Self::__to_inner_type(&self)
67 | }
68 | }
69 | };
70 | // For other tag types, do nothing for now. With concat_idents one could
71 | // provide inherent conversion methods for all types.
72 | ($name:tt, $ty:tt, $($unknown_doc:literal)*) => {};
73 | }
74 |
75 | // Note: These tags appear in the order they are mentioned in the TIFF reference
76 | tags! {
77 | /// TIFF tags
78 | pub enum Tag(u16) unknown("A private or extension tag") {
79 | // Baseline tags:
80 | Artist = 315,
81 | // grayscale images PhotometricInterpretation 1 or 3
82 | BitsPerSample = 258,
83 | CellLength = 265, // TODO add support
84 | CellWidth = 264, // TODO add support
85 | // palette-color images (PhotometricInterpretation 3)
86 | ColorMap = 320, // TODO add support
87 | Compression = 259, // TODO add support for 2 and 32773
88 | Copyright = 33_432,
89 | DateTime = 306,
90 | ExtraSamples = 338, // TODO add support
91 | FillOrder = 266, // TODO add support
92 | FreeByteCounts = 289, // TODO add support
93 | FreeOffsets = 288, // TODO add support
94 | GrayResponseCurve = 291, // TODO add support
95 | GrayResponseUnit = 290, // TODO add support
96 | HostComputer = 316,
97 | ImageDescription = 270,
98 | ImageLength = 257,
99 | ImageWidth = 256,
100 | Make = 271,
101 | MaxSampleValue = 281, // TODO add support
102 | MinSampleValue = 280, // TODO add support
103 | Model = 272,
104 | NewSubfileType = 254, // TODO add support
105 | Orientation = 274, // TODO add support
106 | PhotometricInterpretation = 262,
107 | PlanarConfiguration = 284,
108 | ResolutionUnit = 296, // TODO add support
109 | RowsPerStrip = 278,
110 | SamplesPerPixel = 277,
111 | Software = 305,
112 | StripByteCounts = 279,
113 | StripOffsets = 273,
114 | SubfileType = 255, // TODO add support
115 | Threshholding = 263, // TODO add support
116 | XResolution = 282,
117 | YResolution = 283,
118 | // Advanced tags
119 | Predictor = 317,
120 | TileWidth = 322,
121 | TileLength = 323,
122 | TileOffsets = 324,
123 | TileByteCounts = 325,
124 | // Data Sample Format
125 | SampleFormat = 339,
126 | SMinSampleValue = 340, // TODO add support
127 | SMaxSampleValue = 341, // TODO add support
128 | // JPEG
129 | JPEGTables = 347,
130 | // GeoTIFF
131 | ModelPixelScaleTag = 33550, // (SoftDesk)
132 | ModelTransformationTag = 34264, // (JPL Carto Group)
133 | ModelTiepointTag = 33922, // (Intergraph)
134 | GeoKeyDirectoryTag = 34735, // (SPOT)
135 | GeoDoubleParamsTag = 34736, // (SPOT)
136 | GeoAsciiParamsTag = 34737, // (SPOT)
137 | GdalNodata = 42113, // Contains areas with missing data
138 | }
139 | }
140 |
141 | tags! {
142 | /// The type of an IFD entry (a 2 byte field).
143 | pub enum Type(u16) {
144 | /// 8-bit unsigned integer
145 | BYTE = 1,
146 | /// 8-bit byte that contains a 7-bit ASCII code; the last byte must be zero
147 | ASCII = 2,
148 | /// 16-bit unsigned integer
149 | SHORT = 3,
150 | /// 32-bit unsigned integer
151 | LONG = 4,
152 | /// Fraction stored as two 32-bit unsigned integers
153 | RATIONAL = 5,
154 | /// 8-bit signed integer
155 | SBYTE = 6,
156 | /// 8-bit byte that may contain anything, depending on the field
157 | UNDEFINED = 7,
158 | /// 16-bit signed integer
159 | SSHORT = 8,
160 | /// 32-bit signed integer
161 | SLONG = 9,
162 | /// Fraction stored as two 32-bit signed integers
163 | SRATIONAL = 10,
164 | /// 32-bit IEEE floating point
165 | FLOAT = 11,
166 | /// 64-bit IEEE floating point
167 | DOUBLE = 12,
168 | /// 32-bit unsigned integer (offset)
169 | IFD = 13,
170 | /// BigTIFF 64-bit unsigned integer
171 | LONG8 = 16,
172 | /// BigTIFF 64-bit signed integer
173 | SLONG8 = 17,
174 | /// BigTIFF 64-bit unsigned integer (offset)
175 | IFD8 = 18,
176 | }
177 | }
178 |
179 | tags! {
180 | /// See [TIFF compression tags](https://www.awaresystems.be/imaging/tiff/tifftags/compression.html)
181 | /// for reference.
182 | pub enum CompressionMethod(u16) unknown("A custom compression method") {
183 | None = 1,
184 | Huffman = 2,
185 | Fax3 = 3,
186 | Fax4 = 4,
187 | LZW = 5,
188 | JPEG = 6,
189 | // "Extended JPEG" or "new JPEG" style
190 | ModernJPEG = 7,
191 | Deflate = 8,
192 | OldDeflate = 0x80B2,
193 | PackBits = 0x8005,
194 |
195 | // Self-assigned by libtiff
196 | ZSTD = 0xC350,
197 | }
198 | }
199 |
200 | tags! {
201 | pub enum PhotometricInterpretation(u16) {
202 | WhiteIsZero = 0,
203 | BlackIsZero = 1,
204 | RGB = 2,
205 | RGBPalette = 3,
206 | TransparencyMask = 4,
207 | CMYK = 5,
208 | YCbCr = 6,
209 | CIELab = 8,
210 | }
211 | }
212 |
213 | tags! {
214 | pub enum PlanarConfiguration(u16) {
215 | Chunky = 1,
216 | Planar = 2,
217 | }
218 | }
219 |
220 | tags! {
221 | pub enum Predictor(u16) {
222 | /// No changes were made to the data
223 | None = 1,
224 | /// The images' rows were processed to contain the difference of each pixel from the previous one.
225 | ///
226 | /// This means that instead of having in order `[r1, g1. b1, r2, g2 ...]` you will find
227 | /// `[r1, g1, b1, r2-r1, g2-g1, b2-b1, r3-r2, g3-g2, ...]`
228 | Horizontal = 2,
229 | /// Not currently supported
230 | FloatingPoint = 3,
231 | }
232 | }
233 |
234 | tags! {
235 | /// Type to represent resolution units
236 | pub enum ResolutionUnit(u16) {
237 | None = 1,
238 | Inch = 2,
239 | Centimeter = 3,
240 | }
241 | }
242 |
243 | tags! {
244 | pub enum SampleFormat(u16) unknown("An unknown extension sample format") {
245 | Uint = 1,
246 | Int = 2,
247 | IEEEFP = 3,
248 | Void = 4,
249 | }
250 | }
251 |
--------------------------------------------------------------------------------
/tests/image_tiff/predict.rs:
--------------------------------------------------------------------------------
1 | extern crate tiff;
2 |
3 | use tiff::decoder::{Decoder, DecodingResult};
4 | use tiff::encoder::{colortype, Compression, Predictor, TiffEncoder};
5 | use tiff::ColorType;
6 |
7 | use std::fs::File;
8 | use std::io::{Cursor, Seek, SeekFrom};
9 | use std::path::PathBuf;
10 |
11 | const TEST_IMAGE_DIR: &str = "./tests/image_tiff/images/";
12 |
13 | macro_rules! test_predict {
14 | ($name:ident, $buffer:ident, $buffer_ty:ty) => {
15 | fn $name>(
16 | file: &str,
17 | expected_type: ColorType,
18 | ) {
19 | let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
20 | let file = File::open(path).expect("Cannot find test image!");
21 | let mut decoder = Decoder::new(file).expect("Cannot create decoder!");
22 |
23 | assert_eq!(decoder.colortype().unwrap(), expected_type);
24 | let image_data = match decoder.read_image().unwrap() {
25 | DecodingResult::$buffer(res) => res,
26 | _ => panic!("Wrong data type"),
27 | };
28 |
29 | let mut predicted = Vec::with_capacity(image_data.len());
30 | C::horizontal_predict(&image_data, &mut predicted);
31 |
32 | let sample_size = C::SAMPLE_FORMAT.len();
33 |
34 | (0..sample_size).for_each(|i| {
35 | assert_eq!(predicted[i], image_data[i]);
36 | });
37 |
38 | (sample_size..image_data.len()).for_each(|i| {
39 | predicted[i] = predicted[i].wrapping_add(predicted[i - sample_size]);
40 | assert_eq!(predicted[i], image_data[i]);
41 | });
42 | }
43 | };
44 | }
45 |
46 | test_predict!(test_u8_predict, U8, u8);
47 | test_predict!(test_i8_predict, I8, i8);
48 | test_predict!(test_u16_predict, U16, u16);
49 | test_predict!(test_i16_predict, I16, i16);
50 | test_predict!(test_u32_predict, U32, u32);
51 | test_predict!(test_u64_predict, U64, u64);
52 |
53 | #[test]
54 | fn test_gray_u8_predict() {
55 | test_u8_predict::("minisblack-1c-8b.tiff", ColorType::Gray(8));
56 | }
57 |
58 | #[test]
59 | fn test_gray_i8_predict() {
60 | test_i8_predict::("minisblack-1c-i8b.tiff", ColorType::Gray(8));
61 | }
62 |
63 | #[test]
64 | fn test_rgb_u8_predict() {
65 | test_u8_predict::("rgb-3c-8b.tiff", ColorType::RGB(8));
66 | }
67 |
68 | #[test]
69 | fn test_cmyk_u8_predict() {
70 | test_u8_predict::("cmyk-3c-8b.tiff", ColorType::CMYK(8));
71 | }
72 |
73 | #[test]
74 | fn test_gray_u16_predict() {
75 | test_u16_predict::("minisblack-1c-16b.tiff", ColorType::Gray(16));
76 | }
77 |
78 | #[test]
79 | fn test_gray_i16_predict() {
80 | test_i16_predict::("minisblack-1c-i16b.tiff", ColorType::Gray(16));
81 | }
82 |
83 | #[test]
84 | fn test_rgb_u16_predict() {
85 | test_u16_predict::("rgb-3c-16b.tiff", ColorType::RGB(16));
86 | }
87 |
88 | #[test]
89 | fn test_cmyk_u16_predict() {
90 | test_u16_predict::("cmyk-3c-16b.tiff", ColorType::CMYK(16));
91 | }
92 |
93 | #[test]
94 | fn test_gray_u32_predict() {
95 | test_u32_predict::("gradient-1c-32b.tiff", ColorType::Gray(32));
96 | }
97 |
98 | #[test]
99 | fn test_rgb_u32_predict() {
100 | test_u32_predict::("gradient-3c-32b.tiff", ColorType::RGB(32));
101 | }
102 |
103 | #[test]
104 | fn test_gray_u64_predict() {
105 | test_u64_predict::("gradient-1c-64b.tiff", ColorType::Gray(64));
106 | }
107 |
108 | #[test]
109 | fn test_rgb_u64_predict() {
110 | test_u64_predict::("gradient-3c-64b.tiff", ColorType::RGB(64));
111 | }
112 |
113 | #[test]
114 | fn test_ycbcr_u8_predict() {
115 | test_u8_predict::("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
116 | }
117 |
118 | macro_rules! test_predict_roundtrip {
119 | ($name:ident, $buffer:ident, $buffer_ty:ty) => {
120 | fn $name>(
121 | file: &str,
122 | expected_type: ColorType,
123 | ) {
124 | let path = PathBuf::from(TEST_IMAGE_DIR).join(file);
125 | let img_file = File::open(path).expect("Cannot find test image!");
126 | let mut decoder = Decoder::new(img_file).expect("Cannot create decoder");
127 | assert_eq!(decoder.colortype().unwrap(), expected_type);
128 |
129 | let image_data = match decoder.read_image().unwrap() {
130 | DecodingResult::$buffer(res) => res,
131 | _ => panic!("Wrong data type"),
132 | };
133 |
134 | let mut file = Cursor::new(Vec::new());
135 | {
136 | let mut tiff = TiffEncoder::new(&mut file)
137 | .unwrap()
138 | .with_predictor(Predictor::Horizontal);
139 |
140 | let (width, height) = decoder.dimensions().unwrap();
141 | tiff.write_image::(width, height, &image_data).unwrap();
142 | }
143 | file.seek(SeekFrom::Start(0)).unwrap();
144 | {
145 | let mut decoder = Decoder::new(&mut file).unwrap();
146 | if let DecodingResult::$buffer(img_res) =
147 | decoder.read_image().expect("Decoding image failed")
148 | {
149 | assert_eq!(image_data, img_res);
150 | } else {
151 | panic!("Wrong data type");
152 | }
153 | }
154 | }
155 | };
156 | }
157 |
158 | test_predict_roundtrip!(test_u8_predict_roundtrip, U8, u8);
159 | test_predict_roundtrip!(test_i8_predict_roundtrip, I8, i8);
160 | test_predict_roundtrip!(test_u16_predict_roundtrip, U16, u16);
161 | test_predict_roundtrip!(test_i16_predict_roundtrip, I16, i16);
162 | test_predict_roundtrip!(test_u32_predict_roundtrip, U32, u32);
163 | test_predict_roundtrip!(test_u64_predict_roundtrip, U64, u64);
164 |
165 | #[test]
166 | fn test_gray_u8_predict_roundtrip() {
167 | test_u8_predict_roundtrip::("minisblack-1c-8b.tiff", ColorType::Gray(8));
168 | }
169 |
170 | #[test]
171 | fn test_gray_i8_predict_roundtrip() {
172 | test_i8_predict_roundtrip::("minisblack-1c-i8b.tiff", ColorType::Gray(8));
173 | }
174 |
175 | #[test]
176 | fn test_rgb_u8_predict_roundtrip() {
177 | test_u8_predict_roundtrip::("rgb-3c-8b.tiff", ColorType::RGB(8));
178 | }
179 |
180 | #[test]
181 | fn test_cmyk_u8_predict_roundtrip() {
182 | test_u8_predict_roundtrip::("cmyk-3c-8b.tiff", ColorType::CMYK(8));
183 | }
184 |
185 | #[test]
186 | fn test_gray_u16_predict_roundtrip() {
187 | test_u16_predict_roundtrip::("minisblack-1c-16b.tiff", ColorType::Gray(16));
188 | }
189 |
190 | #[test]
191 | fn test_gray_i16_predict_roundtrip() {
192 | test_i16_predict_roundtrip::(
193 | "minisblack-1c-i16b.tiff",
194 | ColorType::Gray(16),
195 | );
196 | }
197 |
198 | #[test]
199 | fn test_rgb_u16_predict_roundtrip() {
200 | test_u16_predict_roundtrip::("rgb-3c-16b.tiff", ColorType::RGB(16));
201 | }
202 |
203 | #[test]
204 | fn test_cmyk_u16_predict_roundtrip() {
205 | test_u16_predict_roundtrip::("cmyk-3c-16b.tiff", ColorType::CMYK(16));
206 | }
207 |
208 | #[test]
209 | fn test_gray_u32_predict_roundtrip() {
210 | test_u32_predict_roundtrip::("gradient-1c-32b.tiff", ColorType::Gray(32));
211 | }
212 |
213 | #[test]
214 | fn test_rgb_u32_predict_roundtrip() {
215 | test_u32_predict_roundtrip::("gradient-3c-32b.tiff", ColorType::RGB(32));
216 | }
217 |
218 | #[test]
219 | fn test_gray_u64_predict_roundtrip() {
220 | test_u64_predict_roundtrip::("gradient-1c-64b.tiff", ColorType::Gray(64));
221 | }
222 |
223 | #[test]
224 | fn test_rgb_u64_predict_roundtrip() {
225 | test_u64_predict_roundtrip::("gradient-3c-64b.tiff", ColorType::RGB(64));
226 | }
227 |
228 | #[test]
229 | fn test_ycbcr_u8_predict_roundtrip() {
230 | test_u8_predict_roundtrip::("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8));
231 | }
232 |
--------------------------------------------------------------------------------
/.github/workflows/python-wheels.yml:
--------------------------------------------------------------------------------
1 | # This file is (mostly) autogenerated by maturin v1.7.1
2 | # To update, run
3 | #
4 | # maturin generate-ci github -m python/Cargo.toml
5 | #
6 | name: Build wheels
7 |
8 | on:
9 | push:
10 | tags:
11 | - "py-v*"
12 | workflow_dispatch:
13 |
14 | permissions:
15 | contents: read
16 |
17 | concurrency:
18 | group: ${{ github.workflow }}-${{ github.ref }}
19 | cancel-in-progress: true
20 |
21 | jobs:
22 | linux:
23 | runs-on: ${{ matrix.platform.runner }}
24 | strategy:
25 | matrix:
26 | platform:
27 | - runner: ubuntu-latest
28 | target: x86_64
29 | manylinux: auto
30 | - runner: ubuntu-latest
31 | target: x86
32 | manylinux: auto
33 | - runner: ubuntu-latest
34 | target: aarch64
35 | manylinux: "2_24"
36 | - runner: ubuntu-latest
37 | target: armv7
38 | manylinux: auto
39 | - runner: ubuntu-latest
40 | target: s390x
41 | manylinux: auto
42 | - runner: ubuntu-latest
43 | target: ppc64le
44 | manylinux: auto
45 | steps:
46 | - uses: actions/checkout@v4
47 | with:
48 | submodules: "recursive"
49 |
50 | - name: Install uv
51 | uses: astral-sh/setup-uv@v5
52 | with:
53 | enable-cache: true
54 | version: "0.5.x"
55 |
56 | - name: Install Python versions
57 | run: uv python install 3.10 3.13 pypy3.11
58 |
59 | - name: Build abi3-py311 wheels
60 | uses: PyO3/maturin-action@v1
61 | with:
62 | target: ${{ matrix.platform.target }}
63 | args: --release --out dist -i 3.13 --features abi3-py311 --features extension-module --manifest-path python/Cargo.toml
64 | sccache: "true"
65 | manylinux: ${{ matrix.platform.manylinux }}
66 |
67 | - name: Build version-specific wheels
68 | uses: PyO3/maturin-action@v1
69 | with:
70 | target: ${{ matrix.platform.target }}
71 | args: --release --out dist -i 3.10 -i pypy3.11 --features extension-module --manifest-path python/Cargo.toml
72 | sccache: "true"
73 | manylinux: ${{ matrix.platform.manylinux }}
74 |
75 | - name: Upload wheels
76 | uses: actions/upload-artifact@v4
77 | with:
78 | name: wheels-linux-${{ matrix.platform.target }}
79 | path: dist
80 |
81 | musllinux:
82 | runs-on: ${{ matrix.platform.runner }}
83 | strategy:
84 | matrix:
85 | platform:
86 | - runner: ubuntu-latest
87 | target: x86_64
88 | - runner: ubuntu-latest
89 | target: x86
90 | - runner: ubuntu-latest
91 | target: aarch64
92 | - runner: ubuntu-latest
93 | target: armv7
94 | steps:
95 | - uses: actions/checkout@v4
96 | with:
97 | submodules: "recursive"
98 |
99 | - name: Install uv
100 | uses: astral-sh/setup-uv@v5
101 | with:
102 | enable-cache: true
103 | version: "0.5.x"
104 |
105 | - name: Install Python versions
106 | run: uv python install 3.10 3.13 pypy3.11
107 |
108 | - name: Build abi3-py311 wheels
109 | uses: PyO3/maturin-action@v1
110 | with:
111 | target: ${{ matrix.platform.target }}
112 | args: --release --out dist -i 3.13 --features abi3-py311 --features extension-module --manifest-path python/Cargo.toml
113 | sccache: "true"
114 | manylinux: musllinux_1_2
115 |
116 | - name: Build version-specific wheels
117 | uses: PyO3/maturin-action@v1
118 | with:
119 | target: ${{ matrix.platform.target }}
120 | args: --release --out dist -i 3.10 -i pypy3.11 --features extension-module --manifest-path python/Cargo.toml
121 | sccache: "true"
122 | manylinux: musllinux_1_2
123 |
124 | - name: Upload wheels
125 | uses: actions/upload-artifact@v4
126 | with:
127 | name: wheels-musllinux-${{ matrix.platform.target }}
128 | path: dist
129 |
130 | windows:
131 | runs-on: ${{ matrix.platform.runner }}
132 | strategy:
133 | matrix:
134 | platform:
135 | - runner: windows-latest
136 | target: x64
137 | steps:
138 | - uses: actions/checkout@v4
139 | with:
140 | submodules: "recursive"
141 | # There seem to be linking errors on Windows with the uv-provided Python
142 | # executables, so we use the Python versions provided by github actions
143 | # for now.
144 | # Seems to be this question: https://stackoverflow.com/questions/78557803/python-with-rust-cannot-open-input-file-python3-lib
145 | - uses: actions/setup-python@v5
146 | with:
147 | python-version: 3.13
148 | architecture: ${{ matrix.platform.target }}
149 | - name: Remove symlink (not supported in Windows wheels)
150 | run: Remove-Item -Path python/python/async_tiff/store -Force
151 | shell: pwsh
152 |
153 | - name: Build abi3-py311 wheels
154 | uses: PyO3/maturin-action@v1
155 | with:
156 | target: ${{ matrix.platform.target }}
157 | args: --release --out dist -i 3.13 --features abi3-py311 --features extension-module --manifest-path python/Cargo.toml
158 | sccache: "true"
159 |
160 | - name: Build version-specific wheels
161 | uses: PyO3/maturin-action@v1
162 | with:
163 | target: ${{ matrix.platform.target }}
164 | args: --release --out dist -i 3.10 --features extension-module --manifest-path python/Cargo.toml
165 | sccache: "true"
166 |
167 | - name: Upload wheels
168 | uses: actions/upload-artifact@v4
169 | with:
170 | name: wheels-windows-${{ matrix.platform.target }}
171 | path: dist
172 |
173 | macos:
174 | runs-on: ${{ matrix.platform.runner }}
175 | strategy:
176 | matrix:
177 | platform:
178 | - runner: macos-15-intel
179 | target: x86_64
180 | - runner: macos-15
181 | target: aarch64
182 | steps:
183 | - uses: actions/checkout@v4
184 | with:
185 | submodules: "recursive"
186 |
187 | - name: Install uv
188 | uses: astral-sh/setup-uv@v5
189 | with:
190 | enable-cache: true
191 | version: "0.5.x"
192 |
193 | - name: Install Python versions
194 | run: uv python install 3.10 3.13 pypy3.11
195 |
196 | - name: Build abi3-py311 wheels
197 | uses: PyO3/maturin-action@v1
198 | with:
199 | target: ${{ matrix.platform.target }}
200 | args: --release --out dist -i 3.13 --features abi3-py311 --features extension-module --manifest-path python/Cargo.toml
201 | sccache: "true"
202 |
203 | - name: Build version-specific wheels
204 | uses: PyO3/maturin-action@v1
205 | with:
206 | target: ${{ matrix.platform.target }}
207 | args: --release --out dist -i 3.10 -i pypy3.11 --features extension-module --manifest-path python/Cargo.toml
208 | sccache: "true"
209 |
210 | - name: Upload wheels
211 | uses: actions/upload-artifact@v4
212 | with:
213 | name: wheels-macos-${{ matrix.platform.target }}
214 | path: dist
215 |
216 | # sdist:
217 | # runs-on: ubuntu-latest
218 | # strategy:
219 | # matrix:
220 | # steps:
221 | # - uses: actions/checkout@v4
222 | # - name: Build sdist
223 | # uses: PyO3/maturin-action@v1
224 | # with:
225 | # command: sdist
226 | # args: --out dist --manifest-path python/Cargo.toml
227 | # - name: Upload sdist
228 | # uses: actions/upload-artifact@v4
229 | # with:
230 | # name: wheels-sdist
231 | # path: dist
232 |
233 | release:
234 | runs-on: ubuntu-latest
235 | name: Release
236 | # environment:
237 | # name: release
238 | # url: https://pypi.org/p/async-tiff
239 | # permissions:
240 | # # IMPORTANT: this permission is mandatory for trusted publishing
241 | # id-token: write
242 | if: startsWith(github.ref, 'refs/tags/')
243 | needs: [linux, musllinux, windows, macos]
244 | steps:
245 | - uses: actions/download-artifact@v4
246 | with:
247 | pattern: wheels-*
248 | merge-multiple: true
249 | path: dist
250 | - uses: actions/setup-python@v5
251 | with:
252 | python-version: 3.11
253 |
254 | - uses: pypa/gh-action-pypi-publish@release/v1
255 | with:
256 | user: __token__
257 | password: ${{ secrets.PYPI_API_TOKEN }}
258 |
--------------------------------------------------------------------------------
/src/metadata/cache.rs:
--------------------------------------------------------------------------------
1 | //! Caching strategies for metadata fetching.
2 |
3 | use std::ops::Range;
4 | use std::sync::Arc;
5 |
6 | use async_trait::async_trait;
7 | use bytes::{Bytes, BytesMut};
8 | use tokio::sync::Mutex;
9 |
10 | use crate::error::AsyncTiffResult;
11 | use crate::metadata::MetadataFetch;
12 |
13 | /// Logic for managing a cache of sequential buffers
14 | #[derive(Debug)]
15 | struct SequentialBlockCache {
16 | /// Contiguous blocks from offset 0
17 | ///
18 | /// # Invariant
19 | /// - Buffers are contiguous from offset 0
20 | buffers: Vec,
21 |
22 | /// Total length cached (== sum of buffers lengths)
23 | len: u64,
24 | }
25 |
26 | impl SequentialBlockCache {
27 | /// Create a new, empty SequentialBlockCache
28 | fn new() -> Self {
29 | Self {
30 | buffers: vec![],
31 | len: 0,
32 | }
33 | }
34 |
35 | /// Check if the given range is fully contained within the cached buffers
36 | fn contains(&self, range: Range) -> bool {
37 | range.end <= self.len
38 | }
39 |
40 | /// Slice out the given range from the cached buffers
41 | fn slice(&self, range: Range) -> Bytes {
42 | // The size of the output buffer
43 | let out_len = (range.end - range.start) as usize;
44 |
45 | // The remaining range of bytes required. This range is updated as we traverse buffers, so
46 | // the indexes are relative to the current buffer.
47 | let mut remaining = range;
48 | let mut out_buffers: Vec = vec![];
49 |
50 | for buf in &self.buffers {
51 | let current_buf_len = buf.len() as u64;
52 |
53 | // this block falls entirely before the desired range start
54 | if remaining.start >= current_buf_len {
55 | remaining.start -= current_buf_len;
56 | remaining.end -= current_buf_len;
57 | continue;
58 | }
59 |
60 | // we slice bytes out of *this* block
61 | let start = remaining.start as usize;
62 | let length =
63 | (remaining.end - remaining.start).min(current_buf_len - remaining.start) as usize;
64 | let end = start + length;
65 |
66 | // nothing to take from this block
67 | if start == end {
68 | continue;
69 | }
70 |
71 | let chunk = buf.slice(start..end);
72 | out_buffers.push(chunk);
73 |
74 | // consumed some portion; update and potentially break
75 | remaining.start = 0;
76 | if remaining.end <= current_buf_len {
77 | break;
78 | }
79 | remaining.end -= current_buf_len;
80 | }
81 |
82 | if out_buffers.len() == 1 {
83 | out_buffers.into_iter().next().unwrap()
84 | } else {
85 | let mut out = BytesMut::with_capacity(out_len);
86 | for b in out_buffers {
87 | out.extend_from_slice(&b);
88 | }
89 | out.into()
90 | }
91 | }
92 |
93 | fn append_buffer(&mut self, buffer: Bytes) {
94 | self.len += buffer.len() as u64;
95 | self.buffers.push(buffer);
96 | }
97 | }
98 |
99 | /// A MetadataFetch implementation that caches fetched data in exponentially growing chunks,
100 | /// sequentially from the beginning of the file.
101 | #[derive(Debug)]
102 | pub struct ReadaheadMetadataCache {
103 | inner: F,
104 | cache: Arc>,
105 | initial: u64,
106 | multiplier: f64,
107 | }
108 |
109 | impl ReadaheadMetadataCache {
110 | /// Create a new ReadaheadMetadataCache wrapping the given MetadataFetch
111 | pub fn new(inner: F) -> Self {
112 | Self {
113 | inner,
114 | cache: Arc::new(Mutex::new(SequentialBlockCache::new())),
115 | initial: 32 * 1024,
116 | multiplier: 2.0,
117 | }
118 | }
119 |
120 | /// Access the inner MetadataFetch
121 | pub fn inner(&self) -> &F {
122 | &self.inner
123 | }
124 |
125 | /// Set the initial fetch size in bytes, otherwise defaults to 32 KiB
126 | pub fn with_initial_size(mut self, initial: u64) -> Self {
127 | self.initial = initial;
128 | self
129 | }
130 |
131 | /// Set the multiplier for subsequent fetch sizes, otherwise defaults to 2.0
132 | pub fn with_multiplier(mut self, multiplier: f64) -> Self {
133 | self.multiplier = multiplier;
134 | self
135 | }
136 |
137 | fn next_fetch_size(&self, existing_len: u64) -> u64 {
138 | if existing_len == 0 {
139 | self.initial
140 | } else {
141 | (existing_len as f64 * self.multiplier).round() as u64
142 | }
143 | }
144 | }
145 |
146 | #[async_trait]
147 | impl MetadataFetch for ReadaheadMetadataCache {
148 | async fn fetch(&self, range: Range) -> AsyncTiffResult {
149 | let mut cache = self.cache.lock().await;
150 |
151 | // First check if we already have the range cached
152 | if cache.contains(range.start..range.end) {
153 | return Ok(cache.slice(range));
154 | }
155 |
156 | // Compute the correct fetch range
157 | let start_len = cache.len;
158 | let needed = range.end.saturating_sub(start_len);
159 | let fetch_size = self.next_fetch_size(start_len).max(needed);
160 | let fetch_range = start_len..start_len + fetch_size;
161 |
162 | // Perform the fetch while holding mutex
163 | // (this is OK because the mutex is async)
164 | let bytes = self.inner.fetch(fetch_range).await?;
165 |
166 | // Now append safely
167 | cache.append_buffer(bytes);
168 |
169 | Ok(cache.slice(range))
170 | }
171 | }
172 |
173 | #[cfg(test)]
174 | mod test {
175 | use super::*;
176 |
177 | #[derive(Debug)]
178 | struct TestFetch {
179 | data: Bytes,
180 | /// The number of fetches that actually reach the raw Fetch implementation
181 | num_fetches: Arc>,
182 | }
183 |
184 | impl TestFetch {
185 | fn new(data: Bytes) -> Self {
186 | Self {
187 | data,
188 | num_fetches: Arc::new(Mutex::new(0)),
189 | }
190 | }
191 | }
192 |
193 | #[async_trait]
194 | impl MetadataFetch for TestFetch {
195 | async fn fetch(&self, range: Range) -> crate::error::AsyncTiffResult {
196 | if range.start as usize >= self.data.len() {
197 | return Ok(Bytes::new());
198 | }
199 |
200 | let end = (range.end as usize).min(self.data.len());
201 | let slice = self.data.slice(range.start as _..end);
202 | let mut g = self.num_fetches.lock().await;
203 | *g += 1;
204 | Ok(slice)
205 | }
206 | }
207 |
208 | #[tokio::test]
209 | async fn test_readahead_cache() {
210 | let data = Bytes::from_static(b"abcdefghijklmnopqrstuvwxyz");
211 | let fetch = TestFetch::new(data.clone());
212 | let cache = ReadaheadMetadataCache::new(fetch)
213 | .with_initial_size(2)
214 | .with_multiplier(3.0);
215 |
216 | // Make initial request
217 | let result = cache.fetch(0..2).await.unwrap();
218 | assert_eq!(result.as_ref(), b"ab");
219 | assert_eq!(*cache.inner.num_fetches.lock().await, 1);
220 |
221 | // Making a request within the cached range should not trigger a new fetch
222 | let result = cache.fetch(1..2).await.unwrap();
223 | assert_eq!(result.as_ref(), b"b");
224 | assert_eq!(*cache.inner.num_fetches.lock().await, 1);
225 |
226 | // Making a request that exceeds the cached range should trigger a new fetch
227 | let result = cache.fetch(2..5).await.unwrap();
228 | assert_eq!(result.as_ref(), b"cde");
229 | assert_eq!(*cache.inner.num_fetches.lock().await, 2);
230 |
231 | // Multiplier should be accurate: initial was 2, next was 6 (2*3), so total cached is now 8
232 | let result = cache.fetch(5..8).await.unwrap();
233 | assert_eq!(result.as_ref(), b"fgh");
234 | assert_eq!(*cache.inner.num_fetches.lock().await, 2);
235 |
236 | // Should work even for fetch range larger than underlying buffer
237 | let result = cache.fetch(8..20).await.unwrap();
238 | assert_eq!(result.as_ref(), b"ijklmnopqrst");
239 | assert_eq!(*cache.inner.num_fetches.lock().await, 3);
240 | }
241 |
242 | #[test]
243 | fn test_sequential_block_cache_empty_buffers() {
244 | let mut cache = SequentialBlockCache::new();
245 | cache.append_buffer(Bytes::from_static(b"012"));
246 | cache.append_buffer(Bytes::from_static(b""));
247 | cache.append_buffer(Bytes::from_static(b"34"));
248 | cache.append_buffer(Bytes::from_static(b""));
249 | cache.append_buffer(Bytes::from_static(b"5"));
250 | cache.append_buffer(Bytes::from_static(b""));
251 | cache.append_buffer(Bytes::from_static(b"67"));
252 |
253 | // Range, does it exist, expected slice
254 | let test_cases = [
255 | (0..3, true, Bytes::from_static(b"012")),
256 | (4..7, true, Bytes::from_static(b"456")),
257 | (0..8, true, Bytes::from_static(b"01234567")),
258 | (6..6, true, Bytes::from_static(b"")),
259 | (6..9, false, Bytes::from_static(b"")),
260 | (9..9, false, Bytes::from_static(b"")),
261 | (8..10, false, Bytes::from_static(b"")),
262 | ];
263 |
264 | for (range, exists, expected) in test_cases {
265 | assert_eq!(cache.contains(range.clone()), exists);
266 | if exists {
267 | assert_eq!(cache.slice(range.clone()), expected);
268 | }
269 | }
270 | }
271 | }
272 |
--------------------------------------------------------------------------------
/src/reader.rs:
--------------------------------------------------------------------------------
1 | //! Abstractions for network reading.
2 |
3 | use std::fmt::Debug;
4 | use std::io::Read;
5 | use std::ops::Range;
6 | use std::sync::Arc;
7 |
8 | use async_trait::async_trait;
9 | use byteorder::{BigEndian, LittleEndian, ReadBytesExt};
10 | use bytes::buf::Reader;
11 | use bytes::{Buf, Bytes};
12 | use futures::TryFutureExt;
13 |
14 | use crate::error::AsyncTiffResult;
15 |
16 | /// The asynchronous interface used to read COG files
17 | ///
18 | /// This was derived from the Parquet
19 | /// [`AsyncFileReader`](https://docs.rs/parquet/latest/parquet/arrow/async_reader/trait.AsyncFileReader.html)
20 | ///
21 | /// Notes:
22 | ///
23 | /// 1. [`ObjectReader`], available when the `object_store` crate feature
24 | /// is enabled, implements this interface for [`ObjectStore`].
25 | ///
26 | /// 2. You can use [`TokioReader`] to implement [`AsyncFileReader`] for types that implement
27 | /// [`tokio::io::AsyncRead`] and [`tokio::io::AsyncSeek`], for example [`tokio::fs::File`].
28 | ///
29 | /// [`ObjectStore`]: object_store::ObjectStore
30 | ///
31 | /// [`tokio::fs::File`]: https://docs.rs/tokio/latest/tokio/fs/struct.File.html
32 | #[async_trait]
33 | pub trait AsyncFileReader: Debug + Send + Sync + 'static {
34 | /// Retrieve the bytes in `range` as part of a request for image data, not header metadata.
35 | ///
36 | /// This is also used as the default implementation of [`MetadataFetch`] if not overridden.
37 | async fn get_bytes(&self, range: Range) -> AsyncTiffResult;
38 |
39 | /// Retrieve multiple byte ranges as part of a request for image data, not header metadata. The
40 | /// default implementation will call `get_bytes` sequentially
41 | async fn get_byte_ranges(&self, ranges: Vec>) -> AsyncTiffResult> {
42 | let mut result = Vec::with_capacity(ranges.len());
43 |
44 | for range in ranges.into_iter() {
45 | let data = self.get_bytes(range).await?;
46 | result.push(data);
47 | }
48 |
49 | Ok(result)
50 | }
51 | }
52 |
53 | /// This allows Box to be used as an AsyncFileReader,
54 | #[async_trait]
55 | impl AsyncFileReader for Box {
56 | async fn get_bytes(&self, range: Range) -> AsyncTiffResult {
57 | self.as_ref().get_bytes(range).await
58 | }
59 |
60 | async fn get_byte_ranges(&self, ranges: Vec>) -> AsyncTiffResult> {
61 | self.as_ref().get_byte_ranges(ranges).await
62 | }
63 | }
64 |
65 | /// This allows Arc to be used as an AsyncFileReader,
66 | #[async_trait]
67 | impl AsyncFileReader for Arc {
68 | async fn get_bytes(&self, range: Range) -> AsyncTiffResult {
69 | self.as_ref().get_bytes(range).await
70 | }
71 |
72 | async fn get_byte_ranges(&self, ranges: Vec>) -> AsyncTiffResult> {
73 | self.as_ref().get_byte_ranges(ranges).await
74 | }
75 | }
76 |
77 | /// A wrapper for things that implement [AsyncRead] and [AsyncSeek] to also implement
78 | /// [AsyncFileReader].
79 | ///
80 | /// This wrapper is needed because `AsyncRead` and `AsyncSeek` require mutable access to seek and
81 | /// read data, while the `AsyncFileReader` trait requires immutable access to read data.
82 | ///
83 | /// This wrapper stores the inner reader in a `Mutex`.
84 | ///
85 | /// [AsyncRead]: tokio::io::AsyncRead
86 | /// [AsyncSeek]: tokio::io::AsyncSeek
87 | #[cfg(feature = "tokio")]
88 | #[derive(Debug)]
89 | pub struct TokioReader(
90 | tokio::sync::Mutex,
91 | );
92 |
93 | #[cfg(feature = "tokio")]
94 | impl TokioReader {
95 | /// Create a new TokioReader from a reader.
96 | pub fn new(inner: T) -> Self {
97 | Self(tokio::sync::Mutex::new(inner))
98 | }
99 |
100 | async fn make_range_request(&self, range: Range) -> AsyncTiffResult {
101 | use std::io::SeekFrom;
102 |
103 | use tokio::io::{AsyncReadExt, AsyncSeekExt};
104 |
105 | use crate::error::AsyncTiffError;
106 |
107 | let mut file = self.0.lock().await;
108 |
109 | file.seek(SeekFrom::Start(range.start)).await?;
110 |
111 | let to_read = range.end - range.start;
112 | let mut buffer = Vec::with_capacity(to_read as usize);
113 | let read = file.read(&mut buffer).await? as u64;
114 | if read != to_read {
115 | return Err(AsyncTiffError::EndOfFile(to_read, read));
116 | }
117 |
118 | Ok(buffer.into())
119 | }
120 | }
121 |
122 | #[cfg(feature = "tokio")]
123 | #[async_trait]
124 | impl
125 | AsyncFileReader for TokioReader
126 | {
127 | async fn get_bytes(&self, range: Range) -> AsyncTiffResult {
128 | self.make_range_request(range).await
129 | }
130 | }
131 |
132 | /// An AsyncFileReader that reads from an [`ObjectStore`] instance.
133 | #[cfg(feature = "object_store")]
134 | #[derive(Clone, Debug)]
135 | pub struct ObjectReader {
136 | store: Arc,
137 | path: object_store::path::Path,
138 | }
139 |
140 | #[cfg(feature = "object_store")]
141 | impl ObjectReader {
142 | /// Creates a new [`ObjectReader`] for the provided [`ObjectStore`] and path
143 | ///
144 | /// [`ObjectMeta`] can be obtained using [`ObjectStore::list`] or [`ObjectStore::head`]
145 | pub fn new(store: Arc, path: object_store::path::Path) -> Self {
146 | Self { store, path }
147 | }
148 |
149 | async fn make_range_request(&self, range: Range) -> AsyncTiffResult {
150 | let range = range.start as _..range.end as _;
151 | self.store
152 | .get_range(&self.path, range)
153 | .map_err(|e| e.into())
154 | .await
155 | }
156 | }
157 |
158 | #[cfg(feature = "object_store")]
159 | #[async_trait]
160 | impl AsyncFileReader for ObjectReader {
161 | async fn get_bytes(&self, range: Range) -> AsyncTiffResult {
162 | self.make_range_request(range).await
163 | }
164 |
165 | async fn get_byte_ranges(&self, ranges: Vec>) -> AsyncTiffResult>
166 | where
167 | Self: Send,
168 | {
169 | let ranges = ranges
170 | .into_iter()
171 | .map(|r| r.start as _..r.end as _)
172 | .collect::>();
173 | self.store
174 | .get_ranges(&self.path, &ranges)
175 | .await
176 | .map_err(|e| e.into())
177 | }
178 | }
179 |
180 | /// An AsyncFileReader that reads from a URL using reqwest.
181 | #[cfg(feature = "reqwest")]
182 | #[derive(Debug, Clone)]
183 | pub struct ReqwestReader {
184 | client: reqwest::Client,
185 | url: reqwest::Url,
186 | }
187 |
188 | #[cfg(feature = "reqwest")]
189 | impl ReqwestReader {
190 | /// Construct a new ReqwestReader from a reqwest client and URL.
191 | pub fn new(client: reqwest::Client, url: reqwest::Url) -> Self {
192 | Self { client, url }
193 | }
194 |
195 | async fn make_range_request(&self, range: Range) -> AsyncTiffResult {
196 | let url = self.url.clone();
197 | let client = self.client.clone();
198 | // HTTP range is inclusive, so we need to subtract 1 from the end
199 | let range = format!("bytes={}-{}", range.start, range.end - 1);
200 | let response = client
201 | .get(url)
202 | .header("Range", range)
203 | .send()
204 | .await?
205 | .error_for_status()?;
206 | let bytes = response.bytes().await?;
207 | Ok(bytes)
208 | }
209 | }
210 |
211 | #[cfg(feature = "reqwest")]
212 | #[async_trait]
213 | impl AsyncFileReader for ReqwestReader {
214 | async fn get_bytes(&self, range: Range) -> AsyncTiffResult {
215 | self.make_range_request(range).await
216 | }
217 | }
218 |
219 | /// Endianness
220 | #[derive(Debug, Clone, Copy, PartialEq)]
221 | pub enum Endianness {
222 | /// Little Endian
223 | LittleEndian,
224 | /// Big Endian
225 | BigEndian,
226 | }
227 |
228 | pub(crate) struct EndianAwareReader {
229 | reader: Reader,
230 | endianness: Endianness,
231 | }
232 |
233 | impl EndianAwareReader {
234 | pub(crate) fn new(bytes: Bytes, endianness: Endianness) -> Self {
235 | Self {
236 | reader: bytes.reader(),
237 | endianness,
238 | }
239 | }
240 |
241 | /// Read a u8 from the cursor, advancing the internal state by 1 byte.
242 | pub(crate) fn read_u8(&mut self) -> AsyncTiffResult {
243 | Ok(self.reader.read_u8()?)
244 | }
245 |
246 | /// Read a i8 from the cursor, advancing the internal state by 1 byte.
247 | pub(crate) fn read_i8(&mut self) -> AsyncTiffResult {
248 | Ok(self.reader.read_i8()?)
249 | }
250 |
251 | pub(crate) fn read_u16(&mut self) -> AsyncTiffResult {
252 | match self.endianness {
253 | Endianness::LittleEndian => Ok(self.reader.read_u16::()?),
254 | Endianness::BigEndian => Ok(self.reader.read_u16::()?),
255 | }
256 | }
257 |
258 | pub(crate) fn read_i16(&mut self) -> AsyncTiffResult {
259 | match self.endianness {
260 | Endianness::LittleEndian => Ok(self.reader.read_i16::()?),
261 | Endianness::BigEndian => Ok(self.reader.read_i16::()?),
262 | }
263 | }
264 |
265 | pub(crate) fn read_u32(&mut self) -> AsyncTiffResult {
266 | match self.endianness {
267 | Endianness::LittleEndian => Ok(self.reader.read_u32::()?),
268 | Endianness::BigEndian => Ok(self.reader.read_u32::()?),
269 | }
270 | }
271 |
272 | pub(crate) fn read_i32(&mut self) -> AsyncTiffResult {
273 | match self.endianness {
274 | Endianness::LittleEndian => Ok(self.reader.read_i32::()?),
275 | Endianness::BigEndian => Ok(self.reader.read_i32::()?),
276 | }
277 | }
278 |
279 | pub(crate) fn read_u64(&mut self) -> AsyncTiffResult {
280 | match self.endianness {
281 | Endianness::LittleEndian => Ok(self.reader.read_u64::()?),
282 | Endianness::BigEndian => Ok(self.reader.read_u64::()?),
283 | }
284 | }
285 |
286 | pub(crate) fn read_i64(&mut self) -> AsyncTiffResult {
287 | match self.endianness {
288 | Endianness::LittleEndian => Ok(self.reader.read_i64::()?),
289 | Endianness::BigEndian => Ok(self.reader.read_i64::()?),
290 | }
291 | }
292 |
293 | pub(crate) fn read_f32(&mut self) -> AsyncTiffResult {
294 | match self.endianness {
295 | Endianness::LittleEndian => Ok(self.reader.read_f32::()?),
296 | Endianness::BigEndian => Ok(self.reader.read_f32::()?),
297 | }
298 | }
299 |
300 | pub(crate) fn read_f64(&mut self) -> AsyncTiffResult {
301 | match self.endianness {
302 | Endianness::LittleEndian => Ok(self.reader.read_f64::()?),
303 | Endianness::BigEndian => Ok(self.reader.read_f64::()?),
304 | }
305 | }
306 |
307 | #[allow(dead_code)]
308 | pub(crate) fn into_inner(self) -> (Reader, Endianness) {
309 | (self.reader, self.endianness)
310 | }
311 | }
312 |
313 | impl AsRef<[u8]> for EndianAwareReader {
314 | fn as_ref(&self) -> &[u8] {
315 | self.reader.get_ref().as_ref()
316 | }
317 | }
318 |
319 | impl Read for EndianAwareReader {
320 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result {
321 | self.reader.read(buf)
322 | }
323 | }
324 |
--------------------------------------------------------------------------------
/src/geo/geo_key_directory.rs:
--------------------------------------------------------------------------------
1 | #![allow(dead_code)]
2 | #![allow(missing_docs)]
3 |
4 | use std::collections::HashMap;
5 |
6 | use num_enum::{IntoPrimitive, TryFromPrimitive};
7 |
8 | use crate::error::{TiffError, TiffResult};
9 | use crate::tag_value::TagValue;
10 |
11 | /// Geospatial TIFF tag variants
12 | #[derive(Clone, Copy, Debug, PartialEq, TryFromPrimitive, IntoPrimitive, Eq, Hash)]
13 | #[repr(u16)]
14 | pub(crate) enum GeoKeyTag {
15 | // GeoTIFF configuration keys
16 | ModelType = 1024,
17 | RasterType = 1025,
18 | Citation = 1026,
19 |
20 | // Geodetic CRS Parameter Keys
21 | GeographicType = 2048,
22 | GeogCitation = 2049,
23 | GeogGeodeticDatum = 2050,
24 | GeogPrimeMeridian = 2051,
25 | GeogLinearUnits = 2052,
26 | GeogLinearUnitSize = 2053,
27 | GeogAngularUnits = 2054,
28 | GeogAngularUnitSize = 2055,
29 | GeogEllipsoid = 2056,
30 | GeogSemiMajorAxis = 2057,
31 | GeogSemiMinorAxis = 2058,
32 | GeogInvFlattening = 2059,
33 | GeogAzimuthUnits = 2060,
34 | GeogPrimeMeridianLong = 2061,
35 |
36 | // Projected CRS Parameter Keys
37 | ProjectedType = 3072,
38 | ProjCitation = 3073,
39 | Projection = 3074,
40 | ProjCoordTrans = 3075,
41 | ProjLinearUnits = 3076,
42 | ProjLinearUnitSize = 3077,
43 | ProjStdParallel1 = 3078,
44 | ProjStdParallel2 = 3079,
45 | ProjNatOriginLong = 3080,
46 | ProjNatOriginLat = 3081,
47 | ProjFalseEasting = 3082,
48 | ProjFalseNorthing = 3083,
49 | ProjFalseOriginLong = 3084,
50 | ProjFalseOriginLat = 3085,
51 | ProjFalseOriginEasting = 3086,
52 | ProjFalseOriginNorthing = 3087,
53 | ProjCenterLong = 3088,
54 | ProjCenterLat = 3089,
55 | ProjCenterEasting = 3090,
56 | ProjCenterNorthing = 3091,
57 | ProjScaleAtNatOrigin = 3092,
58 | ProjScaleAtCenter = 3093,
59 | ProjAzimuthAngle = 3094,
60 | ProjStraightVertPoleLong = 3095,
61 |
62 | // Vertical CRS Parameter Keys (4096-5119)
63 | Vertical = 4096,
64 | VerticalCitation = 4097,
65 | VerticalDatum = 4098,
66 | VerticalUnits = 4099,
67 | }
68 |
69 | /// Metadata defined by the GeoTIFF standard.
70 | ///
71 | ///
72 | #[derive(Debug, Clone, PartialEq)]
73 | pub struct GeoKeyDirectory {
74 | pub model_type: Option,
75 | pub raster_type: Option,
76 | pub citation: Option,
77 |
78 | pub geographic_type: Option,
79 | pub geog_citation: Option,
80 | pub geog_geodetic_datum: Option,
81 |
82 | /// This key is used to specify a Prime Meridian from the GeoTIFF CRS register or to indicate
83 | /// that the Prime Meridian is user-defined. The default is Greenwich, England.
84 | ///
85 | pub geog_prime_meridian: Option,
86 |
87 | pub geog_linear_units: Option,
88 | pub geog_linear_unit_size: Option,
89 | pub geog_angular_units: Option,
90 | pub geog_angular_unit_size: Option,
91 |
92 | /// This key is provided to specify an ellipsoid (or sphere) from the GeoTIFF CRS register or
93 | /// to indicate that the ellipsoid (or sphere) is user-defined.
94 | pub geog_ellipsoid: Option,
95 | pub geog_semi_major_axis: Option