1
0
mirror of synced 2024-11-27 23:50:47 +01:00

Implement AP2_IMAGE tag support into renderer.

This commit is contained in:
Jennifer Taylor 2021-05-25 02:01:17 +00:00
parent d9a48e150f
commit 089eef1588

View File

@ -2,7 +2,20 @@ from typing import Any, Dict, Generator, List, Set, Tuple, Optional, Union
from PIL import Image # type: ignore
from .blend import affine_composite
from .swf import SWF, Frame, Tag, AP2ShapeTag, AP2DefineSpriteTag, AP2PlaceObjectTag, AP2RemoveObjectTag, AP2DoActionTag, AP2DefineFontTag, AP2DefineEditTextTag, AP2PlaceCameraTag
from .swf import (
SWF,
Frame,
Tag,
AP2ShapeTag,
AP2DefineSpriteTag,
AP2PlaceObjectTag,
AP2RemoveObjectTag,
AP2DoActionTag,
AP2DefineFontTag,
AP2DefineEditTextTag,
AP2PlaceCameraTag,
AP2ImageTag,
)
from .decompile import ByteCode
from .types import Color, Matrix, Point, Rectangle, AP2Trigger, AP2Action, PushAction, StoreRegisterAction, StringConstant, Register, NULL, UNDEFINED, GLOBAL, ROOT, PARENT, THIS, CLIP
from .geo import Shape, DrawParams
@ -14,10 +27,11 @@ class RegisteredClip:
# 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:
def __init__(self, tag_id: Optional[int], frames: List[Frame], tags: List[Tag], labels: Dict[str, int]) -> None:
self.tag_id = tag_id
self.frames = frames
self.tags = tags
self.labels = labels
def __repr__(self) -> str:
return f"RegisteredClip(tag_id={self.tag_id})"
@ -37,6 +51,16 @@ class RegisteredShape:
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})"
class RegisteredImage:
# An image that we should draw directly.
def __init__(self, tag_id: int, reference: str) -> None:
self.tag_id = tag_id
self.reference = reference
def __repr__(self) -> str:
return f"RegisteredImage(tag_id={self.tag_id}, reference={self.reference})"
class RegisteredDummy:
# An imported tag that we could not find.
def __init__(self, tag_id: int) -> None:
@ -65,7 +89,7 @@ class PlacedObject:
self.mask = mask
@property
def source(self) -> Union[RegisteredClip, RegisteredShape, RegisteredDummy]:
def source(self) -> Union[RegisteredClip, RegisteredShape, RegisteredImage, RegisteredDummy]:
raise NotImplementedError("Only implemented in subclass!")
@property
@ -151,27 +175,33 @@ class PlacedClip(PlacedObject):
def __repr__(self) -> str:
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})"
def __resolve_frame(self, frame: Any) -> Optional[int]:
if isinstance(frame, int):
return frame
if isinstance(frame, str):
if frame in self.__source.labels:
return self.__source.labels[frame]
return None
# The following are attributes and functions necessary to support some simple bytecode.
def gotoAndStop(self, frame: Any) -> None:
if not isinstance(frame, int):
# TODO: Technically this should also allow string labels to frames as identified in the
# SWF specification, but we don't support that here.
print(f"WARNING: Non-integer frame {frame} to gotoAndStop function!")
actual_frame = self.__resolve_frame(frame)
if actual_frame is None:
print(f"WARNING: Unrecognized frame {frame} to gotoAndStop function!")
return
if frame <= 0 or frame > len(self.source.frames):
if actual_frame <= 0 or actual_frame > len(self.source.frames):
return
self.requested_frame = frame
self.requested_frame = actual_frame
self.playing = False
def gotoAndPlay(self, frame: Any) -> None:
if not isinstance(frame, int):
# TODO: Technically this should also allow string labels to frames as identified in the
# SWF specification, but we don't support that here.
actual_frame = self.__resolve_frame(frame)
if actual_frame is None:
print(f"WARNING: Non-integer frame {frame} to gotoAndPlay function!")
return
if frame <= 0 or frame > len(self.source.frames):
if actual_frame <= 0 or actual_frame > len(self.source.frames):
return
self.requested_frame = frame
self.requested_frame = actual_frame
self.playing = True
def stop(self) -> None:
@ -196,6 +226,32 @@ class PlacedClip(PlacedObject):
self.requested_frame = val + 1
class PlacedImage(PlacedObject):
# An image that occupies its parent clip at some depth. Placed by an AP2PlaceObjectTag
# referencing an AP2ImageTag.
def __init__(
self,
object_id: int,
depth: int,
rotation_offset: Point,
transform: Matrix,
mult_color: Color,
add_color: Color,
blend: int,
mask: Optional[Mask],
source: RegisteredImage,
) -> None:
super().__init__(object_id, depth, rotation_offset, transform, mult_color, add_color, blend, mask)
self.__source = source
@property
def source(self) -> RegisteredImage:
return self.__source
def __repr__(self) -> str:
return f"PlacedImage(object_id={self.object_id}, depth={self.depth}, source={self.source})"
class PlacedDummy(PlacedObject):
# A reference to an object we can't find because we're missing the import.
def __init__(
@ -275,6 +331,18 @@ class AEPLib:
# I have no idea what this should do, so let's ignore it.
pass
def gotoAndPlay(self, thisptr: Any, frame: Any) -> Any:
# This appears to be a wrapper to allow calling gotoAndPlay on clips.
try:
meth = getattr(thisptr, 'gotoAndPlay')
# Call it, set the return on the stack.
return meth(frame)
except AttributeError:
# Function does not exist!
print(f"WARNING: Tried to call {'gotoAndPlay'}({frame}) on {thisptr} but that method doesn't exist!")
return UNDEFINED
class ASDLib:
def sound_play(self, sound: Any) -> None:
@ -299,7 +367,7 @@ class AFPRenderer(VerboseOutput):
self.swfs: Dict[str, SWF] = swfs
# Internal render parameters.
self.__registered_objects: Dict[int, Union[RegisteredShape, RegisteredClip, RegisteredDummy]] = {}
self.__registered_objects: Dict[int, Union[RegisteredShape, RegisteredClip, RegisteredImage, RegisteredDummy]] = {}
self.__root: Optional[PlacedClip] = None
# List of imports that we provide stub implementations for.
@ -528,8 +596,6 @@ class AFPRenderer(VerboseOutput):
if tag.reference not in self.shapes:
raise Exception(f"Cannot find shape reference {tag.reference}!")
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,
@ -542,14 +608,25 @@ class AFPRenderer(VerboseOutput):
# Didn't place a new clip, didn't change anything.
return None, False
elif isinstance(tag, AP2ImageTag):
self.vprint(f"{prefix} Loading {tag.reference} into object slot {tag.id}")
if tag.reference not in self.textures:
raise Exception(f"Cannot find texture reference {tag.reference}!")
self.__registered_objects[tag.id] = RegisteredImage(
tag.id,
tag.reference,
)
# Didn't place a new clip, didn't change anything.
return None, False
elif isinstance(tag, AP2DefineSpriteTag):
self.vprint(f"{prefix} Loading Sprite into object slot {tag.id}")
if tag.id in self.__registered_objects:
raise Exception(f"Cannot register sprite as object slot {tag.id} is already taken!")
# Register a new clip that we might reference to execute.
self.__registered_objects[tag.id] = RegisteredClip(tag.id, tag.frames, tag.tags)
self.__registered_objects[tag.id] = RegisteredClip(tag.id, tag.frames, tag.tags, tag.labels)
# Didn't place a new clip, didn't change anything.
return None, False
@ -584,6 +661,21 @@ class AFPRenderer(VerboseOutput):
newobj,
)
# Didn't place a new clip, changed the parent clip.
return None, True
elif isinstance(newobj, RegisteredImage):
operating_clip.placed_objects[i] = PlacedImage(
obj.object_id,
obj.depth,
new_rotation_offset,
new_transform,
new_mult_color,
new_add_color,
new_blend,
obj.mask,
newobj,
)
# Didn't place a new clip, changed the parent clip.
return None, True
elif isinstance(newobj, RegisteredClip):
@ -655,6 +747,23 @@ class AFPRenderer(VerboseOutput):
)
)
# Didn't place a new clip, changed the parent clip.
return None, True
elif isinstance(newobj, RegisteredImage):
operating_clip.placed_objects.append(
PlacedImage(
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,
None,
newobj,
)
)
# Didn't place a new clip, changed the parent clip.
return None, True
elif isinstance(newobj, RegisteredClip):
@ -916,6 +1025,14 @@ class AFPRenderer(VerboseOutput):
if texture is not None:
img = affine_composite(img, add_color, mult_color, transform, mask, blend, texture, single_threaded=self.__single_threaded)
elif isinstance(renderable, PlacedImage):
if only_depths is not None and renderable.depth not in only_depths:
# Not on the correct depth plane.
return img
# This is a shape draw reference.
texture = self.textures[renderable.source.reference]
img = affine_composite(img, add_color, mult_color, transform, mask, blend, texture, single_threaded=self.__single_threaded)
elif isinstance(renderable, PlacedDummy):
# Nothing to do!
pass
@ -1016,8 +1133,8 @@ class AFPRenderer(VerboseOutput):
# Return if anything was modified.
return changed
def __handle_imports(self, swf: SWF) -> Dict[int, Union[RegisteredShape, RegisteredClip, RegisteredDummy]]:
external_objects: Dict[int, Union[RegisteredShape, RegisteredClip, RegisteredDummy]] = {}
def __handle_imports(self, swf: SWF) -> Dict[int, Union[RegisteredShape, RegisteredClip, RegisteredImage, RegisteredDummy]]:
external_objects: Dict[int, Union[RegisteredShape, RegisteredClip, RegisteredImage, RegisteredDummy]] = {}
# Go through, recursively resolve imports for all SWF files.
for tag_id, imp in swf.imported_tags.items():
@ -1044,7 +1161,7 @@ class AFPRenderer(VerboseOutput):
# 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]:
def __find_import(self, swf: SWF, tag_id: int) -> Union[RegisteredShape, RegisteredClip, RegisteredImage, RegisteredDummy]:
if tag_id in swf.imported_tags:
external_objects = self.__handle_imports(swf)
if tag_id not in external_objects:
@ -1056,6 +1173,7 @@ class AFPRenderer(VerboseOutput):
None,
swf.frames,
swf.tags,
swf.labels,
)
tag = self.__find_tag(root_clip, tag_id)
@ -1064,7 +1182,7 @@ class AFPRenderer(VerboseOutput):
return RegisteredDummy(tag_id)
return tag
def __find_tag(self, clip: RegisteredClip, tag_id: int) -> Optional[Union[RegisteredShape, RegisteredClip, RegisteredDummy]]:
def __find_tag(self, clip: RegisteredClip, tag_id: int) -> Optional[Union[RegisteredShape, RegisteredClip, RegisteredImage, 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)]
@ -1086,8 +1204,20 @@ class AFPRenderer(VerboseOutput):
self.shapes[tag.reference].draw_params,
)
elif isinstance(tag, AP2ImageTag):
if tag.id == tag_id:
# We need to be able to see this shape to place it.
if tag.reference not in self.textures:
raise Exception(f"Cannot find texture reference {tag.reference}!")
# This matched, so this is the import.
return RegisteredImage(
tag.id,
tag.reference,
)
elif isinstance(tag, AP2DefineSpriteTag):
new_clip = RegisteredClip(tag.id, tag.frames, tag.tags)
new_clip = RegisteredClip(tag.id, tag.frames, tag.tags, tag.labels)
if tag.id == tag_id:
# This matched, so it is the clip that we want to export.
@ -1132,6 +1262,7 @@ class AFPRenderer(VerboseOutput):
None,
swf.frames,
swf.tags,
swf.labels,
),
)
self.__root = root_clip