Add blurhash (#1443)

* Start on blurhash

* More blurhash db stuff

* Refactor GenerateThumbnailBytes to add GenerateThumbnailNumPy

* Add blurhash gen to import

* Add blurhashes to db

* Add blurhash to file metadata api

* Add API docs for blurhash

* Make sure we regen blurhash after thumb regen
This commit is contained in:
Paul Friederichsen 2023-09-23 14:13:21 -05:00 committed by GitHub
parent 428372fb57
commit 850a8c452e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 436 additions and 65 deletions

View File

@ -1486,6 +1486,7 @@ Arguments (in percent-encoded JSON):
* `only_return_identifiers`: true or false (optional, defaulting to false)
* `only_return_basic_information`: true or false (optional, defaulting to false)
* `detailed_url_information`: true or false (optional, defaulting to false)
* `include_blurhash`: true or false (optional, defaulting to false. Only applies when `only_return_basic_information` is true)
* `include_notes`: true or false (optional, defaulting to false)
* `include_services_object`: true or false (optional, defaulting to true)
* `hide_service_keys_tags`: **Deprecated, will be deleted soon!** true or false (optional, defaulting to true)
@ -1534,6 +1535,7 @@ Response:
},
"ipfs_multihashes" : {},
"has_audio" : false,
"blurhash" : "U6PZfSi_.AyE_3t7t7R**0o#DgR4_3R*D%xt",
"num_frames" : null,
"num_words" : null,
"is_inbox" : false,
@ -1605,6 +1607,7 @@ Response:
"55af93e0deabd08ce15ffb2b164b06d1254daab5a18d145e56fa98f71ddb6f11" : "QmReHtaET3dsgh7ho5NVyHb5U13UgJoGipSWbZsnuuM8tb"
},
"has_audio" : true,
"blurhash" : "UHF5?xYk^6#M@-5b,1J5@[or[k6.};FxngOZ",
"num_frames" : 102,
"num_words" : null,
"is_inbox" : false,
@ -1725,6 +1728,8 @@ Size is in bytes. Duration is in milliseconds, and may be an int or a float.
The `thumbnail_width` and `thumbnail_height` are a generally reliable prediction but aren't a promise. The actual thumbnail you get from [/get\_files/thumbnail](#get_files_thumbnail) will be different if the user hasn't looked at it since changing their thumbnail options. You only get these rows for files that hydrus actually generates an actual thumbnail for. Things like pdf won't have it. You can use your own thumb, or ask the api and it'll give you a fixed fallback; those are mostly 200x200, but you can and should size them to whatever you want.
`blurhash` gives a base 83 encoded string of a [blurhash](https://blurha.sh/) generated from the file's thumbnail if the file has a thumbnail.
#### tags
The `tags` structure is similar to the [/add\_tags/add\_tags](#add_tags_add_tags) scheme, excepting that the status numbers are:
@ -1779,7 +1784,7 @@ You can change this behaviour with `create_new_file_ids=true`, but bear in mind
If you ask about file_ids that do not exist, you'll get 404.
If you set `only_return_basic_information=true`, this will be much faster for first-time requests than the full metadata result, but it will be slower for repeat requests. The full metadata object is cached after first fetch, the limited file info object is not.
If you set `only_return_basic_information=true`, this will be much faster for first-time requests than the full metadata result, but it will be slower for repeat requests. The full metadata object is cached after first fetch, the limited file info object is not. You can optionally set `include_blurhash` when using this option to fetch blurhash strings for the files.
If you add `detailed_url_information=true`, a new entry, `detailed_known_urls`, will be added for each file, with a list of the same structure as /`add_urls/get_url_info`. This may be an expensive request if you are querying thousands of files at once.

View File

@ -49,6 +49,7 @@ REGENERATE_FILE_DATA_JOB_FILE_INTEGRITY_PRESENCE_LOG_ONLY = 18
REGENERATE_FILE_DATA_JOB_FILE_HAS_HUMAN_READABLE_EMBEDDED_METADATA = 19
REGENERATE_FILE_DATA_JOB_FILE_HAS_EXIF = 20
REGENERATE_FILE_DATA_JOB_FILE_INTEGRITY_PRESENCE_DELETE_RECORD = 21
REGENERATE_FILE_DATA_JOB_BLURHASH = 22
regen_file_enum_to_str_lookup = {
REGENERATE_FILE_DATA_JOB_FILE_METADATA : 'regenerate file metadata',
@ -72,7 +73,8 @@ regen_file_enum_to_str_lookup = {
REGENERATE_FILE_DATA_JOB_FILE_HAS_EXIF : 'determine if the file has EXIF metadata',
REGENERATE_FILE_DATA_JOB_FILE_HAS_HUMAN_READABLE_EMBEDDED_METADATA : 'determine if the file has non-EXIF human-readable embedded metadata',
REGENERATE_FILE_DATA_JOB_FILE_HAS_ICC_PROFILE : 'determine if the file has an icc profile',
REGENERATE_FILE_DATA_JOB_PIXEL_HASH : 'regenerate pixel duplicate data'
REGENERATE_FILE_DATA_JOB_PIXEL_HASH : 'regenerate pixel duplicate data',
REGENERATE_FILE_DATA_JOB_BLURHASH: 'regenerate blurhash'
}
regen_file_enum_to_description_lookup = {
@ -129,7 +131,8 @@ All missing/Incorrect files will also have their hashes, tags, and URLs exported
REGENERATE_FILE_DATA_JOB_FILE_HAS_EXIF : 'This loads the file to see if it has EXIF metadata, which can be shown in the media viewer and searched with "system:image has exif".',
REGENERATE_FILE_DATA_JOB_FILE_HAS_HUMAN_READABLE_EMBEDDED_METADATA : 'This loads the file to see if it has non-EXIF human-readable metadata, which can be shown in the media viewer and searched with "system:image has human-readable embedded metadata".',
REGENERATE_FILE_DATA_JOB_FILE_HAS_ICC_PROFILE : 'This loads the file to see if it has an ICC profile, which is used in "system:has icc profile" search.',
REGENERATE_FILE_DATA_JOB_PIXEL_HASH : 'This generates a fast unique identifier for the pixels in a still image, which is used in duplicate pixel searches.'
REGENERATE_FILE_DATA_JOB_PIXEL_HASH : 'This generates a fast unique identifier for the pixels in a still image, which is used in duplicate pixel searches.',
REGENERATE_FILE_DATA_JOB_BLURHASH: 'This generates a very small version of the file\'s thumbnail that can be used as a placeholder while the thumbnail loads'
}
NORMALISED_BIG_JOB_WEIGHT = 100
@ -156,7 +159,8 @@ regen_file_enum_to_job_weight_lookup = {
REGENERATE_FILE_DATA_JOB_FILE_HAS_EXIF : 25,
REGENERATE_FILE_DATA_JOB_FILE_HAS_HUMAN_READABLE_EMBEDDED_METADATA : 25,
REGENERATE_FILE_DATA_JOB_FILE_HAS_ICC_PROFILE : 25,
REGENERATE_FILE_DATA_JOB_PIXEL_HASH : 100
REGENERATE_FILE_DATA_JOB_PIXEL_HASH : 100,
REGENERATE_FILE_DATA_JOB_BLURHASH: 25
}
regen_file_enum_to_overruled_jobs = {
@ -181,7 +185,8 @@ regen_file_enum_to_overruled_jobs = {
REGENERATE_FILE_DATA_JOB_FILE_HAS_EXIF : [],
REGENERATE_FILE_DATA_JOB_FILE_HAS_HUMAN_READABLE_EMBEDDED_METADATA : [],
REGENERATE_FILE_DATA_JOB_FILE_HAS_ICC_PROFILE : [],
REGENERATE_FILE_DATA_JOB_PIXEL_HASH : []
REGENERATE_FILE_DATA_JOB_PIXEL_HASH : [],
REGENERATE_FILE_DATA_JOB_BLURHASH: []
}
ALL_REGEN_JOBS_IN_PREFERRED_ORDER = [
@ -197,6 +202,7 @@ ALL_REGEN_JOBS_IN_PREFERRED_ORDER = [
REGENERATE_FILE_DATA_JOB_FILE_METADATA,
REGENERATE_FILE_DATA_JOB_REFIT_THUMBNAIL,
REGENERATE_FILE_DATA_JOB_FORCE_THUMBNAIL,
REGENERATE_FILE_DATA_JOB_BLURHASH,
REGENERATE_FILE_DATA_JOB_SIMILAR_FILES_METADATA,
REGENERATE_FILE_DATA_JOB_CHECK_SIMILAR_FILES_MEMBERSHIP,
REGENERATE_FILE_DATA_JOB_FIX_PERMISSIONS,
@ -1677,7 +1683,8 @@ class ClientFilesManager( object ):
self._AddThumbnailFromBytes( hash, thumbnail_bytes )
return True
def RegenerateThumbnailIfWrongSize( self, media ):
@ -1725,6 +1732,29 @@ class ClientFilesManager( object ):
return do_it
def RegenerateImageBlurHash( self, media ):
hash = media.GetHash()
mime = media.GetMime()
if mime not in HC.MIMES_WITH_THUMBNAILS:
return None
try:
thumbnail_path = self._GenerateExpectedThumbnailPath( hash )
thumbnail_mime = HydrusFileHandling.GetThumbnailMime( thumbnail_path )
numpy_image = ClientImageHandling.GenerateNumPyImage( thumbnail_path, thumbnail_mime )
return HydrusImageHandling.GetImageBlurHashNumPy(numpy_image)
except:
return None
def Reinit( self ):
@ -2371,7 +2401,8 @@ class FilesMaintenanceManager( object ):
try:
self._controller.client_files_manager.RegenerateThumbnail( media_result )
return self._controller.client_files_manager.RegenerateThumbnail( media_result )
except HydrusExceptions.FileMissingException:
@ -2439,6 +2470,9 @@ class FilesMaintenanceManager( object ):
return None
def _RegenBlurHash( self, media ):
return self._controller.client_files_manager.RegenerateImageBlurHash( media )
def _RegenSimilarFilesMetadata( self, media_result ):
@ -2560,11 +2594,13 @@ class FilesMaintenanceManager( object ):
elif job_type == REGENERATE_FILE_DATA_JOB_FORCE_THUMBNAIL:
self._RegenFileThumbnailForce( media_result )
additional_data = self._RegenFileThumbnailForce( media_result )
elif job_type == REGENERATE_FILE_DATA_JOB_REFIT_THUMBNAIL:
was_regenerated = self._RegenFileThumbnailRefit( media_result )
additional_data = was_regenerated
if was_regenerated:
@ -2595,6 +2631,10 @@ class FilesMaintenanceManager( object ):
elif job_type == REGENERATE_FILE_DATA_JOB_FIX_PERMISSIONS:
self._FixFilePermissions( media_result )
elif job_type == REGENERATE_FILE_DATA_JOB_BLURHASH:
additional_data = self._RegenBlurHash( media_result )
elif job_type in (
REGENERATE_FILE_DATA_JOB_FILE_INTEGRITY_PRESENCE_REMOVE_RECORD,

View File

@ -75,7 +75,7 @@ def LoadPDF( path: str ):
return document
def GenerateThumbnailBytesFromPDFPath( path: str, target_resolution: typing.Tuple[int, int], clip_rect = None ) -> bytes:
def GenerateThumbnailNumPyFromPDFPath( path: str, target_resolution: typing.Tuple[int, int], clip_rect = None ) -> bytes:
try:
@ -114,7 +114,7 @@ def GenerateThumbnailBytesFromPDFPath( path: str, target_resolution: typing.Tupl
thumbnail_numpy_image = HydrusImageHandling.ResizeNumPyImage( numpy_image, target_resolution )
return HydrusImageHandling.GenerateThumbnailBytesNumPy( thumbnail_numpy_image )
return thumbnail_numpy_image
except Exception as e:
@ -126,7 +126,7 @@ def GenerateThumbnailBytesFromPDFPath( path: str, target_resolution: typing.Tupl
HydrusPDFHandling.GenerateThumbnailBytesFromPDFPath = GenerateThumbnailBytesFromPDFPath
HydrusPDFHandling.GenerateThumbnailNumPyFromPDFPath = GenerateThumbnailNumPyFromPDFPath
PDF_ASSUMED_DPI = 300

View File

@ -31,7 +31,7 @@ def LoadSVGRenderer( path: str ):
return renderer
def GenerateThumbnailBytesFromSVGPath( path: str, target_resolution: typing.Tuple[int, int], clip_rect = None ) -> bytes:
def GenerateThumbnailNumPyFromSVGPath( path: str, target_resolution: typing.Tuple[int, int], clip_rect = None ) -> bytes:
# TODO: SVGs have no inherent resolution, so all this is pretty stupid. we should render to exactly the res we want and then clip the result, not beforehand
@ -74,7 +74,7 @@ def GenerateThumbnailBytesFromSVGPath( path: str, target_resolution: typing.Tupl
thumbnail_numpy_image = HydrusImageHandling.ResizeNumPyImage( numpy_image, target_resolution )
return HydrusImageHandling.GenerateThumbnailBytesNumPy( thumbnail_numpy_image )
return thumbnail_numpy_image
except:
@ -82,7 +82,7 @@ def GenerateThumbnailBytesFromSVGPath( path: str, target_resolution: typing.Tupl
HydrusSVGHandling.GenerateThumbnailBytesFromSVGPath = GenerateThumbnailBytesFromSVGPath
HydrusSVGHandling.GenerateThumbnailNumPyFromSVGPath = GenerateThumbnailNumPyFromSVGPath
def GetSVGResolution( path: str ):

View File

@ -2775,7 +2775,7 @@ class DB( HydrusDB.HydrusDB ):
return boned_stats
def _GetFileInfoManagers( self, hash_ids: typing.Collection[ int ], sorted = False ) -> typing.List[ ClientMediaManagers.FileInfoManager ]:
def _GetFileInfoManagers( self, hash_ids: typing.Collection[ int ], sorted = False, blurhash = False ) -> typing.List[ ClientMediaManagers.FileInfoManager ]:
( cached_media_results, missing_hash_ids ) = self._weakref_media_result_cache.GetMediaResultsAndMissing( hash_ids )
@ -2790,6 +2790,9 @@ class DB( HydrusDB.HydrusDB ):
# temp hashes to metadata
hash_ids_to_info = { hash_id : ClientMediaManagers.FileInfoManager( hash_id, missing_hash_ids_to_hashes[ hash_id ], size, mime, width, height, duration, num_frames, has_audio, num_words ) for ( hash_id, size, mime, width, height, duration, num_frames, has_audio, num_words ) in self._Execute( 'SELECT * FROM {} CROSS JOIN files_info USING ( hash_id );'.format( temp_table_name ) ) }
if blurhash:
hash_ids_to_blurhash = self.modules_files_metadata_basic.GetHashIdsToBlurHash( temp_table_name )
# build it
@ -2798,6 +2801,10 @@ class DB( HydrusDB.HydrusDB ):
if hash_id in hash_ids_to_info:
file_info_manager = hash_ids_to_info[ hash_id ]
if blurhash and hash_id in hash_ids_to_blurhash:
file_info_manager.blurhash = hash_ids_to_blurhash[hash_id]
else:
@ -3295,6 +3302,8 @@ class DB( HydrusDB.HydrusDB ):
hash_ids_to_tags_managers = self._GetForceRefreshTagsManagersWithTableHashIds( missing_hash_ids, temp_table_name, hash_ids_to_current_file_service_ids = hash_ids_to_current_file_service_ids )
hash_ids_to_blurhash = self.modules_files_metadata_basic.GetHashIdsToBlurHash( temp_table_name )
has_exif_hash_ids = self.modules_files_metadata_basic.GetHasEXIFHashIds( temp_table_name )
has_human_readable_embedded_metadata_hash_ids = self.modules_files_metadata_basic.GetHasHumanReadableEmbeddedMetadataHashIds( temp_table_name )
has_icc_profile_hash_ids = self.modules_files_metadata_basic.GetHasICCProfileHashIds( temp_table_name )
@ -3412,6 +3421,10 @@ class DB( HydrusDB.HydrusDB ):
file_info_manager.has_exif = hash_id in has_exif_hash_ids
file_info_manager.has_human_readable_embedded_metadata = hash_id in has_human_readable_embedded_metadata_hash_ids
file_info_manager.has_icc_profile = hash_id in has_icc_profile_hash_ids
if hash_id in hash_ids_to_blurhash:
file_info_manager.blurhash = hash_ids_to_blurhash[hash_id]
missing_media_results.append( ClientMediaResult.MediaResult( file_info_manager, tags_manager, timestamps_manager, locations_manager, ratings_manager, notes_manager, file_viewing_stats_manager ) )
@ -4614,6 +4627,8 @@ class DB( HydrusDB.HydrusDB ):
self.modules_files_metadata_basic.SetHasHumanReadableEmbeddedMetadata( hash_id, file_import_job.HasHumanReadableEmbeddedMetadata() )
self.modules_files_metadata_basic.SetHasICCProfile( hash_id, file_import_job.HasICCProfile() )
self.modules_files_metadata_basic.SetBlurHash( hash_id, file_import_job.GetBlurhash())
#
file_modified_timestamp = file_import_job.GetFileModifiedTimestamp()

View File

@ -180,8 +180,16 @@ class ClientDBFilesMaintenance( ClientDBModule.ClientDBModule ):
if self.modules_similar_files.FileIsInSystem( hash_id ):
self.modules_similar_files.StopSearchingFile( hash_id )
elif job_type == ClientFiles.REGENERATE_FILE_DATA_JOB_FORCE_THUMBNAIL or job_type == ClientFiles.REGENERATE_FILE_DATA_JOB_REFIT_THUMBNAIL:
self.modules_files_maintenance_queue.AddJobs( ( hash_id, ), ClientFiles.REGENERATE_FILE_DATA_JOB_BLURHASH )
elif job_type == ClientFiles.REGENERATE_FILE_DATA_JOB_BLURHASH:
blurhash: str = additional_data
self.modules_files_metadata_basic.SetBlurHash( hash_id, blurhash )

View File

@ -35,7 +35,8 @@ class ClientDBFilesMetadataBasic( ClientDBModule.ClientDBModule ):
'main.files_info' : ( 'CREATE TABLE IF NOT EXISTS {} ( hash_id INTEGER PRIMARY KEY, size INTEGER, mime INTEGER, width INTEGER, height INTEGER, duration INTEGER, num_frames INTEGER, has_audio INTEGER_BOOLEAN, num_words INTEGER );', 400 ),
'main.has_icc_profile' : ( 'CREATE TABLE IF NOT EXISTS {} ( hash_id INTEGER PRIMARY KEY );', 465 ),
'main.has_exif' : ( 'CREATE TABLE IF NOT EXISTS {} ( hash_id INTEGER PRIMARY KEY );', 505 ),
'main.has_human_readable_embedded_metadata' : ( 'CREATE TABLE IF NOT EXISTS {} ( hash_id INTEGER PRIMARY KEY );', 505 )
'main.has_human_readable_embedded_metadata' : ( 'CREATE TABLE IF NOT EXISTS {} ( hash_id INTEGER PRIMARY KEY );', 505 ),
'main.blurhash' : ( 'CREATE TABLE IF NOT EXISTS {} ( hash_id INTEGER PRIMARY KEY, blurhash TEXT );', 545 ),
}
@ -107,7 +108,8 @@ class ClientDBFilesMetadataBasic( ClientDBModule.ClientDBModule ):
( 'files_info', 'hash_id' ),
( 'has_exif', 'hash_id' ),
( 'has_human_readable_embedded_metadata', 'hash_id' ),
( 'has_icc_profile', 'hash_id' )
( 'has_icc_profile', 'hash_id' ),
( 'blurhash', 'hash_id' )
]
@ -215,6 +217,31 @@ class ClientDBFilesMetadataBasic( ClientDBModule.ClientDBModule ):
else:
self._Execute( 'DELETE FROM has_icc_profile WHERE hash_id = ?;', ( hash_id, ) )
def SetBlurHash( self, hash_id: int, blurhash: str ):
# TODO blurhash db stuff
self._Execute('INSERT OR REPLACE INTO blurhash ( hash_id, blurhash ) VALUES ( ?, ?);', (hash_id, blurhash))
def GetBlurHash( self, hash_id: int ) -> str:
result = self._Execute( 'SELECT blurhash FROM blurhash WHERE hash_id = ?;', ( hash_id, ) ).fetchone()
# TODO blurhash db stuff
if result is None:
raise HydrusExceptions.DataMissing( 'Did not have blurhash information for that file!' )
( blurhash, ) = result
return blurhash
def GetHashIdsToBlurHash( self, hash_ids_table_name: str ):
return dict( self._Execute( 'SELECT hash_id, blurhash FROM {} CROSS JOIN blurhash USING ( hash_id );'.format( hash_ids_table_name ) ) )

View File

@ -130,6 +130,7 @@ class FileImportJob( object ):
self._has_icc_profile = None
self._pixel_hash = None
self._file_modified_timestamp = None
self._blurhash = None
def CheckIsGoodToImport( self ):
@ -353,9 +354,20 @@ class FileImportJob( object ):
percentage_in = HG.client_controller.new_options.GetInteger( 'video_thumbnail_percentage_in' )
thumbnail_numpy = HydrusFileHandling.GenerateThumbnailNumPy(self._temp_path, target_resolution, mime, duration, num_frames, clip_rect = clip_rect, percentage_in = percentage_in)
# this guy handles almost all his own exceptions now, so no need for clever catching. if it fails, we are prob talking an I/O failure, which is not a 'thumbnail failed' error
self._thumbnail_bytes = HydrusFileHandling.GenerateThumbnailBytes( self._temp_path, target_resolution, mime, duration, num_frames, clip_rect = clip_rect, percentage_in = percentage_in )
self._thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesNumPy( thumbnail_numpy )
try:
self._blurhash = HydrusImageHandling.GetImageBlurHashNumPy( thumbnail_numpy )
except:
pass
if mime in HC.FILES_THAT_HAVE_PERCEPTUAL_HASH:
@ -503,6 +515,10 @@ class FileImportJob( object ):
def HasICCProfile( self ) -> bool:
return self._has_icc_profile
def GetBlurhash( self ) -> str:
return self._blurhash
def PubsubContentUpdates( self ):

View File

@ -76,6 +76,7 @@ class FileInfoManager( object ):
self.has_exif = False
self.has_human_readable_embedded_metadata = False
self.has_icc_profile = False
self.blurhash = None
def Duplicate( self ):

View File

@ -66,7 +66,7 @@ LOCAL_BOORU_JSON_BYTE_LIST_PARAMS = set()
CLIENT_API_INT_PARAMS = { 'file_id', 'file_sort_type', 'potentials_search_type', 'pixel_duplicates', 'max_hamming_distance', 'max_num_pairs' }
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' }
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', 'create_new_file_ids', 'detailed_url_information', 'hide_service_keys_tags', 'simple', 'file_sort_asc', 'return_hashes', 'return_file_ids', 'include_notes', 'include_services_object', 'notes', 'note_names', 'doublecheck_file_system' }
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_services_object', 'notes', 'note_names', 'doublecheck_file_system' }
CLIENT_API_JSON_BYTE_LIST_PARAMS = { 'file_service_keys', 'deleted_file_service_keys', 'hashes' }
CLIENT_API_JSON_BYTE_DICT_PARAMS = { 'service_keys_to_tags', 'service_keys_to_actions_to_tags', 'service_keys_to_additional_tags' }
@ -2958,6 +2958,7 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
include_notes = request.parsed_request_args.GetValue( 'include_notes', bool, default_value = False )
include_services_object = request.parsed_request_args.GetValue( 'include_services_object', bool, default_value = True )
create_new_file_ids = request.parsed_request_args.GetValue( 'create_new_file_ids', bool, default_value = False )
include_blurhash = request.parsed_request_args.GetValue( 'include_blurhash', bool, default_value = False )
hashes = ParseHashes( request )
@ -2994,7 +2995,7 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
elif only_return_basic_information:
file_info_managers = HG.client_controller.Read( 'file_info_managers_from_ids', hash_ids )
file_info_managers = HG.client_controller.Read( 'file_info_managers_from_ids', hash_ids, blurhash = include_blurhash )
hashes_to_file_info_managers = { file_info_manager.hash : file_info_manager for file_info_manager in file_info_managers }
@ -3019,6 +3020,10 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
'num_words' : file_info_manager.num_words,
'has_audio' : file_info_manager.has_audio
}
if include_blurhash:
metadata_row['blurhash'] = file_info_manager.blurhash
metadata.append( metadata_row )
@ -3072,7 +3077,8 @@ class HydrusResourceClientAPIRestrictedGetFilesFileMetadata( HydrusResourceClien
'duration' : file_info_manager.duration,
'num_frames' : file_info_manager.num_frames,
'num_words' : file_info_manager.num_words,
'has_audio' : file_info_manager.has_audio
'has_audio' : file_info_manager.has_audio,
'blurhash' : file_info_manager.blurhash
}
if file_info_manager.mime in HC.MIMES_WITH_THUMBNAILS:

View File

@ -116,6 +116,11 @@ headers_and_mime.extend( [
def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames, clip_rect = None, percentage_in = 35 ):
thumbnail_numpy = GenerateThumbnailNumPy(path, target_resolution, mime, duration, num_frames, clip_rect, percentage_in )
return HydrusImageHandling.GenerateThumbnailBytesNumPy(thumbnail_numpy)
def GenerateThumbnailNumPy( path, target_resolution, mime, duration, num_frames, clip_rect = None, percentage_in = 35 ):
if target_resolution == ( 0, 0 ):
target_resolution = ( 128, 128 )
@ -125,7 +130,7 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames,
try:
thumbnail_bytes = HydrusPSDHandling.GenerateThumbnailBytesFromPSDPath( path, target_resolution, clip_rect = clip_rect )
thumbnail_numpy = HydrusPSDHandling.GenerateThumbnailNumPyFromPSDPath( path, target_resolution, clip_rect = clip_rect )
except Exception as e:
@ -139,13 +144,13 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames,
HydrusVideoHandling.RenderImageToImagePath( path, temp_path )
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( temp_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
thumbnail_numpy = HydrusImageHandling.GenerateThumbnailNumPyFromStaticImagePath( temp_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
except Exception as e:
thumb_path = os.path.join( HC.STATIC_DIR, 'psd.png' )
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
thumbnail_numpy = HydrusImageHandling.GenerateThumbnailNumPyFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
finally:
@ -161,13 +166,13 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames,
HydrusClipHandling.ExtractDBPNGToPath( path, temp_path )
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( temp_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
thumbnail_numpy = HydrusImageHandling.GenerateThumbnailNumPyFromStaticImagePath( temp_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
except:
thumb_path = os.path.join( HC.STATIC_DIR, 'clip.png' )
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
thumbnail_numpy = HydrusImageHandling.GenerateThumbnailNumPyFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
finally:
@ -182,13 +187,13 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames,
HydrusKritaHandling.ExtractZippedImageToPath( path, temp_path )
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( temp_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
thumbnail_numpy = HydrusImageHandling.GenerateThumbnailNumPyFromStaticImagePath( temp_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
except Exception as e:
thumb_path = os.path.join( HC.STATIC_DIR, 'krita.png' )
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
thumbnail_numpy = HydrusImageHandling.GenerateThumbnailNumPyFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
finally:
@ -202,13 +207,13 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames,
HydrusProcreateHandling.ExtractZippedThumbnailToPath( path, temp_path )
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( temp_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
thumbnail_numpy = HydrusImageHandling.GenerateThumbnailNumPyFromStaticImagePath( temp_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
except Exception as e:
thumb_path = os.path.join( HC.STATIC_DIR, 'procreate.png' )
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
thumbnail_numpy = HydrusImageHandling.GenerateThumbnailNumPyFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
finally:
@ -219,7 +224,7 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames,
try:
thumbnail_bytes = HydrusSVGHandling.GenerateThumbnailBytesFromSVGPath( path, target_resolution, clip_rect = clip_rect )
thumbnail_numpy = HydrusSVGHandling.GenerateThumbnailNumPyFromSVGPath( path, target_resolution, clip_rect = clip_rect )
except Exception as e:
@ -231,14 +236,14 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames,
thumb_path = os.path.join( HC.STATIC_DIR, 'svg.png' )
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
thumbnail_numpy = HydrusImageHandling.GenerateThumbnailNumPyFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
elif mime == HC.APPLICATION_PDF:
try:
thumbnail_bytes = HydrusPDFHandling.GenerateThumbnailBytesFromPDFPath( path, target_resolution, clip_rect = clip_rect )
thumbnail_numpy = HydrusPDFHandling.GenerateThumbnailNumPyFromPDFPath( path, target_resolution, clip_rect = clip_rect )
except Exception as e:
@ -250,7 +255,7 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames,
thumb_path = os.path.join( HC.STATIC_DIR, 'pdf.png' )
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
thumbnail_numpy = HydrusImageHandling.GenerateThumbnailNumPyFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
elif mime == HC.APPLICATION_FLASH:
@ -261,13 +266,13 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames,
HydrusFlashHandling.RenderPageToFile( path, temp_path, 1 )
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( temp_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
thumbnail_numpy = HydrusImageHandling.GenerateThumbnailNumPyFromStaticImagePath( temp_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
except:
thumb_path = os.path.join( HC.STATIC_DIR, 'flash.png' )
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
thumbnail_numpy = HydrusImageHandling.GenerateThumbnailNumPyFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
finally:
@ -280,7 +285,7 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames,
try:
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( path, target_resolution, mime, clip_rect = clip_rect )
thumbnail_numpy = HydrusImageHandling.GenerateThumbnailNumPyFromStaticImagePath( path, target_resolution, mime, clip_rect = clip_rect )
except Exception as e:
@ -289,7 +294,7 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames,
thumb_path = os.path.join( HC.STATIC_DIR, 'hydrus.png' )
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
thumbnail_numpy = HydrusImageHandling.GenerateThumbnailNumPyFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
else:
@ -340,13 +345,11 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames,
thumb_path = os.path.join( HC.STATIC_DIR, 'hydrus.png' )
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
thumbnail_numpy = HydrusImageHandling.GenerateThumbnailNumPyFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect )
else:
numpy_image = HydrusImageHandling.ResizeNumPyImage( numpy_image, target_resolution ) # just in case ffmpeg doesn't deliver right
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesNumPy( numpy_image )
thumbnail_numpy = HydrusImageHandling.ResizeNumPyImage( numpy_image, target_resolution ) # just in case ffmpeg doesn't deliver right
if renderer is not None:
@ -355,7 +358,7 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames,
return thumbnail_bytes
return thumbnail_numpy
def GetExtraHashesFromPath( path ):

View File

@ -47,6 +47,8 @@ from hydrus.core import HydrusPaths
from hydrus.core import HydrusTemp
from hydrus.core import HydrusPSDHandling
from hydrus.external import blurhash
PIL_SRGB_PROFILE = PILImageCms.createProfile( 'sRGB' )
def EnableLoadTruncatedImages():
@ -436,7 +438,7 @@ def GeneratePILImageFromNumPyImage( numpy_image: numpy.array ) -> PILImage.Image
return pil_image
def GenerateThumbnailBytesFromStaticImagePath( path, target_resolution, mime, clip_rect = None ) -> bytes:
def GenerateThumbnailNumPyFromStaticImagePath( path, target_resolution, mime, clip_rect = None ):
if OPENCV_OK:
@ -449,18 +451,9 @@ def GenerateThumbnailBytesFromStaticImagePath( path, target_resolution, mime, cl
thumbnail_numpy_image = ResizeNumPyImage( numpy_image, target_resolution )
try:
thumbnail_bytes = GenerateThumbnailBytesNumPy( thumbnail_numpy_image )
return thumbnail_bytes
except HydrusExceptions.CantRenderWithCVException:
pass # fallback to PIL
return thumbnail_numpy_image
pil_image = GeneratePILImage( path )
if clip_rect is not None:
@ -470,9 +463,9 @@ def GenerateThumbnailBytesFromStaticImagePath( path, target_resolution, mime, cl
thumbnail_pil_image = pil_image.resize( target_resolution, PILImage.LANCZOS )
thumbnail_bytes = GenerateThumbnailBytesPIL( thumbnail_pil_image )
thumbnail_numpy_image = GenerateNumPyImageFromPILImage(thumbnail_pil_image)
return thumbnail_bytes
return thumbnail_numpy_image
def GenerateThumbnailBytesNumPy( numpy_image ) -> bytes:
@ -1253,3 +1246,6 @@ def StripOutAnyUselessAlphaChannel( numpy_image: numpy.array ) -> numpy.array:
return numpy_image
def GetImageBlurHashNumPy( numpy_image, components_x = 4, components_y = 4 ):
return blurhash.blurhash_encode( numpy_image, components_x, components_y )

View File

@ -2,7 +2,7 @@ import typing
from hydrus.core import HydrusExceptions
def BaseGenerateThumbnailBytesFromPDFPath( path: str, target_resolution: typing.Tuple[int, int], clip_rect = None ) -> bytes:
def BaseGenerateThumbnailNumPyFromPDFPath( path: str, target_resolution: typing.Tuple[int, int], clip_rect = None ) -> bytes:
raise HydrusExceptions.NoThumbnailFileException()
@ -12,5 +12,5 @@ def BaseGetPDFInfo( path: str ):
raise HydrusExceptions.LimitedSupportFileException()
GenerateThumbnailBytesFromPDFPath = BaseGenerateThumbnailBytesFromPDFPath
GenerateThumbnailNumPyFromPDFPath = BaseGenerateThumbnailNumPyFromPDFPath
GetPDFInfo = BaseGetPDFInfo

View File

@ -37,7 +37,7 @@ def MergedPILImageFromPSD( path: str ) -> PILImage:
return HydrusPSDTools.MergedPILImageFromPSD( path )
def GenerateThumbnailBytesFromPSDPath( path: str, target_resolution: typing.Tuple[int, int], clip_rect = None ) -> bytes:
def GenerateThumbnailNumPyFromPSDPath( path: str, target_resolution: typing.Tuple[int, int], clip_rect = None ) -> bytes:
pil_image = MergedPILImageFromPSD( path )
@ -48,9 +48,7 @@ def GenerateThumbnailBytesFromPSDPath( path: str, target_resolution: typing.Tupl
thumbnail_pil_image = pil_image.resize( target_resolution, PILImage.LANCZOS )
thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesPIL( thumbnail_pil_image )
return thumbnail_bytes
return HydrusImageHandling.GenerateNumPyImageFromPILImage(thumbnail_pil_image)
def GetPSDResolution( path: str ):

View File

@ -2,7 +2,7 @@ import typing
from hydrus.core import HydrusExceptions
def BaseGenerateThumbnailBytesFromSVGPath( path: str, target_resolution: typing.Tuple[int, int], clip_rect = None ) -> bytes:
def BaseGenerateThumbnailNumPyFromSVGPath( path: str, target_resolution: typing.Tuple[int, int], clip_rect = None ) -> bytes:
raise HydrusExceptions.NoThumbnailFileException()
@ -12,5 +12,5 @@ def BaseGetSVGResolution( path: str ):
raise HydrusExceptions.NoResolutionFileException()
GenerateThumbnailBytesFromSVGPath = BaseGenerateThumbnailBytesFromSVGPath
GenerateThumbnailNumPyFromSVGPath = BaseGenerateThumbnailNumPyFromSVGPath
GetSVGResolution = BaseGetSVGResolution

256
hydrus/external/blurhash.py vendored Normal file
View File

@ -0,0 +1,256 @@
"""
Pure python blurhash decoder with no additional dependencies, for
both de- and encoding.
Very close port of the original Swift implementation by Dag Ågren.
"""
"""
From https://github.com/halcy/blurhash-python
MIT License
Copyright (c) 2019 Lorenz Diener
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import math
# Alphabet for base 83
alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"
alphabet_values = dict(zip(alphabet, range(len(alphabet))))
def base83_decode(base83_str):
"""
Decodes a base83 string, as used in blurhash, to an integer.
"""
value = 0
for base83_char in base83_str:
value = value * 83 + alphabet_values[base83_char]
return value
def base83_encode(value, length):
"""
Decodes an integer to a base83 string, as used in blurhash.
Length is how long the resulting string should be. Will complain
if the specified length is too short.
"""
if int(value) // (83 ** (length)) != 0:
raise ValueError("Specified length is too short to encode given value.")
result = ""
for i in range(1, length + 1):
digit = int(value) // (83 ** (length - i)) % 83
result += alphabet[int(digit)]
return result
def srgb_to_linear(value):
"""
srgb 0-255 integer to linear 0.0-1.0 floating point conversion.
"""
value = float(value) / 255.0
if value <= 0.04045:
return value / 12.92
return math.pow((value + 0.055) / 1.055, 2.4)
def sign_pow(value, exp):
"""
Sign-preserving exponentiation.
"""
return math.copysign(math.pow(abs(value), exp), value)
def linear_to_srgb(value):
"""
linear 0.0-1.0 floating point to srgb 0-255 integer conversion.
"""
value = max(0.0, min(1.0, value))
if value <= 0.0031308:
return int(value * 12.92 * 255 + 0.5)
return int((1.055 * math.pow(value, 1 / 2.4) - 0.055) * 255 + 0.5)
def blurhash_components(blurhash):
"""
Decodes and returns the number of x and y components in the given blurhash.
"""
if len(blurhash) < 6:
raise ValueError("BlurHash must be at least 6 characters long.")
# Decode metadata
size_info = base83_decode(blurhash[0])
size_y = int(size_info / 9) + 1
size_x = (size_info % 9) + 1
return size_x, size_y
def blurhash_decode(blurhash, width, height, punch = 1.0, linear = False):
"""
Decodes the given blurhash to an image of the specified size.
Returns the resulting image a list of lists of 3-value sRGB 8 bit integer
lists. Set linear to True if you would prefer to get linear floating point
RGB back.
The punch parameter can be used to de- or increase the contrast of the
resulting image.
As per the original implementation it is suggested to only decode
to a relatively small size and then scale the result up, as it
basically looks the same anyways.
"""
if len(blurhash) < 6:
raise ValueError("BlurHash must be at least 6 characters long.")
# Decode metadata
size_info = base83_decode(blurhash[0])
size_y = int(size_info / 9) + 1
size_x = (size_info % 9) + 1
quant_max_value = base83_decode(blurhash[1])
real_max_value = (float(quant_max_value + 1) / 166.0) * punch
# Make sure we at least have the right number of characters
if len(blurhash) != 4 + 2 * size_x * size_y:
raise ValueError("Invalid BlurHash length.")
# Decode DC component
dc_value = base83_decode(blurhash[2:6])
colours = [(
srgb_to_linear(dc_value >> 16),
srgb_to_linear((dc_value >> 8) & 255),
srgb_to_linear(dc_value & 255)
)]
# Decode AC components
for component in range(1, size_x * size_y):
ac_value = base83_decode(blurhash[4+component*2:4+(component+1)*2])
colours.append((
sign_pow((float(int(ac_value / (19 * 19))) - 9.0) / 9.0, 2.0) * real_max_value,
sign_pow((float(int(ac_value / 19) % 19) - 9.0) / 9.0, 2.0) * real_max_value,
sign_pow((float(ac_value % 19) - 9.0) / 9.0, 2.0) * real_max_value
))
# Return image RGB values, as a list of lists of lists,
# consumable by something like numpy or PIL.
pixels = []
for y in range(height):
pixel_row = []
for x in range(width):
pixel = [0.0, 0.0, 0.0]
for j in range(size_y):
for i in range(size_x):
basis = math.cos(math.pi * float(x) * float(i) / float(width)) * \
math.cos(math.pi * float(y) * float(j) / float(height))
colour = colours[i + j * size_x]
pixel[0] += colour[0] * basis
pixel[1] += colour[1] * basis
pixel[2] += colour[2] * basis
if linear == False:
pixel_row.append([
linear_to_srgb(pixel[0]),
linear_to_srgb(pixel[1]),
linear_to_srgb(pixel[2]),
])
else:
pixel_row.append(pixel)
pixels.append(pixel_row)
return pixels
def blurhash_encode(image, components_x = 4, components_y = 4, linear = False):
"""
Calculates the blurhash for an image using the given x and y component counts.
Image should be a 3-dimensional array, with the first dimension being y, the second
being x, and the third being the three rgb components that are assumed to be 0-255
srgb integers (incidentally, this is the format you will get from a PIL RGB image).
You can also pass in already linear data - to do this, set linear to True. This is
useful if you want to encode a version of your image resized to a smaller size (which
you should ideally do in linear colour).
"""
if components_x < 1 or components_x > 9 or components_y < 1 or components_y > 9:
raise ValueError("x and y component counts must be between 1 and 9 inclusive.")
height = float(len(image))
width = float(len(image[0]))
# Convert to linear if neeeded
image_linear = []
if linear == False:
for y in range(int(height)):
image_linear_line = []
for x in range(int(width)):
image_linear_line.append([
srgb_to_linear(image[y][x][0]),
srgb_to_linear(image[y][x][1]),
srgb_to_linear(image[y][x][2])
])
image_linear.append(image_linear_line)
else:
image_linear = image
# Calculate components
components = []
max_ac_component = 0.0
for j in range(components_y):
for i in range(components_x):
norm_factor = 1.0 if (i == 0 and j == 0) else 2.0
component = [0.0, 0.0, 0.0]
for y in range(int(height)):
for x in range(int(width)):
basis = norm_factor * math.cos(math.pi * float(i) * float(x) / width) * \
math.cos(math.pi * float(j) * float(y) / height)
component[0] += basis * image_linear[y][x][0]
component[1] += basis * image_linear[y][x][1]
component[2] += basis * image_linear[y][x][2]
component[0] /= (width * height)
component[1] /= (width * height)
component[2] /= (width * height)
components.append(component)
if not (i == 0 and j == 0):
max_ac_component = max(max_ac_component, abs(component[0]), abs(component[1]), abs(component[2]))
# Encode components
dc_value = (linear_to_srgb(components[0][0]) << 16) + \
(linear_to_srgb(components[0][1]) << 8) + \
linear_to_srgb(components[0][2])
quant_max_ac_component = int(max(0, min(82, math.floor(max_ac_component * 166 - 0.5))))
ac_component_norm_factor = float(quant_max_ac_component + 1) / 166.0
ac_values = []
for r, g, b in components[1:]:
ac_values.append(
int(max(0.0, min(18.0, math.floor(sign_pow(r / ac_component_norm_factor, 0.5) * 9.0 + 9.5)))) * 19 * 19 + \
int(max(0.0, min(18.0, math.floor(sign_pow(g / ac_component_norm_factor, 0.5) * 9.0 + 9.5)))) * 19 + \
int(max(0.0, min(18.0, math.floor(sign_pow(b / ac_component_norm_factor, 0.5) * 9.0 + 9.5))))
)
# Build final blurhash
blurhash = ""
blurhash += base83_encode((components_x - 1) + (components_y - 1) * 9, 1)
blurhash += base83_encode(quant_max_ac_component, 1)
blurhash += base83_encode(dc_value, 4)
for ac_value in ac_values:
blurhash += base83_encode(ac_value, 2)
return blurhash