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:
parent
1018be505f
commit
2b549b84f9
|
@ -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
|
||||
|
||||
|
|
|
@ -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 ):
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 ):
|
||||
|
||||
|
|
|
@ -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 ]:
|
||||
|
||||
|
|
|
@ -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 ):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue