#!/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("> 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(" len(data): return result["frame_count"] = struct.unpack_from(" 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))