hydrus/hydrus/client/ClientSerialisable.py

421 lines
12 KiB
Python
Raw Permalink Normal View History

2018-09-19 21:54:51 +00:00
import collections
2016-11-09 23:13:22 +00:00
import cv2
import numpy
2016-11-16 20:21:43 +00:00
import os
2016-11-09 23:13:22 +00:00
import struct
2020-05-20 21:36:02 +00:00
2019-11-14 03:56:30 +00:00
from qtpy import QtCore as QC
from qtpy import QtGui as QG
from qtpy import QtWidgets as QW
2020-05-20 21:36:02 +00:00
2021-12-22 22:31:23 +00:00
from hydrus.core import HydrusCompression
2020-07-29 20:52:44 +00:00
from hydrus.core import HydrusData
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusPaths
from hydrus.core import HydrusSerialisable
from hydrus.core import HydrusTemp
2024-01-03 21:21:53 +00:00
from hydrus.core.files.images import HydrusImageHandling
2020-07-29 20:52:44 +00:00
2020-05-20 21:36:02 +00:00
from hydrus.client import ClientConstants as CC
2024-02-14 21:20:24 +00:00
from hydrus.client import ClientGlobals as CG
2020-12-02 22:04:38 +00:00
from hydrus.client.gui import ClientGUIFunctions
2016-11-09 23:13:22 +00:00
2021-12-22 22:31:23 +00:00
# ok, the serialised png format is:
# the png is monochrome, no alpha channel (mode 'L' in PIL)
# the data is read left to right in visual pixels. one pixel = one byte
# first two bytes (i.e. ( 0, 0 ) and ( 1, 0 )), are a big endian unsigned short (!H), and say how tall the header is in number of rows. the actual data starts after that
# first four bytes of data are a big endian unsigned int (!I) saying how long the payload is in bytes
# read that many pixels after that, you got the payload
# it should be zlib compressed these days and is most likely a dumped hydrus serialisable object, which is a json guy with a whole complicated loading system. utf-8 it into a string and you are half way there :^)
2016-11-16 20:21:43 +00:00
if cv2.__version__.startswith( '2' ):
2016-11-09 23:13:22 +00:00
2016-11-16 20:21:43 +00:00
IMREAD_UNCHANGED = cv2.CV_LOAD_IMAGE_UNCHANGED
2016-11-09 23:13:22 +00:00
2016-11-16 20:21:43 +00:00
else:
2016-11-09 23:13:22 +00:00
2016-11-16 20:21:43 +00:00
IMREAD_UNCHANGED = cv2.IMREAD_UNCHANGED
2016-11-09 23:13:22 +00:00
2016-11-16 20:21:43 +00:00
2016-12-14 21:19:07 +00:00
def CreateTopImage( width, title, payload_description, text ):
2016-11-09 23:13:22 +00:00
2024-02-14 21:20:24 +00:00
text_extent_qt_image = CG.client_controller.bitmap_manager.GetQtImage( 20, 20, 24 )
2016-11-09 23:13:22 +00:00
2019-11-14 03:56:30 +00:00
painter = QG.QPainter( text_extent_qt_image )
2016-11-16 20:21:43 +00:00
2019-11-14 03:56:30 +00:00
text_font = QW.QApplication.font()
2016-11-16 20:21:43 +00:00
2019-11-14 03:56:30 +00:00
basic_font_size = text_font.pointSize()
2016-11-16 20:21:43 +00:00
2019-11-14 03:56:30 +00:00
payload_description_font = QW.QApplication.font()
2016-11-16 20:21:43 +00:00
2019-11-14 03:56:30 +00:00
payload_description_font.setPointSize( int( basic_font_size * 1.4 ) )
2016-11-16 20:21:43 +00:00
2019-11-14 03:56:30 +00:00
title_font = QW.QApplication.font()
2016-11-16 20:21:43 +00:00
2019-11-14 03:56:30 +00:00
title_font.setPointSize( int( basic_font_size * 2.0 ) )
2016-11-16 20:21:43 +00:00
2019-04-24 22:18:50 +00:00
texts_to_draw = []
2016-11-16 20:21:43 +00:00
2019-04-24 22:18:50 +00:00
current_y = 6
2016-11-16 20:21:43 +00:00
2019-04-24 22:18:50 +00:00
for ( t, f ) in ( ( title, title_font ), ( payload_description, payload_description_font ), ( text, text_font ) ):
2016-11-09 23:13:22 +00:00
2019-11-14 03:56:30 +00:00
painter.setFont( f )
2016-11-16 20:21:43 +00:00
2019-11-14 03:56:30 +00:00
wrapped_texts = WrapText( painter, t, width - 20 )
line_height = painter.fontMetrics().height()
2016-11-16 20:21:43 +00:00
2019-04-24 22:18:50 +00:00
wrapped_texts_with_ys = []
if len( wrapped_texts ) > 0:
current_y += 10
for wrapped_text in wrapped_texts:
wrapped_texts_with_ys.append( ( wrapped_text, current_y ) )
current_y += line_height + 4
2016-11-16 20:21:43 +00:00
2019-04-24 22:18:50 +00:00
texts_to_draw.append( ( wrapped_texts_with_ys, f ) )
2016-11-16 20:21:43 +00:00
2019-04-24 22:18:50 +00:00
current_y += 6
top_height = current_y
2019-11-14 03:56:30 +00:00
del painter
del text_extent_qt_image
2016-11-16 20:21:43 +00:00
#
2024-02-14 21:20:24 +00:00
top_qt_image = CG.client_controller.bitmap_manager.GetQtImage( width, top_height, 24 )
2016-11-16 20:21:43 +00:00
2019-11-14 03:56:30 +00:00
painter = QG.QPainter( top_qt_image )
2016-11-16 20:21:43 +00:00
2019-11-14 03:56:30 +00:00
painter.setBackground( QG.QBrush( QC.Qt.white ) )
2016-11-16 20:21:43 +00:00
2019-11-14 03:56:30 +00:00
painter.eraseRect( painter.viewport() )
2016-11-16 20:21:43 +00:00
#
2020-03-11 21:52:11 +00:00
painter.drawPixmap( width-16-5, 5, CC.global_pixmaps().file_repository )
2016-11-16 20:21:43 +00:00
#
2019-04-24 22:18:50 +00:00
for ( wrapped_texts_with_ys, f ) in texts_to_draw:
2016-11-16 20:21:43 +00:00
2019-11-14 03:56:30 +00:00
painter.setFont( f )
2016-11-16 20:21:43 +00:00
2019-04-24 22:18:50 +00:00
for ( wrapped_text, y ) in wrapped_texts_with_ys:
2020-12-02 22:04:38 +00:00
( text_size, wrapped_text ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, wrapped_text )
2019-04-24 22:18:50 +00:00
2020-12-02 22:04:38 +00:00
ClientGUIFunctions.DrawText( painter, ( width - text_size.width() ) // 2, y, wrapped_text )
2019-04-24 22:18:50 +00:00
2016-11-09 23:13:22 +00:00
2019-11-14 03:56:30 +00:00
del painter
2016-11-09 23:13:22 +00:00
2022-01-26 21:57:04 +00:00
top_image_rgb = ClientGUIFunctions.ConvertQtImageToNumPy( top_qt_image )
2016-11-09 23:13:22 +00:00
2016-11-16 20:21:43 +00:00
top_image = cv2.cvtColor( top_image_rgb, cv2.COLOR_RGB2GRAY )
2016-11-09 23:13:22 +00:00
top_height_header = struct.pack( '!H', top_height )
2019-01-09 22:59:03 +00:00
byte0 = top_height_header[0:1]
byte1 = top_height_header[1:2]
2016-11-09 23:13:22 +00:00
top_image[0][0] = ord( byte0 )
top_image[0][1] = ord( byte1 )
2016-11-16 20:21:43 +00:00
return top_image
def DumpToPNG( width, payload_bytes, title, payload_description, text, path ):
2016-11-16 20:21:43 +00:00
2019-01-09 22:59:03 +00:00
payload_bytes_length = len( payload_bytes )
2016-11-16 20:21:43 +00:00
2019-01-09 22:59:03 +00:00
header_and_payload_bytes_length = payload_bytes_length + 4
2016-11-16 20:21:43 +00:00
2019-01-09 22:59:03 +00:00
payload_height = int( header_and_payload_bytes_length / width )
2016-11-16 20:21:43 +00:00
2019-01-09 22:59:03 +00:00
if ( header_and_payload_bytes_length / width ) % 1.0 > 0:
2016-11-16 20:21:43 +00:00
payload_height += 1
2016-12-14 21:19:07 +00:00
top_image = CreateTopImage( width, title, payload_description, text )
2016-11-16 20:21:43 +00:00
2019-01-09 22:59:03 +00:00
payload_length_header = struct.pack( '!I', payload_bytes_length )
2016-11-09 23:13:22 +00:00
2019-01-09 22:59:03 +00:00
num_empty_bytes = payload_height * width - header_and_payload_bytes_length
2016-11-09 23:13:22 +00:00
2019-01-09 22:59:03 +00:00
header_and_payload_bytes = payload_length_header + payload_bytes + b'\x00' * num_empty_bytes
2016-11-09 23:13:22 +00:00
2019-01-09 22:59:03 +00:00
payload_image = numpy.fromstring( header_and_payload_bytes, dtype = 'uint8' ).reshape( ( payload_height, width ) )
2016-11-09 23:13:22 +00:00
2016-11-16 20:21:43 +00:00
finished_image = numpy.concatenate( ( top_image, payload_image ) )
2016-11-30 20:24:17 +00:00
# this is to deal with unicode paths, which cv2 can't handle
( os_file_handle, temp_path ) = HydrusTemp.GetTempPath( suffix = '.png' )
2016-11-30 20:24:17 +00:00
try:
cv2.imwrite( temp_path, finished_image, [ cv2.IMWRITE_PNG_COMPRESSION, 9 ] )
2020-05-13 19:03:16 +00:00
HydrusPaths.MirrorFile( temp_path, path )
2016-11-30 20:24:17 +00:00
except Exception as e:
HydrusData.ShowException( e )
2023-11-08 21:42:59 +00:00
raise Exception( 'Could not save the png!' ) from e
2016-11-30 20:24:17 +00:00
finally:
HydrusTemp.CleanUpTempPath( os_file_handle, temp_path )
2016-11-30 20:24:17 +00:00
2017-12-20 22:55:48 +00:00
2021-12-22 22:31:23 +00:00
def GetPayloadBytesAndLength( payload_obj ):
2017-12-20 22:55:48 +00:00
2019-01-09 22:59:03 +00:00
if isinstance( payload_obj, bytes ):
2017-12-20 22:55:48 +00:00
2021-12-22 22:31:23 +00:00
return ( HydrusCompression.CompressBytesToBytes( payload_obj ), len( payload_obj ) )
2019-01-09 22:59:03 +00:00
elif isinstance( payload_obj, str ):
2021-12-22 22:31:23 +00:00
return ( HydrusCompression.CompressStringToBytes( payload_obj ), len( payload_obj ) )
2017-12-20 22:55:48 +00:00
else:
2021-12-22 22:31:23 +00:00
payload_string = payload_obj.DumpToString()
return ( HydrusCompression.CompressStringToBytes( payload_string ), len( payload_string ) )
2017-12-20 22:55:48 +00:00
2016-12-14 21:19:07 +00:00
def GetPayloadTypeString( payload_obj ):
2016-11-16 20:21:43 +00:00
2019-01-09 22:59:03 +00:00
if isinstance( payload_obj, bytes ):
return 'Bytes'
elif isinstance( payload_obj, str ):
2017-12-20 22:55:48 +00:00
return 'String'
elif isinstance( payload_obj, HydrusSerialisable.SerialisableList ):
2016-11-16 20:21:43 +00:00
2018-09-19 21:54:51 +00:00
type_string_counts = collections.Counter()
for o in payload_obj:
type_string_counts[ GetPayloadTypeString( o ) ] += 1
2019-01-09 22:59:03 +00:00
type_string = ', '.join( ( HydrusData.ToHumanInt( count ) + ' ' + s for ( s, count ) in list(type_string_counts.items()) ) )
2018-09-19 21:54:51 +00:00
return 'A list of ' + type_string
2016-12-14 21:19:07 +00:00
2017-12-20 22:55:48 +00:00
elif isinstance( payload_obj, HydrusSerialisable.SerialisableBase ):
return payload_obj.SERIALISABLE_NAME
2016-12-14 21:19:07 +00:00
else:
2017-12-20 22:55:48 +00:00
return repr( type( payload_obj ) )
2016-11-16 20:21:43 +00:00
2019-01-09 22:59:03 +00:00
def GetPayloadDescriptionAndBytes( payload_obj ):
2016-11-16 20:21:43 +00:00
2021-12-22 22:31:23 +00:00
( payload_bytes, payload_length ) = GetPayloadBytesAndLength( payload_obj )
2016-12-14 21:19:07 +00:00
2021-12-22 22:31:23 +00:00
payload_description = GetPayloadTypeString( payload_obj ) + ' - ' + HydrusData.ToHumanBytes( payload_length )
2016-12-14 21:19:07 +00:00
2019-01-09 22:59:03 +00:00
return ( payload_description, payload_bytes )
2016-11-09 23:13:22 +00:00
2022-01-26 21:57:04 +00:00
def LoadFromQtImage( qt_image: QG.QImage ):
numpy_image = ClientGUIFunctions.ConvertQtImageToNumPy( qt_image )
return LoadFromNumPyImage( numpy_image )
2023-11-08 21:42:59 +00:00
def LoadFromPNG( path ):
2016-11-09 23:13:22 +00:00
2016-11-30 20:24:17 +00:00
# this is to deal with unicode paths, which cv2 can't handle
( os_file_handle, temp_path ) = HydrusTemp.GetTempPath()
2016-11-30 20:24:17 +00:00
2016-11-16 20:21:43 +00:00
try:
2020-05-13 19:03:16 +00:00
HydrusPaths.MirrorFile( path, temp_path )
2016-11-30 20:24:17 +00:00
2021-06-23 21:11:38 +00:00
try:
2021-12-01 22:12:16 +00:00
# unchanged because we want exact byte data, no conversions or other gubbins
2021-06-23 21:11:38 +00:00
numpy_image = cv2.imread( temp_path, flags = IMREAD_UNCHANGED )
2021-06-30 21:27:35 +00:00
if numpy_image is None:
raise Exception()
2021-06-23 21:11:38 +00:00
except Exception as e:
try:
2023-10-04 20:51:17 +00:00
# dequantize = False because we don't want to convert our greyscale bytes to RGB
2021-06-23 21:11:38 +00:00
2021-12-01 22:12:16 +00:00
pil_image = HydrusImageHandling.GeneratePILImage( temp_path, dequantize = False )
2023-11-08 21:42:59 +00:00
# leave strip_useless_alpha = True in here just to catch the very odd LA situation
2021-12-01 22:12:16 +00:00
numpy_image = HydrusImageHandling.GenerateNumPyImageFromPILImage( pil_image )
2021-06-23 21:11:38 +00:00
except Exception as e:
HydrusData.ShowException( e )
2023-11-08 21:42:59 +00:00
raise Exception( '"{}" did not appear to be a valid image!'.format( path ) ) from e
2021-06-23 21:11:38 +00:00
2016-11-16 20:21:43 +00:00
2016-11-30 20:24:17 +00:00
finally:
HydrusTemp.CleanUpTempPath( os_file_handle, temp_path )
2016-11-30 20:24:17 +00:00
2016-11-16 20:21:43 +00:00
2022-01-26 21:57:04 +00:00
return LoadFromNumPyImage( numpy_image )
2023-11-08 21:42:59 +00:00
2022-01-26 21:57:04 +00:00
def LoadFromNumPyImage( numpy_image: numpy.array ):
2016-11-16 20:21:43 +00:00
try:
2021-05-05 20:12:11 +00:00
height = numpy_image.shape[0]
width = numpy_image.shape[1]
if len( numpy_image.shape ) > 2:
2019-03-13 21:04:21 +00:00
2021-05-05 20:12:11 +00:00
depth = numpy_image.shape[2]
2019-03-13 21:04:21 +00:00
2021-05-05 20:12:11 +00:00
if depth != 1:
2022-12-07 22:41:53 +00:00
numpy_image = numpy_image[:,:,0].copy() # let's fetch one channel. if the png is a perfect RGB conversion of the original (or, let's say, a Firefox bmp export), this actually works
2021-05-05 20:12:11 +00:00
2019-03-13 21:04:21 +00:00
2016-11-16 20:21:43 +00:00
2019-03-13 21:04:21 +00:00
try:
complete_data = numpy_image.tostring()
top_height_header = complete_data[:2]
( top_height, ) = struct.unpack( '!H', top_height_header )
payload_and_header_bytes = complete_data[ width * top_height : ]
except:
raise Exception( 'Header bytes were invalid!' )
2016-11-16 20:21:43 +00:00
2019-03-13 21:04:21 +00:00
try:
payload_length_header = payload_and_header_bytes[:4]
( payload_bytes_length, ) = struct.unpack( '!I', payload_length_header )
payload_bytes = payload_and_header_bytes[ 4 : 4 + payload_bytes_length ]
except:
raise Exception( 'Payload bytes were invalid!' )
2016-11-16 20:21:43 +00:00
except Exception as e:
2021-05-05 20:12:11 +00:00
HydrusData.PrintException( e )
2023-04-26 21:10:03 +00:00
message = 'The image loaded, but it did not seem to be a hydrus serialised png! The error was: {}'.format( repr( e ) )
2024-04-10 20:36:05 +00:00
message += '\n' * 2
2019-03-13 21:04:21 +00:00
message += 'If you believe this is a legit non-resized, non-converted hydrus serialised png, please send it to hydrus_dev.'
2016-11-16 20:21:43 +00:00
2019-03-13 21:04:21 +00:00
raise Exception( message )
2016-11-16 20:21:43 +00:00
2019-01-09 22:59:03 +00:00
return payload_bytes
2016-11-09 23:13:22 +00:00
2021-12-22 22:31:23 +00:00
def LoadStringFromPNG( path: str ) -> str:
payload_bytes = LoadFromPNG( path )
try:
payload_string = HydrusCompression.DecompressBytesToString( payload_bytes )
except:
# older payloads were not compressed
payload_string = str( payload_bytes, 'utf-8' )
return payload_string
2019-11-14 03:56:30 +00:00
def TextExceedsWidth( painter, text, width ):
2016-11-09 23:13:22 +00:00
2020-12-02 22:04:38 +00:00
( text_size, text ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, text )
2016-11-09 23:13:22 +00:00
2020-02-26 22:28:52 +00:00
return text_size.width() > width
2016-11-09 23:13:22 +00:00
2019-11-14 03:56:30 +00:00
def WrapText( painter, text, width ):
2016-11-09 23:13:22 +00:00
2016-11-16 20:21:43 +00:00
words = text.split( ' ' )
2016-11-09 23:13:22 +00:00
2016-11-16 20:21:43 +00:00
lines = []
2016-11-09 23:13:22 +00:00
2016-11-16 20:21:43 +00:00
next_line = []
2016-11-09 23:13:22 +00:00
2016-11-16 20:21:43 +00:00
for word in words:
if word == '':
continue
potential_next_line = list( next_line )
potential_next_line.append( word )
2019-11-14 03:56:30 +00:00
if TextExceedsWidth( painter, ' '.join( potential_next_line ), width ):
2016-11-16 20:21:43 +00:00
if len( potential_next_line ) == 1: # one very long word
lines.append( ' '.join( potential_next_line ) )
next_line = []
else:
lines.append( ' '.join( next_line ) )
next_line = [ word ]
else:
next_line = potential_next_line
2016-11-09 23:13:22 +00:00
2016-11-16 20:21:43 +00:00
if len( next_line ) > 0:
lines.append( ' '.join( next_line ) )
return lines
2016-12-14 21:19:07 +00:00