From a78e8e8e0b9f71c591d31b40168004dbb97b2f23 Mon Sep 17 00:00:00 2001 From: Paul Friederichsen Date: Sat, 10 Aug 2024 15:48:19 -0500 Subject: [PATCH] Render API: Add the ability to resize and encode into different formats (#1578) * Render API: Add the ability to resize and encode into different formats * Update API docs with new render options * Add checks that render width and height are > 0 --- docs/developer_api.md | 11 +- .../networking/ClientLocalServerResources.py | 57 ++++++++- .../core/files/images/HydrusImageHandling.py | 116 +++++++++--------- 3 files changed, 120 insertions(+), 64 deletions(-) diff --git a/docs/developer_api.md b/docs/developer_api.md index 0ec274dd..c14f2d62 100644 --- a/docs/developer_api.md +++ b/docs/developer_api.md @@ -2069,8 +2069,17 @@ Arguments : * `file_id`: (selective, numerical file id for the file) * `hash`: (selective, a hexadecimal SHA256 hash for the file) * `download`: (optional, boolean, default `false`) + * `render_format`: (optional, integer, the filetype enum value to render the file to, default `2` for PNG) + * `render_quality`: (optional, integer, the quality or PNG compression level to use for encoding the image, default `1` for PNG and `80` for JPEG and WEBP) + * `width` and `height`: (optional but must provide both if used, integer, the width and height to scale the image to) 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. + + Currently the only accepted values for `render_format` are: + + * `1` for JPEG (`quality` sets JPEG quality 0 to 100, always progressive 4:2:0 encoding) + * `2` for PNG (`quality` sets the compression level from 0 to 9. A higher value means a smaller size and longer compression time) + * `33` for WEBP (`quality` sets WEBP quality 1 to 100, for values over 100 lossless compression is used) 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. @@ -2082,7 +2091,7 @@ The file you request must be a still image file that Hydrus can render (this inc ``` 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. +: A PNG, JPEG, or WEBP file of the image as would be rendered in the client, optionally resized as specified in the query parameters. It will be converted to sRGB color if the file had a color profile but the rendered file 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. diff --git a/hydrus/client/networking/ClientLocalServerResources.py b/hydrus/client/networking/ClientLocalServerResources.py index 90c487c2..1f325d41 100644 --- a/hydrus/client/networking/ClientLocalServerResources.py +++ b/hydrus/client/networking/ClientLocalServerResources.py @@ -65,7 +65,7 @@ from hydrus.client.search import ClientSearchParseSystemPredicates from hydrus.client.gui import ClientGUIPopupMessages # if a variable name isn't defined here, a GET with it won't work -CLIENT_API_INT_PARAMS = { 'file_id', 'file_sort_type', 'potentials_search_type', 'pixel_duplicates', 'max_hamming_distance', 'max_num_pairs' } +CLIENT_API_INT_PARAMS = { 'file_id', 'file_sort_type', 'potentials_search_type', 'pixel_duplicates', 'max_hamming_distance', 'max_num_pairs', 'width', 'height', 'render_format', 'render_quality' } CLIENT_API_BYTE_PARAMS = { 'hash', 'destination_page_key', 'page_key', 'service_key', 'Hydrus-Client-API-Access-Key', 'Hydrus-Client-API-Session-Key', 'file_service_key', 'deleted_file_service_key', 'tag_service_key', 'tag_service_key_1', 'tag_service_key_2', 'rating_service_key', 'job_status_key' } CLIENT_API_STRING_PARAMS = { 'name', 'url', 'domain', 'search', 'service_name', 'reason', 'tag_display_type', 'source_hash_type', 'desired_hash_type' } CLIENT_API_JSON_PARAMS = { 'basic_permissions', 'tags', 'tags_1', 'tags_2', 'file_ids', 'download', 'only_return_identifiers', 'only_return_basic_information', 'include_blurhash', 'create_new_file_ids', 'detailed_url_information', 'hide_service_keys_tags', 'simple', 'file_sort_asc', 'return_hashes', 'return_file_ids', 'include_notes', 'include_milliseconds', 'include_services_object', 'notes', 'note_names', 'doublecheck_file_system', 'only_in_view' } @@ -2889,6 +2889,19 @@ class HydrusResourceClientAPIRestrictedGetFilesGetRenderedFile( HydrusResourceCl def _threadDoGETJob( self, request: HydrusServerRequest.HydrusRequest ): + if 'render_format' in request.parsed_request_args: + + format = request.parsed_request_args.GetValue( 'render_format', int ) + + if not format in [ HC.IMAGE_PNG, HC.IMAGE_JPEG, HC.IMAGE_WEBP ]: + + raise HydrusExceptions.BadRequestException( 'Invalid render format!' ) + + + else: + + format = HC.IMAGE_PNG + try: media_result: ClientMedia.MediaSingleton @@ -2933,16 +2946,50 @@ class HydrusResourceClientAPIRestrictedGetFilesGetRenderedFile( HydrusResourceCl return - time.sleep( 0.1 ) + time.sleep( 0.01 ) - + numpy_image = renderer.GetNumPyImage() - body = HydrusImageHandling.GeneratePNGBytesNumPy( numpy_image ) + if 'width' in request.parsed_request_args and 'height' in request.parsed_request_args: + + width = request.parsed_request_args.GetValue( 'width', int ) + height = request.parsed_request_args.GetValue( 'height', int ) + + if width < 1: + + raise HydrusExceptions.BadRequestException( 'Width must be greater than 0!' ) + + + if height < 1: + + raise HydrusExceptions.BadRequestException( 'Height must be greater than 0!' ) + + + numpy_image = HydrusImageHandling.ResizeNumPyImage( numpy_image, ( width, height ) ) + + + if 'render_quality' in request.parsed_request_args: + + quality = request.parsed_request_args.GetValue( 'render_quality', int ) + + else: + + if format == HC.IMAGE_PNG: + + quality = 1 # fastest png compression + + else: + + quality = 80 + + + + body = HydrusImageHandling.GenerateFileBytesForRenderAPI( numpy_image, format, quality ) 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 ) + response_context = HydrusServerResources.ResponseContext( 200, mime = format, body = body, is_attachment = is_attachment, max_age = 86400 * 365 ) return response_context diff --git a/hydrus/core/files/images/HydrusImageHandling.py b/hydrus/core/files/images/HydrusImageHandling.py index 2decaa6a..ee3a88e5 100644 --- a/hydrus/core/files/images/HydrusImageHandling.py +++ b/hydrus/core/files/images/HydrusImageHandling.py @@ -353,6 +353,62 @@ def GeneratePILImageFromNumPyImage( numpy_image: numpy.array ) -> PILImage.Image return pil_image +def GenerateFileBytesNumPy( numpy_image, ext: str = '.png', params: list[int] = [] ) -> bytes: + + if len( numpy_image.shape ) == 2: + + convert = cv2.COLOR_GRAY2RGB + + else: + + ( im_height, im_width, depth ) = numpy_image.shape + + 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, params ) + + if result_success: + + return result_byte_array.tostring() + + else: + + raise HydrusExceptions.CantRenderWithCVException( 'Image failed to encode!' ) + + + +def GenerateFileBytesForRenderAPI( numpy_image, format: int, quality: int ): + + ext = HC.mime_ext_lookup[format] + + params = [] + + if format == HC.IMAGE_PNG: + + params = [ cv2.IMWRITE_PNG_COMPRESSION, quality ] + + elif format == HC.IMAGE_JPEG: + + params = [ cv2.IMWRITE_JPEG_QUALITY, quality, cv2.IMWRITE_JPEG_PROGRESSIVE, 1 ] + + elif format == HC.IMAGE_WEBP: + + params = [ cv2.IMWRITE_WEBP_QUALITY, quality ] + + + return GenerateFileBytesNumPy( numpy_image, ext, params ) + + def GenerateThumbnailNumPyFromStaticImagePath( path, target_resolution, mime ): numpy_image = GenerateNumPyImage( path, mime ) @@ -367,28 +423,11 @@ def GenerateThumbnailBytesFromNumPy( numpy_image ) -> bytes: if len( numpy_image.shape ) == 2: depth = 3 - - convert = cv2.COLOR_GRAY2RGB - + else: ( im_height, im_width, depth ) = numpy_image.shape - numpy_image = HydrusImageNormalisation.StripOutAnyUselessAlphaChannel( numpy_image ) - - if depth == 4: - - convert = cv2.COLOR_RGBA2BGRA - - else: - - convert = cv2.COLOR_RGB2BGR - - - - numpy_image = cv2.cvtColor( numpy_image, convert ) - - ( im_height, im_width, depth ) = numpy_image.shape if depth == 4: @@ -403,18 +442,7 @@ def GenerateThumbnailBytesFromNumPy( numpy_image ) -> bytes: params = CV_JPEG_THUMBNAIL_ENCODE_PARAMS - ( result_success, result_byte_array ) = cv2.imencode( ext, numpy_image, params ) - - if result_success: - - thumbnail_bytes = result_byte_array.tostring() - - return thumbnail_bytes - - else: - - raise HydrusExceptions.CantRenderWithCVException( 'Thumb failed to encode!' ) - + return GenerateFileBytesNumPy(numpy_image, ext, params) def GenerateThumbnailBytesFromPIL( pil_image: PILImage.Image ) -> bytes: @@ -439,34 +467,6 @@ def GenerateThumbnailBytesFromPIL( 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 GetImagePixelHash( path, mime ) -> bytes: