Files
swf-convertion/swf_inspector.py
T
2026-03-24 18:11:39 -06:00

195 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
SWF Inspector - Reads SWF binary headers to extract metadata without
running the file.
Parses: SWF version, frame rate, frame count, dimensions, compression type.
Reference: https://open-flash.github.io/mirrors/swf-spec-19.pdf
"""
import logging
import struct
import zlib
from pathlib import Path
from typing import Optional
log = logging.getLogger("swf_inspector")
# SWF signatures
SIG_UNCOMPRESSED = b"FWS"
SIG_ZLIB = b"CWS"
SIG_LZMA = b"ZWS"
class SWFInspector:
"""
Reads SWF file headers and returns useful metadata.
Usage
-----
info = SWFInspector().inspect(Path("my.swf"))
print(info)
# {'version': 8, 'compression': 'none', 'fps': 24.0,
# 'frame_count': 300, 'width': 550, 'height': 400}
"""
def inspect(self, swf_path: Path) -> dict:
"""Return a dict of metadata about the SWF file."""
result = {
"path": str(swf_path),
"size_bytes": swf_path.stat().st_size if swf_path.exists() else None,
"version": None,
"compression": None,
"fps": None,
"frame_count": None,
"width": None,
"height": None,
"estimated_duration_seconds": None,
"error": None,
}
if not swf_path.exists():
result["error"] = "File not found"
return result
try:
with open(swf_path, "rb") as f:
header = f.read(8)
if len(header) < 8:
result["error"] = "File too small to be a valid SWF"
return result
sig = header[:3]
version = header[3]
file_length = struct.unpack_from("<I", header, 4)[0]
result["version"] = version
result["file_length"] = file_length
if sig == SIG_UNCOMPRESSED:
result["compression"] = "none"
self._parse_body(swf_path, result, offset=8)
elif sig == SIG_ZLIB:
result["compression"] = "zlib"
self._parse_zlib_body(swf_path, result)
elif sig == SIG_LZMA:
result["compression"] = "lzma"
result["error"] = "LZMA-compressed SWF: metadata extraction limited (version 13+)"
# Still try to get basic info from what's available
else:
result["error"] = f"Unknown SWF signature: {sig!r}"
except Exception as e:
result["error"] = str(e)
log.debug(f"SWF inspection error for {swf_path}: {e}", exc_info=True)
# Compute estimated duration
if result["fps"] and result["frame_count"]:
result["estimated_duration_seconds"] = round(
result["frame_count"] / result["fps"], 2
)
return result
def _parse_body(self, swf_path: Path, result: dict, offset: int, data: bytes = None):
"""Parse the uncompressed body of a SWF starting at `offset`."""
try:
if data is None:
with open(swf_path, "rb") as f:
f.seek(offset)
data = f.read(64) # Only need first ~32 bytes
pos = 0
# RECT structure: Nbits (5 bits) then 4 values each Nbits wide
if len(data) < 4:
return
nbits = (data[pos] >> 3) & 0x1F # top 5 bits of first byte
rect_bits = 5 + 4 * nbits
rect_bytes = (rect_bits + 7) // 8
if len(data) < rect_bytes + 4:
return
# Parse RECT fields (Xmin, Xmax, Ymin, Ymax) in twips (1/20 px)
bits = int.from_bytes(data[pos : pos + rect_bytes], "big")
total_bits = rect_bytes * 8
shift = total_bits - 5 - nbits
xmin = self._signed_bits(bits >> shift, nbits)
shift -= nbits
xmax = self._signed_bits(bits >> shift, nbits)
shift -= nbits
ymin = self._signed_bits(bits >> shift, nbits)
shift -= nbits
ymax = self._signed_bits(bits >> shift, nbits)
result["width"] = round((xmax - xmin) / 20)
result["height"] = round((ymax - ymin) / 20)
pos += rect_bytes
# FrameRate: FIXED8 (8.8 fixed point, little-endian)
if pos + 2 > len(data):
return
frame_rate_raw = struct.unpack_from("<H", data, pos)[0]
result["fps"] = round(frame_rate_raw / 256.0, 2)
pos += 2
# FrameCount: UI16
if pos + 2 > len(data):
return
result["frame_count"] = struct.unpack_from("<H", data, pos)[0]
except Exception as e:
log.debug(f"Body parse error: {e}")
def _parse_zlib_body(self, swf_path: Path, result: dict):
"""Decompress zlib SWF body and parse it."""
try:
with open(swf_path, "rb") as f:
f.seek(8) # skip signature + version + file_length
compressed = f.read()
decompressed = zlib.decompress(compressed)
self._parse_body(swf_path, result, offset=0, data=decompressed)
except zlib.error as e:
result["error"] = f"zlib decompression failed: {e}"
except Exception as e:
result["error"] = str(e)
@staticmethod
def _signed_bits(value: int, nbits: int) -> int:
"""Convert an unsigned integer to signed given bit width."""
value &= (1 << nbits) - 1
if value >= (1 << (nbits - 1)):
value -= (1 << nbits)
return value
def inspect_many(self, paths: list) -> dict:
"""Inspect multiple SWF files, returning {filename: info} dict."""
return {p.name: self.inspect(p) for p in paths}
def print_report(self, swf_path: Path):
"""Pretty-print inspection results for a single SWF."""
info = self.inspect(swf_path)
print(f"\n{'='*50}")
print(f"SWF: {info['path']}")
print(f"{'='*50}")
if info.get("error"):
print(f" ⚠ Error: {info['error']}")
print(f" Version : {info['version']}")
print(f" Compression: {info['compression']}")
print(f" Dimensions : {info['width']} × {info['height']} px")
print(f" FPS : {info['fps']}")
print(f" Frames : {info['frame_count']}")
print(f" Est. length: {info['estimated_duration_seconds']}s")
print(f" File size : {info['size_bytes']:,} bytes" if info['size_bytes'] else " File size : N/A")
print()
if __name__ == "__main__":
import sys
inspector = SWFInspector()
for path in sys.argv[1:]:
inspector.print_report(Path(path))