Add /get_files/render API to render static images (#1437)

* Add /get_files/render API to render static images

* Cleanup and increase cache time for render

* Add docs for render endpoint
This commit is contained in:
Paul Friederichsen 2023-09-16 15:21:22 -05:00 committed by GitHub
parent 1018be505f
commit 2b549b84f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 152 additions and 9 deletions

View File

@ -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
@ -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

View File

@ -93,6 +93,11 @@ class ImageRenderer( ClientCachesBase.CacheableObject ):
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 ):

View File

@ -91,6 +91,7 @@ class HydrusServiceClientAPI( HydrusClientService ):
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()

View File

@ -39,6 +39,7 @@ from hydrus.client import ClientAPI
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
@ -2812,6 +2813,68 @@ class HydrusResourceClientAPIRestrictedGetFilesGetFile( HydrusResourceClientAPIR
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 ):

View File

@ -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 ]:

View File

@ -18,6 +18,7 @@ class HydrusRequest( Request ):
self.client_api_permissions = None
self.disconnect_callables = []
self.preferred_mime = HC.APPLICATION_JSON
self.disconnected = False
def IsGET( self ):

View File

@ -560,7 +560,7 @@ class HydrusResource( Resource ):
response_context = request.hydrus_response_context
response_context: ResponseContext = request.hydrus_response_context
if response_context.HasPath():
@ -604,6 +604,13 @@ class HydrusResource( Resource ):
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():
@ -623,9 +630,6 @@ class HydrusResource( Resource ):
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 ) )
@ -686,8 +690,7 @@ class HydrusResource( Resource ):
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:
@ -698,7 +701,7 @@ class HydrusResource( Resource ):
request.setHeader( 'Content-Length', str( content_length ) )
self._reportDataUsed( request, content_length )
self._reportRequestUsed( request )
@ -782,6 +785,8 @@ class HydrusResource( Resource ):
def _errbackDisconnected( self, failure, request: HydrusServerRequest.HydrusRequest, request_deferred: defer.Deferred ):
request_deferred.cancel()
request.disconnected = True
for c in request.disconnect_callables:
@ -1222,7 +1227,7 @@ class HydrusResourceWelcome( HydrusResource ):
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:
@ -1248,6 +1253,16 @@ class ResponseContext( object ):
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
@ -1256,6 +1271,7 @@ class ResponseContext( object ):
self._path = path
self._cookies = cookies
self._is_attachment = is_attachment
self._max_age = max_age
def GetBodyBytes( self ):
@ -1270,6 +1286,8 @@ class ResponseContext( object ):
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