Initial commit, mostly working with many issues

This commit is contained in:
DataHoarder 2020-09-20 15:16:51 +02:00
commit f968dcf144
34 changed files with 2312 additions and 0 deletions

11
.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
/vendor/
.idea/
/*.ass
/*.jpg
/*.png
/*.zip
/*.mp4
/*.mp3
/*.webm
/*.mkv
/*.swf

17
composer.json Normal file
View file

@ -0,0 +1,17 @@
{
"name": "weebdatahoarder/swf2ass",
"type": "project",
"license": "MIT",
"autoload": {
"psr-4": {
"swf2ass\\": "src/"
}
},
"require": {
"ext-mbstring": "*",
"ext-dom": "*",
"ext-imagick": "*",
"ext-zlib": "*",
"ext-gmp": "*"
}
}

24
composer.lock generated Normal file
View file

@ -0,0 +1,24 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "1a514369d72ef0aef2c5763c81494e78",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"ext-mbstring": "*",
"ext-dom": "*",
"ext-imagick": "*",
"ext-zlib": "*",
"ext-gmp": "*"
},
"platform-dev": [],
"plugin-api-version": "2.0.0"
}

522
image2ass.php Normal file
View file

@ -0,0 +1,522 @@
<?php
require_once __DIR__ . "/vendor/autoload.php";
const SWFMILL_BINARY = "swfmill";
const TWIP_SIZE = 20;
const ADVANCED_MOTION_INTERPOLATION = true;
function normalizePath($path) {
$newPath = [];
$lastCursor = new \swf2ass\Coordinate(0, 0);
foreach ($path as $e) {
/** @var \swf2ass\LineRecord|\swf2ass\MoveRecord $e */
if (!$e->start->equals($lastCursor)) {
$newPath[] = new \swf2ass\MoveRecord($e->start, $lastCursor);
}
$newPath[] = $e;
$lastCursor = $e->coord;
}
return $newPath;
}
/**
* @param \swf2ass\LineRecord[] $path
*/
function cleanupPath(array $path) {
$path = array_values($path);
/** @var \swf2ass\LineRecord[] $newPath */
$newPath = [];
$currentPath = null;
$cornerMap = [];
foreach ($path as $i => $p) {
if (!isset($cornerMap[$p->start->x . "_" . $p->start->y])) {
$cornerMap[$p->start->x . "_" . $p->start->y] = [];
}
$cornerMap[$p->start->x . "_" . $p->start->y][$i] = $p;
if (!isset($cornerMap[$p->coord->x . "_" . $p->coord->y])) {
$cornerMap[$p->coord->x . "_" . $p->coord->y] = [];
}
$cornerMap[$p->coord->x . "_" . $p->coord->y][$i] = $p;
}
$index = 0;
while (count($path) > 0) {
$currentPath = $path[$index++] ?? null;
if ($currentPath !== null) {
$dupes = 0;
foreach ($cornerMap[$currentPath->start->x . "_" . $currentPath->start->y] as $i => $p) {
if ((($currentPath->start->equals($p->start) and $currentPath->coord->equals($p->coord)) or ($currentPath->start->equals($p->coord) and $currentPath->coord->equals($p->start)))) {
unset($path[$i]);
unset($cornerMap[$k1 = $p->start->x . "_" . $p->start->y][$i]);
unset($cornerMap[$k2 = $p->coord->x . "_" . $p->coord->y][$i]);
if ($currentPath !== $p) {
++$dupes;
}
if (count($cornerMap[$k1]) === 0) {
unset($cornerMap[$k1]);
}
if (count($cornerMap[$k2]) === 0) {
unset($cornerMap[$k2]);
}
}
}
if ($dupes === 0) {
$newPath[] = $currentPath;
}
//fwrite(STDERR, "\rdup " . count($newPath) . "/" . count($path) . " ");
}
}
//fwrite(STDERR, "\n");
$cornerMap = [];
foreach ($newPath as $i => $p) {
if (!isset($cornerMap[$p->start->x . "_" . $p->start->y])) {
$cornerMap[$p->start->x . "_" . $p->start->y] = [];
}
$cornerMap[$p->start->x . "_" . $p->start->y][$i] = $p;
if (!isset($cornerMap[$p->coord->x . "_" . $p->coord->y])) {
$cornerMap[$p->coord->x . "_" . $p->coord->y] = [];
}
$cornerMap[$p->coord->x . "_" . $p->coord->y][$i] = $p;
}
$sortedPath = [];
$startPath = null;
$currentVector = null;
$index = 0;
while (count($newPath) > 0) {
$currentPath = $newPath[$index++] ?? null;
if ($currentPath !== null) {
$startPath = $currentPath;
$currentVector = $currentPath->coord->sub($currentPath->start);
$sortedPath[] = $startPath;
unset($newPath[$index - 1]);
unset($cornerMap[$startPath->start->x . "_" . $startPath->start->y][$index - 1]);
unset($cornerMap[$startPath->coord->x . "_" . $startPath->coord->y][$index - 1]);
while (($nextPath = findNextCorner($currentPath->coord, $newPath, $cornerMap)) !== null) {
$nextVector = $nextPath->coord->sub($nextPath->start);
if ($nextVector->equals($currentVector)) { //Enlongate
$currentPath->coord = $nextPath->coord;
continue;
}
$sortedPath[] = $nextPath;
if ($nextPath->coord->equals($nextPath->start)) { //Reached the end!
$startPath = null;
break;
}
$currentPath = $nextPath;
$currentVector = $nextVector;
}
//fwrite(STDERR, "\rsort " . count($sortedPath) . "/" . count($newPath) . " ");
}
}
//fwrite(STDERR, "\n");
return $sortedPath;
}
/**
* @param \swf2ass\Coordinate $corner
* @param \swf2ass\LineRecord[] $path
*
* @return ?\swf2ass\LineRecord
*/
function findNextCorner(\swf2ass\Coordinate $corner, array &$path, array &$cornerMap): ?\swf2ass\LineRecord {
foreach ($cornerMap[$corner->x . "_" . $corner->y] as $i => $p) {
unset($path[$i]);
unset($cornerMap[$p->start->x . "_" . $p->start->y][$i]);
unset($cornerMap[$p->coord->x . "_" . $p->coord->y][$i]);
if ($p->start->equals($corner)) {
return $p;
} else if ($p->coord->equals($corner)) {
return $p->reverse();
}
}
return null;
}
function offsetPath($path, \swf2ass\Coordinate $offset): array {
$newPath = [];
foreach ($path as $e) {
if ($e instanceof \swf2ass\LineRecord) {
$newPath[] = new \swf2ass\LineRecord($e->coord->add($offset), $e->start->add($offset));
} else if ($e instanceof \swf2ass\MoveRecord) {
$newPath[] = new \swf2ass\MoveRecord($e->coord->add($offset), $e->start->add($offset));
}
$newPath[] = $e;
}
return $newPath;
}
function getBoundingBox($path) {
$bb = new \swf2ass\Rectangle(new \swf2ass\Coordinate(PHP_INT_MAX, PHP_INT_MAX), new \swf2ass\Coordinate(PHP_INT_MIN, PHP_INT_MIN));
/*if(count($path) > 0){
$first = reset($path);
if($first instanceof MoveRecord or $first instanceof LineRecord){
$bb->topLeft = $first->coord;
$bb->bottomRight = $first->coord;
}
}*/
foreach ($path as $i => $e) {
/** @var \swf2ass\LineRecord|\swf2ass\MoveRecord $e */
if ($i === 0 and $e instanceof \swf2ass\MoveRecord) {
continue;
}
if (!$bb->inBounds($e->coord)) {
$bb->topLeft->x = min($e->coord->x, $bb->topLeft->x);
$bb->topLeft->y = min($e->coord->y, $bb->topLeft->y);
$bb->bottomRight->x = max($e->coord->x, $bb->bottomRight->x);
$bb->bottomRight->y = max($e->coord->y, $bb->bottomRight->y);
}
if (!$bb->inBounds($e->start)) {
$bb->topLeft->x = min($e->start->x, $bb->topLeft->x);
$bb->topLeft->y = min($e->start->y, $bb->topLeft->y);
$bb->bottomRight->x = max($e->start->x, $bb->bottomRight->x);
$bb->bottomRight->y = max($e->start->y, $bb->bottomRight->y);
}
}
return $bb;
}
function calculateAreaOfPath($path) {
$totalArea = 0;
/** @var \swf2ass\Coordinate[][] $subPolygons */
$subPolygons = [];
/** @var \swf2ass\Coordinate[] $currentPolygon */
$currentPolygon = [];
foreach ($path as $n) {
if ($n instanceof \swf2ass\MoveRecord) {
if (count($currentPolygon) > 0) {
$subPolygons[] = $currentPolygon;
$currentPolygon = [];
}
} else if ($n instanceof \swf2ass\LineRecord) {
if (count($currentPolygon) === 0) {
$currentPolygon[] = $n->start;
}
$currentPolygon[] = $n->coord;
}
}
if (count($currentPolygon) > 0) {
$subPolygons[] = $currentPolygon;
$currentPolygon = [];
}
foreach ($subPolygons as $polygon) {
$NumPoints = count($polygon);
if ($polygon[$NumPoints - 1]->equals($polygon[0])) {
$NumPoints--;
} else {
//Add the first point at the end of the array.
$polygon[$NumPoints] = $polygon[0];
}
if ($NumPoints < 3) {
break;
} else {
$area = 0;
$lastPoint = $polygon[$NumPoints - 1];
foreach ($polygon as $point) {
$area += ($lastPoint->x * $point->y - $lastPoint->y * $point->x);
$lastPoint = $point;
}
$totalArea += ($area / 2.0);
}
}
return $totalArea;
}
/**
* @param \swf2ass\Color[][] $pixelMatrix
* @param int $resX
* @param int $resY
* @param bool $doFullFrameOptimizations
* @param $size
* @return array
*/
function renderFrame(array $pixelMatrix, int $resX, int $resY, bool $doFullFrameOptimizations, &$size): array {
$size = 0;
$colors = [];
$paths = [];
foreach ($pixelMatrix as $y => $row) {
foreach ($row as $x => $color) {
$k = $color->toString(true);
if (!isset($paths[$k])) {
$colors[$k] = $color;
$paths[$k] = [];
}
foreach ((new \swf2ass\Square(new \swf2ass\Coordinate($x, $y), 1))->draw() as $p) {
$paths[$k][] = $p;
}
}
}
$optimizedPaths = [];
foreach ($paths as $k => $path) {
$path = cleanupPath($path);
$path = normalizePath($path);
$optimizedPaths[$k] = $path;
}
if ($doFullFrameOptimizations) {
uasort($optimizedPaths, function ($a, $b) {
return calculateAreaOfPath($a) - calculateAreaOfPath($b);
});
//Largest path
$k0 = array_key_first($optimizedPaths);
$path = $optimizedPaths[$k0];
$optimizedPaths[$k0] = [];
foreach ((new \swf2ass\Rectangle(new \swf2ass\Coordinate(0, 0), new \swf2ass\Coordinate($resX, $resY)))->draw() as $p) {
$optimizedPaths[$k0][] = $p;
}
}
$assLines = [];
foreach ($optimizedPaths as $k => $path) {
$pos = new \swf2ass\Coordinate(0, 0);
$bb = getBoundingBox($path);
$pos = $bb->topLeft;
/*$first = reset($path);
if($first instanceof MoveRecord){
$pos = $first->coord;
unset($path[0]);// = new MoveRecord(new Coordinate(0, 0), $pos);
}*/
if (count($path) <= 1) {
continue;
}
$assLine = "{\\an7\\bord0\\shad0\\pos({$pos->x},{$pos->y})";
$assLine .= $colors[$k]->toString(true) . "\\p1}";
foreach ($path as $edge) {
if ($edge instanceof \swf2ass\MoveRecord) {
$coords = $edge->coord->sub($pos);
$assLine .= "m " . $coords->x . " " . $coords->y . " ";
} else if ($edge instanceof \swf2ass\LineRecord) {
$coords = $edge->coord->sub($pos);
$assLine .= "l " . $coords->x . " " . $coords->y . " ";
}
}
$assLine .= "{\\p0}";
$assLines[] = $assLine;
$size += strlen($assLine);
}
return $assLines;
}
function outputFrame($frame, $endTime) {
$assHeader = "Dialogue: " . $frame[0] . "," . \swf2ass\Utils::timeToStamp($frame[1]) . "," . \swf2ass\Utils::timeToStamp($endTime) . ",f," . $frame[3] . ",0,0,0,,";
foreach ($frame[2] as $line) {
echo $assHeader . $line . PHP_EOL;
}
}
$fps = 30000 / 1001;//30;
$colorN = 2;//64;
$pframeColorDistance = 16;
if (is_dir($argv[1])) {
$frames = glob($argv[1] . "/*.png");
} else {
$frames = [$argv[1]];
}
sort($frames);
/** @var \swf2ass\Color[][] $frameBuffer */
$frameBuffer = null;
$quantizedFrameBuffer = $frameBuffer;
$dynamicPalette = false;
$palette = [];
for ($i = 0; $i < $colorN; ++$i) {
$palette[] = new \swf2ass\Color((int)round($i * (255 / ($colorN - 1))), (int)round($i * (255 / ($colorN - 1))), (int)round($i * (255 / ($colorN - 1))));
}
$frameNumber = 1;
$endFrame = 100;
$lastFrameNumber = null;
$currentFrames = [];
/**
* @param \swf2ass\Color $color
* @param \swf2ass\Color[] $palette
*
* @return \swf2ass\Color
*/
function getClosestColor(\swf2ass\Color $color, array $palette) {
$minColor = null;
$minDistance = null;
foreach ($palette as $c2) {
if (($d = $c2->distance($color)) < $minDistance or $minColor === null) {
$minColor = $c2;
$minDistance = $d;
}
}
return $minColor;
}
foreach ($frames as $frame) {
if (count($frames) === 1 or preg_match("/([0-9]+)\\.png/", $frame, $matches) > 0) {
if (count($frames) === 1) {
$frameNumber = 0;
} else {
$frameNumber = ((int)ltrim($matches[1], "0"));
if ($frameNumber > $endFrame) {
$endFrame = $frameNumber;
}
}
$im = new Imagick();
$im->readImage($frame);
$im->transformImageColorspace(Imagick::COLORSPACE_SRGB);
$resolution = $im->getImageGeometry();
$resX = $resolution["width"];
$resY = $resolution["height"];
$iframe = [];
$quantizedIframe = [];
$pframe = [];
$quantizedPframe = [];
for ($y = 0; $y < $resY; ++$y) {
$iframe[$y] = [];
$quantizedIframe[$y] = [];
$pframe[$y] = [];
$quantizedPframe[$y] = [];
for ($x = 0; $x < $resX; ++$x) {
$pixel = $im->getImagePixelColor($x, $y);
$c = $pixel->getColor(true);
$color = new \swf2ass\Color((int)round($c["r"] * 255), (int)round($c["g"] * 255), (int)round($c["b"] * 255), isset($c["a"]) ? (int)round((1 - $c["a"]) * 255) : null);
$iframe[$y][$x] = $color;
if ($frameBuffer !== null and $frameBuffer[$y][$x]->distance($color) > $pframeColorDistance) {
$pframe[$y][$x] = $color;
$frameBuffer[$y][$x] = $pframe[$y][$x];
}
}
}
if ($dynamicPalette) {
$im->quantizeImage($colorN, Imagick::COLORSPACE_SRGB, 0, false, true);
}
foreach ($iframe as $y => $row) {
foreach ($row as $x => $color) {
$pixel = $im->getImagePixelColor($x, $y);
$c = $pixel->getColor(true);
$color = new \swf2ass\Color((int)round($c["r"] * 255), (int)round($c["g"] * 255), (int)round($c["b"] * 255), isset($c["a"]) ? (int)round((1 - $c["a"]) * 255) : null);
if (!$dynamicPalette) {
$color = getClosestColor($color, $palette);
}
$quantizedIframe[$y][$x] = $color;
if (isset($pframe[$y][$x]) and $quantizedFrameBuffer[$y][$x]->distance($color) > $pframeColorDistance) {
$quantizedPframe[$y][$x] = $color;
$quantizedFrameBuffer[$y][$x] = $color;
}
}
}
if ($lastFrameNumber === null) {
$headerFps = $fps * 2;
echo <<<ASSHEADER
[Script Info]
; Script generated by Aegisub 3.2.2
; http://www.aegisub.org/
ScriptType: v4.00+
WrapStyle: 0
ScaledBorderAndShadow: yes
YCbCr Matrix: TV.709
PlayResX: $resX
PlayResY: $resY
[Aegisub Project Garbage]
Last Style Storage: Default
Video File: ?dummy:$headerFps:10000:$resX:$resY:160:160:160:c
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: f,Arial,1,&H00FFFFFF,&H000000FF,&H000000FF,&H000000FF,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
ASSHEADER;
}
fwrite(STDERR, "=== frame $frameNumber\n");
$iframeSize = "---";
$pframeRender = renderFrame($quantizedPframe, $resX, $resY, false, $pframeSize);
if ($frameBuffer === null or $pframeSize > 0) {
$iframeRender = renderFrame($quantizedIframe, $resX, $resY, true, $iframeSize);
}
if ($frameBuffer === null or ($pframeSize > 0 and $iframeSize <= $pframeSize)) {
fwrite(STDERR, " | IS IFRAME $frameNumber |$iframeSize|$pframeSize\n");
$frameBuffer = $iframe;
$quantizedFrameBuffer = $quantizedIframe;
foreach ($currentFrames as $f) {
outputFrame($f, $frameNumber * (1 / $fps));
}
$currentFrames = [];
$currentFrames[] = [$frameNumber, $frameNumber * (1 / $fps), $iframeRender, "i"];
} else if ($pframeSize > 0) {
fwrite(STDERR, " | IS PFRAME $frameNumber |$iframeSize|$pframeSize\n");
$currentFrames[] = [$frameNumber, $frameNumber * (1 / $fps), $pframeRender, "p"];
} else {
fwrite(STDERR, " | IS DROP $frameNumber |$iframeSize|$pframeSize\n");
}
$lastFrameNumber = $frameNumber;
}
}
foreach ($currentFrames as $f) {
outputFrame($f, $endFrame * (1 / $fps));
}

