522 lines
16 KiB
PHP
522 lines
16 KiB
PHP
<?php
|
|
|
|
require_once __DIR__ . "/vendor/autoload.php";
|
|
|
|
const SWFMILL_BINARY = "swfmill";
|
|
const ADVANCED_MOTION_INTERPOLATION = true;
|
|
|
|
|
|
function normalizePath($path) {
|
|
$newPath = [];
|
|
$lastCursor = new \swf2ass\Vector2(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\Vector2 $corner
|
|
* @param \swf2ass\LineRecord[] $path
|
|
*
|
|
* @return ?\swf2ass\LineRecord
|
|
*/
|
|
function findNextCorner(\swf2ass\Vector2 $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\Vector2 $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\Vector2(PHP_INT_MAX, PHP_INT_MAX), new \swf2ass\Vector2(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\Vector2[][] $subPolygons */
|
|
$subPolygons = [];
|
|
/** @var \swf2ass\Vector2[] $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\Vector2($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\Vector2(0, 0), new \swf2ass\Vector2($resX, $resY)))->draw() as $p) {
|
|
$optimizedPaths[$k0][] = $p;
|
|
}
|
|
}
|
|
|
|
$assLines = [];
|
|
|
|
foreach ($optimizedPaths as $k => $path) {
|
|
$pos = new \swf2ass\Vector2(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));
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|