Skip to content

Commit

Permalink
Add /get_files/render API to render static images (#1437)
Browse files Browse the repository at this point in the history
* Add /get_files/render API to render static images

* Cleanup and increase cache time for render

* Add docs for render endpoint
  • Loading branch information
floogulinc authored Sep 16, 2023
1 parent 1018be5 commit 2b549b8
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 9 deletions.
32 changes: 31 additions & 1 deletion docs/developer_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1820,7 +1820,7 @@ Arguments :
* `hash`: (selective, a hexadecimal SHA256 hash for the file)
* `download`: (optional, boolean, default `false`)

Only use one of file_id or hash. As with metadata fetching, you may only use the hash argument if you have access to all files. If you are tag-restricted, you will have to use a file_id in the last search you ran.
Only use one of file_id or hash. As with metadata fetching, you may only use the hash argument if you have access to all files. If you are tag-restricted, you will have to use a file_id in the last search you ran.

``` title="Example request"
/get_files/file?file_id=452158
Expand Down Expand Up @@ -1866,6 +1866,36 @@ Response:
If you get a 'default' filetype thumbnail like the pdf or hydrus one, you will be pulling the defaults straight from the hydrus/static folder. They will most likely be 200x200 pixels.


### **GET `/get_files/render`** { id="get_files_render" }

_Get an image file as rendered by Hydrus._

Restricted access:
: YES. Search for Files permission needed. Additional search permission limits may apply.

Required Headers: n/a

Arguments :
:
* `file_id`: (selective, numerical file id for the file)
* `hash`: (selective, a hexadecimal SHA256 hash for the file)
* `download`: (optional, boolean, default `false`)

Only use one of file_id or hash. As with metadata fetching, you may only use the hash argument if you have access to all files. If you are tag-restricted, you will have to use a file_id in the last search you ran.

The file you request must be a still image file that Hydrus can render (this includes PSD files). This request uses the client image cache.

``` title="Example request"
/get_files/render?file_id=452158
```
``` title="Example request"
/get_files/render?hash=7f30c113810985b69014957c93bc25e8eb4cf3355dae36d8b9d011d8b0cf623a&download=true
```

Response:
: A PNG file of the image as would be rendered in the client. It will be converted to sRGB color if the file had a color profile but the rendered PNG will not have any color profile.

By default, this will set the `Content-Disposition` header to `inline`, which causes a web browser to show the file. If you set `download=true`, it will set it to `attachment`, which triggers the browser to automatically download it (or open the 'save as' dialog) instead.

## Managing File Relationships

Expand Down
5 changes: 5 additions & 0 deletions hydrus/client/ClientRendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ def __init__( self, media, this_is_for_metadata_alone = False ):
self._this_is_for_metadata_alone = this_is_for_metadata_alone

HG.client_controller.CallToThread( self._Initialise )


def GetNumPyImage(self):

return self._numpy_image


def _GetNumPyImage( self, clip_rect: QC.QRect, target_resolution: QC.QSize ):
Expand Down
1 change: 1 addition & 0 deletions hydrus/client/networking/ClientLocalServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def _InitRoot( self ):
get_files.putChild( b'file_hashes', ClientLocalServerResources.HydrusResourceClientAPIRestrictedGetFilesFileHashes( self._service, self._client_requests_domain ) )
get_files.putChild( b'file', ClientLocalServerResources.HydrusResourceClientAPIRestrictedGetFilesGetFile( self._service, self._client_requests_domain ) )
get_files.putChild( b'thumbnail', ClientLocalServerResources.HydrusResourceClientAPIRestrictedGetFilesGetThumbnail( self._service, self._client_requests_domain ) )
get_files.putChild( b'render', ClientLocalServerResources.HydrusResourceClientAPIRestrictedGetFilesGetRenderedFile( self._service, self._client_requests_domain) )

add_notes = NoResource()

Expand Down
63 changes: 63 additions & 0 deletions hydrus/client/networking/ClientLocalServerResources.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from hydrus.client import ClientConstants as CC
from hydrus.client import ClientLocation
from hydrus.client import ClientThreading
from hydrus.client import ClientRendering
from hydrus.client.importing import ClientImportFiles
from hydrus.client.importing.options import FileImportOptions
from hydrus.client.media import ClientMedia
Expand Down Expand Up @@ -2812,6 +2813,68 @@ def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ):

return response_context


class HydrusResourceClientAPIRestrictedGetFilesGetRenderedFile( HydrusResourceClientAPIRestrictedGetFiles ):

def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ):

try:

media_result: ClientMedia.MediaSingleton

if 'file_id' in request.parsed_request_args:

file_id = request.parsed_request_args.GetValue( 'file_id', int )

request.client_api_permissions.CheckPermissionToSeeFiles( ( file_id, ) )

( media_result, ) = HG.client_controller.Read( 'media_results_from_ids', ( file_id, ) )

elif 'hash' in request.parsed_request_args:

request.client_api_permissions.CheckCanSeeAllFiles()

hash = request.parsed_request_args.GetValue( 'hash', bytes )

media_result = HG.client_controller.Read( 'media_result', hash )

else:

raise HydrusExceptions.BadRequestException( 'Please include a file_id or hash parameter!' )


except HydrusExceptions.DataMissing as e:

raise HydrusExceptions.NotFoundException( 'One or more of those file identifiers was missing!' )

if not media_result.IsStaticImage():

raise HydrusExceptions.BadRequestException('Requested file is not an image!')


hash = media_result.GetHash()

renderer: ClientRendering.ImageRenderer = HG.client_controller.GetCache( 'images' ).GetImageRenderer( media_result )

while not renderer.IsReady():

if request.disconnected:

return

time.sleep( 0.1 )


numpy_image = renderer.GetNumPyImage()

body = HydrusImageHandling.GeneratePNGBytesNumPy(numpy_image)

is_attachment = request.parsed_request_args.GetValue( 'download', bool, default_value = False )

response_context = HydrusServerResources.ResponseContext( 200, mime = HC.IMAGE_PNG, body = body, is_attachment = is_attachment, max_age = 86400 * 365 )

return response_context


class HydrusResourceClientAPIRestrictedGetFilesFileHashes( HydrusResourceClientAPIRestrictedGetFiles ):

Expand Down
25 changes: 25 additions & 0 deletions hydrus/core/HydrusImageHandling.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,31 @@ def GenerateThumbnailBytesPIL( pil_image: PILImage.Image ) -> bytes:

return thumbnail_bytes

def GeneratePNGBytesNumPy( numpy_image ) -> bytes:

( im_height, im_width, depth ) = numpy_image.shape

ext = '.png'

if depth == 4:

convert = cv2.COLOR_RGBA2BGRA

else:

convert = cv2.COLOR_RGB2BGR

numpy_image = cv2.cvtColor( numpy_image, convert )

( result_success, result_byte_array ) = cv2.imencode( ext, numpy_image )

if result_success:

return result_byte_array.tostring()

else:

raise HydrusExceptions.CantRenderWithCVException( 'Image failed to encode!' )

def GetEXIFDict( pil_image: PILImage.Image ) -> typing.Optional[ dict ]:

Expand Down
1 change: 1 addition & 0 deletions hydrus/core/networking/HydrusServerRequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def __init__( self, *args, **kwargs ):
self.client_api_permissions = None
self.disconnect_callables = []
self.preferred_mime = HC.APPLICATION_JSON
self.disconnected = False


def IsGET( self ):
Expand Down
34 changes: 26 additions & 8 deletions hydrus/core/networking/HydrusServerResources.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ def _callbackRenderResponseContext( self, request: HydrusServerRequest.HydrusReq



response_context = request.hydrus_response_context
response_context: ResponseContext = request.hydrus_response_context

if response_context.HasPath():

Expand Down Expand Up @@ -604,6 +604,13 @@ def _callbackRenderResponseContext( self, request: HydrusServerRequest.HydrusReq

content_disposition_type = 'inline'

max_age = response_context.GetMaxAge()

if max_age is not None:

request.setHeader( 'Expires', time.strftime( '%a, %d %b %Y %H:%M:%S GMT', time.gmtime( time.time() + max_age ) ) )

request.setHeader( 'Cache-Control', 'max-age={}'.format( max_age ) )

if response_context.HasPath():

Expand All @@ -623,9 +630,6 @@ def _callbackRenderResponseContext( self, request: HydrusServerRequest.HydrusReq

request.setHeader( 'Content-Disposition', str( content_disposition ) )

request.setHeader( 'Expires', time.strftime( '%a, %d %b %Y %H:%M:%S GMT', time.gmtime( time.time() + 86400 * 365 ) ) )
request.setHeader( 'Cache-Control', 'max-age={}'.format( 86400 * 365 ) )

if len( offset_and_block_size_pairs ) <= 1:

request.setHeader( 'Content-Type', str( content_type ) )
Expand Down Expand Up @@ -686,8 +690,7 @@ def _callbackRenderResponseContext( self, request: HydrusServerRequest.HydrusReq
request.setHeader( 'Content-Type', content_type )
request.setHeader( 'Content-Length', str( content_length ) )
request.setHeader( 'Content-Disposition', content_disposition )
request.setHeader( 'Cache-Control', 'max-age={}'.format( 4 ) ) # hydrus won't change its mind about dynamic data under 4 seconds even if you ask repeatedly


request.write( body_bytes )

else:
Expand All @@ -698,7 +701,7 @@ def _callbackRenderResponseContext( self, request: HydrusServerRequest.HydrusReq

request.setHeader( 'Content-Length', str( content_length ) )



self._reportDataUsed( request, content_length )
self._reportRequestUsed( request )
Expand Down Expand Up @@ -782,6 +785,8 @@ def _DecompressionBombsOK( self, request: HydrusServerRequest.HydrusRequest ):
def _errbackDisconnected( self, failure, request: HydrusServerRequest.HydrusRequest, request_deferred: defer.Deferred ):

request_deferred.cancel()

request.disconnected = True

for c in request.disconnect_callables:

Expand Down Expand Up @@ -1222,7 +1227,7 @@ def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ):

class ResponseContext( object ):

def __init__( self, status_code, mime = HC.APPLICATION_JSON, body = None, path = None, cookies = None, is_attachment = False ):
def __init__( self, status_code, mime = HC.APPLICATION_JSON, body = None, path = None, cookies = None, is_attachment = False, max_age = None ):

if body is None:

Expand All @@ -1248,6 +1253,16 @@ def __init__( self, status_code, mime = HC.APPLICATION_JSON, body = None, path =
if cookies is None:

cookies = []

if max_age is None:

if body is not None:

max_age = 4

elif path is not None:

max_age = 86400 * 365


self._status_code = status_code
Expand All @@ -1256,6 +1271,7 @@ def __init__( self, status_code, mime = HC.APPLICATION_JSON, body = None, path =
self._path = path
self._cookies = cookies
self._is_attachment = is_attachment
self._max_age = max_age


def GetBodyBytes( self ):
Expand All @@ -1270,6 +1286,8 @@ def GetMime( self ): return self._mime
def GetPath( self ): return self._path

def GetStatusCode( self ): return self._status_code

def GetMaxAge( self ): return self._max_age

def HasBody( self ): return self._body_bytes is not None

Expand Down

0 comments on commit 2b549b8

Please sign in to comment.