66
src/BitmapDefinition.php Normal file
View file

@ -0,0 +1,66 @@
<?php
namespace swf2ass;
class BitmapDefinition implements ObjectDefinition {
public $id;
public Coordinate $size;
/** @var Color[][] */
public $pixels;
public function __construct($id, Coordinate $size, array $pixels) {
$this->id = $id;
$this->size = $size;
$this->pixels = $pixels;
}
static function fromXML(\DOMElement $element): BitmapDefinition {
//Utils::dump_element($element);
$size = new Coordinate((int)$element->getAttribute("width"), (int)$element->getAttribute("height"));
$pixels = array_fill(0, $size->y, array_fill(0, $size->x, new Color(0, 0, 0)));
$format = (int)$element->getAttribute("format");
switch ($format) {
default:
throw new \Exception("Not supported format $format");
break;
//ALPHABITMAPDATA
case 5:
$content = zlib_decode(base64_decode(trim($element->textContent)));
$i = 0;
for ($y = 0; $y < $size->y; ++$y) {
for ($x = 0; $x < $size->x; ++$x) {
$pixel = unpack("Ca/Cr/Cg/Cb", substr($content, $i * 4, 4));
$pixels[$y][$x] = new Color($pixel["r"], $pixel["g"], $pixel["b"], $pixel["a"] !== 255 ? $pixel["a"] : null);
$i++;
}
}
break;
}
return new BitmapDefinition($element->getAttribute("objectID"), $size, $pixels);
}
public function getPixel(Coordinate $c) {
return $this->pixels[$c->y][$c->x];
}
public function toPNG() {
$im = new \Imagick();
$im->newImage($this->size->x, $this->size->y, new \ImagickPixel("#000000ff"));
$im->setImageFormat("png");
$iterator = $im->getPixelIterator();
foreach ($iterator as $row => $pixels) {
foreach ($pixels as $col => $pixel) {
$c = $this->getPixel(new Coordinate($col, $row));
$colorStr = "#" . bin2hex(chr($c->r)) . bin2hex(chr($c->g)) . bin2hex(chr($c->b)) . bin2hex(chr(255 - ($c->a ?? 0)));
$pixel->setColor($colorStr);
}
$iterator->syncIterator();
}
return (string)$im;
}
}

