Source code for scfile.core.encoder
"""
Base class for file format encoders.
Defines the contract for serializing structured content into binary data.
"""
from abc import ABC, abstractmethod
from io import BytesIO
from typing import Generic, Optional, Self, TypeAlias
from scfile.structures.models import Flag
from scfile.structures.models.transforms import SceneTransform
from scfile.types import PathLike
from .base import BaseFile, FileMode, IOStream
from .content import ContentType, ModelContent
from .options import Options
EncoderTransforms: TypeAlias = Optional[list[SceneTransform]]
[docs]
class FileEncoder(BaseFile, Generic[ContentType], ABC):
"""
Base class for encoding structured content into binary data.
Subclasses define the format-specific serialization logic.
"""
transforms: EncoderTransforms = None
"""Format-specific transforms applied to model data before serialization."""
def __init__(
self,
data: ContentType,
options: Optional[Options] = None,
output: Optional[IOStream] = None,
):
"""
Initialize encoder.
Args:
data: Structured content to encode.
options (optional): Shared handlers options.
output (optional): File path or binary IO stream. Defaults to in-memory buffer.
Note:
Data is not written during initialization.
Call :meth:`encode` to perform the actual serialization.
"""
self.data: ContentType = data
self.options: Options = options or Options()
super().__init__(stream=output or BytesIO(), mode="wb+")
[docs]
def encode(
self,
transforms: EncoderTransforms = None,
) -> Self:
"""
Runs encoding pipeline.
Args:
transforms: Override the default transforms for this call.
Returns:
Self (chaining).
"""
self.prelude()
self.transform(transforms=transforms)
self.add_signature()
self.serialize()
return self
[docs]
def prelude(self) -> None:
"""Hook called before transforms, signature and serialization."""
pass
[docs]
def add_signature(self) -> None:
"""Write the format signature to the output stream."""
if self.signature:
self.write(self.signature)
[docs]
@abstractmethod
def serialize(self) -> None:
"""Write ``self.data`` to the output stream. Called by :meth:`encode`."""
...
[docs]
def save_as(
self,
path: PathLike,
mode: FileMode = "wb",
) -> Self:
"""
Write encoded data to file by name. Keeps encoder open.
Args:
path: Output file path.
mode: File mode (binary).
"""
if self.size() == 0:
self.encode()
with open(path, mode=mode) as fp:
fp.write(self.getvalue())
return self
[docs]
def export_as(
self,
path: PathLike,
mode: FileMode = "wb",
) -> Self:
"""
Write encoded data to file by stem. Format suffix appended. Keeps the encoder open.
Args:
path: Output file path.
mode: File mode (binary).
"""
return self.save_as(
path=f"{path}{self.suffix}",
mode=mode,
)
[docs]
def save(
self,
path: PathLike,
mode: FileMode = "wb",
) -> None:
"""
Write encoded data to file by name. Closes encoder.
Args:
path: Output file path.
mode: File mode (binary).
"""
self.save_as(path=path, mode=mode)
self.close()
[docs]
def export(
self,
path: PathLike,
mode: FileMode = "wb",
) -> None:
"""
Write encoded data to file by stem. Format suffix appended. Closes encoder.
Args:
path: Output file path.
mode: File mode (binary).
"""
self.save(path=f"{path}{self.suffix}", mode=mode)
self.close()
[docs]
def getvalue(self) -> bytes:
if self.size() == 0:
self.encode()
return super().getvalue()
@property
def _skeleton_presented(self) -> bool:
if isinstance(self.data, ModelContent):
return self.data.flags.get(Flag.SKELETON, False) and self.options.skeleton
return False
@property
def _animation_presented(self) -> bool:
if isinstance(self.data, ModelContent):
return self._skeleton_presented and self.options.animation
return False