initial commit

This commit is contained in:
2026-03-24 18:11:39 -06:00
commit 352c8e0b73
11 changed files with 2576 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
#!/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))