Initial commit, mostly working with many issues
This commit is contained in:
commit
f968dcf144
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
/vendor/
|
||||
.idea/
|
||||
/*.ass
|
||||
/*.jpg
|
||||
/*.png
|
||||
/*.zip
|
||||
/*.mp4
|
||||
/*.mp3
|
||||
/*.webm
|
||||
/*.mkv
|
||||
/*.swf
|
17
composer.json
Normal file
17
composer.json
Normal 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
24
composer.lock
generated
Normal 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
522
image2ass.php
Normal 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
66
src/BitmapDefinition.php
Normal 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
39
src/Color.php
Normal 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
43
src/ColorTransform.php
Normal 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
7
src/ComplexShape.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace swf2ass;
|
||||
|
||||
interface ComplexShape {
|
||||
public function draw();
|
||||
}
|
42
src/Coordinate.php
Normal file
42
src/Coordinate.php
Normal 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
29
src/CubicCurveRecord.php
Normal 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
33
src/CurrentState.php
Normal 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
221
src/DisplayEntry.php
Normal 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
13
src/FrameInformation.php
Normal 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
17
src/GradientItem.php
Normal 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)));
|
||||
}
|
||||
}
|
49
src/JPEGBitmapDefinition.php
Normal file
49
src/JPEGBitmapDefinition.php
Normal 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
28
src/LineRecord.php
Normal 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
9
src/LineStyle.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace swf2ass;
|
||||
|
||||
class LineStyle {
|
||||
/** @var Color|LinearGradient */
|
||||
public $item;
|
||||
public $width;
|
||||
}
|
24
src/LinearGradient.php
Normal file
24
src/LinearGradient.php
Normal 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
65
src/MatrixTransform.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
30
src/MorphShapeDefinition.php
Normal file
30
src/MorphShapeDefinition.php
Normal 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
28
src/MoveRecord.php
Normal 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
7
src/ObjectDefinition.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace swf2ass;
|
||||
|
||||
interface ObjectDefinition {
|
||||
|
||||
}
|
32
src/QuadraticCurveRecord.php
Normal file
32
src/QuadraticCurveRecord.php
Normal 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
9
src/Record.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace swf2ass;
|
||||
|
||||
interface Record {
|
||||
public function getStart(): Coordinate;
|
||||
|
||||
public function reverse(): Record;
|
||||
}
|
73
src/Rectangle.php
Normal file
73
src/Rectangle.php
Normal 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
15
src/Shape.php
Normal 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
29
src/ShapeDefinition.php
Normal 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
281
src/ShapeList.php
Normal 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
10
src/Square.php
Normal 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
216
src/StyleContainer.php
Normal 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
61
src/StyleList.php
Normal 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
20
src/StyleRecord.php
Normal 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
181
src/Utils.php
Normal 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
61
swf2ass.php
Normal 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);
|
||||
}*/
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in a new issue