Skip to content

Commit d040726

Browse files
authored
Proof of concept for opening COG in Python (#17)
1 parent 883b388 commit d040726

File tree

9 files changed

+84
-4
lines changed

9 files changed

+84
-4
lines changed

python/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,25 @@ crate-type = ["cdylib"]
1919
[dependencies]
2020
async-tiff = { path = "../" }
2121
bytes = "1.8"
22+
# Match the version used by pyo3-object-store
23+
object_store = { git = "https://github.com/apache/arrow-rs", rev = "7a15e4b47ca97df2edef689c9f2ebd2f3888b79e" }
2224
pyo3 = { version = "0.23.0", features = ["macros"] }
25+
pyo3-async-runtimes = "0.23"
2326
pyo3-bytes = "0.1.2"
27+
pyo3-object_store = { git = "https://github.com/developmentseed/obstore", rev = "28ba07a621c1c104f084fb47ae7f8d08b1eae3ea" }
2428
thiserror = "1"
2529
tiff = "0.9.1"
2630

31+
# We opt-in to using rustls as the TLS provider for reqwest, which is the HTTP
32+
# library used by object_store.
33+
# https://github.com/seanmonstar/reqwest/issues/2025
34+
reqwest = { version = "*", default-features = false, features = [
35+
"rustls-tls-native-roots",
36+
] }
37+
2738
[profile.release]
2839
lto = true
2940
codegen-units = 1
41+
42+
[patch.crates-io]
43+
object_store = { git = "https://github.com/apache/arrow-rs", rev = "7a15e4b47ca97df2edef689c9f2ebd2f3888b79e" }

python/python/async_tiff/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from ._async_tiff import *
12
from ._async_tiff import ___version
23

34
__version__: str = ___version()

python/src/ifd.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,9 @@ impl PyImageFileDirectory {
215215
self.0.model_tiepoint()
216216
}
217217
}
218+
219+
impl From<ImageFileDirectory> for PyImageFileDirectory {
220+
fn from(value: ImageFileDirectory) -> Self {
221+
Self(value)
222+
}
223+
}

python/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
mod enums;
44
mod geo;
55
mod ifd;
6+
mod tiff;
67

78
use pyo3::prelude::*;
89

910
use crate::geo::PyGeoKeyDirectory;
1011
use crate::ifd::PyImageFileDirectory;
12+
use crate::tiff::PyTIFF;
1113

1214
const VERSION: &str = env!("CARGO_PKG_VERSION");
1315

@@ -43,6 +45,10 @@ fn _async_tiff(py: Python, m: &Bound<PyModule>) -> PyResult<()> {
4345
m.add_wrapped(wrap_pyfunction!(___version))?;
4446
m.add_class::<PyGeoKeyDirectory>()?;
4547
m.add_class::<PyImageFileDirectory>()?;
48+
m.add_class::<PyTIFF>()?;
49+
50+
pyo3_object_store::register_store_module(py, m, "async_tiff")?;
51+
pyo3_object_store::register_exceptions_module(py, m, "async_tiff")?;
4652

4753
Ok(())
4854
}

python/src/tiff.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use async_tiff::{COGReader, ObjectReader};
2+
use pyo3::prelude::*;
3+
use pyo3::types::PyType;
4+
use pyo3_async_runtimes::tokio::future_into_py;
5+
use pyo3_object_store::PyObjectStore;
6+
7+
use crate::PyImageFileDirectory;
8+
9+
#[pyclass(name = "TIFF", frozen)]
10+
pub(crate) struct PyTIFF(COGReader);
11+
12+
#[pymethods]
13+
impl PyTIFF {
14+
#[classmethod]
15+
#[pyo3(signature = (path, *, store))]
16+
fn open<'py>(
17+
_cls: &'py Bound<PyType>,
18+
py: Python<'py>,
19+
path: String,
20+
store: PyObjectStore,
21+
) -> PyResult<Bound<'py, PyAny>> {
22+
let reader = ObjectReader::new(store.into_inner(), path.into());
23+
let cog_reader = future_into_py(py, async move {
24+
Ok(PyTIFF(COGReader::try_open(Box::new(reader)).await.unwrap()))
25+
})?;
26+
Ok(cog_reader)
27+
}
28+
29+
fn ifds(&self) -> Vec<PyImageFileDirectory> {
30+
let ifds = self.0.ifds();
31+
ifds.as_ref().iter().map(|ifd| ifd.clone().into()).collect()
32+
}
33+
}

python/tests/__init__.py

Whitespace-only changes.

python/tests/test_cog.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import async_tiff
2+
from async_tiff import TIFF
3+
from async_tiff.store import S3Store
4+
5+
store = S3Store("sentinel-cogs", region="us-west-2", skip_signature=True)
6+
path = "sentinel-s2-l2a-cogs/12/S/UF/2022/6/S2B_12SUF_20220609_0_L2A/B04.tif"
7+
8+
# 2 min, 15s
9+
tiff = await TIFF.open(path, store=store)
10+
ifds = tiff.ifds()
11+
ifd = ifds[0]
12+
ifd.tile_height
13+
ifd.tile_width
14+
ifd.photometric_interpretation
15+
gkd = ifd.geo_key_directory
16+
gkd.citation

src/async_reader.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::io::{Cursor, SeekFrom};
1+
use std::io::Cursor;
22
use std::ops::Range;
33
use std::sync::Arc;
44

@@ -25,7 +25,7 @@ use crate::error::{AiocogeoError, Result};
2525
/// [`ObjectStore`]: object_store::ObjectStore
2626
///
2727
/// [`tokio::fs::File`]: https://docs.rs/tokio/latest/tokio/fs/struct.File.html
28-
pub trait AsyncFileReader: Send {
28+
pub trait AsyncFileReader: Send + Sync {
2929
/// Retrieve the bytes in `range`
3030
fn get_bytes(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes>>;
3131

@@ -57,12 +57,12 @@ impl AsyncFileReader for Box<dyn AsyncFileReader + '_> {
5757
}
5858

5959
#[cfg(feature = "tokio")]
60-
impl<T: tokio::io::AsyncRead + tokio::io::AsyncSeek + Unpin + Send> AsyncFileReader for T {
60+
impl<T: tokio::io::AsyncRead + tokio::io::AsyncSeek + Unpin + Send + Sync> AsyncFileReader for T {
6161
fn get_bytes(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes>> {
6262
use tokio::io::{AsyncReadExt, AsyncSeekExt};
6363

6464
async move {
65-
self.seek(SeekFrom::Start(range.start)).await?;
65+
self.seek(std::io::SeekFrom::Start(range.start)).await?;
6666

6767
let to_read = (range.end - range.start).try_into().unwrap();
6868
let mut buffer = Vec::with_capacity(to_read);

src/cog.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ impl COGReader {
2525
Ok(Self { reader, ifds })
2626
}
2727

28+
pub fn ifds(&self) -> &ImageFileDirectories {
29+
&self.ifds
30+
}
31+
2832
/// Return the EPSG code representing the crs of the image
2933
pub fn epsg(&self) -> Option<u16> {
3034
let ifd = &self.ifds.as_ref()[0];

0 commit comments

Comments
 (0)