├── 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 | ![](assets/naip-example.jpg) 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, 96 | pub geog_semi_minor_axis: Option, 97 | pub geog_inv_flattening: Option, 98 | pub geog_azimuth_units: Option, 99 | 100 | /// This key allows definition of a user-defined Prime Meridian, the location of which is 101 | /// defined by its longitude relative to the international reference meridian (for the earth 102 | /// this is Greenwich). 103 | pub geog_prime_meridian_long: Option, 104 | 105 | pub projected_type: Option, 106 | pub proj_citation: Option, 107 | pub projection: Option, 108 | pub proj_coord_trans: Option, 109 | pub proj_linear_units: Option, 110 | pub proj_linear_unit_size: Option, 111 | pub proj_std_parallel1: Option, 112 | pub proj_std_parallel2: Option, 113 | pub proj_nat_origin_long: Option, 114 | pub proj_nat_origin_lat: Option, 115 | pub proj_false_easting: Option, 116 | pub proj_false_northing: Option, 117 | pub proj_false_origin_long: Option, 118 | pub proj_false_origin_lat: Option, 119 | pub proj_false_origin_easting: Option, 120 | pub proj_false_origin_northing: Option, 121 | pub proj_center_long: Option, 122 | pub proj_center_lat: Option, 123 | pub proj_center_easting: Option, 124 | pub proj_center_northing: Option, 125 | pub proj_scale_at_nat_origin: Option, 126 | pub proj_scale_at_center: Option, 127 | pub proj_azimuth_angle: Option, 128 | pub proj_straight_vert_pole_long: Option, 129 | 130 | pub vertical: Option, 131 | pub vertical_citation: Option, 132 | pub vertical_datum: Option, 133 | pub vertical_units: Option, 134 | } 135 | 136 | impl GeoKeyDirectory { 137 | /// Construct a new [`GeoKeyDirectory`] from tag values. 138 | pub(crate) fn from_tags(mut tag_data: HashMap) -> TiffResult { 139 | let mut model_type = None; 140 | let mut raster_type = None; 141 | let mut citation = None; 142 | 143 | let mut geographic_type = None; 144 | let mut geog_citation = None; 145 | let mut geog_geodetic_datum = None; 146 | let mut geog_prime_meridian = None; 147 | let mut geog_linear_units = None; 148 | let mut geog_linear_unit_size = None; 149 | let mut geog_angular_units = None; 150 | let mut geog_angular_unit_size = None; 151 | let mut geog_ellipsoid = None; 152 | let mut geog_semi_major_axis = None; 153 | let mut geog_semi_minor_axis = None; 154 | let mut geog_inv_flattening = None; 155 | let mut geog_azimuth_units = None; 156 | let mut geog_prime_meridian_long = None; 157 | 158 | let mut projected_type = None; 159 | let mut proj_citation = None; 160 | let mut projection = None; 161 | let mut proj_coord_trans = None; 162 | let mut proj_linear_units = None; 163 | let mut proj_linear_unit_size = None; 164 | let mut proj_std_parallel1 = None; 165 | let mut proj_std_parallel2 = None; 166 | let mut proj_nat_origin_long = None; 167 | let mut proj_nat_origin_lat = None; 168 | let mut proj_false_easting = None; 169 | let mut proj_false_northing = None; 170 | let mut proj_false_origin_long = None; 171 | let mut proj_false_origin_lat = None; 172 | let mut proj_false_origin_easting = None; 173 | let mut proj_false_origin_northing = None; 174 | let mut proj_center_long = None; 175 | let mut proj_center_lat = None; 176 | let mut proj_center_easting = None; 177 | let mut proj_center_northing = None; 178 | let mut proj_scale_at_nat_origin = None; 179 | let mut proj_scale_at_center = None; 180 | let mut proj_azimuth_angle = None; 181 | let mut proj_straight_vert_pole_long = None; 182 | 183 | let mut vertical = None; 184 | let mut vertical_citation = None; 185 | let mut vertical_datum = None; 186 | let mut vertical_units = None; 187 | 188 | tag_data.drain().try_for_each(|(tag, value)| { 189 | match tag { 190 | GeoKeyTag::ModelType => model_type = Some(value.into_u16()?), 191 | GeoKeyTag::RasterType => raster_type = Some(value.into_u16()?), 192 | GeoKeyTag::Citation => citation = Some(value.into_string()?), 193 | GeoKeyTag::GeographicType => geographic_type = Some(value.into_u16()?), 194 | GeoKeyTag::GeogCitation => geog_citation = Some(value.into_string()?), 195 | GeoKeyTag::GeogGeodeticDatum => geog_geodetic_datum = Some(value.into_u16()?), 196 | GeoKeyTag::GeogPrimeMeridian => geog_prime_meridian = Some(value.into_u16()?), 197 | GeoKeyTag::GeogLinearUnits => geog_linear_units = Some(value.into_u16()?), 198 | GeoKeyTag::GeogLinearUnitSize => geog_linear_unit_size = Some(value.into_f64()?), 199 | GeoKeyTag::GeogAngularUnits => geog_angular_units = Some(value.into_u16()?), 200 | GeoKeyTag::GeogAngularUnitSize => geog_angular_unit_size = Some(value.into_f64()?), 201 | GeoKeyTag::GeogEllipsoid => geog_ellipsoid = Some(value.into_u16()?), 202 | GeoKeyTag::GeogSemiMajorAxis => geog_semi_major_axis = Some(value.into_f64()?), 203 | GeoKeyTag::GeogSemiMinorAxis => geog_semi_minor_axis = Some(value.into_f64()?), 204 | GeoKeyTag::GeogInvFlattening => geog_inv_flattening = Some(value.into_f64()?), 205 | GeoKeyTag::GeogAzimuthUnits => geog_azimuth_units = Some(value.into_u16()?), 206 | GeoKeyTag::GeogPrimeMeridianLong => { 207 | geog_prime_meridian_long = Some(value.into_f64()?) 208 | } 209 | GeoKeyTag::ProjectedType => projected_type = Some(value.into_u16()?), 210 | GeoKeyTag::ProjCitation => proj_citation = Some(value.into_string()?), 211 | GeoKeyTag::Projection => projection = Some(value.into_u16()?), 212 | GeoKeyTag::ProjCoordTrans => proj_coord_trans = Some(value.into_u16()?), 213 | GeoKeyTag::ProjLinearUnits => proj_linear_units = Some(value.into_u16()?), 214 | GeoKeyTag::ProjLinearUnitSize => proj_linear_unit_size = Some(value.into_f64()?), 215 | GeoKeyTag::ProjStdParallel1 => proj_std_parallel1 = Some(value.into_f64()?), 216 | GeoKeyTag::ProjStdParallel2 => proj_std_parallel2 = Some(value.into_f64()?), 217 | GeoKeyTag::ProjNatOriginLong => proj_nat_origin_long = Some(value.into_f64()?), 218 | GeoKeyTag::ProjNatOriginLat => proj_nat_origin_lat = Some(value.into_f64()?), 219 | GeoKeyTag::ProjFalseEasting => proj_false_easting = Some(value.into_f64()?), 220 | GeoKeyTag::ProjFalseNorthing => proj_false_northing = Some(value.into_f64()?), 221 | GeoKeyTag::ProjFalseOriginLong => proj_false_origin_long = Some(value.into_f64()?), 222 | GeoKeyTag::ProjFalseOriginLat => proj_false_origin_lat = Some(value.into_f64()?), 223 | GeoKeyTag::ProjFalseOriginEasting => { 224 | proj_false_origin_easting = Some(value.into_f64()?) 225 | } 226 | GeoKeyTag::ProjFalseOriginNorthing => { 227 | proj_false_origin_northing = Some(value.into_f64()?) 228 | } 229 | GeoKeyTag::ProjCenterLong => proj_center_long = Some(value.into_f64()?), 230 | GeoKeyTag::ProjCenterLat => proj_center_lat = Some(value.into_f64()?), 231 | GeoKeyTag::ProjCenterEasting => proj_center_easting = Some(value.into_f64()?), 232 | GeoKeyTag::ProjCenterNorthing => proj_center_northing = Some(value.into_f64()?), 233 | GeoKeyTag::ProjScaleAtNatOrigin => { 234 | proj_scale_at_nat_origin = Some(value.into_f64()?) 235 | } 236 | GeoKeyTag::ProjScaleAtCenter => proj_scale_at_center = Some(value.into_f64()?), 237 | GeoKeyTag::ProjAzimuthAngle => proj_azimuth_angle = Some(value.into_f64()?), 238 | GeoKeyTag::ProjStraightVertPoleLong => { 239 | proj_straight_vert_pole_long = Some(value.into_f64()?) 240 | } 241 | GeoKeyTag::Vertical => vertical = Some(value.into_u16()?), 242 | GeoKeyTag::VerticalCitation => vertical_citation = Some(value.into_string()?), 243 | GeoKeyTag::VerticalDatum => vertical_datum = Some(value.into_u16()?), 244 | GeoKeyTag::VerticalUnits => vertical_units = Some(value.into_u16()?), 245 | }; 246 | Ok::<_, TiffError>(()) 247 | })?; 248 | 249 | Ok(Self { 250 | model_type, 251 | raster_type, 252 | citation, 253 | 254 | geographic_type, 255 | geog_citation, 256 | geog_geodetic_datum, 257 | geog_prime_meridian, 258 | geog_linear_units, 259 | geog_linear_unit_size, 260 | geog_angular_units, 261 | geog_angular_unit_size, 262 | geog_ellipsoid, 263 | geog_semi_major_axis, 264 | geog_semi_minor_axis, 265 | geog_inv_flattening, 266 | geog_azimuth_units, 267 | geog_prime_meridian_long, 268 | 269 | projected_type, 270 | proj_citation, 271 | projection, 272 | proj_coord_trans, 273 | proj_linear_units, 274 | proj_linear_unit_size, 275 | proj_std_parallel1, 276 | proj_std_parallel2, 277 | proj_nat_origin_long, 278 | proj_nat_origin_lat, 279 | proj_false_easting, 280 | proj_false_northing, 281 | proj_false_origin_long, 282 | proj_false_origin_lat, 283 | proj_false_origin_easting, 284 | proj_false_origin_northing, 285 | proj_center_long, 286 | proj_center_lat, 287 | proj_center_easting, 288 | proj_center_northing, 289 | proj_scale_at_nat_origin, 290 | proj_scale_at_center, 291 | proj_azimuth_angle, 292 | proj_straight_vert_pole_long, 293 | 294 | vertical, 295 | vertical_citation, 296 | vertical_datum, 297 | vertical_units, 298 | }) 299 | } 300 | 301 | /// Return the EPSG code representing the crs of the image 302 | /// 303 | /// This will return either [`GeoKeyDirectory::projected_type`] or 304 | /// [`GeoKeyDirectory::geographic_type`]. 305 | pub fn epsg_code(&self) -> Option { 306 | if let Some(projected_type) = self.projected_type { 307 | Some(projected_type) 308 | } else { 309 | self.geographic_type 310 | } 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /src/tag_value.rs: -------------------------------------------------------------------------------- 1 | use self::TagValue::{ 2 | Ascii, Byte, Double, Float, Ifd, IfdBig, List, Rational, RationalBig, SRational, SRationalBig, 3 | Short, Signed, SignedBig, SignedByte, SignedShort, Unsigned, UnsignedBig, 4 | }; 5 | use crate::error::{TiffError, TiffFormatError, TiffResult}; 6 | // use super::error::{TiffError, TiffFormatError, TiffResult}; 7 | 8 | #[allow(unused_qualifications)] 9 | #[derive(Debug, Clone, PartialEq)] 10 | #[non_exhaustive] 11 | #[expect(missing_docs)] 12 | pub enum TagValue { 13 | Byte(u8), 14 | Short(u16), 15 | SignedByte(i8), 16 | SignedShort(i16), 17 | Signed(i32), 18 | SignedBig(i64), 19 | Unsigned(u32), 20 | UnsignedBig(u64), 21 | Float(f32), 22 | Double(f64), 23 | List(Vec), 24 | Rational(u32, u32), 25 | RationalBig(u64, u64), 26 | SRational(i32, i32), 27 | SRationalBig(i64, i64), 28 | Ascii(String), 29 | Ifd(u32), 30 | IfdBig(u64), 31 | } 32 | 33 | impl TagValue { 34 | /// Convert this TagValue into a u8, returning an error if the type is incompatible. 35 | pub fn into_u8(self) -> TiffResult { 36 | match self { 37 | Byte(val) => Ok(val), 38 | val => Err(TiffError::FormatError(TiffFormatError::ByteExpected(val))), 39 | } 40 | } 41 | 42 | /// Convert this TagValue into an i8, returning an error if the type is incompatible. 43 | pub fn into_i8(self) -> TiffResult { 44 | match self { 45 | SignedByte(val) => Ok(val), 46 | val => Err(TiffError::FormatError(TiffFormatError::SignedByteExpected( 47 | val, 48 | ))), 49 | } 50 | } 51 | 52 | /// Convert this TagValue into a u16, returning an error if the type is incompatible. 53 | pub fn into_u16(self) -> TiffResult { 54 | match self { 55 | Byte(val) => Ok(val.into()), 56 | Short(val) => Ok(val), 57 | Unsigned(val) => Ok(u16::try_from(val)?), 58 | UnsignedBig(val) => Ok(u16::try_from(val)?), 59 | val => Err(TiffError::FormatError(TiffFormatError::ShortExpected(val))), 60 | } 61 | } 62 | 63 | /// Convert this TagValue into an i16, returning an error if the type is incompatible. 64 | pub fn into_i16(self) -> TiffResult { 65 | match self { 66 | SignedByte(val) => Ok(val.into()), 67 | SignedShort(val) => Ok(val), 68 | Signed(val) => Ok(i16::try_from(val)?), 69 | SignedBig(val) => Ok(i16::try_from(val)?), 70 | val => Err(TiffError::FormatError( 71 | TiffFormatError::SignedShortExpected(val), 72 | )), 73 | } 74 | } 75 | 76 | /// Convert this TagValue into a u32, returning an error if the type is incompatible. 77 | pub fn into_u32(self) -> TiffResult { 78 | match self { 79 | Byte(val) => Ok(val.into()), 80 | Short(val) => Ok(val.into()), 81 | Unsigned(val) => Ok(val), 82 | UnsignedBig(val) => Ok(u32::try_from(val)?), 83 | Ifd(val) => Ok(val), 84 | IfdBig(val) => Ok(u32::try_from(val)?), 85 | val => Err(TiffError::FormatError( 86 | TiffFormatError::UnsignedIntegerExpected(val), 87 | )), 88 | } 89 | } 90 | 91 | /// Convert this TagValue into an i32, returning an error if the type is incompatible. 92 | pub fn into_i32(self) -> TiffResult { 93 | match self { 94 | SignedByte(val) => Ok(val.into()), 95 | SignedShort(val) => Ok(val.into()), 96 | Signed(val) => Ok(val), 97 | SignedBig(val) => Ok(i32::try_from(val)?), 98 | val => Err(TiffError::FormatError( 99 | TiffFormatError::SignedIntegerExpected(val), 100 | )), 101 | } 102 | } 103 | 104 | /// Convert this TagValue into a u64, returning an error if the type is incompatible. 105 | pub fn into_u64(self) -> TiffResult { 106 | match self { 107 | Byte(val) => Ok(val.into()), 108 | Short(val) => Ok(val.into()), 109 | Unsigned(val) => Ok(val.into()), 110 | UnsignedBig(val) => Ok(val), 111 | Ifd(val) => Ok(val.into()), 112 | IfdBig(val) => Ok(val), 113 | val => Err(TiffError::FormatError( 114 | TiffFormatError::UnsignedIntegerExpected(val), 115 | )), 116 | } 117 | } 118 | 119 | /// Convert this TagValue into an i64, returning an error if the type is incompatible. 120 | pub fn into_i64(self) -> TiffResult { 121 | match self { 122 | SignedByte(val) => Ok(val.into()), 123 | SignedShort(val) => Ok(val.into()), 124 | Signed(val) => Ok(val.into()), 125 | SignedBig(val) => Ok(val), 126 | val => Err(TiffError::FormatError( 127 | TiffFormatError::SignedIntegerExpected(val), 128 | )), 129 | } 130 | } 131 | 132 | /// Convert this TagValue into a f32, returning an error if the type is incompatible. 133 | pub fn into_f32(self) -> TiffResult { 134 | match self { 135 | Float(val) => Ok(val), 136 | val => Err(TiffError::FormatError( 137 | TiffFormatError::SignedIntegerExpected(val), 138 | )), 139 | } 140 | } 141 | 142 | /// Convert this TagValue into a f64, returning an error if the type is incompatible. 143 | pub fn into_f64(self) -> TiffResult { 144 | match self { 145 | Double(val) => Ok(val), 146 | val => Err(TiffError::FormatError( 147 | TiffFormatError::SignedIntegerExpected(val), 148 | )), 149 | } 150 | } 151 | 152 | /// Convert this TagValue into a String, returning an error if the type is incompatible. 153 | pub fn into_string(self) -> TiffResult { 154 | match self { 155 | Ascii(val) => Ok(val), 156 | val => Err(TiffError::FormatError( 157 | TiffFormatError::SignedIntegerExpected(val), 158 | )), 159 | } 160 | } 161 | 162 | /// Convert this TagValue into a Vec, returning an error if the type is incompatible. 163 | pub fn into_u32_vec(self) -> TiffResult> { 164 | match self { 165 | List(vec) => { 166 | let mut new_vec = Vec::with_capacity(vec.len()); 167 | for v in vec { 168 | new_vec.push(v.into_u32()?) 169 | } 170 | Ok(new_vec) 171 | } 172 | Byte(val) => Ok(vec![val.into()]), 173 | Short(val) => Ok(vec![val.into()]), 174 | Unsigned(val) => Ok(vec![val]), 175 | UnsignedBig(val) => Ok(vec![u32::try_from(val)?]), 176 | Rational(numerator, denominator) => Ok(vec![numerator, denominator]), 177 | RationalBig(numerator, denominator) => { 178 | Ok(vec![u32::try_from(numerator)?, u32::try_from(denominator)?]) 179 | } 180 | Ifd(val) => Ok(vec![val]), 181 | IfdBig(val) => Ok(vec![u32::try_from(val)?]), 182 | Ascii(val) => Ok(val.chars().map(u32::from).collect()), 183 | val => Err(TiffError::FormatError( 184 | TiffFormatError::UnsignedIntegerExpected(val), 185 | )), 186 | } 187 | } 188 | 189 | /// Convert this TagValue into a Vec, returning an error if the type is incompatible. 190 | pub fn into_u8_vec(self) -> TiffResult> { 191 | match self { 192 | List(vec) => { 193 | let mut new_vec = Vec::with_capacity(vec.len()); 194 | for v in vec { 195 | new_vec.push(v.into_u8()?) 196 | } 197 | Ok(new_vec) 198 | } 199 | Byte(val) => Ok(vec![val]), 200 | val => Err(TiffError::FormatError(TiffFormatError::ByteExpected(val))), 201 | } 202 | } 203 | 204 | /// Convert this TagValue into a Vec, returning an error if the type is incompatible. 205 | pub fn into_u16_vec(self) -> TiffResult> { 206 | match self { 207 | List(vec) => { 208 | let mut new_vec = Vec::with_capacity(vec.len()); 209 | for v in vec { 210 | new_vec.push(v.into_u16()?) 211 | } 212 | Ok(new_vec) 213 | } 214 | Byte(val) => Ok(vec![val.into()]), 215 | Short(val) => Ok(vec![val]), 216 | val => Err(TiffError::FormatError(TiffFormatError::ShortExpected(val))), 217 | } 218 | } 219 | 220 | /// Convert this TagValue into a Vec, returning an error if the type is incompatible. 221 | pub fn into_i32_vec(self) -> TiffResult> { 222 | match self { 223 | List(vec) => { 224 | let mut new_vec = Vec::with_capacity(vec.len()); 225 | for v in vec { 226 | match v { 227 | SRational(numerator, denominator) => { 228 | new_vec.push(numerator); 229 | new_vec.push(denominator); 230 | } 231 | SRationalBig(numerator, denominator) => { 232 | new_vec.push(i32::try_from(numerator)?); 233 | new_vec.push(i32::try_from(denominator)?); 234 | } 235 | _ => new_vec.push(v.into_i32()?), 236 | } 237 | } 238 | Ok(new_vec) 239 | } 240 | SignedByte(val) => Ok(vec![val.into()]), 241 | SignedShort(val) => Ok(vec![val.into()]), 242 | Signed(val) => Ok(vec![val]), 243 | SignedBig(val) => Ok(vec![i32::try_from(val)?]), 244 | SRational(numerator, denominator) => Ok(vec![numerator, denominator]), 245 | SRationalBig(numerator, denominator) => { 246 | Ok(vec![i32::try_from(numerator)?, i32::try_from(denominator)?]) 247 | } 248 | val => Err(TiffError::FormatError( 249 | TiffFormatError::SignedIntegerExpected(val), 250 | )), 251 | } 252 | } 253 | 254 | /// Convert this TagValue into a Vec, returning an error if the type is incompatible. 255 | pub fn into_f32_vec(self) -> TiffResult> { 256 | match self { 257 | List(vec) => { 258 | let mut new_vec = Vec::with_capacity(vec.len()); 259 | for v in vec { 260 | new_vec.push(v.into_f32()?) 261 | } 262 | Ok(new_vec) 263 | } 264 | Float(val) => Ok(vec![val]), 265 | val => Err(TiffError::FormatError( 266 | TiffFormatError::UnsignedIntegerExpected(val), 267 | )), 268 | } 269 | } 270 | 271 | /// Convert this TagValue into a Vec, returning an error if the type is incompatible. 272 | pub fn into_f64_vec(self) -> TiffResult> { 273 | match self { 274 | List(vec) => { 275 | let mut new_vec = Vec::with_capacity(vec.len()); 276 | for v in vec { 277 | new_vec.push(v.into_f64()?) 278 | } 279 | Ok(new_vec) 280 | } 281 | Double(val) => Ok(vec![val]), 282 | val => Err(TiffError::FormatError( 283 | TiffFormatError::UnsignedIntegerExpected(val), 284 | )), 285 | } 286 | } 287 | 288 | /// Convert this TagValue into a Vec, returning an error if the type is incompatible. 289 | pub fn into_u64_vec(self) -> TiffResult> { 290 | match self { 291 | List(vec) => { 292 | let mut new_vec = Vec::with_capacity(vec.len()); 293 | for v in vec { 294 | new_vec.push(v.into_u64()?) 295 | } 296 | Ok(new_vec) 297 | } 298 | Byte(val) => Ok(vec![val.into()]), 299 | Short(val) => Ok(vec![val.into()]), 300 | Unsigned(val) => Ok(vec![val.into()]), 301 | UnsignedBig(val) => Ok(vec![val]), 302 | Rational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]), 303 | RationalBig(numerator, denominator) => Ok(vec![numerator, denominator]), 304 | Ifd(val) => Ok(vec![val.into()]), 305 | IfdBig(val) => Ok(vec![val]), 306 | Ascii(val) => Ok(val.chars().map(u32::from).map(u64::from).collect()), 307 | val => Err(TiffError::FormatError( 308 | TiffFormatError::UnsignedIntegerExpected(val), 309 | )), 310 | } 311 | } 312 | 313 | /// Convert this TagValue into a Vec, returning an error if the type is incompatible. 314 | pub fn into_i64_vec(self) -> TiffResult> { 315 | match self { 316 | List(vec) => { 317 | let mut new_vec = Vec::with_capacity(vec.len()); 318 | for v in vec { 319 | match v { 320 | SRational(numerator, denominator) => { 321 | new_vec.push(numerator.into()); 322 | new_vec.push(denominator.into()); 323 | } 324 | SRationalBig(numerator, denominator) => { 325 | new_vec.push(numerator); 326 | new_vec.push(denominator); 327 | } 328 | _ => new_vec.push(v.into_i64()?), 329 | } 330 | } 331 | Ok(new_vec) 332 | } 333 | SignedByte(val) => Ok(vec![val.into()]), 334 | SignedShort(val) => Ok(vec![val.into()]), 335 | Signed(val) => Ok(vec![val.into()]), 336 | SignedBig(val) => Ok(vec![val]), 337 | SRational(numerator, denominator) => Ok(vec![numerator.into(), denominator.into()]), 338 | SRationalBig(numerator, denominator) => Ok(vec![numerator, denominator]), 339 | val => Err(TiffError::FormatError( 340 | TiffFormatError::SignedIntegerExpected(val), 341 | )), 342 | } 343 | } 344 | } 345 | --------------------------------------------------------------------------------