2021-04-20 23:41:09 +02:00
from typing import Dict , List , Tuple , Optional , Union
2021-04-16 01:18:33 +02:00
from PIL import Image # type: ignore
2021-05-16 02:20:23 +02:00
from . blend import affine_composite
2021-05-17 05:55:41 +02:00
from . swf import SWF , Frame , Tag , AP2ShapeTag , AP2DefineSpriteTag , AP2PlaceObjectTag , AP2RemoveObjectTag , AP2DoActionTag , AP2DefineFontTag , AP2DefineEditTextTag , AP2PlaceCameraTag
2021-05-21 23:31:13 +02:00
from . types import Color , Matrix , Point , Rectangle
2021-05-11 00:26:26 +02:00
from . geo import Shape , DrawParams
2021-04-16 01:18:33 +02:00
from . util import VerboseOutput
2021-05-11 00:26:26 +02:00
class RegisteredClip :
2021-04-18 01:30:46 +02:00
# A movie clip that we are rendering, frame by frame. These are manifest by the root
2021-04-20 23:41:28 +02:00
# SWF as well as AP2DefineSpriteTags which are essentially embedded movie clips. The
# tag_id is the AP2DefineSpriteTag that created us, or None if this is the clip for
# the root of the movie.
def __init__ ( self , tag_id : Optional [ int ] , frames : List [ Frame ] , tags : List [ Tag ] ) - > None :
2021-04-16 01:18:33 +02:00
self . tag_id = tag_id
self . frames = frames
self . tags = tags
2021-05-11 00:26:26 +02:00
def __repr__ ( self ) - > str :
return f " RegisteredClip(tag_id= { self . tag_id } ) "
2021-04-16 01:18:33 +02:00
2021-05-11 00:26:26 +02:00
class RegisteredShape :
# A shape that we are rendering, as placed by some placed clip somewhere.
def __init__ ( self , tag_id : int , vertex_points : List [ Point ] , tex_points : List [ Point ] , tex_colors : List [ Color ] , draw_params : List [ DrawParams ] ) - > None :
self . tag_id = tag_id
self . vertex_points : List [ Point ] = vertex_points
self . tex_points : List [ Point ] = tex_points
self . tex_colors : List [ Color ] = tex_colors
self . draw_params : List [ DrawParams ] = draw_params
2021-05-21 18:58:39 +02:00
self . rectangle : Optional [ Image . image ] = None
2021-04-18 01:31:08 +02:00
2021-05-11 00:26:26 +02:00
def __repr__ ( self ) - > str :
return f " RegisteredShape(tag_id= { self . tag_id } , vertex_points= { self . vertex_points } , tex_points= { self . tex_points } , tex_colors= { self . tex_colors } , draw_params= { self . draw_params } ) "
2021-05-21 18:58:01 +02:00
class RegisteredDummy :
# An imported tag that we could not find.
def __init__ ( self , tag_id : int ) - > None :
self . tag_id = tag_id
def __repr__ ( self ) - > str :
return f " RegisteredDummy(tag_id= { self . tag_id } ) "
2021-05-11 00:26:26 +02:00
class PlacedObject :
# An object that occupies the screen at some depth.
def __init__ ( self , object_id : int , depth : int , rotation_offset : Point , transform : Matrix , mult_color : Color , add_color : Color , blend : int ) - > None :
self . __object_id = object_id
self . __depth = depth
self . rotation_offset = rotation_offset
self . transform = transform
self . mult_color = mult_color
self . add_color = add_color
self . blend = blend
2021-04-20 23:41:28 +02:00
2021-04-18 01:30:46 +02:00
@property
2021-05-21 18:58:01 +02:00
def source ( self ) - > Union [ RegisteredClip , RegisteredShape , RegisteredDummy ] :
2021-05-11 00:26:26 +02:00
raise NotImplementedError ( " Only implemented in subclass! " )
2021-04-16 01:18:33 +02:00
2021-04-18 01:30:46 +02:00
@property
2021-05-11 00:26:26 +02:00
def depth ( self ) - > int :
return self . __depth
2021-04-18 01:31:08 +02:00
@property
2021-05-11 00:26:26 +02:00
def object_id ( self ) - > int :
return self . __object_id
2021-04-18 01:30:46 +02:00
def __repr__ ( self ) - > str :
2021-05-11 00:26:26 +02:00
return f " PlacedObject(object_id= { self . object_id } , depth= { self . depth } ) "
2021-04-16 01:18:33 +02:00
2021-05-11 00:26:26 +02:00
class PlacedShape ( PlacedObject ) :
# A shape that occupies its parent clip at some depth. Placed by an AP2PlaceObjectTag
# referencing an AP2ShapeTag.
def __init__ ( self , object_id : int , depth : int , rotation_offset : Point , transform : Matrix , mult_color : Color , add_color : Color , blend : int , source : RegisteredShape ) - > None :
super ( ) . __init__ ( object_id , depth , rotation_offset , transform , mult_color , add_color , blend )
self . __source = source
2021-04-16 01:18:33 +02:00
2021-04-18 01:30:46 +02:00
@property
2021-05-11 00:26:26 +02:00
def source ( self ) - > RegisteredShape :
return self . __source
def __repr__ ( self ) - > str :
return f " PlacedShape(object_id= { self . object_id } , depth= { self . depth } , source= { self . source } ) "
class PlacedClip ( PlacedObject ) :
# A movieclip that occupies its parent clip at some depth. Placed by an AP2PlaceObjectTag
# referencing an AP2DefineSpriteTag. Essentially an embedded movie clip.
def __init__ ( self , object_id : int , depth : int , rotation_offset : Point , transform : Matrix , mult_color : Color , add_color : Color , blend : int , source : RegisteredClip ) - > None :
super ( ) . __init__ ( object_id , depth , rotation_offset , transform , mult_color , add_color , blend )
self . placed_objects : List [ PlacedObject ] = [ ]
self . frame : int = 0
self . __source = source
2021-04-18 01:30:46 +02:00
@property
2021-05-11 00:26:26 +02:00
def source ( self ) - > RegisteredClip :
return self . __source
def advance ( self ) - > None :
if self . frame < len ( self . source . frames ) :
self . frame + = 1
@property
def finished ( self ) - > bool :
return self . frame == len ( self . source . frames )
2021-04-18 01:30:46 +02:00
def __repr__ ( self ) - > str :
2021-05-17 00:19:07 +02:00
return f " PlacedClip(object_id= { self . object_id } , depth= { self . depth } , source= { self . source } , frame= { self . frame } , total_frames= { len ( self . source . frames ) } , finished= { self . finished } ) "
2021-04-18 01:30:46 +02:00
2021-04-16 01:18:33 +02:00
2021-05-21 18:58:01 +02:00
class PlacedDummy ( PlacedObject ) :
# A reference to an object we can't find because we're missing the import.
def __init__ ( self , object_id : int , depth : int , rotation_offset : Point , transform : Matrix , mult_color : Color , add_color : Color , blend : int , source : RegisteredDummy ) - > None :
super ( ) . __init__ ( object_id , depth , rotation_offset , transform , mult_color , add_color , blend )
self . __source = source
@property
def source ( self ) - > RegisteredDummy :
return self . __source
2021-04-16 01:18:33 +02:00
class AFPRenderer ( VerboseOutput ) :
2021-05-16 17:15:06 +02:00
def __init__ ( self , shapes : Dict [ str , Shape ] = { } , textures : Dict [ str , Image . Image ] = { } , swfs : Dict [ str , SWF ] = { } , single_threaded : bool = False ) - > None :
2021-04-16 01:18:33 +02:00
super ( ) . __init__ ( )
2021-05-16 17:15:06 +02:00
# Options for rendering
self . __single_threaded = single_threaded
2021-05-21 18:58:01 +02:00
# Library of shapes (draw instructions), textures (actual images) and swfs (us and other files for imports).
2021-04-16 01:18:33 +02:00
self . shapes : Dict [ str , Shape ] = shapes
2021-04-18 01:30:46 +02:00
self . textures : Dict [ str , Image . Image ] = textures
2021-04-16 01:18:33 +02:00
self . swfs : Dict [ str , SWF ] = swfs
2021-05-11 00:26:26 +02:00
# Internal render parameters.
2021-05-21 18:58:01 +02:00
self . __registered_objects : Dict [ int , Union [ RegisteredShape , RegisteredClip , RegisteredDummy ] ] = { }
2021-04-16 01:18:33 +02:00
def add_shape ( self , name : str , data : Shape ) - > None :
2021-04-18 01:30:46 +02:00
# Register a named shape with the renderer.
2021-04-16 01:18:33 +02:00
if not data . parsed :
data . parse ( )
self . shapes [ name ] = data
2021-04-18 01:30:46 +02:00
def add_texture ( self , name : str , data : Image . Image ) - > None :
# Register a named texture (already loaded PIL image) with the renderer.
2021-04-21 02:01:35 +02:00
self . textures [ name ] = data . convert ( " RGBA " )
2021-04-16 01:18:33 +02:00
def add_swf ( self , name : str , data : SWF ) - > None :
2021-04-18 01:30:46 +02:00
# Register a named SWF with the renderer.
2021-04-16 01:18:33 +02:00
if not data . parsed :
data . parse ( )
self . swfs [ name ] = data
2021-05-21 23:31:13 +02:00
def render_path (
self ,
path : str ,
background_color : Optional [ Color ] = None ,
only_depths : Optional [ List [ int ] ] = None ,
movie_transform : Matrix = Matrix . identity ( ) ,
verbose : bool = False ,
) - > Tuple [ int , List [ Image . Image ] ] :
2021-05-17 00:19:07 +02:00
# Given a path to a SWF root animation, attempt to render it to a list of frames.
2021-04-16 01:18:33 +02:00
for name , swf in self . swfs . items ( ) :
2021-05-17 00:19:07 +02:00
if swf . exported_name == path :
2021-04-16 01:18:33 +02:00
# This is the SWF we care about.
with self . debugging ( verbose ) :
2021-04-21 03:06:48 +02:00
swf . color = background_color or swf . color
2021-05-21 23:31:13 +02:00
return self . __render ( swf , only_depths , movie_transform )
raise Exception ( f ' { path } not found in registered SWFs! ' )
def compute_path_location (
self ,
path : str ,
) - > Rectangle :
# Given a path to a SWF root animation, find its bounding rectangle.
for name , swf in self . swfs . items ( ) :
if swf . exported_name == path :
# This is the SWF we care about.
return swf . location
2021-04-16 01:18:33 +02:00
raise Exception ( f ' { path } not found in registered SWFs! ' )
2021-04-16 23:08:41 +02:00
def list_paths ( self , verbose : bool = False ) - > List [ str ] :
2021-04-18 01:30:46 +02:00
# Given the loaded animations, return a list of possible paths to render.
2021-04-16 23:08:41 +02:00
paths : List [ str ] = [ ]
for name , swf in self . swfs . items ( ) :
paths . append ( swf . exported_name )
return paths
2021-05-11 00:26:26 +02:00
def __place ( self , tag : Tag , operating_clip : PlacedClip , prefix : str = " " ) - > Tuple [ Optional [ PlacedClip ] , bool ] :
2021-04-18 01:30:46 +02:00
# "Place" a tag on the screen. Most of the time, this means performing the action of the tag,
# such as defining a shape (registering it with our shape list) or adding/removing an object.
2021-04-16 01:18:33 +02:00
if isinstance ( tag , AP2ShapeTag ) :
2021-05-11 00:26:26 +02:00
self . vprint ( f " { prefix } Loading { tag . reference } into object slot { tag . id } " )
2021-04-16 01:18:33 +02:00
if tag . reference not in self . shapes :
raise Exception ( f " Cannot find shape reference { tag . reference } ! " )
2021-05-11 00:26:26 +02:00
if tag . id in self . __registered_objects :
raise Exception ( f " Cannot register { tag . reference } as object slot { tag . id } is already taken! " )
self . __registered_objects [ tag . id ] = RegisteredShape (
tag . id ,
self . shapes [ tag . reference ] . vertex_points ,
self . shapes [ tag . reference ] . tex_points ,
self . shapes [ tag . reference ] . tex_colors ,
self . shapes [ tag . reference ] . draw_params ,
)
2021-04-16 01:18:33 +02:00
2021-05-11 00:26:26 +02:00
# Didn't place a new clip, didn't change anything.
return None , False
2021-04-18 01:30:46 +02:00
2021-04-16 01:18:33 +02:00
elif isinstance ( tag , AP2DefineSpriteTag ) :
2021-05-11 00:26:26 +02:00
self . vprint ( f " { prefix } Loading Sprite into object slot { tag . id } " )
2021-04-20 23:41:28 +02:00
2021-05-11 00:26:26 +02:00
if tag . id in self . __registered_objects :
raise Exception ( f " Cannot register sprite as object slot { tag . id } is already taken! " )
2021-04-16 01:18:33 +02:00
2021-04-18 01:31:08 +02:00
# Register a new clip that we might reference to execute.
2021-05-11 00:26:26 +02:00
self . __registered_objects [ tag . id ] = RegisteredClip ( tag . id , tag . frames , tag . tags )
# Didn't place a new clip, didn't change anything.
return None , False
2021-04-18 01:31:08 +02:00
2021-04-20 23:41:28 +02:00
elif isinstance ( tag , AP2PlaceObjectTag ) :
2021-04-16 01:18:33 +02:00
if tag . update :
2021-05-15 06:22:43 +02:00
for i in range ( len ( operating_clip . placed_objects ) - 1 , - 1 , - 1 ) :
obj = operating_clip . placed_objects [ i ]
2021-04-16 01:19:56 +02:00
2021-04-18 01:30:46 +02:00
if obj . object_id == tag . object_id and obj . depth == tag . depth :
2021-05-15 06:22:43 +02:00
new_mult_color = tag . mult_color or obj . mult_color
new_add_color = tag . add_color or obj . add_color
new_transform = tag . transform or obj . transform
new_rotation_offset = tag . rotation_offset or obj . rotation_offset
new_blend = tag . blend or obj . blend
if tag . source_tag_id is not None and tag . source_tag_id != obj . source . tag_id :
# This completely updates the pointed-at object.
self . vprint ( f " { prefix } Replacing Object source { obj . source . tag_id } with { tag . source_tag_id } on object with Object ID { tag . object_id } onto Depth { tag . depth } " )
newobj = self . __registered_objects [ tag . source_tag_id ]
if isinstance ( newobj , RegisteredShape ) :
operating_clip . placed_objects [ i ] = PlacedShape (
obj . object_id ,
obj . depth ,
new_rotation_offset ,
new_transform ,
new_mult_color ,
new_add_color ,
new_blend ,
newobj ,
)
# Didn't place a new clip, changed the parent clip.
return None , True
elif isinstance ( newobj , RegisteredClip ) :
new_clip = PlacedClip (
tag . object_id ,
tag . depth ,
new_rotation_offset ,
new_transform ,
new_mult_color ,
new_add_color ,
new_blend ,
newobj ,
)
operating_clip . placed_objects [ i ] = new_clip
# Placed a new clip, changed the parent.
return new_clip , True
2021-05-21 18:58:01 +02:00
elif isinstance ( newobj , RegisteredDummy ) :
operating_clip . placed_objects [ i ] = PlacedDummy (
obj . object_id ,
obj . depth ,
new_rotation_offset ,
new_transform ,
new_mult_color ,
new_add_color ,
new_blend ,
newobj ,
)
# Didn't place a new clip, changed the parent clip.
return None , True
2021-05-15 06:22:43 +02:00
else :
raise Exception ( f " Unrecognized object with Tag ID { tag . source_tag_id } ! " )
else :
# As far as I can tell, pretty much only color and matrix stuff can be updated.
self . vprint ( f " { prefix } Updating Object ID { tag . object_id } on Depth { tag . depth } " )
obj . mult_color = new_mult_color
obj . add_color = new_add_color
obj . transform = new_transform
obj . rotation_offset = new_rotation_offset
obj . blend = new_blend
return None , True
2021-04-20 23:41:28 +02:00
2021-05-11 00:26:26 +02:00
# Didn't place a new clip, did change something.
2021-05-15 06:22:43 +02:00
print ( f " WARNING: Couldn ' t find tag { tag . object_id } on depth { tag . depth } to update! " )
return None , False
2021-04-16 01:18:33 +02:00
else :
2021-04-20 23:41:28 +02:00
if tag . source_tag_id is None :
raise Exception ( " Cannot place a tag with no source ID and no update flags! " )
2021-04-16 01:18:33 +02:00
2021-04-20 23:41:28 +02:00
# TODO: Handle ON_LOAD triggers for this object. Many of these are just calls into
# the game to set the current frame that we're on, but sometimes its important.
2021-04-18 01:30:46 +02:00
2021-05-11 00:26:26 +02:00
if tag . source_tag_id in self . __registered_objects :
self . vprint ( f " { prefix } Placing Object { tag . source_tag_id } with Object ID { tag . object_id } onto Depth { tag . depth } " )
newobj = self . __registered_objects [ tag . source_tag_id ]
if isinstance ( newobj , RegisteredShape ) :
operating_clip . placed_objects . append (
PlacedShape (
tag . object_id ,
tag . depth ,
tag . rotation_offset or Point . identity ( ) ,
tag . transform or Matrix . identity ( ) ,
tag . mult_color or Color ( 1.0 , 1.0 , 1.0 , 1.0 ) ,
tag . add_color or Color ( 0.0 , 0.0 , 0.0 , 0.0 ) ,
tag . blend or 0 ,
newobj ,
)
)
# Didn't place a new clip, changed the parent clip.
return None , True
elif isinstance ( newobj , RegisteredClip ) :
placed_clip = PlacedClip (
tag . object_id ,
tag . depth ,
tag . rotation_offset or Point . identity ( ) ,
tag . transform or Matrix . identity ( ) ,
tag . mult_color or Color ( 1.0 , 1.0 , 1.0 , 1.0 ) ,
tag . add_color or Color ( 0.0 , 0.0 , 0.0 , 0.0 ) ,
tag . blend or 0 ,
newobj ,
)
operating_clip . placed_objects . append ( placed_clip )
# Placed a new clip, changed the parent.
return placed_clip , True
2021-05-21 18:58:01 +02:00
elif isinstance ( newobj , RegisteredDummy ) :
operating_clip . placed_objects . append (
PlacedDummy (
tag . object_id ,
tag . depth ,
tag . rotation_offset or Point . identity ( ) ,
tag . transform or Matrix . identity ( ) ,
tag . mult_color or Color ( 1.0 , 1.0 , 1.0 , 1.0 ) ,
tag . add_color or Color ( 0.0 , 0.0 , 0.0 , 0.0 ) ,
tag . blend or 0 ,
newobj ,
)
)
# Didn't place a new clip, changed the parent clip.
return None , True
2021-05-11 00:26:26 +02:00
else :
raise Exception ( f " Unrecognized object with Tag ID { tag . source_tag_id } ! " )
2021-04-21 01:19:14 +02:00
raise Exception ( f " Cannot find a shape or sprite with Tag ID { tag . source_tag_id } ! " )
2021-05-11 00:26:26 +02:00
2021-04-16 01:18:33 +02:00
elif isinstance ( tag , AP2RemoveObjectTag ) :
self . vprint ( f " { prefix } Removing Object ID { tag . object_id } from Depth { tag . depth } " )
if tag . object_id != 0 :
# Remove the identified object by object ID and depth.
2021-04-18 01:31:08 +02:00
# Remember removed objects so we can stop any clips.
removed_objects = [
2021-05-11 00:26:26 +02:00
obj for obj in operating_clip . placed_objects
2021-04-18 01:31:08 +02:00
if obj . object_id == tag . object_id and obj . depth == tag . depth
]
2021-04-18 01:30:46 +02:00
2021-04-18 01:31:08 +02:00
# Get rid of the objects that we're removing from the master list.
2021-05-11 00:26:26 +02:00
operating_clip . placed_objects = [
obj for obj in operating_clip . placed_objects
2021-04-18 01:30:46 +02:00
if not ( obj . object_id == tag . object_id and obj . depth == tag . depth )
2021-04-16 01:18:33 +02:00
]
else :
2021-04-18 01:30:46 +02:00
# Remove the last placed object at this depth. The placed objects list isn't
# ordered so much as apppending to the list means the last placed object at a
# depth comes last.
2021-04-21 01:19:14 +02:00
removed_objects = [ ]
2021-05-11 00:26:26 +02:00
for i in range ( len ( operating_clip . placed_objects ) ) :
real_index = len ( operating_clip . placed_objects ) - ( i + 1 )
2021-04-16 01:18:33 +02:00
2021-05-11 00:26:26 +02:00
if operating_clip . placed_objects [ real_index ] . depth == tag . depth :
removed_objects = operating_clip . placed_objects [ real_index : ( real_index + 1 ) ]
operating_clip . placed_objects = operating_clip . placed_objects [ : real_index ] + operating_clip . placed_objects [ ( real_index + 1 ) : ]
2021-04-16 01:18:33 +02:00
break
2021-04-18 01:31:08 +02:00
2021-04-21 01:19:14 +02:00
if not removed_objects :
print ( f " WARNING: Couldn ' t find object to remove by ID { tag . object_id } and depth { tag . depth } ! " )
2021-05-11 00:26:26 +02:00
# Didn't place a new clip, changed parent clip.
return None , True
2021-04-16 01:20:27 +02:00
elif isinstance ( tag , AP2DoActionTag ) :
print ( " WARNING: Unhandled DO_ACTION tag! " )
2021-05-17 00:18:44 +02:00
if self . verbose :
print ( tag . bytecode . decompile ( ) )
2021-05-11 00:26:26 +02:00
# Didn't place a new clip.
return None , False
2021-04-16 23:28:53 +02:00
elif isinstance ( tag , AP2DefineFontTag ) :
print ( " WARNING: Unhandled DEFINE_FONT tag! " )
2021-05-11 00:26:26 +02:00
# Didn't place a new clip.
return None , False
2021-04-16 23:28:53 +02:00
elif isinstance ( tag , AP2DefineEditTextTag ) :
print ( " WARNING: Unhandled DEFINE_EDIT_TEXT tag! " )
2021-05-11 00:26:26 +02:00
# Didn't place a new clip.
return None , False
2021-05-17 05:55:41 +02:00
elif isinstance ( tag , AP2PlaceCameraTag ) :
print ( " WARNING: Unhandled PLACE_CAMERA tag! " )
# Didn't place a new clip.
return None , False
2021-04-16 01:18:33 +02:00
else :
raise Exception ( f " Failed to process tag: { tag } " )
2021-05-16 21:40:06 +02:00
def __render_object (
self ,
img : Image . Image ,
renderable : PlacedObject ,
parent_transform : Matrix ,
only_depths : Optional [ List [ int ] ] = None ,
prefix : str = " " ,
) - > Image . Image :
2021-05-16 21:39:25 +02:00
self . vprint ( f " { prefix } Rendering placed object ID { renderable . object_id } from sprite { renderable . source . tag_id } onto Depth { renderable . depth } " )
2021-05-11 00:26:26 +02:00
# Compute the affine transformation matrix for this object.
2021-05-21 23:32:02 +02:00
transform = renderable . transform . multiply ( parent_transform ) . translate ( Point . identity ( ) . subtract ( renderable . rotation_offset ) )
2021-04-16 01:18:33 +02:00
2021-04-20 23:41:28 +02:00
# Render individual shapes if this is a sprite.
2021-05-11 00:26:26 +02:00
if isinstance ( renderable , PlacedClip ) :
2021-05-22 03:30:28 +02:00
# This is a sprite placement reference. Make sure that we render lower depths
# first, but preserved placed order as well.
depths = set ( obj . depth for obj in renderable . placed_objects )
for depth in sorted ( depths ) :
for obj in renderable . placed_objects :
if obj . depth != depth :
continue
img = self . __render_object ( img , obj , transform , only_depths = only_depths , prefix = prefix + " " )
2021-05-11 00:26:26 +02:00
elif isinstance ( renderable , PlacedShape ) :
# This is a shape draw reference.
shape = renderable . source
# Calculate add color if it is present.
add_color = ( renderable . add_color or Color ( 0.0 , 0.0 , 0.0 , 0.0 ) ) . as_tuple ( )
mult_color = renderable . mult_color or Color ( 1.0 , 1.0 , 1.0 , 1.0 )
blend = renderable . blend or 0
# Now, render out shapes.
for params in shape . draw_params :
if not ( params . flags & 0x1 ) :
# Not instantiable, don't render.
2021-05-16 17:15:06 +02:00
return img
2021-05-16 21:40:06 +02:00
if only_depths is not None and renderable . depth not in only_depths :
# Not on the correct depth plane.
return img
2021-05-11 00:26:26 +02:00
if params . flags & 0x4 :
# TODO: Need to support blending and UV coordinate colors here.
print ( " WARNING: Unhandled UV coordinate color! " )
texture = None
if params . flags & 0x2 :
# We need to look up the texture for this.
if params . region not in self . textures :
raise Exception ( f " Cannot find texture reference { params . region } ! " )
texture = self . textures [ params . region ]
2021-05-21 18:58:39 +02:00
if params . flags & 0x8 :
# TODO: This texture gets further blended somehow? Not sure this is ever used.
print ( f " WARNING: Unhandled texture blend color { params . blend } " )
elif params . flags & 0x8 :
if shape . rectangle is None :
# This is a raw rectangle. Its possible that the number of vertex points is
# not 4, or that the four points in the vertex_points aren't the four corners
# of a rectangle, but let's assume that doesn't happen for now.
x_points = set ( p . x for p in shape . vertex_points )
y_points = set ( p . y for p in shape . vertex_points )
left = min ( x_points )
right = max ( x_points )
top = min ( y_points )
bottom = max ( y_points )
shape . rectangle = Image . new ( ' RGBA ' , ( int ( right - left ) , int ( bottom - top ) ) , ( params . blend . as_tuple ( ) ) )
texture = shape . rectangle
2021-05-11 00:26:26 +02:00
2021-05-21 18:58:39 +02:00
if texture is not None :
2021-05-11 00:26:26 +02:00
# See if we can cheat and use the faster blitting method.
if (
add_color == ( 0 , 0 , 0 , 0 ) and
mult_color . r == 1.0 and
mult_color . g == 1.0 and
mult_color . b == 1.0 and
mult_color . a == 1.0 and
transform . b == 0.0 and
transform . c == 0.0 and
transform . a == 1.0 and
transform . d == 1.0 and
2021-05-16 02:18:59 +02:00
( blend == 0 or blend == 2 )
2021-05-11 00:26:26 +02:00
) :
# We can!
2021-05-21 23:32:02 +02:00
cutin = transform . multiply_point ( Point . identity ( ) )
2021-05-11 00:26:26 +02:00
cutoff = Point . identity ( )
if cutin . x < 0 :
cutoff . x = - cutin . x
cutin . x = 0
if cutin . y < 0 :
cutoff . y = - cutin . y
cutin . y = 0
img . alpha_composite ( texture , cutin . as_tuple ( ) , cutoff . as_tuple ( ) )
else :
2021-05-16 02:20:23 +02:00
# We can't, so do the slow render that's correct.
2021-05-21 23:32:02 +02:00
img = affine_composite ( img , add_color , mult_color , transform , blend , texture , single_threaded = self . __single_threaded )
2021-05-21 18:58:01 +02:00
elif isinstance ( renderable , PlacedDummy ) :
# Nothing to do!
pass
2021-05-11 00:26:26 +02:00
else :
raise Exception ( f " Unknown placed object type to render { renderable } ! " )
2021-04-18 01:31:36 +02:00
2021-05-16 17:15:06 +02:00
return img
2021-05-11 00:26:26 +02:00
def __process_tags ( self , clip : PlacedClip , prefix : str = " " ) - > bool :
self . vprint ( f " { prefix } Handling placed clip { clip . object_id } at depth { clip . depth } " )
# Track whether anything in ourselves or our children changes during this processing.
changed = False
# Clips that are part of our own placed objects which we should handle.
child_clips = [ c for c in clip . placed_objects if isinstance ( c , PlacedClip ) ]
# Execute each tag in the frame.
if not clip . finished :
frame = clip . source . frames [ clip . frame ]
tags = clip . source . tags [ frame . start_tag_offset : ( frame . start_tag_offset + frame . num_tags ) ]
for tagno , tag in enumerate ( tags ) :
# Perform the action of this tag.
self . vprint ( f " { prefix } Sprite Tag ID: { clip . source . tag_id } , Current Tag: { frame . start_tag_offset + tagno } , Num Tags: { frame . num_tags } " )
new_clip , clip_changed = self . __place ( tag , clip , prefix = prefix )
changed = changed or clip_changed
# If we create a new movie clip, process it as well for this frame.
if new_clip :
2021-05-15 06:22:17 +02:00
changed = self . __process_tags ( new_clip , prefix = prefix + " " ) or changed
2021-05-11 00:26:26 +02:00
# Now, handle each of the existing clips.
for child in child_clips :
2021-05-15 06:22:17 +02:00
changed = self . __process_tags ( child , prefix = prefix + " " ) or changed
2021-05-11 00:26:26 +02:00
# Now, advance the frame for this clip.
clip . advance ( )
self . vprint ( f " { prefix } Finished handling placed clip { clip . object_id } at depth { clip . depth } " )
# Return if anything was modified.
return changed
2021-05-21 18:58:01 +02:00
def __handle_imports ( self , swf : SWF ) - > Dict [ int , Union [ RegisteredShape , RegisteredClip , RegisteredDummy ] ] :
external_objects : Dict [ int , Union [ RegisteredShape , RegisteredClip , RegisteredDummy ] ] = { }
# Go through, recursively resolve imports for all SWF files.
for tag_id , imp in swf . imported_tags . items ( ) :
for name , other in self . swfs . items ( ) :
if other . exported_name == imp . swf :
# This SWF should have the tag reference.
if imp . tag not in other . exported_tags :
print ( f " WARNING: { swf . exported_name } imports { imp } but that import is not in { other . exported_name } ! " )
external_objects [ tag_id ] = RegisteredDummy ( tag_id )
break
else :
external_objects [ tag_id ] = self . __find_import ( other , other . exported_tags [ imp . tag ] )
break
else :
print ( f " WARNING: { swf . exported_name } imports { imp } but that SWF is not in our library! " )
external_objects [ tag_id ] = RegisteredDummy ( tag_id )
# Fix up tag IDs to point at our local definition of them.
for tid in external_objects :
external_objects [ tid ] . tag_id = tid
# Return our newly populated registered object table containing all imports!
return external_objects
def __find_import ( self , swf : SWF , tag_id : int ) - > Union [ RegisteredShape , RegisteredClip , RegisteredDummy ] :
if tag_id in swf . imported_tags :
external_objects = self . __handle_imports ( swf )
if tag_id not in external_objects :
raise Exception ( f " Logic error, tag ID { tag_id } is an export for { swf . exported_name } but we didn ' t populate it! " )
return external_objects [ tag_id ]
# We need to do a basic placement to find the registered object so we can return it.
root_clip = RegisteredClip (
None ,
swf . frames ,
swf . tags ,
)
tag = self . __find_tag ( root_clip , tag_id )
if tag is None :
print ( f " WARNING: { swf . exported_name } exports { swf . imported_tags [ tag_id ] } but does not manifest an object! " )
return RegisteredDummy ( tag_id )
return tag
def __find_tag ( self , clip : RegisteredClip , tag_id : int ) - > Optional [ Union [ RegisteredShape , RegisteredClip , RegisteredDummy ] ] :
# Fake-execute this clip to find the tag we need to manifest.
for frame in clip . frames :
tags = clip . tags [ frame . start_tag_offset : ( frame . start_tag_offset + frame . num_tags ) ]
for tagno , tag in enumerate ( tags ) :
# Attempt to place any tags.
if isinstance ( tag , AP2ShapeTag ) :
if tag . id == tag_id :
# We need to be able to see this shape to place it.
if tag . reference not in self . shapes :
raise Exception ( f " Cannot find shape reference { tag . reference } ! " )
# This matched, so this is the import.
return RegisteredShape (
tag . id ,
self . shapes [ tag . reference ] . vertex_points ,
self . shapes [ tag . reference ] . tex_points ,
self . shapes [ tag . reference ] . tex_colors ,
self . shapes [ tag . reference ] . draw_params ,
)
elif isinstance ( tag , AP2DefineSpriteTag ) :
new_clip = RegisteredClip ( tag . id , tag . frames , tag . tags )
if tag . id == tag_id :
# This matched, so it is the clip that we want to export.
return new_clip
# Recursively look in this as well.
maybe_tag = self . __find_tag ( new_clip , tag_id )
if maybe_tag is not None :
return maybe_tag
# We didn't find the tag we were after.
return None
2021-05-21 23:31:13 +02:00
def __render ( self , swf : SWF , only_depths : Optional [ List [ int ] ] , movie_transform : Matrix ) - > Tuple [ int , List [ Image . Image ] ] :
2021-05-21 18:58:01 +02:00
# First, let's attempt to resolve imports.
self . __registered_objects = self . __handle_imports ( swf )
2021-05-21 23:31:13 +02:00
# Initialize overall frame advancement stuff.
2021-04-16 01:18:33 +02:00
spf = 1.0 / swf . fps
2021-04-18 01:30:46 +02:00
frames : List [ Image . Image ] = [ ]
2021-04-16 01:18:33 +02:00
frameno : int = 0
2021-04-18 01:31:08 +02:00
2021-05-21 23:31:13 +02:00
# Calculate actual size based on given movie transform.
actual_size = movie_transform . multiply_point ( Point ( swf . location . width , swf . location . height ) ) . as_tuple ( )
2021-05-11 00:26:26 +02:00
# Create a root clip for the movie to play.
root_clip = PlacedClip (
- 1 ,
- 1 ,
Point . identity ( ) ,
2021-05-21 23:32:19 +02:00
Matrix . identity ( ) ,
2021-05-11 00:26:26 +02:00
Color ( 1.0 , 1.0 , 1.0 , 1.0 ) ,
Color ( 0.0 , 0.0 , 0.0 , 0.0 ) ,
0 ,
RegisteredClip (
None ,
swf . frames ,
swf . tags ,
) ,
)
2021-04-16 01:18:33 +02:00
2021-05-21 18:58:01 +02:00
# Now play the frames of the root clip.
2021-04-21 02:01:35 +02:00
try :
2021-05-17 00:19:07 +02:00
while not root_clip . finished :
2021-04-21 02:01:35 +02:00
# Create a new image to render into.
2021-05-16 02:18:59 +02:00
time = spf * frameno
2021-04-21 02:01:35 +02:00
color = swf . color or Color ( 0.0 , 0.0 , 0.0 , 0.0 )
2021-05-17 06:04:38 +02:00
self . vprint ( f " Rendering frame { frameno } / { len ( root_clip . source . frames ) } ( { round ( time , 2 ) } s) " )
2021-04-21 02:01:35 +02:00
# Go through all registered clips, place all needed tags.
2021-05-11 00:26:26 +02:00
changed = self . __process_tags ( root_clip )
2021-04-21 02:01:35 +02:00
if changed or frameno == 0 :
2021-05-22 03:30:28 +02:00
# Now, render out the placed objects.
2021-05-21 23:31:13 +02:00
curimage = Image . new ( " RGBA " , actual_size , color = color . as_tuple ( ) )
2021-05-21 23:32:19 +02:00
curimage = self . __render_object ( curimage , root_clip , movie_transform , only_depths = only_depths )
2021-04-21 02:01:35 +02:00
else :
# Nothing changed, make a copy of the previous render.
self . vprint ( " Using previous frame render " )
curimage = frames [ - 1 ] . copy ( )
2021-04-16 01:18:33 +02:00
2021-05-11 00:26:26 +02:00
# Advance our bookkeeping.
2021-04-21 02:01:35 +02:00
frames . append ( curimage )
frameno + = 1
except KeyboardInterrupt :
# Allow ctrl-c to end early and render a partial animation.
2021-05-21 23:31:13 +02:00
print ( f " WARNING: Interrupted early, will render only { len ( frames ) } / { len ( root_clip . source . frames ) } frames of animation! " )
2021-04-18 01:30:46 +02:00
2021-04-16 01:18:33 +02:00
return int ( spf * 1000.0 ) , frames