diff --git a/python/Cargo.toml b/python/Cargo.toml index 5e35095..56b40c9 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -19,11 +19,25 @@ crate-type = ["cdylib"] [dependencies] async-tiff = { path = "../" } bytes = "1.8" +# Match the version used by pyo3-object-store +object_store = { git = "https://github.com/apache/arrow-rs", rev = "7a15e4b47ca97df2edef689c9f2ebd2f3888b79e" } pyo3 = { version = "0.23.0", features = ["macros"] } +pyo3-async-runtimes = "0.23" pyo3-bytes = "0.1.2" +pyo3-object_store = { git = "https://github.com/developmentseed/obstore", rev = "28ba07a621c1c104f084fb47ae7f8d08b1eae3ea" } thiserror = "1" tiff = "0.9.1" +# We opt-in to using rustls as the TLS provider for reqwest, which is the HTTP +# library used by object_store. +# https://github.com/seanmonstar/reqwest/issues/2025 +reqwest = { version = "*", default-features = false, features = [ + "rustls-tls-native-roots", +] } + [profile.release] lto = true codegen-units = 1 + +[patch.crates-io] +object_store = { git = "https://github.com/apache/arrow-rs", rev = "7a15e4b47ca97df2edef689c9f2ebd2f3888b79e" } diff --git a/python/python/async_tiff/__init__.py b/python/python/async_tiff/__init__.py index 351d8c4..0973927 100644 --- a/python/python/async_tiff/__init__.py +++ b/python/python/async_tiff/__init__.py @@ -1,3 +1,4 @@ +from ._async_tiff import * from ._async_tiff import ___version __version__: str = ___version() diff --git a/python/src/ifd.rs b/python/src/ifd.rs index ce024e2..b4f3d1f 100644 --- a/python/src/ifd.rs +++ b/python/src/ifd.rs @@ -215,3 +215,9 @@ impl PyImageFileDirectory { self.0.model_tiepoint() } } + +impl From for PyImageFileDirectory { + fn from(value: ImageFileDirectory) -> Self { + Self(value) + } +} diff --git a/python/src/lib.rs b/python/src/lib.rs index 9e4ed38..3e6dc63 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -3,11 +3,13 @@ mod enums; mod geo; mod ifd; +mod tiff; use pyo3::prelude::*; use crate::geo::PyGeoKeyDirectory; use crate::ifd::PyImageFileDirectory; +use crate::tiff::PyTIFF; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -43,6 +45,10 @@ fn _async_tiff(py: Python, m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(___version))?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + + pyo3_object_store::register_store_module(py, m, "async_tiff")?; + pyo3_object_store::register_exceptions_module(py, m, "async_tiff")?; Ok(()) } diff --git a/python/src/tiff.rs b/python/src/tiff.rs new file mode 100644 index 0000000..af7623c --- /dev/null +++ b/python/src/tiff.rs @@ -0,0 +1,33 @@ +use async_tiff::{COGReader, ObjectReader}; +use pyo3::prelude::*; +use pyo3::types::PyType; +use pyo3_async_runtimes::tokio::future_into_py; +use pyo3_object_store::PyObjectStore; + +use crate::PyImageFileDirectory; + +#[pyclass(name = "TIFF", frozen)] +pub(crate) struct PyTIFF(COGReader); + +#[pymethods] +impl PyTIFF { + #[classmethod] + #[pyo3(signature = (path, *, store))] + fn open<'py>( + _cls: &'py Bound, + py: Python<'py>, + path: String, + store: PyObjectStore, + ) -> PyResult> { + let reader = ObjectReader::new(store.into_inner(), path.into()); + let cog_reader = future_into_py(py, async move { + Ok(PyTIFF(COGReader::try_open(Box::new(reader)).await.unwrap())) + })?; + Ok(cog_reader) + } + + fn ifds(&self) -> Vec { + let ifds = self.0.ifds(); + ifds.as_ref().iter().map(|ifd| ifd.clone().into()).collect() + } +} diff --git a/python/tests/__init__.py b/python/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/tests/test_cog.py b/python/tests/test_cog.py new file mode 100644 index 0000000..3cf0fe4 --- /dev/null +++ b/python/tests/test_cog.py @@ -0,0 +1,16 @@ +import async_tiff +from async_tiff import TIFF +from async_tiff.store import S3Store + +store = S3Store("sentinel-cogs", region="us-west-2", skip_signature=True) +path = "sentinel-s2-l2a-cogs/12/S/UF/2022/6/S2B_12SUF_20220609_0_L2A/B04.tif" + +# 2 min, 15s +tiff = await TIFF.open(path, store=store) +ifds = tiff.ifds() +ifd = ifds[0] +ifd.tile_height +ifd.tile_width +ifd.photometric_interpretation +gkd = ifd.geo_key_directory +gkd.citation diff --git a/src/async_reader.rs b/src/async_reader.rs index 4ce53a6..946f5d1 100644 --- a/src/async_reader.rs +++ b/src/async_reader.rs @@ -1,4 +1,4 @@ -use std::io::{Cursor, SeekFrom}; +use std::io::Cursor; use std::ops::Range; use std::sync::Arc; @@ -25,7 +25,7 @@ use crate::error::{AiocogeoError, Result}; /// [`ObjectStore`]: object_store::ObjectStore /// /// [`tokio::fs::File`]: https://docs.rs/tokio/latest/tokio/fs/struct.File.html -pub trait AsyncFileReader: Send { +pub trait AsyncFileReader: Send + Sync { /// Retrieve the bytes in `range` fn get_bytes(&mut self, range: Range) -> BoxFuture<'_, Result>; @@ -57,12 +57,12 @@ impl AsyncFileReader for Box { } #[cfg(feature = "tokio")] -impl AsyncFileReader for T { +impl AsyncFileReader for T { fn get_bytes(&mut self, range: Range) -> BoxFuture<'_, Result> { use tokio::io::{AsyncReadExt, AsyncSeekExt}; async move { - self.seek(SeekFrom::Start(range.start)).await?; + self.seek(std::io::SeekFrom::Start(range.start)).await?; let to_read = (range.end - range.start).try_into().unwrap(); let mut buffer = Vec::with_capacity(to_read); diff --git a/src/cog.rs b/src/cog.rs index a31a40a..d26d4ec 100644 --- a/src/cog.rs +++ b/src/cog.rs @@ -25,6 +25,10 @@ impl COGReader { Ok(Self { reader, ifds }) } + pub fn ifds(&self) -> &ImageFileDirectories { + &self.ifds + } + /// Return the EPSG code representing the crs of the image pub fn epsg(&self) -> Option { let ifd = &self.ifds.as_ref()[0];