39
src/Color.php Normal file
View file

@ -0,0 +1,39 @@
<?php
namespace swf2ass;
class Color {
public $r;
public $g;
public $b;
public $alpha;
public function __construct($r, $g, $b, $alpha = null) {
$this->r = $r;
$this->g = $g;
$this->b = $b;
$this->alpha = $alpha;
}
public function distance(Color $color): float {
return sqrt(pow($color->r - $this->r, 2) + pow($color->g - $this->g, 2) + pow($color->b - $this->b, 2));
}
public function toString($nullAlpha = false) {
$c = "\\1c&H" . strtoupper(Utils::padHex(dechex($this->b ?? 0))) . strtoupper(Utils::padHex(dechex($this->g ?? 0))) . strtoupper(Utils::padHex(dechex($this->r ?? 0))) . "&";
if (($this->alpha === null or $this->alpha === 0) and $nullAlpha) {
} else {
$c .= "\\1a&H" . strtoupper(Utils::padHex(dechex($this->alpha ?? 0))) . "&";
}
return $c;
}
public function applyTransform(ColorTransform $transform): Color {
return new Color(max(0, min((($this->r * ($transform->mult->r ?? 256)) / 256) + ($transform->add->r ?? 0), 255)), max(0, min((($this->g * ($transform->mult->g ?? 256)) / 256) + ($transform->add->g ?? 0), 255)), max(0, min((($this->b * ($transform->mult->b ?? 256)) / 256) + ($transform->add->b ?? 0), 255)), max(0, min((($this->alpha * ($transform->mult->alpha ?? 256)) / 256) + ($transform->add->alpha ?? 0), 255)),);
}
public static function fromXML(\DOMElement $element): Color {
return new Color($element->getAttribute("red"), $element->getAttribute("green"), $element->getAttribute("blue"), $element->hasAttribute("alpha") ? $element->getAttribute("alpha") : null);
}
}

43
src/ColorTransform.php Normal file
View file

@ -0,0 +1,43 @@
<?php
namespace swf2ass;
class ColorTransform {
public $mult;
public $add;
public function __construct(Color $mult = null, Color $add = null) {
$this->mult = $mult;
$this->add = $add;
}
public function applyToStyleContainer(StyleContainer $styles): StyleContainer {
if ($styles->original_color1 instanceof Color) {
$styles->color1 = $styles->original_color1->applyTransform($this);
}
if ($styles->original_color3 instanceof Color) {
//$styles->color3 = $styles->original_color3->applyTransform($this);
}
return $styles;
}
public static function fromXML(\DOMElement $element): ColorTransform {
$add = new Color(null, null, null, null);
$mult = new Color(null, null, null, null);
if ($element->hasAttribute("factorRed")) {
$mult->r = (int)$element->getAttribute("factorRed");
$mult->g = (int)$element->getAttribute("factorGreen");
$mult->b = (int)$element->getAttribute("factorBlue");
$mult->alpha = (int)$element->getAttribute("factorAlpha");
}
if ($element->hasAttribute("offsetRed")) {
$add->r = (int)$element->getAttribute("offsetRed");
$add->g = (int)$element->getAttribute("offsetGreen");
$add->b = (int)$element->getAttribute("offsetBlue");
$add->alpha = (int)$element->getAttribute("offsetAlpha");
}
return new ColorTransform($mult, $add);
}
}

7
src/ComplexShape.php Normal file
View file

@ -0,0 +1,7 @@
<?php
namespace swf2ass;
interface ComplexShape {
public function draw();
}

42
src/Coordinate.php Normal file
View file

@ -0,0 +1,42 @@
<?php
namespace swf2ass;
class Coordinate {
public $x;
public $y;
public function __construct($x, $y) {
$this->x = $x;
$this->y = $y;
}
public function equals(Coordinate $b): bool {
return $b->x === $this->x and $b->y === $this->y;
}
public function distance(Coordinate $b): float {
return sqrt(pow($this->x - $b->x, 2) + pow($this->y - $b->y, 2));
}
public function applyTransform(MatrixTransform $transform): Coordinate {
return new Coordinate($this->x * ($transform->scaleX ?? 1) + $this->y * ($transform->skewY ?? 0) + ($transform->transX ?? 0), $this->y * ($transform->scaleY ?? 1) + $this->x * ($transform->skewX ?? 0) + ($transform->transY ?? 0));
}
public function add(Coordinate $b): Coordinate {
return new Coordinate($this->x + $b->x, $this->y + $b->y);
}
public function sub(Coordinate $b): Coordinate {
return new Coordinate($this->x - $b->x, $this->y - $b->y);
}
public function divide($size): Coordinate {
return new Coordinate($this->x / $size, $this->y / $size);
}
public function toPixel($twipSize = TWIP_SIZE): Coordinate {
return $this->divide($twipSize);
}
}

29
src/CubicCurveRecord.php Normal file
View file

@ -0,0 +1,29 @@
<?php
namespace swf2ass;
class CubicCurveRecord implements Record {
public Coordinate $start;
public Coordinate $control1;
public Coordinate $control2;
public Coordinate $anchor;
public function __construct(Coordinate $control1, Coordinate $control2, Coordinate $anchor, Coordinate $start) {
$this->control1 = $control1;
$this->control2 = $control2;
$this->anchor = $anchor;
$this->start = $start;
}
public function getStart(): Coordinate {
return $this->start;
}
public function reverse(): CubicCurveRecord {
return new CubicCurveRecord($this->control2, $this->control1, $this->start, $this->anchor);
}
public static function fromQuadraticRecord(QuadraticCurveRecord $q): CubicCurveRecord {
return new CubicCurveRecord(new Coordinate(($q->start->x + 2 * $q->control->x) / 3, ($q->start->y + 2 * $q->control->y) / 3), new Coordinate(($q->anchor->x + 2 * $q->control->x) / 3, ($q->anchor->y + 2 * $q->control->y) / 3), $q->anchor, $q->start);
}
}

33
src/CurrentState.php Normal file
View file

@ -0,0 +1,33 @@
<?php
namespace swf2ass;
class CurrentState {
public Color $backgroundColor;
public int $frameRate;
public int $frameCount;
public int $frameNumber;
public Rectangle $viewPort;
/** @var ObjectDefinition[] */
public array $objects;
/** @var DisplayEntry[] */
public array $displayList;
/** @var DisplayEntry[] */
public array $clipList = [];
public function __construct() {
$this->backgroundColor = new Color(255, 255, 255);
$this->frameRate = 60;
$this->frameCount = 100;
$this->frameNumber = 0;
$this->viewPort = new Rectangle(new Coordinate(0, 0), new Coordinate(480, 360));
$this->objects = [];
$this->displayList = [];
$this->clipList = [];
}
}

221
src/DisplayEntry.php Normal file
View file

