Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion scratchattach/editor/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,19 @@

@dataclass(init=True, repr=True)
class AssetFile:
"""
Represents the file information for an asset
- stores the filename, data, and md5 hash
"""
filename: str
_data: bytes = field(repr=False, default=None)
_md5: str = field(repr=False, default=None)

@property
def data(self):
"""
Return the contents of the asset file, as bytes
"""
if self._data is None:
# Download and cache
rq = requests.get(f"https://assets.scratch.mit.edu/internalapi/asset/{self.filename}/get/")
Expand All @@ -28,6 +35,9 @@ def data(self):

@property
def md5(self):
"""
Compute/retrieve the md5 hash value of the asset file data
"""
if self._md5 is None:
self._md5 = md5(self.data).hexdigest()

Expand Down Expand Up @@ -60,29 +70,50 @@ def __repr__(self):

@property
def folder(self):
"""
Get the folder name of this asset, based on the asset name. Uses the turbowarp syntax
"""
return commons.get_folder_name(self.name)

@property
def name_nfldr(self):
"""
Get the asset name after removing the folder name
"""
return commons.get_name_nofldr(self.name)

@property
def file_name(self):
"""
Get the exact file name, as it would be within an sb3 file
equivalent to the md5ext value using in scratch project JSON
"""
return f"{self.id}.{self.data_format}"

@property
def md5ext(self):
"""
Get the exact file name, as it would be within an sb3 file
equivalent to the md5ext value using in scratch project JSON
"""
return self.file_name

@property
def parent(self):
"""
Return the project that this asset is attached to. If there is no attached project,
try returning the attached sprite
"""
if self.project is None:
return self.sprite
else:
return self.project

@property
def asset_file(self) -> AssetFile:
"""
Get the associated asset file object for this asset object
"""
for asset_file in self.parent.asset_data:
if asset_file.filename == self.file_name:
return asset_file
Expand All @@ -94,6 +125,9 @@ def asset_file(self) -> AssetFile:

@staticmethod
def from_json(data: dict):
"""
Load asset data from project.json
"""
_name = data.get("name")
_file_name = data.get("md5ext")
if _file_name is None:
Expand All @@ -105,6 +139,9 @@ def from_json(data: dict):
return Asset(_name, _file_name)

def to_json(self) -> dict:
"""
Convert asset data to project.json format
"""
return {
"name": self.name,

Expand All @@ -113,6 +150,7 @@ def to_json(self) -> dict:
"dataFormat": self.data_format,
}

# todo: implement below:
"""
@staticmethod
def from_file(fp: str, name: str = None):
Expand All @@ -134,7 +172,7 @@ def __init__(self,
rotation_center_y: int | float = 50,
_sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT):
"""
A costume. An asset with additional properties
A costume (image). An asset with additional properties
https://en.scratch-wiki.info/wiki/Scratch_File_Format#Costumes
"""
super().__init__(name, file_name, _sprite)
Expand All @@ -145,6 +183,9 @@ def __init__(self,

@staticmethod
def from_json(data):
"""
Load costume data from project.json
"""
_asset_load = Asset.from_json(data)

bitmap_resolution = data.get("bitmapResolution")
Expand All @@ -156,6 +197,9 @@ def from_json(data):
bitmap_resolution, rotation_center_x, rotation_center_y)

def to_json(self) -> dict:
"""
Convert costume to project.json format
"""
_json = super().to_json()
_json.update({
"bitmapResolution": self.bitmap_resolution,
Expand Down Expand Up @@ -184,13 +228,19 @@ def __init__(self,

@staticmethod
def from_json(data):
"""
Load sound from project.json
"""
_asset_load = Asset.from_json(data)

rate = data.get("rate")
sample_count = data.get("sampleCount")
return Sound(_asset_load.name, _asset_load.file_name, rate, sample_count)

def to_json(self) -> dict:
"""
Convert Sound to project.json format
"""
_json = super().to_json()
commons.noneless_update(_json, {
"rate": self.rate,
Expand Down
60 changes: 47 additions & 13 deletions scratchattach/editor/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@


class Base(ABC):
"""
Abstract base class for most sa.editor classes. Implements copy functions
"""
def dcopy(self):
"""
:return: A **deep** copy of self
Expand All @@ -31,6 +34,10 @@ def copy(self):


