228 lines
7.6 KiB
Python
228 lines
7.6 KiB
Python
import numpy
|
|
|
|
from hydrus.core import HydrusData
|
|
from hydrus.core import HydrusExceptions
|
|
from hydrus.core import HydrusGlobals as HG
|
|
from hydrus.core import HydrusTime
|
|
from hydrus.core.files.images import HydrusImageHandling
|
|
from hydrus.core.files.images import HydrusImageNormalisation
|
|
|
|
class GIFRenderer( object ):
|
|
|
|
def __init__( self, path, num_frames, target_resolution ):
|
|
|
|
if HG.media_load_report_mode:
|
|
|
|
HydrusData.ShowText( 'Loading GIF: ' + path )
|
|
|
|
|
|
self._path = path
|
|
self._num_frames = num_frames
|
|
self._target_resolution = target_resolution
|
|
self._cannot_seek_to_or_beyond_this_index = None
|
|
self._frames_we_could_not_render = set()
|
|
|
|
self._current_render_index = 0
|
|
self._last_valid_numpy_frame = None
|
|
|
|
self._Initialise()
|
|
|
|
|
|
def _GetRecoveryFrame( self ) -> numpy.array:
|
|
|
|
if self._last_valid_numpy_frame is None:
|
|
|
|
numpy_image = numpy.zeros( ( self._target_resolution[1], self._target_resolution[0], 4 ), dtype = 'uint8' )
|
|
numpy_image[:,:,3] = 255 # numpy is great!
|
|
|
|
else:
|
|
|
|
numpy_image = self._last_valid_numpy_frame
|
|
|
|
|
|
return numpy_image
|
|
|
|
|
|
def _Initialise( self ):
|
|
|
|
if HG.media_load_report_mode:
|
|
|
|
HydrusData.ShowText( 'Loading GIF with PIL' )
|
|
|
|
|
|
# dequantize = False since we'll be doing that later for each frame in turn
|
|
# if we do it now, it collapses down to a one frame object
|
|
self._pil_image = HydrusImageHandling.GeneratePILImage( self._path, dequantize = False )
|
|
|
|
self._pil_global_palette = self._pil_image.palette
|
|
|
|
# years-old weirdo fix, taking it out 2023-11
|
|
'''
|
|
# believe it or not, doing this actually fixed a couple of gifs!
|
|
try:
|
|
|
|
self._pil_image.seek( 1 )
|
|
self._pil_image.seek( 0 )
|
|
|
|
except Exception as e:
|
|
|
|
raise HydrusExceptions.DamagedOrUnusualFileException( 'Could not initialise GIF!' ) from e
|
|
|
|
'''
|
|
|
|
|
|
def _MoveRendererOnOneFrame( self ):
|
|
|
|
self._current_render_index = ( self._current_render_index + 1 ) % self._num_frames
|
|
|
|
we_are_in_the_dangerzone = self._cannot_seek_to_or_beyond_this_index is not None and self._current_render_index >= self._cannot_seek_to_or_beyond_this_index
|
|
|
|
if self._current_render_index == 0 or we_are_in_the_dangerzone:
|
|
|
|
self._RewindGIF( reinitialise = True )
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
self._pil_image.seek( self._current_render_index )
|
|
|
|
size = self._pil_image.size
|
|
|
|
# this out of the blue: <PIL.GifImagePlugin.GifImageFile image mode=RGBA size=85171x53524 at 0x1BF0386C460>
|
|
# 8GB memory 20 second fail render
|
|
if size[0] > 16384 or size[1] > 16384:
|
|
|
|
raise HydrusExceptions.DamagedOrUnusualFileException( 'Crazy GIF frame went bananas!' )
|
|
|
|
|
|
except:
|
|
|
|
# this can raise OSError in some 'trancated file' circumstances
|
|
# trying to render beyond with PIL is rife with trouble, so we won't try
|
|
if self._cannot_seek_to_or_beyond_this_index is None:
|
|
|
|
self._cannot_seek_to_or_beyond_this_index = self._current_render_index
|
|
|
|
else:
|
|
|
|
self._cannot_seek_to_or_beyond_this_index = min( self._cannot_seek_to_or_beyond_this_index, self._current_render_index )
|
|
|
|
|
|
self._RewindGIF( reinitialise = True )
|
|
|
|
|
|
|
|
|
|
def _RenderCurrentFrameAndResizeIt( self ) -> numpy.array:
|
|
|
|
if self._cannot_seek_to_or_beyond_this_index is not None and self._current_render_index >= self._cannot_seek_to_or_beyond_this_index:
|
|
|
|
numpy_image = self._GetRecoveryFrame()
|
|
|
|
elif self._current_render_index in self._frames_we_could_not_render:
|
|
|
|
numpy_image = self._GetRecoveryFrame()
|
|
|
|
else:
|
|
|
|
time_started = HydrusTime.GetNowFloat()
|
|
|
|
try:
|
|
|
|
current_frame = HydrusImageNormalisation.DequantizePILImage( self._pil_image )
|
|
|
|
# don't have to worry about pasting alpha-having transparent frames over the previous frame--PIL seems to handle this these days!
|
|
self._pil_canvas = current_frame
|
|
|
|
numpy_image = HydrusImageHandling.GenerateNumPyImageFromPILImage( self._pil_canvas, strip_useless_alpha = False )
|
|
|
|
except:
|
|
|
|
# PIL can produce an IOError, which is an OSError(!!!), on a truncated file, lfg
|
|
# so let's just bail out in that case mate
|
|
|
|
self._frames_we_could_not_render.add( self._current_render_index )
|
|
|
|
time_to_error = HydrusTime.GetNowFloat() - time_started
|
|
|
|
if time_to_error > 2.0:
|
|
|
|
# this is a crazy file that, with its broken frame, needs to re-render the whole thing or something
|
|
# don't push any further
|
|
|
|
self._cannot_seek_to_or_beyond_this_index = min( self._frames_we_could_not_render )
|
|
|
|
|
|
numpy_image = self._GetRecoveryFrame()
|
|
|
|
|
|
|
|
try:
|
|
|
|
resized_numpy_image = HydrusImageHandling.ResizeNumPyImage( numpy_image, self._target_resolution )
|
|
|
|
except:
|
|
|
|
self._frames_we_could_not_render.add( self._current_render_index )
|
|
self._cannot_seek_to_or_beyond_this_index = min( self._frames_we_could_not_render )
|
|
|
|
numpy_image = self._GetRecoveryFrame()
|
|
|
|
resized_numpy_image = HydrusImageHandling.ResizeNumPyImage( numpy_image, self._target_resolution )
|
|
|
|
|
|
self._last_valid_numpy_frame = numpy_image
|
|
|
|
return resized_numpy_image
|
|
|
|
|
|
def _RewindGIF( self, reinitialise = False ):
|
|
|
|
self._pil_image.seek( 0 )
|
|
|
|
self._current_render_index = 0
|
|
|
|
if reinitialise:
|
|
|
|
self._Initialise()
|
|
|
|
|
|
|
|
def read_frame( self ):
|
|
|
|
numpy_image = self._RenderCurrentFrameAndResizeIt()
|
|
|
|
self._MoveRendererOnOneFrame()
|
|
|
|
return numpy_image
|
|
|
|
|
|
def set_position( self, index ):
|
|
|
|
if self._cannot_seek_to_or_beyond_this_index is not None and index >= self._cannot_seek_to_or_beyond_this_index:
|
|
|
|
return
|
|
|
|
|
|
if index == self._current_render_index:
|
|
|
|
return
|
|
|
|
elif index < self._current_render_index:
|
|
|
|
self._RewindGIF()
|
|
|
|
|
|
while self._current_render_index < index:
|
|
|
|
self._MoveRendererOnOneFrame()
|
|
|
|
|
|
|
|
def Stop( self ):
|
|
|
|
pass
|
|
|
|
|