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 transform( self, transforms: EncoderTransforms = None, ): """Apply format-specific transforms to model data.""" transforms = transforms or self.transforms if transforms and isinstance(self.data, ModelContent): scene = self.data.scene for tr in transforms: scene = tr(scene) self.data.scene = scene
[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