hydrus/hydrus/client/ClientSerialisable.py

421 lines
12 KiB
Python

import collections
import cv2
import numpy
import os
import shutil
import struct
from qtpy import QtCore as QC
from qtpy import QtGui as QG
from qtpy import QtWidgets as QW
from hydrus.core import HydrusCompression
from hydrus.core import HydrusData
from hydrus.core import HydrusGlobals as HG
from hydrus.core import HydrusImageHandling
from hydrus.core import HydrusPaths
from hydrus.core import HydrusSerialisable
from hydrus.core import HydrusTemp
from hydrus.client import ClientConstants as CC
from hydrus.client.gui import ClientGUIFunctions
from hydrus.client.gui import QtPorting as QP
# 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 :^)
if cv2.__version__.startswith( '2' ):
IMREAD_UNCHANGED = cv2.CV_LOAD_IMAGE_UNCHANGED
else:
IMREAD_UNCHANGED = cv2.IMREAD_UNCHANGED
def CreateTopImage( width, title, payload_description, text ):
text_extent_qt_image = HG.client_controller.bitmap_manager.GetQtImage( 20, 20, 24 )
painter = QG.QPainter( text_extent_qt_image )
text_font = QW.QApplication.font()
basic_font_size = text_font.pointSize()
payload_description_font = QW.QApplication.font()
payload_description_font.setPointSize( int( basic_font_size * 1.4 ) )
title_font = QW.QApplication.font()
title_font.setPointSize( int( basic_font_size * 2.0 ) )
texts_to_draw = []
current_y = 6
for ( t, f ) in ( ( title, title_font ), ( payload_description, payload_description_font ), ( text, text_font ) ):
painter.setFont( f )
wrapped_texts = WrapText( painter, t, width - 20 )
line_height = painter.fontMetrics().height()
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
texts_to_draw.append( ( wrapped_texts_with_ys, f ) )
current_y += 6
top_height = current_y
del painter
del text_extent_qt_image
#
top_qt_image = HG.client_controller.bitmap_manager.GetQtImage( width, top_height, 24 )
painter = QG.QPainter( top_qt_image )
painter.setBackground( QG.QBrush( QC.Qt.white ) )
painter.eraseRect( painter.viewport() )
#
painter.drawPixmap( width-16-5, 5, CC.global_pixmaps().file_repository )
#
for ( wrapped_texts_with_ys, f ) in texts_to_draw:
painter.setFont( f )
for ( wrapped_text, y ) in wrapped_texts_with_ys:
( text_size, wrapped_text ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, wrapped_text )
ClientGUIFunctions.DrawText( painter, ( width - text_size.width() ) // 2, y, wrapped_text )
del painter
top_image_rgb = ClientGUIFunctions.ConvertQtImageToNumPy( top_qt_image )
top_image = cv2.cvtColor( top_image_rgb, cv2.COLOR_RGB2GRAY )
top_height_header = struct.pack( '!H', top_height )
byte0 = top_height_header[0:1]
byte1 = top_height_header[1:2]
top_image[0][0] = ord( byte0 )
top_image[0][1] = ord( byte1 )
return top_image
def DumpToPNG( width, payload_bytes, title, payload_description, text, path ):
payload_bytes_length = len( payload_bytes )
header_and_payload_bytes_length = payload_bytes_length + 4
payload_height = int( header_and_payload_bytes_length / width )
if ( header_and_payload_bytes_length / width ) % 1.0 > 0:
payload_height += 1
top_image = CreateTopImage( width, title, payload_description, text )
payload_length_header = struct.pack( '!I', payload_bytes_length )
num_empty_bytes = payload_height * width - header_and_payload_bytes_length
header_and_payload_bytes = payload_length_header + payload_bytes + b'\x00' * num_empty_bytes
payload_image = numpy.fromstring( header_and_payload_bytes, dtype = 'uint8' ).reshape( ( payload_height, width ) )
finished_image = numpy.concatenate( ( top_image, payload_image ) )
# this is to deal with unicode paths, which cv2 can't handle
( os_file_handle, temp_path ) = HydrusTemp.GetTempPath( suffix = '.png' )
try:
cv2.imwrite( temp_path, finished_image, [ cv2.IMWRITE_PNG_COMPRESSION, 9 ] )
HydrusPaths.MirrorFile( temp_path, path )
except Exception as e:
HydrusData.ShowException( e )
raise Exception( 'Could not save the png!' )
finally:
HydrusTemp.CleanUpTempPath( os_file_handle, temp_path )
def GetPayloadBytesAndLength( payload_obj ):
if isinstance( payload_obj, bytes ):
return ( HydrusCompression.CompressBytesToBytes( payload_obj ), len( payload_obj ) )
elif isinstance( payload_obj, str ):
return ( HydrusCompression.CompressStringToBytes( payload_obj ), len( payload_obj ) )
else:
payload_string = payload_obj.DumpToString()
return ( HydrusCompression.CompressStringToBytes( payload_string ), len( payload_string ) )
def GetPayloadTypeString( payload_obj ):
if isinstance( payload_obj, bytes ):
return 'Bytes'
elif isinstance( payload_obj, str ):
return 'String'
elif isinstance( payload_obj, HydrusSerialisable.SerialisableList ):
type_string_counts = collections.Counter()
for o in payload_obj:
type_string_counts[ GetPayloadTypeString( o ) ] += 1
type_string = ', '.join( ( HydrusData.ToHumanInt( count ) + ' ' + s for ( s, count ) in list(type_string_counts.items()) ) )
return 'A list of ' + type_string
elif isinstance( payload_obj, HydrusSerialisable.SerialisableBase ):
return payload_obj.SERIALISABLE_NAME
else:
return repr( type( payload_obj ) )
def GetPayloadDescriptionAndBytes( payload_obj ):
( payload_bytes, payload_length ) = GetPayloadBytesAndLength( payload_obj )
payload_description = GetPayloadTypeString( payload_obj ) + ' - ' + HydrusData.ToHumanBytes( payload_length )
return ( payload_description, payload_bytes )
def LoadFromQtImage( qt_image: QG.QImage ):
# assume this for now
depth = 3
numpy_image = ClientGUIFunctions.ConvertQtImageToNumPy( qt_image )
return LoadFromNumPyImage( numpy_image )
def LoadFromPNG( path ):
# this is to deal with unicode paths, which cv2 can't handle
( os_file_handle, temp_path ) = HydrusTemp.GetTempPath()
try:
HydrusPaths.MirrorFile( path, temp_path )
try:
# unchanged because we want exact byte data, no conversions or other gubbins
numpy_image = cv2.imread( temp_path, flags = IMREAD_UNCHANGED )
if numpy_image is None:
raise Exception()
except Exception as e:
try:
# dequantize = False because we don't want to convert to RGB
pil_image = HydrusImageHandling.GeneratePILImage( temp_path, dequantize = False )
numpy_image = HydrusImageHandling.GenerateNumPyImageFromPILImage( pil_image )
except Exception as e:
HydrusData.ShowException( e )
raise Exception( '"{}" did not appear to be a valid image!'.format( path ) )
finally:
HydrusTemp.CleanUpTempPath( os_file_handle, temp_path )
return LoadFromNumPyImage( numpy_image )
def LoadFromNumPyImage( numpy_image: numpy.array ):
try:
height = numpy_image.shape[0]
width = numpy_image.shape[1]
if len( numpy_image.shape ) > 2:
depth = numpy_image.shape[2]
if depth != 1:
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
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!' )
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!' )
except Exception as e:
HydrusData.PrintException( e )
message = 'The image loaded, but it did not seem to be a hydrus serialised png! The error was: {}'.format( str( e ) )
message += os.linesep * 2
message += 'If you believe this is a legit non-resized, non-converted hydrus serialised png, please send it to hydrus_dev.'
raise Exception( message )
return payload_bytes
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
def TextExceedsWidth( painter, text, width ):
( text_size, text ) = ClientGUIFunctions.GetTextSizeFromPainter( painter, text )
return text_size.width() > width
def WrapText( painter, text, width ):
words = text.split( ' ' )
lines = []
next_line = []
for word in words:
if word == '':
continue
potential_next_line = list( next_line )
potential_next_line.append( word )
if TextExceedsWidth( painter, ' '.join( potential_next_line ), width ):
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
if len( next_line ) > 0:
lines.append( ' '.join( next_line ) )
return lines