@ -0,0 +1,221 @@
<?php
namespace swf2ass;
class DisplayEntry {
public $object;
public $currentColorTransform = null;
public $currentTransform = null;
/** @var FrameInformation[] */
public $frames = [];
public $startFrame;
public $depth;
public $clipDepth = 0;
public function getLines(CurrentState $currentState) {
if ($this->clipDepth > 0) {
return [];
}
if ($this->depth != 2) {
//return [];
}
$dialogue = [];
$startTime = $this->startFrame * (1 / $currentState->frameRate);
$endTime = ($this->startFrame + 1) * (1 / $currentState->frameRate);
if ($this->object instanceof ShapeDefinition) {
$shapeDef = $this->object;
//TODO: animation
$currentShapeList = $shapeDef->styles;
foreach ($shapeDef->shapeList->shapes as $shape) {
$currentFrameStyles = [new StyleContainer()];
$edges = $shape->edges;
if ($shape->styleRecord->styleList instanceof StyleList) {
$currentShapeList = $shape->styleRecord->styleList;
}
$fillStyleInside = null;
if ($shape->styleRecord->fillStyleInside > 0) {
$fillStyleInside = $currentShapeList->getFillStyle($shape->styleRecord->fillStyleInside - 1);
}
$lineStyle = null;
if ($shape->styleRecord->lineStyle > 0) {
$lineStyle = $currentShapeList->getLineStyle($shape->styleRecord->lineStyle - 1);
$hasUnopenedPath = false;
$newEdges = [];
$currentSegment = [];
foreach ($edges as $edge) {
if (($edge instanceof MoveRecord) and count($currentSegment) > 0) {
$reversedPath = array_reverse($currentSegment);
$end = $reversedPath[0]->reverse()->start;
$start = $currentSegment[0]->start;
$diff = abs($end->x - $start->x) + abs($end->y - $start->y);
//Close paths that are left open by drawing backwards
if ($diff >= 1) {
foreach ($reversedPath as $node) {
$n = $node->reverse();
$newEdges[] = $n;
}
$hasUnopenedPath = true;
}
$currentSegment = [];
}
$newEdges[] = $edge;
$currentSegment[] = $edge;
}
if (count($currentSegment) > 0) {
$reversedPath = array_reverse($currentSegment);
$end = $reversedPath[0]->reverse()->start;
$start = $currentSegment[0]->start;
$diff = abs($end->x - $start->x) + abs($end->y - $start->y);
//Close paths that are left open by drawing backwards
if ($diff >= 1) {
foreach ($reversedPath as $node) {
$n = $node->reverse();
$newEdges[] = $n;
}
$hasUnopenedPath = true;
}
$currentSegment = [];
}
if ($hasUnopenedPath) {
$edges = $newEdges;
if (end($edges) instanceof MoveRecord) {
array_pop($edges);
}
//Need to half the border for double path
if ($lineStyle instanceof LineStyle) {
$lineStyle = clone $lineStyle;
$lineStyle->width /= 2;
}
}
}
if ($fillStyleInside instanceof LinearGradient) {
$fillStyleInside = $fillStyleInside->colors[1]->color;
}
if ($lineStyle instanceof LineStyle and $lineStyle->item instanceof LinearGradient) {
$oStyle = $lineStyle;
$lineStyle = new LineStyle();
$lineStyle->item = $oStyle->item->colors[1]->color;
$lineStyle->width = $oStyle->width;
}
if ($fillStyleInside !== null) {
$currentFrameStyles[0]->original_color1 = $currentFrameStyles[0]->color1 = $fillStyleInside;
}
if ($lineStyle !== null) {
$currentFrameStyles[0]->original_color3 = $currentFrameStyles[0]->color3 = $lineStyle->item;
$currentFrameStyles[0]->bord = round($lineStyle->width / TWIP_SIZE, 1);
}
$currentFrameStyles[0]->shad = 0;
$assLine = "{\\an7\\shad0\\p1}";
foreach ($edges as $edge) {
if ($edge instanceof MoveRecord) {
$coords = $edge->coord->toPixel();
$assLine .= "m " . round($coords->x, 2) . " " . round($coords->y, 2) . " ";
} else if ($edge instanceof LineRecord) {
$coords = $edge->coord->toPixel();
$assLine .= "l " . round($coords->x, 2) . " " . round($coords->y, 2) . " ";
} else if ($edge instanceof QuadraticCurveRecord) {
$edge = CubicCurveRecord::fromQuadraticRecord($edge);
$control1 = $edge->control1->toPixel();
$control2 = $edge->control2->toPixel();
$anchor = $edge->anchor->toPixel();
$assLine .= "b " . round($control1->x, 2) . " " . round($control1->y, 2) . " " . round($control2->x, 2) . " " . round($control2->y, 2) . " " . round($anchor->x, 2) . " " . round($anchor->y, 2) . " ";
} else if ($edge instanceof CubicCurveRecord) {
$control1 = $edge->control1->toPixel();
$control2 = $edge->control2->toPixel();
$anchor = $edge->anchor->toPixel();
$assLine .= "b " . round($control1->x, 2) . " " . round($control1->y, 2) . " " . round($control2->x, 2) . " " . round($control2->y, 2) . " " . round($anchor->x, 2) . " " . round($anchor->y, 2) . " ";
}
}
$assLine .= "{\\p0}";
$lastLineStart = $startTime;
$lastFrameStart = $this->startFrame;
$af = 0;
$frames = 0;
foreach ($this->frames as $frame => $frameInformation) {
$currentTime = $frame * (1 / $currentState->frameRate);
$endCurrentTime = $currentTime + (1 / $currentState->frameRate);
if ($endCurrentTime > $endTime) {
$endTime = $endCurrentTime;
}
$lastStyle = end($currentFrameStyles);
$frameStyle = clone $lastStyle;
$frameStyle->resetTransformedStyles();
if ($frameInformation->transform instanceof MatrixTransform) {
$frameInformation->transform->applyToStyleContainer($frameStyle);
}
if ($frameInformation->colorTransform instanceof ColorTransform) {
$frameInformation->colorTransform->applyToStyleContainer($frameStyle);
}
if ($frames === 0) {
$currentFrameStyles[array_key_last($currentFrameStyles)] = $frameStyle;
} else if (!$lastStyle->isEqualish($frameStyle)) {
if (ADVANCED_MOTION_INTERPOLATION) {
$transition = $lastStyle->doTransitionTo($frameStyle, $frame, $frame, max(0, $frame - 1), $frame);
} else {
$transition = $lastStyle->doTransitionTo($frameStyle, $frame, $frame, $frame, $frame);
}
if ($transition === false) {
$assHeader = "Dialogue: {$this->depth}," . Utils::timeToStamp($lastLineStart) . "," . Utils::timeToStamp($currentTime) . ",f,objectid:{$shapeDef->id},0,0,0,af:" . ($af++) . ",";
foreach ($currentFrameStyles as $i => $style) {
$assHeader .= "{" . $style->toString($lastFrameStart, (1 / $currentState->frameRate), $currentFrameStyles[$i - 1] ?? null, $i === 0) . "}";
}
$dialogue[] = $assHeader . $assLine;
$lastLineStart = $currentTime;
$lastFrameStart = $frame;
$currentFrameStyles = [$frameStyle];
} else if ($transition instanceof StyleContainer) {
$currentFrameStyles[] = $transition;
}
}
++$frames;
}
$assHeader = "Dialogue: {$this->depth}," . Utils::timeToStamp($lastLineStart) . "," . Utils::timeToStamp($endTime) . ",f,objectid:{$shapeDef->id},0,0,0,af:" . ($af++) . ",";
foreach ($currentFrameStyles as $i => $style) {
$assHeader .= "{" . $style->toString($lastFrameStart, (1 / $currentState->frameRate), $currentFrameStyles[$i - 1] ?? null, $i === 0) . "}";
}
$dialogue[] = $assHeader . $assLine;
}
}
return $dialogue;
}
}

13
src/FrameInformation.php Normal file
View file

@ -0,0 +1,13 @@
<?php
namespace swf2ass;
class FrameInformation {
public int $frame;
/** @var ?ColorTransform */
public ?ColorTransform $colorTransform = null;
/** @var ?MatrixTransform */
public ?MatrixTransform $transform = null;
}

17
src/GradientItem.php Normal file
View file

