Skip to content

Commit

Permalink
Use custom format handler and regroup endpoints (#30)
Browse files Browse the repository at this point in the history
* Use custom format handler

* Fix thumbnail links

* Group endpoints with prefixes
  • Loading branch information
banesullivan authored Jun 15, 2022
1 parent cd31c6d commit f148dca
Show file tree
Hide file tree
Showing 26 changed files with 126 additions and 210 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
staticfiles/

.DS_Store

# Created by https://www.gitignore.io/api/django
# Edit at https://www.gitignore.io/?templates=django

Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ please reach out at kitware@kitware.com. We look forward to the conversation!
### 🌟 Features

Rich set of RESTful endpoints to extract information from large image formats:
- Image metadata (`/metadata`, `/metadata_internal`)
- Image metadata (`/info/metadata`, `/info/metadata_internal`)
- Tile serving (`/tiles/{z}/{x}/{y}.png?projection=EPSG:3857`)
- Region extraction (`/region.tif?left=v&right=v&top=v&bottom=v`)
- Image thumbnails (`/thumbnail.png`)
- Individual pixels (`/pixel?left=v&top=v`)
- Band histograms (`/histogram`)
- Region extraction (`/data/region.tif?left=v&right=v&top=v&bottom=v`)
- Image thumbnails (`/data/thumbnail.png`)
- Individual pixels (`/data/pixel?left=v&top=v`)
- Band histograms (`/data/histogram`)

Support for any storage backend:
- Supports Django's `FileField`
Expand Down Expand Up @@ -370,7 +370,7 @@ that you can easily create a false color image from multispectral imagery.
View a single band with a Matplotlib colormap:

```js
var thumbnailUrl = `http://localhost:8000/api/image-file/${imageId}/thumbnail.png?band=3&palette=viridis&min=50&max=250`;
var thumbnailUrl = `http://localhost:8000/api/image-file/${imageId}/data/thumbnail.png?band=3&palette=viridis&min=50&max=250`;
```

2. A complex specification for styling across frames and bands to create composite images using a [JSON specification defined by `large-image`](https://girder.github.io/large_image/tilesource_options.html#style).
Expand All @@ -387,7 +387,7 @@ var style = {
]
};
var styleEncoded = encodeURIComponent(JSON.stringify(style))
var thumbnailUrl = `http://localhost:8000/api/image-file/${imageId}/thumbnail.png?style=${styleEncoded}`;
var thumbnailUrl = `http://localhost:8000/api/image-file/${imageId}/data/thumbnail.png?style=${styleEncoded}`;
```


Expand Down
2 changes: 1 addition & 1 deletion demo/myimages/imagefiles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def a_html(url, label):

class Mixin:
def thumbnail(self):
return thumbnail_html(reverse('imagefile-thumbnail-png', args=[self.pk]))
return thumbnail_html(reverse('imagefile-thumbnail', args=[self.pk, 'png']))

thumbnail.allow_tags = True # type: ignore

Expand Down
10 changes: 5 additions & 5 deletions demo/myimages/imagefiles/tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
@pytest.mark.django_db(transaction=True)
def test_metadata(api_client, image_file_geotiff):
response = api_client.get(
f'/api/imagefile/{image_file_geotiff.pk}/metadata?projection=EPSG:3857'
f'/api/imagefile/{image_file_geotiff.pk}/info/metadata?projection=EPSG:3857'
)
assert status.is_success(response.status_code)
metadata = response.data
Expand All @@ -18,7 +18,7 @@ def test_metadata(api_client, image_file_geotiff):

@pytest.mark.django_db(transaction=True)
def test_metadata_internal(api_client, image_file_geotiff):
response = api_client.get(f'/api/imagefile/{image_file_geotiff.pk}/metadata_internal')
response = api_client.get(f'/api/imagefile/{image_file_geotiff.pk}/info/metadata_internal')
assert status.is_success(response.status_code)
metadata = response.data
assert metadata['geospatial']
Expand All @@ -27,15 +27,15 @@ def test_metadata_internal(api_client, image_file_geotiff):

@pytest.mark.django_db(transaction=True)
def test_bands(api_client, image_file_geotiff):
response = api_client.get(f'/api/imagefile/{image_file_geotiff.pk}/bands')
response = api_client.get(f'/api/imagefile/{image_file_geotiff.pk}/info/bands')
assert status.is_success(response.status_code)
bands = response.data
assert isinstance(bands[1], dict)


@pytest.mark.django_db(transaction=True)
def test_frames(api_client, image_file_geotiff):
response = api_client.get(f'/api/imagefile/{image_file_geotiff.pk}/frames')
response = api_client.get(f'/api/imagefile/{image_file_geotiff.pk}/info/frames')
assert status.is_success(response.status_code)
data = response.data
assert isinstance(data['frames'], list)
Expand All @@ -45,7 +45,7 @@ def test_frames(api_client, image_file_geotiff):

@pytest.mark.django_db(transaction=True)
def test_band(api_client, image_file_geotiff):
response = api_client.get(f'/api/imagefile/{image_file_geotiff.pk}/band?band=1')
response = api_client.get(f'/api/imagefile/{image_file_geotiff.pk}/info/band?band=1')
assert status.is_success(response.status_code)
band = response.data
assert band['interpretation']
106 changes: 23 additions & 83 deletions django_large_image/rest/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,33 +23,27 @@

class DataMixin(LargeImageMixinBase):
@method_decorator(cache_page(CACHE_TIMEOUT))
def thumbnail(self, request: Request, pk: int = None, format: str = None) -> HttpResponse:
encoding = tilesource.format_to_encoding(format)
width = int(self.get_query_param(request, 'max_width', 256))
height = int(self.get_query_param(request, 'max_height', 256))
source = self.get_tile_source(request, pk, encoding=encoding)
thumb_data, mime_type = source.getThumbnail(encoding=encoding, width=width, height=height)
return HttpResponse(thumb_data, content_type=mime_type)

@swagger_auto_schema(
method='GET',
operation_summary=thumbnail_summary,
manual_parameters=thumbnail_parameters,
)
@action(detail=False, url_path='thumbnail.png')
def thumbnail_png(self, request: Request, pk: int = None) -> HttpResponse:
return self.thumbnail(request, pk, format='png')
@action(detail=False, url_path=r'data/thumbnail.(?P<fmt>png|jpg|jpeg|tif)')
def thumbnail(self, request: Request, pk: int = None, fmt: str = 'png') -> HttpResponse:
encoding = tilesource.format_to_encoding(fmt)
width = int(self.get_query_param(request, 'max_width', 256))
height = int(self.get_query_param(request, 'max_height', 256))
source = self.get_tile_source(request, pk, encoding=encoding)
thumb_data, mime_type = source.getThumbnail(encoding=encoding, width=width, height=height)
return HttpResponse(thumb_data, content_type=mime_type)

@swagger_auto_schema(
method='GET',
operation_summary=thumbnail_summary,
manual_parameters=thumbnail_parameters,
operation_summary=region_summary,
manual_parameters=region_parameters + params.STYLE,
)
@action(detail=False, url_path='thumbnail.jpeg')
def thumbnail_jpeg(self, request: Request, pk: int = None) -> HttpResponse:
return self.thumbnail(request, pk, format='jpeg')

def region(self, request: Request, pk: int = None, format: str = None) -> HttpResponse:
@action(detail=False, url_path=r'data/region.(?P<fmt>png|jpg|jpeg|tif)')
def region(self, request: Request, pk: int = None, fmt: str = 'tif') -> HttpResponse:
"""Return the region tile binary from world coordinates in given EPSG.
Note
Expand All @@ -62,7 +56,7 @@ def region(self, request: Request, pk: int = None, format: str = None) -> HttpRe
"""
source = self.get_tile_source(request, pk)
units = self.get_query_param(request, 'units')
encoding = tilesource.format_to_encoding(format)
encoding = tilesource.format_to_encoding(fmt)
left = float(self.get_query_param(request, 'left'))
right = float(self.get_query_param(request, 'right'))
top = float(self.get_query_param(request, 'top'))
Expand All @@ -83,39 +77,12 @@ def region(self, request: Request, pk: int = None, format: str = None) -> HttpRe
tile_binary = open(path, 'rb')
return HttpResponse(tile_binary, content_type=mime_type)

@swagger_auto_schema(
method='GET',
operation_summary=region_summary,
manual_parameters=region_parameters,
)
@action(detail=False, url_path=r'region.tif')
def region_tif(self, request: Request, pk: int = None) -> HttpResponse:
return self.region(request, pk, format='tif')

@swagger_auto_schema(
method='GET',
operation_summary=region_summary,
manual_parameters=region_parameters + params.STYLE,
)
@action(detail=False, url_path=r'region.png')
def region_png(self, request: Request, pk: int = None) -> HttpResponse:
return self.region(request, pk, format='png')

@swagger_auto_schema(
method='GET',
operation_summary=region_summary,
manual_parameters=region_parameters + params.STYLE,
)
@action(detail=False, url_path=r'region.jpeg')
def region_jpeg(self, request: Request, pk: int = None) -> HttpResponse:
return self.region(request, pk, format='jpeg')

@swagger_auto_schema(
method='GET',
operation_summary=pixel_summary,
manual_parameters=pixel_parameters,
)
@action(detail=False)
@action(detail=False, url_path='data/pixel')
def pixel(self, request: Request, pk: int = None) -> Response:
left = int(self.get_query_param(request, 'left'))
top = int(self.get_query_param(request, 'top'))
Expand All @@ -128,7 +95,7 @@ def pixel(self, request: Request, pk: int = None) -> Response:
operation_summary=histogram_summary,
manual_parameters=histogram_parameters,
)
@action(detail=False)
@action(detail=False, url_path='data/histogram')
def histogram(self, request: Request, pk: int = None) -> Response:
only_min_max = not utilities.param_nully(self.get_query_param(request, 'onlyMinMax', False))
density = not utilities.param_nully(self.get_query_param(request, 'density', False))
Expand Down Expand Up @@ -158,52 +125,25 @@ class DataDetailMixin(DataMixin):
operation_summary=thumbnail_summary,
manual_parameters=thumbnail_parameters,
)
@action(detail=True, url_path='thumbnail.png')
def thumbnail_png(self, request: Request, pk: int = None) -> HttpResponse:
return super().thumbnail_png(request, pk)

@swagger_auto_schema(
method='GET',
operation_summary=thumbnail_summary,
manual_parameters=thumbnail_parameters,
)
@action(detail=True, url_path='thumbnail.jpeg')
def thumbnail_jpeg(self, request: Request, pk: int = None) -> HttpResponse:
return super().thumbnail_jpeg(request, pk)

@swagger_auto_schema(
method='GET',
operation_summary=region_summary,
manual_parameters=region_parameters,
)
@action(detail=True, url_path=r'region.tif')
def region_tif(self, request: Request, pk: int = None) -> HttpResponse:
return super().region_tif(request, pk)

@swagger_auto_schema(
method='GET',
operation_summary=region_summary,
manual_parameters=region_parameters + params.STYLE,
)
@action(detail=True, url_path=r'region.png')
def region_png(self, request: Request, pk: int = None) -> HttpResponse:
return super().region_png(request, pk)
@action(detail=True, url_path='data/thumbnail.(?P<fmt>png|jpg|jpeg|tif)')
def thumbnail(self, request: Request, pk: int = None, fmt: str = 'png') -> HttpResponse:
return super().thumbnail(request, pk, fmt)

@swagger_auto_schema(
method='GET',
operation_summary=region_summary,
manual_parameters=region_parameters + params.STYLE,
)
@action(detail=True, url_path=r'region.jpeg')
def region_jpeg(self, request: Request, pk: int = None) -> HttpResponse:
return super().region_jpeg(request, pk)
@action(detail=True, url_path=r'data/region.(?P<fmt>png|jpg|jpeg|tif)')
def region(self, request: Request, pk: int = None, fmt: str = 'tif') -> HttpResponse:
return super().region(request, pk, fmt)

@swagger_auto_schema(
method='GET',
operation_summary=pixel_summary,
manual_parameters=pixel_parameters,
)
@action(detail=True)
@action(detail=True, url_path='data/pixel')
def pixel(self, request: Request, pk: int = None) -> Response:
return super().pixel(request, pk)

Expand All @@ -212,6 +152,6 @@ def pixel(self, request: Request, pk: int = None) -> Response:
operation_summary=histogram_summary,
manual_parameters=histogram_parameters,
)
@action(detail=True)
@action(detail=True, url_path='data/histogram')
def histogram(self, request: Request, pk: int = None) -> Response:
return super().histogram(request, pk)
24 changes: 12 additions & 12 deletions django_large_image/rest/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class MetaDataMixin(LargeImageMixinBase):
operation_summary=metadata_summary,
manual_parameters=metadata_parameters,
)
@action(detail=False)
@action(detail=False, url_path='info/metadata')
def metadata(self, request: Request, pk: int = None) -> Response:
source = self.get_tile_source(request, pk, style=False)
metadata = tilesource.get_metadata(source)
Expand All @@ -49,7 +49,7 @@ def metadata(self, request: Request, pk: int = None) -> Response:
operation_summary=metadata_internal_summary,
manual_parameters=metadata_internal_parameters,
)
@action(detail=False)
@action(detail=False, url_path='info/metadata_internal')
def metadata_internal(self, request: Request, pk: int = None) -> Response:
source = self.get_tile_source(request, pk, style=False)
metadata = tilesource.get_metadata_internal(source)
Expand All @@ -60,7 +60,7 @@ def metadata_internal(self, request: Request, pk: int = None) -> Response:
operation_summary=bands_summary,
manual_parameters=bands_parameters,
)
@action(detail=False)
@action(detail=False, url_path='info/bands')
def bands(self, request: Request, pk: int = None) -> Response:
source = self.get_tile_source(request, pk, style=False)
metadata = source.getBandInformation()
Expand All @@ -71,7 +71,7 @@ def bands(self, request: Request, pk: int = None) -> Response:
operation_summary=bands_summary,
manual_parameters=band_parameters,
)
@action(detail=False)
@action(detail=False, url_path='info/band')
def band(self, request: Request, pk: int = None) -> Response:
# TODO: handle frame choice
band = int(self.get_query_param(request, 'band', 1))
Expand All @@ -84,7 +84,7 @@ def band(self, request: Request, pk: int = None) -> Response:
operation_summary=frames_summary,
manual_parameters=frames_parameters,
)
@action(detail=False)
@action(detail=False, url_path='info/frames')
def frames(self, request: Request, pk: int = None) -> Response:
source = self.get_tile_source(request, pk, style=False)
data = tilesource.get_frames(source)
Expand All @@ -94,7 +94,7 @@ def frames(self, request: Request, pk: int = None) -> Response:
method='GET',
operation_summary=tiffdump_summary,
)
@action(detail=False)
@action(detail=False, url_path='info/tiffdump')
def tiffdump(self, request: Request, pk: int = None) -> Response:
if tifftools is None: # pragma: no cover
raise APIException('`tifftools` is not installed on the server.')
Expand Down Expand Up @@ -123,7 +123,7 @@ class MetaDataDetailMixin(MetaDataMixin):
operation_summary=metadata_summary,
manual_parameters=metadata_parameters,
)
@action(detail=True)
@action(detail=True, url_path='info/metadata')
def metadata(self, request: Request, pk: int = None) -> Response:
return super().metadata(request, pk)

Expand All @@ -132,7 +132,7 @@ def metadata(self, request: Request, pk: int = None) -> Response:
operation_summary=metadata_internal_summary,
manual_parameters=metadata_internal_parameters,
)
@action(detail=True)
@action(detail=True, url_path='info/metadata_internal')
def metadata_internal(self, request: Request, pk: int = None) -> Response:
return super().metadata_internal(request, pk)

Expand All @@ -141,7 +141,7 @@ def metadata_internal(self, request: Request, pk: int = None) -> Response:
operation_summary=bands_summary,
manual_parameters=bands_parameters,
)
@action(detail=True)
@action(detail=True, url_path='info/bands')
def bands(self, request: Request, pk: int = None) -> Response:
return super().bands(request, pk)

Expand All @@ -150,7 +150,7 @@ def bands(self, request: Request, pk: int = None) -> Response:
operation_summary=bands_summary,
manual_parameters=band_parameters,
)
@action(detail=True)
@action(detail=True, url_path='info/band')
def band(self, request: Request, pk: int = None) -> Response:
return super().band(request, pk)

Expand All @@ -159,14 +159,14 @@ def band(self, request: Request, pk: int = None) -> Response:
operation_summary=frames_summary,
manual_parameters=frames_parameters,
)
@action(detail=True)
@action(detail=True, url_path='info/frames')
def frames(self, request: Request, pk: int = None) -> Response:
return super().frames(request, pk)

@swagger_auto_schema(
method='GET',
operation_summary=tiffdump_summary,
)
@action(detail=True)
@action(detail=True, url_path='info/tiffdump')
def tiffdump(self, request: Request, pk: int = None) -> Response:
return super().tiffdump(request, pk)
Loading

0 comments on commit f148dca

Please sign in to comment.