swf2ass/swf2ass.php

245 lines
8.9 KiB
PHP

<?php
require_once __DIR__ . "/vendor/autoload.php";
$settings = [
"videoScaleMultiplier" => 1, //TODO: not finished, leave at 1
//TODO: make this actually interpolate smooth transitions in a baked way
"videoRateMultiplier" => 1, //Sets the "framerate" multiplier for output video file, not lines. Helps with smoothing transitions if enabled.
"bakeTransforms" => false, //TODO: fix ASS matrix transform rendering and remove this
"timerSpeed" => 100, //NOTE: libass does not implement "Timer:", which is used by this setting. Leave at 100 by default
"timePrecision" => 2, //NOTE: libass does not implement anything different from 2. Leave at 2 by default,
"smoothTransitions" => false, //All transitions will happen smoothly from start till finish instead of happening instantly on frame thresholds. NOTE: this can break objects that did not get selected to be animated, or matrix transforms
"gradientSlices" => /*24,*/\swf2ass\Gradient::AUTO_SLICES,
];
$swfContent = file_get_contents($argv[1]);
$signature = hash("sha256", $swfContent, false);
$swf = new \swf\SWF($swfContent);
$swfContent = null;
unset($swfContent);
$fp = fopen($argv[2], "w+");
$fromFrame = isset($argv[3]) ? (int) $argv[3] : null;
$frameEnd = isset($argv[4]) ? (int) $argv[4] : null;
if($fromFrame !== null and $frameEnd === null){
$frameEnd = $fromFrame;
}
class RemovalEntry{
//TODO: accept names as well?
public ?int $objectId;
public ?array $depth;
/**
* @param int $objectId
* @param int[] $depth
*/
public function __construct(?int $objectId, ?array $depth){
$this->objectId = $objectId;
$this->depth = $depth;
}
public function equals(\swf2ass\RenderedObject $object): bool {
return ($this->objectId === null or $object->objectId === $this->objectId) and ($this->depth === null or (count($object->depth) >= count($this->depth) and array_slice($object->depth, 0, count($this->depth)) === $this->depth));
}
}
$frameOffset = 0;
$objectRemovalEntries = [];
//TODO: make this a JSON file elsewhere
$knownFlashSignatures = [
"52e75b7d6831293ebf4e5b28574a60f5bce10b1eae8afa4e69ab213a98b0b008" => [
"name" => "IJSW.swf",
"remove" => [
//removes playback menus
new RemovalEntry(null /*2*/, [0, 31]),
new RemovalEntry(null /*3*/, [0, 145]),
new RemovalEntry(null /*69*/, [0, 146]),
]
],
"229d7569ebf3b3b04fb03aa86162ab646d96be0353e8f4be32b1f6bf4e96af4e" => [
"name" => "cirno's_arithmetic_school.swf",
"remove" => [
//remove lyrics button/mask
new RemovalEntry(null /*30*/, [0, 259]),
new RemovalEntry(null /*31*/, [0, 264]),
]
],
"1203faf6504edf4845e72b957ce6b5c5d92597ecc0328aa36eacaef32eb7125a" => [
"name" => "cirno's_arithmetic_school_2011.swf",
"remove" => [
//remove lyrics button/mask
new RemovalEntry(null /*30*/, [0, 229]),
new RemovalEntry(null /*31*/, [0, 234]),
]
],
"3bdc7f8bbfb648e825f79dbe6095288871b1780b1d37f8cd2ad4c50cae40b5ca" => [
"name" => "IOSYS_CirnoENG.swf",
"remove" => [
//remove lyrics button/mask
new RemovalEntry(null /*33*/, [0, 1721]),
new RemovalEntry(null /*34*/, [0, 1752]),
]
],
];
if(isset($knownFlashSignatures[$signature])){
$e = $knownFlashSignatures[$signature];
echo "Found known signature for " . $e["name"] .", adding rules\n";
$objectRemovalEntries = $e["remove"];
if(isset($e["frameOffset"])){
$frameOffset = $e["frameOffset"];
}
if(isset($e["frameEnd"])){
$frameEnd = $e["frameEnd"];
}
}
//Function to decide whether to display object or not
function filterObject(\swf2ass\RenderedObject $object) : bool{
global $objectRemovalEntries;
/** @var RemovalEntry[] $objectRemovalEntries */
foreach ($objectRemovalEntries as $entry){
if($entry->equals($object)){
return true;
}
}
return false;
}
$testVectors = [];
if ($swf->header["signature"]) {
$processor = new \swf2ass\SWFProcessor($swf);
\swf2ass\ass\ASSRenderer::setSettings($settings);
$assRenderer = new \swf2ass\ass\ASSRenderer($processor->getFrameRate(), $processor->getViewPort());
$keyFrameInterval = 10 * $processor->getFrameRate(); //kf every 10 seconds TODO: make this dynamic, per-shape
$lastFrame = null;
while(($frame = $processor->nextFrameOutput()) !== null){
$lastFrame = $frame;
if(!$processor->isPlaying() or $processor->getLoops() > 0){
break;
}
$audio = $processor->getAudio();
if($audio !== null and $frameOffset === 0){
if($audio->getStartFrame() === null){
continue;
}
$frameOffset = $audio->getStartFrame();
}
$frame->setFrameOffset($frameOffset);
$rendered = $frame->getFrame()->render(0, [], null, null);
if($frame->getFrameNumber() === 0){
foreach ($rendered->getObjects() as $ob){
echo "frame 0: object {$ob->objectId} depth: " . implode(",", $ob->depth) . PHP_EOL;
}
}
$filteredRendered = new \swf2ass\RenderedFrame();
$drawCalls = 0;
$drawItems = 0;
$filteredObjects = 0;
$clipCalls = 0;
$clipItems = 0;
foreach ($rendered->getObjects() as $object){
if(filterObject($object)){
++$filteredObjects;
continue;
}
if($object->clip !== null){
++$clipCalls;
$clipItems += count($object->clip->getShape()->getRecords());
}
foreach ($object->drawPathList->commands as $path){
++$drawCalls;
$drawItems += count($path->commands->getRecords());
}
$filteredRendered->add($object);
}
echo "=== frame ".$frame->getFrameNumber()."/".$processor->getExpectedFrameCount()." ~ $frameOffset : Depth count: " . count($frame->getFrame()->getDepthMap()) . " :: Object count: " . count($filteredRendered->getObjects()) ." :: Paths: $drawCalls draw calls, $drawItems items :: Filtered: $filteredObjects :: Clips: $clipCalls draw calls, $clipItems items" . PHP_EOL;
if($fromFrame !== null){
if($frame->getFrameNumber() < $fromFrame){
continue;
}else{
foreach ($rendered->getObjects() as $object){
$count = 0;
foreach ($object->drawPathList->commands as $i => $path){
foreach ($path->commands->getRecords() as $j => $record){
$v = [
"objectId" => $object->objectId,
"depth" => implode(".", $object->getDepth()) . "[$i][$j]",
"transform" => $object->matrixTransform->toArray(false)
];
if($record instanceof \swf2ass\MoveRecord or $record instanceof \swf2ass\LineRecord){
$v["vector"] = $record->start->toArray();
$testVectors[] = $v;
$v["vector"] = $record->to->toArray();
$testVectors[] = $v;
}else if($record instanceof \swf2ass\QuadraticCurveRecord){
$v["vector"] = $record->start->toArray();
$testVectors[] = $v;
$v["vector"] = $record->control->toArray();
$testVectors[] = $v;
$v["vector"] = $record->anchor->toArray();
$testVectors[] = $v;
}
break;
}
break;
}
}
}
}
foreach ($assRenderer->renderFrame($frame, $filteredRendered) as $line){
fwrite($fp, $line . "\n");
}
if($frame->getFrameNumber() > 0 and $frame->getFrameNumber() % $keyFrameInterval === 0){
foreach ($assRenderer->flush($frame) as $line){
fwrite($fp, $line . "\n");
}
}
if($frameEnd !== null and $frame->getFrameNumber() >= $frameEnd){
break;
}
}
foreach ($assRenderer->flush($lastFrame) as $line){
fwrite($fp, $line . "\n");
}
if($processor->getAudio() !== null and $processor->getAudio()->getFormat() === \swf2ass\AudioStream::FORMAT_MP3){
$audioFp = fopen($argv[2] . ".mp3", "w+");
fwrite($audioFp, $processor->getAudio()->getAudioData());
fclose($audioFp);
}
}
if(count($testVectors) > 0){
file_put_contents($argv[2] . ".test.json", json_encode($testVectors, JSON_PRETTY_PRINT));
}
fclose($fp);