class JSONSerializable(Base, ABC):
"""
'Interface' for to_json() and from_json() methods
Also implements save_json() using to_json()
"""
@staticmethod
@abstractmethod
def from_json(data: dict | list | Any):
Expand All @@ -41,12 +48,19 @@ def to_json(self) -> dict | list | Any:
pass

def save_json(self, name: str = ''):
"""
Save a json file
"""
data = self.to_json()
with open(f"{self.__class__.__name__.lower()}{name}.json", "w") as f:
json.dump(data, f)


class JSONExtractable(JSONSerializable, ABC):
"""
Interface for objects that can be loaded from zip archives containing json files (sprite/project)
Only has one method - load_json
"""
@staticmethod
@abstractmethod
def load_json(data: str | bytes | TextIOWrapper | BinaryIO, load_assets: bool = True, _name: Optional[str] = None) -> tuple[
Expand All @@ -62,35 +76,35 @@ def load_json(data: str | bytes | TextIOWrapper | BinaryIO, load_assets: bool =


class ProjectSubcomponent(JSONSerializable, ABC):
"""
Base class for any class with an associated project
"""
def __init__(self, _project: Optional[project.Project] = None):
self.project = _project


class SpriteSubComponent(JSONSerializable, ABC):
"""
Base class for any class with an associated sprite
"""
def __init__(self, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT):
if _sprite is build_defaulting.SPRITE_DEFAULT:
_sprite = build_defaulting.current_sprite()

self.sprite = _sprite

# @property
# def sprite(self):
# if self._sprite is None:
# print("ok, ", build_defaulting.current_sprite())
# return build_defaulting.current_sprite()
# else:
# return self._sprite

# @sprite.setter
# def sprite(self, value):
# self._sprite = value

@property
def project(self) -> project.Project:
"""
Get associated project by proxy of the associated sprite
"""
return self.sprite.project


class IDComponent(SpriteSubComponent, ABC):
"""
Base class for classes with an id attribute
"""
def __init__(self, _id: str, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT):
self.id = _id
super().__init__(_sprite)
Expand All @@ -103,7 +117,6 @@ class NamedIDComponent(IDComponent, ABC):
"""
Base class for Variables, Lists and Broadcasts (Name + ID + sprite)
"""

def __init__(self, _id: str, name: str, _sprite: sprite.Sprite = build_defaulting.SPRITE_DEFAULT):
self.name = name
super().__init__(_id, _sprite)
Expand All @@ -113,30 +126,51 @@ def __repr__(self):


class BlockSubComponent(JSONSerializable, ABC):
"""
Base class for classes with associated blocks
"""
def __init__(self, _block: Optional[block.Block] = None):
self.block = _block

@property
def sprite(self) -> sprite.Sprite:
"""
Fetch sprite by proxy of the block
"""
return self.block.sprite

@property
def project(self) -> project.Project:
"""
Fetch project by proxy of the sprite (by proxy of the block)
"""
return self.sprite.project


class MutationSubComponent(JSONSerializable, ABC):
"""
Base class for classes with associated mutations
"""
def __init__(self, _mutation: Optional[mutation.Mutation] = None):
self.mutation = _mutation

@property
def block(self) -> block.Block:
"""
Fetch block by proxy of mutation
"""
return self.mutation.block

@property
def sprite(self) -> sprite.Sprite:
"""
Fetch sprite by proxy of block (by proxy of mutation)
"""
return self.block.sprite

@property
def project(self) -> project.Project:
"""
Fetch project by proxy of sprite (by proxy of block (by proxy of mutation))
"""
return self.sprite.project
Loading