diff --git a/CHANGES.md b/CHANGES.md index e559429e3..25a70aeb6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ * adapt for cogeo-mosaic `3.0.0rc2` and add `backend_options` attribute in MosaicTilerFactory (https://github.com/developmentseed/titiler/pull/247) * update FastAPI requirements * update minimal python version to 3.6 +* add `**render_params.kwargs` to pass custom render params in `image.render` method (https://github.com/developmentseed/titiler/pull/259) **breaking change** diff --git a/docs/concepts/dependencies.md b/docs/concepts/dependencies.md index f63fea5a1..e30bfbba5 100644 --- a/docs/concepts/dependencies.md +++ b/docs/concepts/dependencies.md @@ -5,9 +5,10 @@ In titiler `Factories`, we use the dependencies to define the inputs for each en Example: ```python +# Custom Dependency @dataclass class ImageParams: - """Common Image parameters.""" + """Common Preview/Crop parameters.""" max_size: Optional[int] = Query( 1024, description="Maximum image size to read onto." @@ -15,27 +16,44 @@ class ImageParams: height: Optional[int] = Query(None, description="Force output image height.") width: Optional[int] = Query(None, description="Force output image width.") + kwargs: Dict = field(init=False, default_factory=dict) + def __post_init__(self): """Post Init.""" - super().__post_init__() - if self.width and self.height: self.max_size = None + if self.width is not None: + self.kwargs["width"] = self.width + + if self.height is not None: + self.kwargs["height"] = self.height -@router.get(r"/preview.png") + if self.max_size is not None: + self.kwargs["max_size"] = self.max_size + + +# Simple preview endpoint +@router.get("/preview.png") def preview( - url: str = Query(..., description="data set URL"), params: ImageParams = Depends(), + url: str = Query(..., description="data set URL"), + params: ImageParams = Depends(), ): with COGReader(url) as cog: - data, mask = cog.preview( - max_size=params.max_size, - width=params.width, - height=params.height, - ) + img = cog.preview(**params.kwargs) + ... ``` +!!! important + + In the example above, we create a custom `ImageParams` dependency which will then be injected to the `preview` endpoint to add **max_size**, **height** and **width** querystring parameters. As for most of `titiler` dependencies, the `ImageParams` class will host the input querystrings to a `kwargs` dictionary, which will then be used to pass the options to the `cog.preview()` methods. + + Note: when calling the `cog.preview()` method, we use the `**` operator to pass the dictionary key/value pairs as keyword arguments. + + +### TiTiler Dependencies + The `factories` allow users to set multiple default dependencies. Here is the list of common dependencies and their default values: * **path_dependency**: Set dataset path (url). diff --git a/tests/conftest.py b/tests/conftest.py index 1b31e0caf..46d295328 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -66,4 +66,4 @@ def parse_img(content: bytes) -> Dict[Any, Any]: """Read tile image and return metadata.""" with MemoryFile(content) as mem: with mem.open() as dst: - return dst.meta + return dst.profile diff --git a/tests/test_CustomRender.py b/tests/test_CustomRender.py new file mode 100644 index 000000000..a9edb384f --- /dev/null +++ b/tests/test_CustomRender.py @@ -0,0 +1,63 @@ +# """Test TiTiler Custom Render Params.""" + +from dataclasses import dataclass +from typing import Optional, Union + +import numpy + +from titiler.dependencies import RenderParams +from titiler.endpoints import factory + +from .conftest import DATA_DIR, parse_img + +from fastapi import FastAPI, Query + +from starlette.testclient import TestClient + + +@dataclass +class CustomRenderParams(RenderParams): + + output_nodata: Optional[Union[str, int, float]] = Query( + None, title="Tiff Ouptut Nodata value", + ) + output_compression: Optional[str] = Query( + None, title="Tiff compression schema", + ) + + def __post_init__(self): + super().__post_init__() + if self.output_nodata is not None: + self.kwargs["nodata"] = ( + numpy.nan if self.output_nodata == "nan" else float(self.output_nodata) + ) + if self.output_compression is not None: + self.kwargs["compress"] = self.output_compression + + +def test_CustomRender(): + """Test Custom Render Params dependency.""" + app = FastAPI() + cog = factory.TilerFactory(render_dependency=CustomRenderParams) + app.include_router(cog.router) + client = TestClient(app) + + response = client.get(f"/tiles/8/87/48.tif?url={DATA_DIR}/cog.tif") + assert response.status_code == 200 + assert response.headers["content-type"] == "image/tiff; application=geotiff" + meta = parse_img(response.content) + assert meta["driver"] == "GTiff" + assert meta["nodata"] is None + assert meta["count"] == 2 + assert not meta.get("compress") + + response = client.get( + f"/tiles/8/87/48.tif?url={DATA_DIR}/cog.tif&return_mask=false&output_nodata=0&output_compression=deflate" + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/tiff; application=geotiff" + meta = parse_img(response.content) + assert meta["driver"] == "GTiff" + assert meta["nodata"] == 0 + assert meta["count"] == 1 + assert meta["compress"] == "deflate" diff --git a/titiler/dependencies.py b/titiler/dependencies.py index ed5a9f1cd..a24d8cdf9 100644 --- a/titiler/dependencies.py +++ b/titiler/dependencies.py @@ -72,7 +72,7 @@ def TMSParams( class DefaultDependency: """Dependency Base Class""" - kwargs: dict = field(init=False, default_factory=dict) + kwargs: Dict = field(init=False, default_factory=dict) @dataclass diff --git a/titiler/endpoints/factory.py b/titiler/endpoints/factory.py index 9c22b121f..77d9e74b5 100644 --- a/titiler/endpoints/factory.py +++ b/titiler/endpoints/factory.py @@ -320,9 +320,8 @@ def tile( **dataset_params.kwargs, **kwargs, ) - colormap = render_params.colormap or getattr( - src_dst, "colormap", None - ) + dst_colormap = getattr(src_dst, "colormap", None) + timings.append(("dataread", round(t.elapsed * 1000, 2))) if not format: @@ -339,8 +338,9 @@ def tile( content = image.render( add_mask=render_params.return_mask, img_format=format.driver, - colormap=colormap, + colormap=render_params.colormap or dst_colormap, **format.profile, + **render_params.kwargs, ) timings.append(("format", round(t.elapsed * 1000, 2))) @@ -608,6 +608,7 @@ def preview( img_format=format.driver, colormap=colormap, **format.profile, + **render_params.kwargs, ) timings.append(("format", round(t.elapsed * 1000, 2))) @@ -676,6 +677,7 @@ def part( img_format=format.driver, colormap=colormap, **format.profile, + **render_params.kwargs, ) timings.append(("format", round(t.elapsed * 1000, 2))) @@ -1132,6 +1134,7 @@ def tile( img_format=format.driver, colormap=render_params.colormap, **format.profile, + **render_params.kwargs, ) timings.append(("format", round(t.elapsed * 1000, 2)))