@ -0,0 +1,17 @@
<?php
namespace swf2ass;
class GradientItem {
public $position;
public $color;
public function __construct($position, Color $color) {
$this->position = $position;
$this->color = $color;
}
public static function fromXML(\DOMElement $element): GradientItem {
return new GradientItem($element->getAttribute("position"), Color::fromXML($element->getElementsByTagName("color")->item(0)->getElementsByTagName("Color")->item(0)));
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace swf2ass;
class JPEGBitmapDefinition extends BitmapDefinition {
static function fromXML(\DOMElement $element): BitmapDefinition {
//Utils::dump_element($element);
$im = new \Imagick();
$data = base64_decode(trim($element->textContent));
$bytes = $element->hasAttribute("offset_to_alpha") ? substr($data, 0, $element->getAttribute("offset_to_alpha")) : $data;
$isJPEG = substr($bytes, 0, 2) === "\xff\xd8" || substr($bytes, 0, 4) === "\xff\xd9\xff\xd8";
if ($isJPEG) { //Fix freaking broken JPEGs
if (substr($bytes, 0, 4) === "\xff\xd9\xff\xd8") { //Invalid header before marker
$bytes = substr($bytes, 4);
}
$size = strlen($bytes);
for ($i = 0; $i < $size; $i += 4) {
if (substr($bytes, $i, 4) === "\xff\xd9\xff\xd8") {
$bytes = substr($bytes, 0, $i) . substr($bytes, $i + 4);
break;
}
}
}
$im->readImageBlob($bytes);
$im->transformImageColorspace(\Imagick::COLORSPACE_SRGB);
$resolution = $im->getImageGeometry();
$size = new Coordinate($resolution["width"], $resolution["height"]);
$pixels = array_fill(0, $size->y, array_fill(0, $size->x, new Color(0, 0, 0)));
$iterator = $im->getPixelIterator();
foreach ($iterator as $row => $p) {
foreach ($p as $col => $pixel) {
$c = $pixel->getColor(true);
$pixels[$row][$col] = new \swf2ass\Color((int)round($c["r"] * 255), (int)round($c["g"] * 255), (int)round($c["b"] * 255), isset($c["a"]) ? (int)round((1 - $c["a"]) * 255) : null);
}
}
//TODO: add transparency
return new JPEGBitmapDefinition($element->getAttribute("objectID"), $size, $pixels);
}
}

28
src/LineRecord.php Normal file
View file

@ -0,0 +1,28 @@
<?php
namespace swf2ass;
class LineRecord implements Record {
public Coordinate $start;
public Coordinate $coord;
public function __construct(Coordinate $coord, Coordinate $start) {
$this->coord = $coord;
$this->start = $start;
}
public function getStart(): Coordinate {
return $this->start;
}
public function reverse(): LineRecord {
return new LineRecord($this->start, $this->coord);
}
public static function fromXML(\DOMElement $element, $cursorX = 0, $cursorY = 0): LineRecord {
$x = (int)$element->getAttribute("x");
$y = (int)$element->getAttribute("y");
return new LineRecord(new Coordinate($cursorX + $x, $cursorY + $y), new Coordinate($cursorX, $cursorY));
}
}

9
src/LineStyle.php Normal file
View file

@ -0,0 +1,9 @@
<?php
namespace swf2ass;
class LineStyle {
/** @var Color|LinearGradient */
public $item;
public $width;
}

24
src/LinearGradient.php Normal file
View file

@ -0,0 +1,24 @@
<?php
namespace swf2ass;
class LinearGradient {
/** @var GradientItem[] */
public $colors;
/** @var MatrixTransform|null */
public ?MatrixTransform $transform;
public function __construct($colors = [], MatrixTransform $transform = null) {
$this->colors = $colors;
$this->transform = $transform;
}
public static function fromXML(\DOMElement $element): LinearGradient {
$colors = [];
foreach ($element->getElementsByTagName("GradientItem") as $item) {
$colors[] = GradientItem::fromXML($item);
}
$matrix = $element->getElementsByTagName("matrix");
return new LinearGradient($colors, $matrix->count() > 0 ? MatrixTransform::fromXML($matrix->item(0)) : null);
}
}

65
src/MatrixTransform.php Normal file
View file

@ -0,0 +1,65 @@
<?php
namespace swf2ass;
class MatrixTransform {
public $scaleX;
public $scaleY;
public $skewX;
public $skewY;
public $transX;
public $transY;
public function __construct($scaleX = 1, $scaleY = 1, $skewX = 0, $skewY = 0, $transX = 0, $transY = 0) {
$this->scaleX = $scaleX;
$this->scaleY = $scaleY;
$this->skewX = $skewX;
$this->skewY = $skewY;
$this->transX = $transX;
$this->transY = $transY;
}
public function applyToStyleContainer(StyleContainer $styles): StyleContainer {
if ($this->scaleX !== null) {
if ($this->scaleX < 0) {
$styles->fry = 180;
}
$styles->fscx = round(abs($this->scaleX) * 100, 2);
} else {
$styles->fry = null;
$styles->fscx = null;
}
if ($this->scaleY !== null) {
if ($this->scaleY < 0) {
$styles->frx = 180;
}
$styles->fscy = round(abs($this->scaleY) * 100, 2);
} else {
$styles->frx = null;
$styles->fscy = null;
}
if ($this->skewX !== null) {
$styles->fax = round($this->skewX, 4);
} else {
$styles->fax = null;
}
if ($this->skewY !== null) {
$styles->fay = round($this->skewY, 4);
} else {
$styles->fay = null;
}
if ($this->transX !== null) {
$styles->pos = new Coordinate((int)$this->transX, (int)$this->transY);
} else {
$styles->pos = null;
}
return $styles;
}
static function fromXML(\DOMElement $element): MatrixTransform {
return new MatrixTransform($element->hasAttribute("scaleX") ? $element->getAttribute("scaleX") : null, $element->hasAttribute("scaleY") ? $element->getAttribute("scaleY") : null, $element->hasAttribute("skewX") ? $element->getAttribute("skewX") : null, $element->hasAttribute("skewY") ? $element->getAttribute("skewY") : null, $element->hasAttribute("transX") ? $element->getAttribute("transX") : null, $element->hasAttribute("transY") ? $element->getAttribute("transY") : null);
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace swf2ass;
class MorphShapeDefinition implements ObjectDefinition {
public $id;
public Rectangle $startBounds;
public Rectangle $endBounds;
public function __construct($id, Rectangle $bounds, StyleList $styles, ShapeList $shapes) {
$this->id = $id;
$this->bounds = $bounds;
$this->styles = $styles;
$this->shapeList = $shapes;
}
static function fromXML(\DOMElement $element): MorphShapeDefinition {
Utils::dump_element($element);
$data = base64_decode(trim($element->textContent));
$offset = 0;
$bits = Utils::bin2binary($data);
$startBounds = Rectangle::fromData($bits, $offset);
$endBounds = Rectangle::fromData($bits, $offset);
$endEdgesOffset = Utils::binary2dec(substr($bits, $offset, 32)) * 8;
var_dump($startBounds);
var_dump($endBounds);
exit(); //TODO
}
}

28
src/MoveRecord.php Normal file
View file

@ -0,0 +1,28 @@
<?php
namespace swf2ass;
class MoveRecord implements Record {
public Coordinate $start;
public Coordinate $coord;
public function __construct(Coordinate $coord, Coordinate $start) {
$this->coord = $coord;
$this->start = $start;
}
public function getStart(): Coordinate {
return $this->start;
}
public function reverse(): MoveRecord {
return new MoveRecord($this->start, $this->coord);
}
public static function fromXML(\DOMElement $element, $cursorX = 0, $cursorY = 0): MoveRecord {
$x = (int)$element->getAttribute("x");
$y = (int)$element->getAttribute("y");
return new MoveRecord(new Coordinate($x, $y), new Coordinate($cursorX, $cursorY));
}
}

7
src/ObjectDefinition.php Normal file
View file

@ -0,0 +1,7 @@
<?php
namespace swf2ass;
interface ObjectDefinition {
}

View file

@ -0,0 +1,32 @@
<?php
namespace swf2ass;
class QuadraticCurveRecord implements Record {
public Coordinate $start;
public Coordinate $control;
public Coordinate $anchor;
public function __construct(Coordinate $control, Coordinate $anchor, Coordinate $start) {
$this->control = $control;
$this->anchor = $anchor;
$this->start = $start;
}
public function getStart(): Coordinate {
return $this->start;
}
public function reverse(): QuadraticCurveRecord {
return new QuadraticCurveRecord($this->control, $this->start, $this->anchor);
}
public static function fromXML(\DOMElement $element, $cursorX = 0, $cursorY = 0): QuadraticCurveRecord {
$controlX = (int)$element->getAttribute("x1");
$controlY = (int)$element->getAttribute("y1");
$anchorX = (int)$element->getAttribute("x2");
$anchorY = (int)$element->getAttribute("y2");
return new QuadraticCurveRecord(new Coordinate($cursorX + $controlX, $cursorY + $controlY), new Coordinate($cursorX + $controlX + $anchorX, $cursorY + $controlY + $anchorY), new Coordinate($cursorX, $cursorY));
}
}

9
src/Record.php Normal file
View file

@ -0,0 +1,9 @@
<?php
namespace swf2ass;
interface Record {
public function getStart(): Coordinate;
public function reverse(): Record;
}

73
src/Rectangle.php Normal file
View file

@ -0,0 +1,73 @@
<?php
namespace swf2ass;
class Rectangle implements ComplexShape {
public Coordinate $topLeft;
public Coordinate $bottomRight;
public function __construct(Coordinate $topLeft, Coordinate $bottomRight) {
$this->topLeft = $topLeft;
$this->bottomRight = $bottomRight;
}
public function inBounds(Coordinate $pos): bool {
return $pos->x >= $this->topLeft->x and $pos->y >= $this->topLeft->y and $pos->x <= $this->bottomRight->x and $pos->y <= $this->bottomRight->y;
}
public function getWidth() {
return ($this->bottomRight->x - $this->topLeft->x);
}
public function getHeight() {
return ($this->bottomRight->y - $this->topLeft->y);
}
public function getArea() {
return $this->getWidth() * $this->getHeight();
}
public function draw(): array {
return [new LineRecord(new Coordinate($this->topLeft->x, $this->bottomRight->y), $this->topLeft), new LineRecord($this->bottomRight, new Coordinate($this->topLeft->x, $this->bottomRight->y)), new LineRecord(new Coordinate($this->bottomRight->x, $this->topLeft->y), $this->bottomRight), new LineRecord($this->topLeft, new Coordinate($this->bottomRight->x, $this->topLeft->y)),];
}
public function drawOpen(): array {
return [new LineRecord(new Coordinate($this->topLeft->x, $this->bottomRight->y), $this->topLeft), new LineRecord($this->bottomRight, new Coordinate($this->topLeft->x, $this->bottomRight->y)), new LineRecord(new Coordinate($this->bottomRight->x, $this->topLeft->y), $this->bottomRight),];
}
public function divide($size): Rectangle {
return new Rectangle($this->topLeft->divide($size), $this->bottomRight->divide($size));
}
public function toPixel($twipSize = TWIP_SIZE): Rectangle {
return $this->divide($twipSize);
}
public static function fromXML(\DOMElement $element): Rectangle {
return new Rectangle(new Coordinate($element->getAttribute("left"), $element->getAttribute("top")), new Coordinate($element->getAttribute("right"), $element->getAttribute("bottom")));
}
public static function fromData($bitdata, &$offset): Rectangle {
$nbits = Utils::binary2dec(substr($bitdata, $offset, 5));
var_dump($nbits);
$offset += 5;
$xMin = Utils::binary2dec(substr($bitdata, $offset, $nbits));
$offset += $nbits;
$xMax = Utils::binary2dec(substr($bitdata, $offset, $nbits));
$offset += $nbits;
$yMin = Utils::binary2dec(substr($bitdata, $offset, $nbits));
$offset += $nbits;
$yMax = Utils::binary2dec(substr($bitdata, $offset, $nbits));
$offset += $nbits;
return new Rectangle(new Coordinate($xMin, $yMin), new Coordinate($xMax, $yMax));
}
}

15
src/Shape.php Normal file
View file

@ -0,0 +1,15 @@
<?php
namespace swf2ass;
class Shape {
/** @var QuadraticCurveRecord[]|CubicCurveRecord[]|LineRecord[]|MoveRecord[] */
public $edges;
public $styleRecord;
public function __construct($edges = [], StyleRecord $record = null) {
$this->edges = $edges;
$this->styleRecord = $record;
}
}

29
src/ShapeDefinition.php Normal file
View file

@ -0,0 +1,29 @@
<?php
namespace swf2ass;
class ShapeDefinition implements ObjectDefinition {
public $id;
public Rectangle $bounds;
public StyleList $styles;
public ShapeList $shapeList;
public function __construct($id, Rectangle $bounds, StyleList $styles, ShapeList $shapes) {
$this->id = $id;
$this->bounds = $bounds;
$this->styles = $styles;
$this->shapeList = $shapes;
}
static function fromXML(\DOMElement $element): ShapeDefinition {
$shapes = new ShapeList([]);
$styles = StyleList::fromXML($element->getElementsByTagName("styles")->item(0)->getElementsByTagName("StyleList")->item(0));
foreach ($element->getElementsByTagName("shapes")->item(0)->childNodes as $node) {
if ($node instanceof \DOMElement and $node->nodeName === "Shape") {
$shapes = $shapes->merge(ShapeList::fromXML($node, $styles));
}
}
return new ShapeDefinition($element->getAttribute("objectID"), Rectangle::fromXML($element->getElementsByTagName("bounds")->item(0)->getElementsByTagName("Rectangle")->item(0)), $styles, $shapes);
}
}

281
src/ShapeList.php Normal file
View file

@ -0,0 +1,281 @@
<?php
namespace swf2ass;
class ShapeList {
/** @var Shape[] */
public $shapes;
public function __construct($shapes = []) {
$this->shapes = $shapes;
}
public function merge(ShapeList $b): ShapeList {
return new ShapeList(array_merge($this->shapes, $b->shapes));
}
/**
* @param Record[] $subPath
* @param $lineStyleIdx
* @param $fillStyleIdx
* @param $fillStyleInsideIdx
* @param Record[][] $currentFillEdgeMap
* @param Record[][] $currentLineEdgeMap
*/
private static function processSubPath(array $subPath, $lineStyleIdx, $fillStyleIdx, $fillStyleInsideIdx, array &$currentFillEdgeMap, array &$currentLineEdgeMap) {
if ($fillStyleIdx > 0) {
if (!isset($currentFillEdgeMap[$fillStyleIdx])) {
$currentFillEdgeMap[$fillStyleIdx] = [];
}
foreach ($subPath as $n) {
$currentFillEdgeMap[$fillStyleIdx][] = $n->reverse();
}
}
if ($fillStyleInsideIdx > 0) {
if (!isset($currentFillEdgeMap[$fillStyleInsideIdx])) {
$currentFillEdgeMap[$fillStyleInsideIdx] = [];
}
foreach ($subPath as $n) {
$currentFillEdgeMap[$fillStyleInsideIdx][] = $n;
}
}
if ($lineStyleIdx > 0) {
if (!isset($currentLineEdgeMap[$lineStyleIdx])) {
$currentLineEdgeMap[$lineStyleIdx] = [];
}
foreach ($subPath as $n) {
$currentLineEdgeMap[$lineStyleIdx][] = $n;
}
}
}
/**
* @param Record[][] $edgeMap
*/
private static function cleanEdgeMap(array &$edgeMap) {
foreach ($edgeMap as $styleIdx => $subPath) {
if (count($subPath) > 0) {
$idx = 0;
/** @var Record $prevEdge */
$prevEdge = null;
/** @var Record $prevEdgeReverse */
$prevEdgeReverse = null;
$tmpPath = [];
while (count($subPath) > 0) {
$idx = 0;
while ($idx < count($subPath)) {
if ($prevEdge === null or $prevEdgeReverse->getStart()->equals($subPath[$idx]->getStart())) {
$edge = array_splice($subPath, $idx, 1)[0];
$tmpPath[] = $edge;
$prevEdge = $edge;
$prevEdgeReverse = $edge->reverse();
} else {
$found = false;
for ($i = 0; $i < count($subPath); ++$i) {
if ($prevEdgeReverse->getStart()->equals($subPath[$i]->getStart())) {
$idx = $i;
$found = true;
break;
}
}
if (!$found) {
$idx = 0;
$prevEdge = null;
$prevEdgeReverse = null;
}
}
}
}
$edgeMap[$styleIdx] = [];
$pos = new Coordinate(0, 0);
foreach ($tmpPath as $node) {
if (!$pos->equals($node->getStart())) {
$edgeMap[$styleIdx][] = new MoveRecord($node->getStart(), $pos);
}
$edgeMap[$styleIdx][] = $node;
$pos = $node->reverse()->getStart();
}
}
}
}
public static function fromXML(\DOMElement $element, StyleList $currentStyles): ShapeList {
/** @var Shape[] $shapes */
$shapes = [];
/** @var Record[] $edgeRecords */
$edgeRecords = [];
$styleRecord = new StyleRecord();
$styleRecord->styleList = clone $currentStyles;
$fillStyleIdxOffset = 0;
$lineStyleIdxOffset = 0;
$cursorX = 0;
$cursorY = 0;
foreach ($element->getElementsByTagName("edges")->item(0)->childNodes as $node) {
if ($node instanceof \DOMElement) {
if ($node->nodeName === "CurveTo") {
$curve = QuadraticCurveRecord::fromXML($node, $cursorX, $cursorY);
$cursorX = $curve->anchor->x;
$cursorY = $curve->anchor->y;
$edgeRecords[] = $curve;
} else if ($node->nodeName === "LineTo") {
$line = LineRecord::fromXML($node, $cursorX, $cursorY);
$cursorX = $line->coord->x;
$cursorY = $line->coord->y;
$edgeRecords[] = $line;
} else if ($node->nodeName === "ShapeSetup") {
$record = StyleRecord::nullStyle();
$styleChange = false;
if ($node->hasChildNodes()) {
$newStyles = StyleList::fromXML($node->getElementsByTagName("styles")->item(0)->getElementsByTagName("StyleList")->item(0));
foreach ($newStyles->fillStyles as $s) {
$styleRecord->styleList->fillStyles[] = $s;
++$fillStyleIdxOffset;
}
foreach ($newStyles->lineStyles as $s) {
$styleRecord->styleList->lineStyles[] = $s;
++$lineStyleIdxOffset;
}
$styleChange = true;
$record->styleList = $styleRecord->styleList;
}
if ($node->hasAttribute("fillStyle0")) {
$fillStyle = (int)$node->getAttribute("fillStyle0");
$styleRecord->fillStyle = $fillStyle + ($fillStyle > 0 ? $fillStyleIdxOffset : 0);
$styleChange = true;
$record->fillStyle = $styleRecord->fillStyle;
}
if ($node->hasAttribute("fillStyle1")) {
$fillStyleInside = (int)$node->getAttribute("fillStyle1");
$styleRecord->fillStyleInside = $fillStyleInside + ($fillStyleInside > 0 ? $fillStyleIdxOffset : 0);
$styleChange = true;
$record->fillStyleInside = $styleRecord->fillStyleInside;
}
if ($node->hasAttribute("lineStyle")) {
$lineStyle = (int)$node->getAttribute("lineStyle");
$styleRecord->lineStyle = $lineStyle + ($lineStyle > 0 ? $lineStyleIdxOffset : 0);
$styleChange = true;
$record->lineStyle = $styleRecord->lineStyle;
}
if ($styleChange) {
$edgeRecords[] = $record;
}
if ($node->hasAttribute("x")) {
$move = MoveRecord::fromXML($node, $cursorX, $cursorY);
$cursorX = $move->coord->x;
$cursorY = $move->coord->y;
$edgeRecords[] = $move;
}
}
}
}
/** @var Record[] $subPath */
$subPath = [];
$currentFillStyleIdx = 0;
$currentFillStyleInsideIdx = 0;
$currentLineStyleIdx = 0;
/** @var Record[][] $currentFillEdgeMap */
$currentFillEdgeMap = [];
/** @var Record[] $currentLineEdgeMap */
$currentLineEdgeMap = [];
$fillEdgeMaps = [];
$lineEdgeMaps = [];
$numGroups = 0;
$lastNode = null;
foreach ($edgeRecords as $edge) {
if ($edge instanceof LineRecord or $edge instanceof CubicCurveRecord or $edge instanceof QuadraticCurveRecord) {
$subPath[] = $edge;
} elseif ($edge instanceof MoveRecord) {
//$subPath[] = $edge;
} else if ($edge instanceof StyleRecord) {
$changed = false;
if ($edge->lineStyle !== null or $edge->fillStyle !== null or $edge->fillStyleInside !== null) {
self::processSubPath($subPath, $currentLineStyleIdx, $currentFillStyleIdx, $currentFillStyleInsideIdx, $currentFillEdgeMap, $currentLineEdgeMap);
/** @var Record[] $subPath */
$subPath = [];
$changed = true;
}
if ($changed and $edge->lineStyle === 0 and $edge->fillStyle === 0 and $edge->fillStyleInside === 0) {
self::cleanEdgeMap($currentFillEdgeMap);
self::cleanEdgeMap($currentLineEdgeMap);
$fillEdgeMaps[] = $currentFillEdgeMap;
$lineEdgeMaps[] = $currentLineEdgeMap;
$currentFillEdgeMap = [];
$currentLineEdgeMap = [];
$currentFillStyleIdx = 0;
$currentFillStyleInsideIdx = 0;
$currentLineStyleIdx = 0;
++$numGroups;
} else {
if ($edge->lineStyle !== null and $edge->lineStyle !== $currentLineStyleIdx) {
$currentLineStyleIdx = $edge->lineStyle;
}
if ($edge->fillStyle !== null and $edge->fillStyle !== $currentFillStyleIdx) {
$currentFillStyleIdx = $edge->fillStyle;
}
if ($edge->fillStyleInside !== null and $edge->fillStyleInside !== $currentFillStyleInsideIdx) {
$currentFillStyleInsideIdx = $edge->fillStyleInside;
}
}
}
}
self::processSubPath($subPath, $currentLineStyleIdx, $currentFillStyleIdx, $currentFillStyleInsideIdx, $currentFillEdgeMap, $currentLineEdgeMap);
self::cleanEdgeMap($currentFillEdgeMap);
self::cleanEdgeMap($currentLineEdgeMap);
$fillEdgeMaps[] = $currentFillEdgeMap;
$lineEdgeMaps[] = $currentLineEdgeMap;
$currentFillEdgeMap = [];
$currentLineEdgeMap = [];
++$numGroups;
for ($i = 0; $i < $numGroups; ++$i) {
//ksort($fillEdgeMaps[$i]);
ksort($lineEdgeMaps[$i]);
foreach ($fillEdgeMaps[$i] as $fillStyle => $records) {
if (count($records) <= 1) {
continue;
}
$record = new StyleRecord();
$record->fillStyleInside = $fillStyle;
$record->styleList = $styleRecord->styleList;
$shapes[] = new Shape($records, $record);
}
foreach ($lineEdgeMaps[$i] as $lineStyle => $records) {
if (count($records) <= 1) {
continue;
}
$record = new StyleRecord();
$record->lineStyle = $lineStyle;
$record->styleList = $styleRecord->styleList;
$shapes[] = $s = new Shape($records, $record);
}
}
return new ShapeList($shapes);
}
}

10
src/Square.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace swf2ass;
class Square extends Rectangle {
public function __construct(Coordinate $topLeft, $size = 1) {
parent::__construct($topLeft, $topLeft->add(new Coordinate($size, $size)));
}
}

216
src/StyleContainer.php Normal file
View file

@ -0,0 +1,216 @@
<?php
namespace swf2ass;
class StyleContainer {
public $frx = null, $fry = null;
public $fscx = null, $fscy = null;
public $fax = null, $fay = null;
/** @var ?Coordinate */
public $pos = null;
/** @var ?Coordinate[] */
public $move = null;
/** @var ?Color */
public $color1 = null;
/** @var ?Color */
public $color3 = null;
/** @var ?Color */
public $original_color1 = null;
/** @var ?Color */
public $original_color3 = null;
public $bord = null;
public $shad = null;
public $moveTransitionStart = null;
public $moveTransitionEnd = null;
public $transitionStart = null;
public $transitionEnd = null;
public function resetTransformedStyles() {
$this->frx = $this->fry = $this->fscx = $this->fscy = $this->fax = $this->fay;
$this->color1 = $this->original_color1;
$this->color3 = $this->original_color3;
$this->pos = null;
$this->move = null;
$this->transitionStart = null;
$this->transitionEnd = null;
$this->moveTransitionStart = null;
$this->moveTransitionEnd = null;
}
public function isEqualishPosition(StyleContainer $container) {
$pos = $this->move !== null ? $this->move[1] : $this->pos;
return $pos === $container->pos or ($pos !== null and $container->pos !== null and $pos->equals($container->pos));
}
public function isEqualish(StyleContainer $container, $checkPos = true) {
if ($checkPos and !$this->isEqualishPosition($container)) {
return false;
}
foreach (get_object_vars($this) as $k => $value) {
if ($k === "move" or $k === "pos" or $k === "transitionStart" or $k === "transitionEnd" or $k === "moveTransitionStart" or $k === "moveTransitionEnd") {
} elseif ($k === "color1" or $k === "color3" or $k === "original_color1" or $k === "original_color3") {
if (!(($container->{$k} !== null and $value !== null and $container->{$k} == $value) or $container->{$k} === $value)) {
return false;
}
} else if ($container->{$k} !== $value) {
return false;
}
}
return true;
}
public function toString($startFrame = 0, $frameDuration = 1 / 60, StyleContainer $prevValues = null, $outputEmpty = true) {
$a = "";
if ($this->pos !== null and !($prevValues !== null and $prevValues->pos !== null and !$prevValues->pos->equals($this->pos))) {
$p = $this->pos->toPixel();
$a .= "\\pos(" . round($p->x, 2) . "," . round($p->y, 2) . ")";
}
if ($this->move !== null) {
$p1 = $this->move[0]->toPixel();
$p2 = $this->move[1]->toPixel();
$a .= "\\move(" . round($p1->x, 2) . "," . round($p1->y, 2) . "," . round($p2->x, 2) . "," . round($p2->y, 2);
if ($this->moveTransitionStart !== null) {
$a .= "," . round(($this->moveTransitionStart - $startFrame) * $frameDuration * 1000) . "," . round((($this->moveTransitionEnd ?? $this->moveTransitionStart) - $startFrame) * $frameDuration * 1000);
}
$a .= ")";
}
if ($this->transitionStart !== null) {
$a .= "\\t(" . round(($this->transitionStart - $startFrame) * $frameDuration * 1000) . "," . round(($this->transitionEnd ?? $this->transitionStart) * $frameDuration * 1000);
}
foreach (get_object_vars($this) as $k => $value) {
if ($k === "move" or $k === "pos" or $k === "transitionStart" or $k === "transitionEnd" or $k === "moveTransitionStart" or $k === "moveTransitionEnd" or $k === "original_color1" or $k === "original_color3") {
} elseif ($k === "color1") {
if ($prevValues !== null and $prevValues->{$k} !== null and $prevValues->{$k} == $value) {
continue;
}
if ($value !== null) {
$a .= "\\1c&H" . strtoupper(Utils::padHex(dechex($value->b ?? 0))) . strtoupper(Utils::padHex(dechex($value->g ?? 0))) . strtoupper(Utils::padHex(dechex($value->r ?? 0))) . "&\\1a&H" . strtoupper(Utils::padHex(dechex($value->alpha ?? 0))) . "&";
} else if ($outputEmpty) {
$a .= "\\1a&HFF&";
}
} elseif ($k === "color3") {
if ($prevValues !== null and $prevValues->{$k} !== null and $prevValues->{$k} == $value) {
continue;
}
if ($value !== null) {
$a .= "\\3c&H" . strtoupper(Utils::padHex(dechex($value->b ?? 0))) . strtoupper(Utils::padHex(dechex($value->g ?? 0))) . strtoupper(Utils::padHex(dechex($value->r ?? 0))) . "&\\1a&H" . strtoupper(Utils::padHex(dechex($value->alpha ?? 0))) . "&";
} else if ($outputEmpty) {
$a .= "\\3a&HFF&";
}
} else {
if ($prevValues !== null and $prevValues->{$k} !== null and $prevValues->{$k} === $value) {
continue;
}
if ($value !== null) {
$a .= "\\$k$value";
}
}
}
if ($this->transitionStart !== null) {
$a .= ")";
}
return $a;
}
public function doTransitionTo(StyleContainer $style, $transitionStart, $transitionEnd, $moveTransitionStart, $moveTransitionEnd) {
$firstStyle = $this;
$secondStyle = clone $style;
if ($firstStyle->move !== null) {
if ($firstStyle->isEqualishPosition($secondStyle)) {
$secondStyle->pos = $firstStyle->pos;
$secondStyle->move = $firstStyle->move;
$secondStyle->moveTransitionStart = $firstStyle->moveTransitionStart;
$secondStyle->moveTransitionEnd = $firstStyle->moveTransitionEnd;
$firstStyle->pos = null;
$firstStyle->move = null;
$firstStyle->moveTransitionStart = null;
$firstStyle->moveTransitionEnd = null;
} else if (ADVANCED_MOTION_INTERPOLATION and $firstStyle->transitionEnd !== null and $transitionEnd !== null) {
$diffA = $firstStyle->move[1]->sub($firstStyle->move[0])->divide($firstStyle->moveTransitionEnd - $firstStyle->moveTransitionStart);
$diffB = $secondStyle->pos->sub($firstStyle->move[0])->divide($moveTransitionEnd - $firstStyle->moveTransitionStart);
$diff = $diffA->sub($diffB);
if (abs($diff->x) <= 1 and abs($diff->y) <= 1) { //Eh, close enough, it's linear!
$move = $firstStyle->move;
$move[1] = $secondStyle->pos;
$moveTransitionStart = $firstStyle->moveTransitionStart;
if ($firstStyle->isEqualish($secondStyle, false)) {
$firstStyle->pos = null;
$firstStyle->move = $move;
$firstStyle->moveTransitionStart = $moveTransitionStart;
$firstStyle->moveTransitionEnd = $moveTransitionEnd;
return true;
} else {
$firstStyle->pos = null;
$firstStyle->move = null;
$firstStyle->moveTransitionStart = null;
$firstStyle->moveTransitionEnd = null;
$secondStyle->pos = null;
$secondStyle->move = $move;
$secondStyle->moveTransitionStart = $moveTransitionStart;
$secondStyle->moveTransitionEnd = $moveTransitionEnd;
}
} else {
return false;
}
} else {
return false;
}
} else if ($firstStyle->pos !== null and $secondStyle->pos !== null) {
if ($firstStyle->isEqualishPosition($secondStyle)) {
$secondStyle->pos = $firstStyle->pos;
$secondStyle->move = $firstStyle->move;
$secondStyle->moveTransitionStart = $firstStyle->moveTransitionStart;
$secondStyle->moveTransitionEnd = $firstStyle->moveTransitionEnd;
$firstStyle->pos = null;
$firstStyle->move = null;
$firstStyle->moveTransitionStart = null;
$firstStyle->moveTransitionEnd = null;
} else {
$secondStyle->move = [$firstStyle->pos, $secondStyle->pos];
$secondStyle->moveTransitionStart = $moveTransitionStart;
$secondStyle->moveTransitionEnd = $moveTransitionEnd;
$secondStyle->pos = null;
$firstStyle->pos = null;
}
}
$secondStyle->transitionStart = $transitionStart;
$secondStyle->transitionEnd = $transitionEnd;
foreach (get_object_vars($firstStyle) as $k => $value) {
if ($k === "move" or $k === "pos" or $k === "transitionStart" or $k === "transitionEnd" or $k === "moveTransitionStart" or $k === "moveTransitionEnd" or $k === "original_color1" or $k === "original_color3") {
} elseif ($k === "color1" or $k === "color3") {
if ($value !== null and $secondStyle->{$k} !== null and $value == $secondStyle->{$k}) {
$secondStyle->{$k} = null;
}
} elseif ($value === $secondStyle->{$k}) {
$secondStyle->{$k} = null;
}
}
return $secondStyle;
}
}

61
src/StyleList.php Normal file
View file

@ -0,0 +1,61 @@
<?php
namespace swf2ass;
class StyleList {
/** @var LinearGradient[]|Color[] */
public $fillStyles;
/** @var LineStyle */
public $lineStyles;
public function __construct($fillStyles = [], $lineStyles = []) {
$this->fillStyles = $fillStyles;
$this->lineStyles = $lineStyles;
}
public static function fromXML(\DOMElement $element): StyleList {
$fillStyles = [];
$lineStyles = [];
foreach ($element->getElementsByTagName("fillStyles")->item(0)->childNodes as $node) {
if ($node instanceof \DOMElement) {
if ($node->nodeName === "Solid") {
$fillStyles[] = Color::fromXML($node->getElementsByTagName("color")->item(0)->getElementsByTagName("Color")->item(0));
} else if ($node->nodeName === "LinearGradient") {
$fillStyles[] = LinearGradient::fromXML($node);
}
}
}
foreach ($element->getElementsByTagName("lineStyles")->item(0)->childNodes as $node) {
if ($node instanceof \DOMElement) {
$ob = new LineStyle();
$ob->width = max(TWIP_SIZE, (int)$node->getAttribute("width"));
$colors = $node->getElementsByTagName("color");
$grads = $node->getElementsByTagName("coLinearGradientlor");
if ($colors->count() > 0) {
$ob->item = Color::fromXML($colors->item(0)->getElementsByTagName("Color")->item(0));
} else if ($grads->count() > 0) {
$ob->item = LinearGradient::fromXML($grads->item(0)->getElementsByTagName("LinearGradient")->item(0));
}
$lineStyles[] = $ob;
}
}
return new StyleList($fillStyles, $lineStyles);
}
/**
* @param $i
* @return Color|LinearGradient|mixed|null
*/
public function getFillStyle($i) {
return $this->fillStyles[$i] ?? null;
}
/**
* @param $i
* @return Color|LinearGradient|mixed|null
*/
public function getLineStyle($i) {
return $this->lineStyles[$i] ?? null;
}
}

20
src/StyleRecord.php Normal file
View file

@ -0,0 +1,20 @@
<?php
namespace swf2ass;
class StyleRecord {
public ?StyleList $styleList = null;
public ?int $fillStyle = 0;
public ?int $fillStyleInside = 0;
public ?int $lineStyle = 0;
public static function nullStyle(): StyleRecord {
$record = new StyleRecord();
$record->styleList = null;
$record->lineStyle = null;
$record->fillStyle = null;
$record->fillStyleInside = null;
return $record;
}
}

181
src/Utils.php Normal file
View file

@ -0,0 +1,181 @@
<?php
namespace swf2ass;
abstract class Utils {
static function timeToStamp($time) {
//global $offset;
//$time += $offset;
$time = max(0, $time);
$hours = floor($time / 3600);
$time = $time - $hours * 3600;
$minutes = floor($time / 60);
$seconds = $time - $minutes * 60;
$s = explode(".", strval(round($seconds, 2)));
//$s = explode(".", strval(round($seconds, 4)));
if (!isset($s[1])) {
$s[1] = 0;
}
//return $hours . ":" . str_pad($minutes, 2, "0", STR_PAD_LEFT).':'.str_pad($s[0], 2, "0", STR_PAD_LEFT) . "." . str_pad($s[1], 4, "0", STR_PAD_RIGHT);
return $hours . ":" . str_pad($minutes, 2, "0", STR_PAD_LEFT) . ':' . str_pad($s[0], 2, "0", STR_PAD_LEFT) . "." . str_pad($s[1], 2, "0", STR_PAD_RIGHT);
}
static function padHex($h, $l = 2) {
return str_pad($h, $l, "0", STR_PAD_LEFT);
}
static function processSWF(\DOMElement $element, &$audio = null) {
$currentState = new CurrentState();
$currentState->frameRate = $element->getAttribute("framerate");
$currentState->frameCount = $element->getAttribute("frames");
$rect = $element->getElementsByTagName("size")->item(0)->getElementsByTagName("Rectangle")->item(0);
if ($rect instanceof \DOMElement and $rect->nodeName === "Rectangle") {
$currentState->viewPort = Rectangle::fromXML($rect)->toPixel();
}
$sound = null;
$rootTag = $element->getElementsByTagName("tags")->item(0);
$e = [];
if ($rootTag instanceof \DOMElement) {
foreach ($rootTag->childNodes as $node) {
if ($node instanceof \DOMElement) {
if ($node->nodeName === "SoundStreamHead") {
$sound = $node;
} else if ($node->nodeName === "SoundStreamBlock" and $sound !== null) {
$data = base64_decode(trim($node->textContent));
$audio .= substr($data, 2);
} else if ($node->nodeName === "SetBackgroundColor") {
$currentState->backgroundColor = Color::fromXML($node->getElementsByTagName("color")->item(0)->getElementsByTagName("Color")->item(0));
} else if ($node->nodeName === "DefineMorphShape") {
//$shapeDef = MorphShapeDefinition::fromXML($node);
//$currentState->objects[$shapeDef->id] = $shapeDef;
//TODO
var_dump("TODO DefineMorphShape");
} else if ($node->nodeName === "DefineShape" or $node->nodeName === "DefineShape2" or $node->nodeName === "DefineShape3" or $node->nodeName === "DefineShape4" or $node->nodeName === "DefineShape5") {
$shapeDef = ShapeDefinition::fromXML($node);
$currentState->objects[$shapeDef->id] = $shapeDef;
} else if ($node->nodeName === "RemoveObject" or $node->nodeName === "RemoveObject2") {
$depth = (int)$node->getAttribute("depth");
if ($currentState->displayList[$depth] ?? null instanceof DisplayEntry) {
//TODO: clips
foreach ($currentState->displayList[$depth]->getLines($currentState) as $line) {
yield $line . PHP_EOL;
}
}
unset($currentState->displayList[$depth]);
unset($currentState->clipList[$depth]);
} else if ($node->nodeName === "PlaceObject2" or $node->nodeName === "PlaceObject3") {
$depth = (int)$node->getAttribute("depth");
$transform = null;
$transformNodes = $node->getElementsByTagName("transform");
if ($transformNodes->count() > 0) {
$transform = MatrixTransform::fromXML($transformNodes->item(0)->getElementsByTagName("Transform")->item(0));
}
$colorTransform = null;
$colorTransformNodes = $node->getElementsByTagName("colorTransform");
if ($colorTransformNodes->count() > 0) {
$colorTransform = ColorTransform::fromXML($colorTransformNodes->item(0)->getElementsByTagName("ColorTransform2")->item(0));
}
$objectID = $node->getAttribute("objectID");
if ($objectID !== "") {
$ob = new DisplayEntry();
$objectID = $node->getAttribute("objectID");
if (!isset($currentState->objects[$objectID])) {
continue;
throw new \Exception("Unknown object id $objectID");
}
$ob->startFrame = $currentState->frameNumber;
$ob->depth = $depth;
if ($node->hasAttribute("clipDepth")) {
$ob->clipDepth = (int)$node->getAttribute("clipDepth");
$currentState->clipList[$ob->clipDepth] = $ob;
}
$ob->object = $currentState->objects[$objectID];
if ($currentState->displayList[$depth] ?? null instanceof DisplayEntry) {
//TODO: clips
foreach ($currentState->displayList[$depth]->getLines($currentState) as $line) {
yield $line . PHP_EOL;
}
if ($node->getAttribute("replace") === "1") {
$ob->currentTransform = $currentState->displayList[$depth]->currentTransform;
$ob->currentColorTransform = $currentState->displayList[$depth]->currentColorTransform;
}
}
$currentState->displayList[$depth] = $ob;
}
$ob = $currentState->displayList[$depth] ?? null;
if (!$ob instanceof DisplayEntry) {
continue;
throw new \Exception("Unknown depth id $depth");
}
if ($transform !== null) {
$ob->currentTransform = $transform;
}
if ($colorTransform !== null) {
$ob->currentColorTransform = $colorTransform;
}
} else if ($node->nodeName === "ShowFrame") {
//fwrite(STDERR, "Frame {$currentState->frameNumber}". PHP_EOL);
ksort($currentState->displayList);
foreach ($currentState->displayList as $depth => $displayItem) {
/** @var DisplayEntry $displayItem */
$info = new FrameInformation();
$info->transform = $displayItem->currentTransform;
$info->colorTransform = $displayItem->currentColorTransform;
$info->frame = $currentState->frameNumber;
$displayItem->frames[$currentState->frameNumber] = $info;
}
++$currentState->frameNumber;
} else if ($node->nodeName === "SoundStreamBlock") {
} else if ($node->nodeName === "DefineSprite") {
//var_dump($node);
} else if ($node->nodeName === "DefineBitsJPEG2" || $node->nodeName === "DefineBitsJPEG3") {
$bitmapDef = JPEGBitmapDefinition::fromXML($node);
$currentState->objects[$bitmapDef->id] = $bitmapDef;
//file_put_contents("image_" . $bitmapDef->id .".png", $bitmapDef->toPNG());
} else if ($node->nodeName === "DefineBitsLossless" || $node->nodeName === "DefineBitsLossless2") {
$bitmapDef = BitmapDefinition::fromXML($node);
$currentState->objects[$bitmapDef->id] = $bitmapDef;
//file_put_contents("image_" . $bitmapDef->id .".png", $bitmapDef->toPNG());
} else {
fwrite(STDERR, $node->nodeName . PHP_EOL);
}
}
}
}
}
static function dump_element(\DOMElement $element) {
var_dump($element->ownerDocument->saveXML($element));
}
static function bin2binary($bin) {
return gmp_strval(gmp_init(bin2hex($bin), 16), 2);
}
static function binary2dec($bin) {
return gmp_intval(gmp_init($bin, 2));
}
}

61
swf2ass.php Normal file
View file

@ -0,0 +1,61 @@
<?php
require_once __DIR__ . "/vendor/autoload.php";
const SWFMILL_BINARY = "swfmill";
const TWIP_SIZE = 20;
const ADVANCED_MOTION_INTERPOLATION = true;
ob_start();
passthru(escapeshellarg(SWFMILL_BINARY) . " swf2xml " . escapeshellarg($argv[1]) . " stdout");
$contents = ob_get_contents();
ob_end_clean();
$swf = new \DOMDocument();
$swf->loadXML($contents);
$soundStream = "";
$fp = fopen($argv[2], "w+");
fwrite($fp, <<<ASSHEADER
[Script Info]
; Script generated by Aegisub 3.2.2
; http://www.aegisub.org/
ScriptType: v4.00+
WrapStyle: 0
ScaledBorderAndShadow: yes
YCbCr Matrix: TV.709
PlayResX: 480
PlayResY: 360
[Aegisub Project Garbage]
Last Style Storage: Default
Video File: ?dummy:60.000000:10000:960:720:160:160:160:c
Video AR Value: 1.333333
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: f,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
ASSHEADER
);
$headerTag = $swf->getElementsByTagName("Header")->item(0);
if ($headerTag instanceof DOMElement) {
foreach (\swf2ass\Utils::processSWF($headerTag, $soundStream) as $line) {
fwrite($fp, $line);
};
}
/*
if($soundStream !== ""){
file_put_contents($argv[2] . ".mp3", $soundStream);
}*/