Morph shapes, proper ASS transforms, known signatures

This commit is contained in:
DataHoarder 2022-01-05 22:19:15 +01:00
parent f1196e854f
commit 0b5e7f8635
24 changed files with 948 additions and 413 deletions

View file

@ -15,6 +15,8 @@
"ext-zlib": "*",
"ext-gmp": "*",
"markrogoyski/math-php": "2.*",
"kudm761/martinez-rueda-php": "^0.1.2"
"kudm761/martinez-rueda-php": "^0.1.2",
"ext-json": "*",
"ext-decimal": "*"
}
}

View file

@ -26,6 +26,8 @@ namespace swf;
////////////////////////////////////////////////////////////////////////////////
// Basic IO
////////////////////////////////////////////////////////////////////////////////
use swf2ass\math\RealNumber;
class SWFio {
public $b; // Byte array (file contents)
public $bytePos; // Byte position
@ -83,19 +85,19 @@ class SWFio {
}
// Bit values
public function collectFB($num) { //XXX NOT SURE - NEEDS FIX
public function collectFB($num) : RealNumber { //XXX NOT SURE - NEEDS FIX
$ret = $this->collectBits($num);
if (($ret & (1 << ($num - 1))) == 0) {
// Positive
$hi = ($ret >> 16) & 0xffff;
$lo = $ret & 0xffff;
$ret = $hi + $lo / 65536.0;
$ret = (new RealNumber($lo))->divide(65536)->add($hi);
} else {
// Negative
$ret = (1 << $num) - $ret;
$hi = ($ret >> 16) & 0xffff;
$lo = $ret & 0xffff;
$ret = -($hi + $lo / 65536.0);
$ret = (new RealNumber($lo))->divide(65536)->add($hi)->negate();
}
// echo sprintf("collectFB, num is %d, will return [0x%04x]\n", $num, $ret);
return $ret;
@ -114,7 +116,7 @@ class SWFio {
}
// Fixed point numbers
public function collectFixed8() {
public function collectFixed8() { //TODO: change to RealNumber
$lo0 = $lo = $this->collectUI8();
$hi0 = $hi = $this->collectUI8();
if ($hi < 128) {
@ -132,7 +134,7 @@ class SWFio {
public function collectFixed() {
$lo = $this->collectUI16();
$hi = $this->collectUI16();
$ret = $hi + $lo / 65536.0;
$ret = (new RealNumber($lo))->divide(65536)->add($hi);;
// echo sprintf("collectFixed hi=[0x%X], lo=[0x%X], return [%s]\n", $hi, $lo, $ret);
return $ret;
}

View file

@ -23,7 +23,7 @@ class BitmapDefinition implements ObjectDefinition {
return $this->id;
}
public function getShapeList(): DrawPathList {
public function getShapeList(?float $ratio): DrawPathList {
return $this->drawPathList;
}

View file

@ -15,9 +15,9 @@ class DrawPathList {
return new DrawPathList(array_merge($this->commands, $b->commands));
}
public static function fromArray(array $element, StyleList $currentStyles): DrawPathList {
public static function fromArray(array $element, StyleList $currentStyles, ?array $otherElement = null): DrawPathList {
$converter = new ShapeConverter($element, $currentStyles);
$converter = new ShapeConverter($element, $currentStyles, $otherElement);
return $converter->commands;
}

View file

@ -4,20 +4,47 @@ namespace swf2ass;
use MathPHP\LinearAlgebra\MatrixFactory;
use MathPHP\LinearAlgebra\NumericMatrix;
use swf2ass\math\RealNumber;
use swf2ass\math\RealMatrix;
class MatrixTransform {
private NumericMatrix $matrix;
private RealMatrix $matrix;
public function __construct(?Vector2 $scale, ?Vector2 $rotateSkew, ?Vector2 $translation) {
$this->matrix = MatrixFactory::createNumeric([
[$scale !== null ? $scale->x : 1, $rotateSkew !== null ? $rotateSkew->y : 0, 0],
[$rotateSkew !== null ? $rotateSkew->x : 0, $scale !== null ? $scale->y : 1, 0],
[$translation !== null ? $translation->x : 0, $translation !== null ? $translation->y : 0, 1]
$this->matrix = new RealMatrix([
[$scale !== null ? $scale->x : 1 /* a */, /* b */ $rotateSkew !== null ? $rotateSkew->x : 0, $translation !== null ? $translation->x : 0],
[$rotateSkew !== null ? $rotateSkew->y : 0 /* c */, /* d */ $scale !== null ? $scale->y : 1, $translation !== null ? $translation->y : 0],
[0, 0, 1]
]);
}
public static function fromMatrix(RealMatrix $matrix) : MatrixTransform{
$o = new MatrixTransform(null, null, null);
$o->matrix = $matrix;
return $o;
}
public static function fromArray(array $array) : MatrixTransform{
return new MatrixTransform(new Vector2($array[0][0], $array[1][1]), new Vector2($array[1][0], $array[0][1]), new Vector2($array[0][2], $array[1][2]));
}
public function toArray(bool $translation = true) : array{
if($translation){
return [
[$this->get_a()->toFixed(), $this->get_b()->toFixed(), $this->get_tx()->toFixed()],
[$this->get_c()->toFixed(), $this->get_d()->toFixed(), $this->get_ty()->toFixed()],
[0, 0, 1]
];
}else{
return [
[$this->get_a()->toFixed(), $this->get_b()->toFixed(), 0],
[$this->get_c()->toFixed(), $this->get_d()->toFixed(), 0],
[0, 0, 1]
];
}
}
public static function scale(Vector2 $scale) : MatrixTransform{
return new MatrixTransform($scale, null, null);
}
@ -47,35 +74,35 @@ class MatrixTransform {
public function combine(MatrixTransform $other): MatrixTransform {
$result = clone $this;
$result->matrix = $other->matrix->multiply($this->matrix);
$result->matrix = $this->matrix->multiply($other->matrix);
return $result;
}
public function get_a(){
public function get_a() : RealNumber{
return $this->matrix->get(0, 0);
}
public function get_b(){
public function get_b() : RealNumber{
return $this->matrix->get(0, 1);
}
public function get_c(){
public function get_c() : RealNumber{
return $this->matrix->get(1, 0);
}
public function get_d(){
public function get_d() : RealNumber{
return $this->matrix->get(1, 1);
}
public function get_e(){
public function get_tx() : RealNumber{
return $this->matrix->get(2, 0);
}
public function get_f(){
public function get_ty() : RealNumber{
return $this->matrix->get(2, 1);
}
public function getMatrix() : NumericMatrix{
public function getMatrix() : RealMatrix{
return $this->matrix;
}
@ -85,11 +112,11 @@ class MatrixTransform {
public function applyToVector(Vector2 $vector, bool $applyTranslation = true): Vector2 {
if($applyTranslation){
$result = MatrixFactory::createFromRowVector([$vector->x, $vector->y, 1])->multiply($this->matrix);
$result = $this->matrix->multiply(MatrixFactory::createFromColumnVector([$vector->x, $vector->y, 1]));
}else{
$result = MatrixFactory::createFromRowVector([$vector->x, $vector->y])->multiply($this->matrix->submatrix(0, 0, 1, 1));
$result = $this->matrix->submatrix(0, 0, 1, 1)->multiply(MatrixFactory::createFromColumnVector([$vector->x, $vector->y]));
}
return new Vector2($result->get(0, 0), $result->get(0, 1));
return new Vector2($result->get(0, 0)->toFloat(), $result->get(1, 0)->toFloat());
}
public function applyToShape(Shape $shape, bool $applyTranslation = true): Shape {
@ -105,7 +132,7 @@ class MatrixTransform {
return $this->matrix->isEqual($other->matrix);
}
static function fromArray(array $element): MatrixTransform {
static function fromSWFArray(array $element): MatrixTransform {
return new MatrixTransform(
isset($element["scaleX"]) ? new Vector2($element["scaleX"], $element["scaleY"]) : null,
isset($element["rotateSkew0"]) ? new Vector2($element["rotateSkew1"], $element["rotateSkew0"]) : null,

View file

@ -0,0 +1,119 @@
<?php
namespace swf2ass;
class MorphShapeDefinition implements ObjectDefinition {
public int $id;
public Rectangle $startBounds;
public Rectangle $endBounds;
public DrawPathList $startShapeList;
public DrawPathList $endShapeList;
public float $ratio = 0;
public function __construct(int $id, Rectangle $startBounds, Rectangle $endBounds, DrawPathList $startShapeList, DrawPathList $endShapeList) {
$this->id = $id;
$this->startBounds = $startBounds;
$this->endBounds = $endBounds;
$this->startShapeList = $startShapeList;
$this->endShapeList = $endShapeList;
if(count($this->startShapeList->commands) !== count($this->endShapeList->commands)){
throw new \Exception("Morph command count is different: start " . count($this->startShapeList->commands) . " != end " . count($this->endShapeList->commands));
}
}
public function getObjectId(): int {
return $this->id;
}
public function getShapeList(?float $ratio): DrawPathList {
//TODO: cache shapes by ratio
if($ratio === null or abs($ratio) < Constants::EPSILON){
return $this->startShapeList;
}
if(abs($ratio - 1.0) < Constants::EPSILON){
return $this->endShapeList;
}
$drawPathList = new DrawPathList();
foreach ($this->startShapeList->commands as $i => $c1){
$c2 = $this->endShapeList->commands[$i];
$records1 = $c1->commands->getRecords();
$records2 = $c2->commands->getRecords();
$shape = new Shape();
foreach ($records1 as $j => $r1){
$r2 = $records2[$j];
//Convert line records that morph into/from curves
if ($r1 instanceof LineRecord and $r2 instanceof QuadraticCurveRecord){
$r1 = QuadraticCurveRecord::fromLineRecord($r1);
}else if ($r2 instanceof LineRecord and $r1 instanceof QuadraticCurveRecord){
$r2 = QuadraticCurveRecord::fromLineRecord($r2);
}
if($r1 instanceof LineRecord and $r2 instanceof LineRecord){
$shape->addRecord(new LineRecord(self::lerpVector2($r1->to, $r2->to, $ratio), self::lerpVector2($r1->start, $r2->start, $ratio)));
}else if($r1 instanceof QuadraticCurveRecord and $r2 instanceof QuadraticCurveRecord){
$shape->addRecord(new QuadraticCurveRecord(self::lerpVector2($r1->control, $r2->control, $ratio), self::lerpVector2($r1->anchor, $r2->anchor, $ratio), self::lerpVector2($r1->start, $r2->start, $ratio)));
}else if($r1 instanceof MoveRecord and $r2 instanceof MoveRecord){
$shape->addRecord(new MoveRecord(self::lerpVector2($r1->to, $r2->to, $ratio), self::lerpVector2($r1->start, $r2->start, $ratio)));
}else{
var_dump($r1);
var_dump($r2);
throw new \Exception();
}
}
//TODO: morph styles
if($c1->style instanceof FillStyleRecord and $c2->style instanceof FillStyleRecord){
if($c1->style->fill instanceof Color){
$drawPathList->commands[] = DrawPath::fill(new FillStyleRecord(self::lerpColor($c1->style->fill, $c2->style->fill, $ratio)), $shape);
}else if($c1->style->fill instanceof Gradient){
//TODO: proper gradients
$drawPathList->commands[] = DrawPath::fill(new FillStyleRecord(self::lerpColor($c1->style->fill->getItems()[0]->color, $c2->style->fill->getItems()[0]->color, $ratio)), $shape);
}else{
var_dump($c1->style);
var_dump($c2->style);
throw new \Exception();
}
}else if($c1->style instanceof LineStyleRecord and $c2->style instanceof LineStyleRecord){
$drawPathList->commands[] = DrawPath::stroke(new LineStyleRecord(self::lerpInteger($c1->style->width, $c2->style->width, $ratio), self::lerpColor($c1->style->color, $c2->style->color, $ratio)), $shape, $c1->is_closed);
}else{
var_dump($c1->style);
var_dump($c2->style);
throw new \Exception();
}
}
return $drawPathList;
}
private static function lerpInteger(int $start, int $end, float $ratio): int {
return $start + ($end - $start) * $ratio;
}
private static function lerpFloat(float $start, float $end, float $ratio): float {
return $start + ($end - $start) * $ratio;
}
private static function lerpVector2(Vector2 $start, Vector2 $end, float $ratio): Vector2 {
return $start->add($end->sub($start)->multiply($ratio));
}
private static function lerpColor(Color $start, Color $end, float $ratio): Color {
return new Color(self::lerpInteger($start->r, $end->r, $ratio), self::lerpInteger($start->g, $end->g, $ratio), self::lerpInteger($start->b, $end->b, $ratio), self::lerpInteger($start->alpha, $end->alpha, $ratio));
}
static function fromArray(array $element): MorphShapeDefinition {
$styles = MorphStyleList::fromArray($element);
$start = DrawPathList::fromArray($element["startEdges"], $styles->getStartStyleList());
$end = DrawPathList::fromArray($element["endEdges"], $styles->getEndStyleList(), $element["startEdges"]);
return new MorphShapeDefinition($element["characterId"], Rectangle::fromArray($element["startBounds"]), Rectangle::fromArray($element["endBounds"]), $start, $end);
}
}

152
src/MorphStyleList.php Normal file
View file

@ -0,0 +1,152 @@
<?php
namespace swf2ass;
class MorphStyleList {
/** @var FillStyleRecord[] */
public array $startFillStyles;
/** @var FillStyleRecord[] */
public array $endFillStyles;
/** @var LineStyleRecord[] */
public array $startLineStyles;
/** @var LineStyleRecord[] */
public array $endLineStyles;
/**
* @param FillStyleRecord[] $startFillStyles
* @param FillStyleRecord[] $endFillStyles
* @param LineStyleRecord[] $startLineStyles
* @param LineStyleRecord[] $endLineStyles
*/
public function __construct(array $startFillStyles = [], array $endFillStyles = [], array $startLineStyles = [], array $endLineStyles = []) {
$this->startFillStyles = $startFillStyles;
$this->endFillStyles = $endFillStyles;
$this->startLineStyles = $startLineStyles;
$this->endLineStyles = $endLineStyles;
}
/**
* @param array $node
* @return FillStyleRecord[]
* @throws \Exception
*/
public static function parseFillStyleRecord(array $node): array {
switch ($node["fillStyleType"]) {
case 0x00: // Solid fill
return [new FillStyleRecord(Color::fromArray($node["startColor"])), new FillStyleRecord(Color::fromArray($node["endColor"]))];
break;
case 0x10: // Linear gradient fill
//TODO
return [new FillStyleRecord(new Color(0, 0, 0, 20)), new FillStyleRecord(new Color(0, 0, 255, 20))];
return [new FillStyleRecord(LinearGradient::fromArray($node["gradient"], MatrixTransform::fromSWFArray($node["startGradientMatrix"]))), new FillStyleRecord(LinearGradient::fromArray($node["gradient"], MatrixTransform::fromSWFArray($node["endGradientMatrix"])))];
break;
case 0x12: // Radial gradient fill
//TODO
return [new FillStyleRecord(new Color(0, 0, 0, 20)), new FillStyleRecord(new Color(0, 0, 255, 20))];
return [new FillStyleRecord(RadialGradient::fromArray($node["gradient"], MatrixTransform::fromSWFArray($node["startGradientMatrix"]))), new FillStyleRecord(RadialGradient::fromArray($node["gradient"], MatrixTransform::fromSWFArray($node["endGradientMatrix"])))];
break;
case 0x13: // Focal gradient fill
//TODO
return [new FillStyleRecord(new Color(0, 0, 0, 20)), new FillStyleRecord(new Color(0, 0, 255, 20))];
return [new FillStyleRecord(FocalGradient::fromArray($node["focalGradient"], MatrixTransform::fromSWFArray($node["startGradientMatrix"]))), new FillStyleRecord(FocalGradient::fromArray($node["focalGradient"], MatrixTransform::fromSWFArray($node["endGradientMatrix"])))];
break;
case 0x40: // Repeating bitmap fill
case 0x41: // Clipped bitmap fill
case 0x42: // Non-smoothed repeating bitmap
case 0x43: // Non-smoothed clipped bitmap
var_dump($node);
return $node["bitmapId"] === 65535 ? [new FillStyleRecord(new Color(0, 0, 0, 0)), new FillStyleRecord(new Color(0, 0, 0, 0))] : [new FillStyleRecord(new Color(0, 0, 0, 20)), new FillStyleRecord(new Color(255, 0, 0, 20))];
break;
default:
var_dump($node);
throw new \Exception("Unknown style " . $node["type"]);
}
}
public function getStartStyleList() : StyleList{
return new StyleList($this->startFillStyles, $this->startLineStyles);
}
public function getEndStyleList() : StyleList{
return new StyleList($this->endFillStyles, $this->endLineStyles);
}
public static function fromArray(array $element): MorphStyleList {
$startFillStyles = [];
$endFillStyles = [];
$startLineStyles = [];
$endLineStyles = [];
foreach ($element["morphFillStyles"] as $node) {
$record = self::parseFillStyleRecord($node);
$startFillStyles[] = $record[0];
$endFillStyles[] = $record[1];
}
foreach ($element["morphLineStyles"] as $node) {
$startColor = isset($node["startColor"]) ? Color::fromArray($node["startColor"]) : null;
$endColor = isset($node["endColor"]) ? Color::fromArray($node["endColor"]) : null;
//TODO: fill flag
if ($startColor === null) {
var_dump($node);
//TODO
/*
if(isset($node["fillType"])){
$color = self::parseFillStyleRecord($node["fillType"])->fill;
if($color instanceof Gradient){
$color = $color->getItems()[0]->color;
}
}
*/
}
if ($endColor === null) {
var_dump($node);
//TODO
/*
if(isset($node["fillType"])){
$color = self::parseFillStyleRecord($node["fillType"])->fill;
if($color instanceof Gradient){
$color = $color->getItems()[0]->color;
}
}
*/
}
//TODO: any reason for max(Constants::TWIP_SIZE)?
$startLineStyles[] = new LineStyleRecord(max(Constants::TWIP_SIZE, $node["startWidth"]), $startColor);
$endLineStyles[] = new LineStyleRecord(max(Constants::TWIP_SIZE, $node["endWidth"]), $startColor);
}
return new MorphStyleList($startFillStyles, $endFillStyles, $startLineStyles, $endLineStyles);
}
/**
* @param $i
* @return FillStyleRecord|null
*/
public function getStartFillStyle($i): ?FillStyleRecord {
return $this->startFillStyles[$i] ?? null;
}
/**
* @param $i
* @return FillStyleRecord|null
*/
public function getEndFillStyle($i): ?FillStyleRecord {
return $this->endFillStyles[$i] ?? null;
}
/**
* @param $i
* @return LineStyleRecord|null
*/
public function getStartLineStyle($i): ?LineStyleRecord {
return $this->startLineStyles[$i] ?? null;
}
/**
* @param $i
* @return LineStyleRecord|null
*/
public function getEndLineStyle($i): ?LineStyleRecord {
return $this->endLineStyles[$i] ?? null;
}
}

View file

@ -6,5 +6,5 @@ interface ObjectDefinition {
public function getObjectId(): int;
public function getShapeList(): DrawPathList;
public function getShapeList(float $ratio): DrawPathList;
}

View file

@ -2,9 +2,6 @@
namespace swf2ass;
use MathPHP\LinearAlgebra\Matrix;
use MathPHP\LinearAlgebra\MatrixFactory;
/*
* Contains adapted code from http://antigrain.com/research/adaptive_bezier/index.html
* Anti-Grain Geometry (AGG) - Version 2.5
@ -64,6 +61,15 @@ class QuadraticCurveRecord implements Record {
return new QuadraticCurveRecord($control, $anchor, $cursor);
}
public static function fromLineRecord(LineRecord $l): QuadraticCurveRecord {
$delta = $l->to->sub($l->start)->divide(2);
return new QuadraticCurveRecord(
$l->start->add($delta),
$l->start->add($delta->multiply(2)),
$l->start
);
}
/**
* @return LineRecord[]
*/

View file

@ -21,10 +21,6 @@ class RenderedObject {
$this->clip = $clip;
}
public function getShape() : Shape{
}
/**
* @return int[]
*/

View file

@ -42,6 +42,11 @@ class SWFTreeProcessor {
}
switch ($node["tagType"]) {
case "DefineMorphShape":
case "DefineMorphShape2":
$shape = MorphShapeDefinition::fromArray($node);
$this->objects->add($shape);
break;
case "DefineShape":
case "DefineShape2":
case "DefineShape3":
@ -75,12 +80,14 @@ class SWFTreeProcessor {
break;
case "DefineBitsLossless":
case "DefineBitsLossless2":
break; //TODO
$bitmap = BitmapDefinition::fromArray($node);
$this->objects->add($bitmap);
break;
case "DefineBitsJPEG2":
case "DefineBitsJPEG3":
break; //TODO
$bitmap = JPEGBitmapDefinition::fromArray($node);
$this->objects->add($bitmap);
break;
@ -107,11 +114,11 @@ class SWFTreeProcessor {
$this->layout->remove($depth);
break;
}
//TODO: ratio, which also seems to exists for Sprites
$ratio = isset($node["ratio"]) ? $node["ratio"] / 65535 : null;
$transform = isset($node["matrix"]) ? MatrixTransform::fromArray($node["matrix"]) : null;
$transform = isset($node["matrix"]) ? MatrixTransform::fromSWFArray($node["matrix"]) : null;
@ -127,12 +134,16 @@ class SWFTreeProcessor {
if ($colorTransform !== null) {
$currentObject->setColorTransform($colorTransform);
}
if($ratio !== null){
$currentObject->setRatio($ratio);
}
break;
}
$view = $clipDepth !== null ? new ClippingViewLayout($clipDepth, $objectID, $object, $this->layout) : new ViewLayout($objectID, $object, $this->layout);
$view = $clipDepth !== null ? new ClippingViewLayout($clipDepth, $objectID, $object, $this->layout, $ratio) : new ViewLayout($objectID, $object, $this->layout, $ratio);
$view->setMatrixTransform($transform);
$view->setColorTransform($colorTransform);
$view->setRatio($ratio);
if ($replace) {
$this->layout->replace($depth, $view);

View file

@ -49,7 +49,7 @@ class Shape {
var_dump((new \swf2ass\ass\drawTag(new Shape($self->getRecords()), 1))->encode(new \swf2ass\ass\ASSLine(), 1));
var_dump((new Shape($other->getRecords()))->getArea());
var_dump((new \swf2ass\ass\drawTag(new Shape($other->getRecords()), 1))->encode(new \swf2ass\ass\ASSLine(), 1));
//fgets(STDIN);
fgets(STDIN);
return [$this]; //TODO: fix this breakage, some clips being overlapping shapes????
}
}

View file

@ -19,7 +19,7 @@ class ShapeConverter {
public DrawPathList $commands;
public function __construct(array $element, StyleList $currentStyles) {
public function __construct(array $element, StyleList $currentStyles, ?array $otherElement = null) {
$this->styles = $currentStyles;
$this->position = new Vector2(0, 0);
$this->fills = new PendingPathMap();
@ -28,7 +28,47 @@ class ShapeConverter {
$finished = false;
foreach ($element as $node) {
reset($element);
if($otherElement !== null){
reset($otherElement);
}
do{
$otherNode = current($otherElement ?? []);
if($otherNode === false){
$otherNode = null;
}
$node = current($element);
if($node === false){
break;
}
$advanceNode = true;
if($otherNode !== null){
if($otherNode["type"] === "StyleChangeRecord"){
if($node["type"] === "StyleChangeRecord"){
//Inject style record entries
foreach (["stateNewStyles", "stateLineStyle", "stateFillStyle0", "stateFillStyle1", "fillStyles", "lineStyles", "fillStyle0", "fillStyle1", "lineStyle"] as $k){
if(isset($otherNode[$k])){
$node[$k] = $otherNode[$k];
}
}
}else{
//Inject style record
$node = $otherNode;
$node["stateMoveTo"] = 0;
unset($node["moveDeltaX"]);
unset($node["moveDeltaY"]);
$advanceNode = false;
}
}
next($otherElement);
}
if($advanceNode){
next($element);
}
if($finished){
var_dump($node);
throw new \Exception("More paths after end");
@ -97,7 +137,7 @@ class ShapeConverter {
} else if ($node["type"] === "EndShapeRecord") {
$finished = true;
}
}
}while(true);
$this->flush_layer();
}

View file

@ -17,7 +17,7 @@ class ShapeDefinition implements ObjectDefinition {
return $this->id;
}
public function getShapeList(): DrawPathList {
public function getShapeList(?float $ratio): DrawPathList {
return $this->shapeList;
}

View file

@ -24,7 +24,7 @@ class SpriteDefinition implements MultiFrameObjectDefinition {
return $this->id;
}
public function getShapeList(): DrawPathList {
public function getShapeList(?float $ratio): DrawPathList {
$list = new DrawPathList();
foreach ($this->frames[$this->frameCounter]->render(0, [], null, null)->getObjects() as $object) {
$list = $list->merge($object->drawPathList);

View file

@ -9,7 +9,11 @@ class StyleList {
/** @var LineStyleRecord[] */
public array $lineStyles;
public function __construct($fillStyles = [], $lineStyles = []) {
/**
* @param FillStyleRecord[] $fillStyles
* @param LineStyleRecord[] $lineStyles
*/
public function __construct(array $fillStyles = [], array $lineStyles = []) {
$this->fillStyles = $fillStyles;
$this->lineStyles = $lineStyles;
}
@ -20,13 +24,13 @@ class StyleList {
return new FillStyleRecord(Color::fromArray($node["color"]));
break;
case 0x10: // Linear gradient fill
return new FillStyleRecord(LinearGradient::fromArray($node["gradient"], MatrixTransform::fromArray($node["matrix"])));
return new FillStyleRecord(LinearGradient::fromArray($node["gradient"], MatrixTransform::fromSWFArray($node["matrix"])));
break;
case 0x12: // Radial gradient fill
return new FillStyleRecord(RadialGradient::fromArray($node["gradient"], MatrixTransform::fromArray($node["matrix"])));
return new FillStyleRecord(RadialGradient::fromArray($node["gradient"], MatrixTransform::fromSWFArray($node["matrix"])));
break;
case 0x13: // Focal gradient fill
return new FillStyleRecord(FocalGradient::fromArray($node["focalGradient"], MatrixTransform::fromArray($node["matrix"])));
return new FillStyleRecord(FocalGradient::fromArray($node["focalGradient"], MatrixTransform::fromSWFArray($node["matrix"])));
break;
case 0x40: // Repeating bitmap fill
case 0x41: // Clipped bitmap fill

View file

@ -84,4 +84,8 @@ class Vector2 {
public function toPixel($twipSize = Constants::TWIP_SIZE): Vector2 {
return $this->divide($twipSize);
}
public function __toString() {
return "Vector2({$this->x}, {$this->y})";
}
}

View file

@ -16,6 +16,8 @@ class ViewLayout {
private int $objectId;
private ?float $ratio = null;
public function __construct(int $objectId, ?ObjectDefinition $object, ?ViewLayout $parent = null) {
$this->objectId = $objectId;
if ($object !== null and $objectId !== $object->getObjectId()) {
@ -98,12 +100,20 @@ class ViewLayout {
$this->colorTransform = $transform;
}
public function getRatio() : ?float{
return $this->ratio;
}
public function setRatio(?float $ratio){
$this->ratio = $ratio;
}
public function nextFrame(ActionList $actionList): ViewFrame {
if ($this->object !== null) {
if ($this->object instanceof MultiFrameObjectDefinition) {
$frame = $this->object->nextFrame();
} else {
$frame = new ViewFrame($this->getObjectId(), $this->object->getShapeList());
$frame = new ViewFrame($this->getObjectId(), $this->object->getShapeList($this->ratio));
}
} else {
$frame = new ViewFrame($this->getObjectId(), null);

View file

@ -3,16 +3,18 @@
namespace swf2ass\ass;
use swf2ass\FrameInformation;
use swf2ass\MatrixTransform;
use swf2ass\Rectangle;
use swf2ass\RenderedFrame;
use swf2ass\RenderedObject;
use swf2ass\Vector2;
class ASSRenderer {
private ?string $header;
/** @var ASSLine[] */
private array $runningBuffer = [];
private array $settings = [];
private array $settings;
public function getSetting($name, $default = null){
@ -20,17 +22,17 @@ class ASSRenderer {
}
public function __construct(float $frameRate, Rectangle $viewPort, array $settings = []) {
$display = $viewPort->toPixel();
$width = $display->getWidth();
$height = $display->getHeight();
$ar = $width / $height;
$this->settings = $settings;
$display = $viewPort->toPixel();
$width = $display->getWidth() * $this->getSetting("videoScaleMultiplier", 1);
$height = $display->getHeight() * $this->getSetting("videoScaleMultiplier", 1);
$ar = $width / $height;
if(($frameRate * 2) <= 60){
$frameRate *= 2;
}
$timerPrecision = str_replace(".", ",", sprintf("%.4F", (100 / $this->getSetting("timerSpeed", 100)) * 100));
$timerPrecision = sprintf("%.4F", (100 / $this->getSetting("timerSpeed", 100)) * 100);
$this->header = <<<ASSHEADER
[Script Info]
@ -73,7 +75,12 @@ ASSHEADER;
$runningBuffer = [];
$scale = MatrixTransform::scale(new Vector2($this->getSetting("videoScaleMultiplier", 1), $this->getSetting("videoScaleMultiplier", 1)));
foreach ($objects as $object) {
$object = clone $object;
$object->matrixTransform = $scale->combine($object->matrixTransform);
$depth = $object->getDepth();
/** @var ASSLine $tagsToTransition */

View file

@ -5,13 +5,13 @@ namespace swf2ass\ass;
use swf2ass\Constants;
use swf2ass\LineStyleRecord;
use swf2ass\StyleRecord;
use swf2ass\Vector2;
class borderTag implements ASSStyleTag {
/** @var int|float */
private $size;
private Vector2 $size;
public function __construct($size = 0) {
public function __construct(Vector2 $size) {
$this->size = $size;
}
@ -20,17 +20,21 @@ class borderTag implements ASSStyleTag {
}
public function encode(ASSLine $line, float $frameDurationMs): string {
return "\\bord" . round($this->size, 2);
if($this->size->x === $this->size->y){
return sprintf("\\bord%.02F", $this->size->x);
}else{
return sprintf("\\xbord%.02F\\ybord%.02F", $this->size->x, $this->size->y);
}
}
public static function fromStyleRecord(StyleRecord $record): ?borderTag {
if ($record instanceof LineStyleRecord) {
return new borderTag($record->width / Constants::TWIP_SIZE);
return new borderTag(new Vector2($record->width / Constants::TWIP_SIZE, $record->width / Constants::TWIP_SIZE));
}
return new borderTag();
return new borderTag(new Vector2(0, 0));
}
public function equals(ASSTag $tag): bool {
return $tag instanceof $this and abs($this->size - $tag->size) <= Constants::EPSILON;
return $tag instanceof $this and $this->size->equals($tag->size);
}
}

View file

@ -3,6 +3,7 @@
namespace swf2ass\ass;
use swf2ass\Constants;
use swf2ass\math\RealNumber;
use swf2ass\MatrixTransform;
use swf2ass\Vector2;
@ -11,8 +12,10 @@ class matrixTransformTag implements ASSPositioningTag {
private scaleTag $scale;
private rotationTag $rotation;
private shearingTag $shear;
private MatrixTransform $transform;
public function __construct(Vector2 $scale, $rotationX, $rotationY, $rotationZ, $shearX, $shearY) {
public function __construct(MatrixTransform $transform, Vector2 $scale, $rotationX, $rotationY, $rotationZ, $shearX, $shearY) {
$this->transform = $transform;
$this->scale = new scaleTag($scale);
$this->rotation = new rotationTag($rotationX, $rotationY, $rotationZ);
$this->shear = new shearingTag(new Vector2($shearX, $shearY));
@ -24,257 +27,20 @@ class matrixTransformTag implements ASSPositioningTag {
}
public function encode(ASSLine $line, float $frameDurationMs): string {
return $this->scale->encode($line, $frameDurationMs) . $this->rotation->encode($line, $frameDurationMs) . $this->shear->encode($line, $frameDurationMs);
return sprintf("\\matrix(%.05F,%.05F,%.05F,%.05F,%.02F,%.02F)", $this->transform->get_a()->toFloat(), $this->transform->get_b()->toFloat(), $this->transform->get_c()->toFloat(), $this->transform->get_d()->toFloat(), $this->transform->get_tx()->divide(Constants::TWIP_SIZE)->toFloat(), $this->transform->get_ty()->divide(Constants::TWIP_SIZE)->toFloat())
. $this->scale->encode($line, $frameDurationMs) . $this->rotation->encode($line, $frameDurationMs) . $this->shear->encode($line, $frameDurationMs);
}
public function equals(ASSTag $tag): bool {
return $tag instanceof $this and $this->scale->equals($tag->scale) and $this->rotation->equals($tag->rotation) and $this->shear->equals($tag->shear);
return $tag instanceof $this and $this->transform->equals($tag->transform) and $this->scale->equals($tag->scale) and $this->rotation->equals($tag->rotation) and $this->shear->equals($tag->shear);
}
public static function fromMatrixTransform(MatrixTransform $transform): ?matrixTransformTag {
/*
!! Everything here is untested and best check for errors in the calculations !!
Choose \fay, \fax, \fscx and \fscy to create a (almost) general 2D-transform-matrix
(Rotations are sometimes also used)
(ASS Transform order: combined faxy, scale, frz, frx, fry)
If shearOrigin == scaleOrigin == rotationOrigin == (0,0)^T
(which iirc is true for drawings with \org(0,0)?),
and there are no run splits, and if assuming \frz0 for now,
ASS will effectively use the following transform matrix
| scale_x 0 | | 1 fax |
| | * | |
| 0 scale_y | | fay 1 |
| scale_x scale_x fax |
= | |
| scale_y fay scale_y |
Let's assume that's true.
Then given an arbitrary transform matrix M,
with known coefficients a, b, c, d \in \R
| a b |
M = | |
| c d |
we get:
I: scale_x = a
II: scale_y = d
III: scale_x fax = b
IV: scale_y fay = c
If (a = b = 0) or (c = d = 0), then scale_x = 0
or scale_y = 0 and all other values can be set to zero
as the event vanishes anyway.
If (a = 0 and b != 0) or (c != 0 and d = 0), then
we can't solve this as is directly. Ignore this
case for now, we'll later describe how to adapt the solution for this.
If scale_x = a != 0 != d = scale_y, it follows that
III => fax = b / scale_x
IV => fay = c / scale_y
But even in this case, there's one issue:
scale_x, scale_y may be negative,
but ASS only allows positive scale values.
To remedy this we'll use 3D rotations to emulate negative scale.
It is easy to see, that by themselves a
rotation around x by π, is equivalent to scale_y = -1 and
a rotation around y by π is equivalent to scale_x = -1.
If we use \fscx and \fscy to scale by the respective absolute values
and immediately follow this up by a -1-scale if needed the result
will be equivalent to a scale by the proper value.
In general this isn't so straightforward as ASS always applies rotations
in the order of z, x, y, but we chose rot_z = 0, resulting in it being a
dismissable identity operation.
If scale_x is negative, pass the absolute value to \fscx and add \fry180.
If scale_y is negative, pass the absolute value to \fscy and add \frx180.
ASS-3D-rotation matrices:
| 1 0 0 |
| |
Rx = | 0 cos(rot_x) sin(rot_x) |
| |
| 0 sin(rot_x) -cos(rot_x) |
| cos(rot_y) 0 sin(rot_y) |
| |
Ry = | 0 1 0 |
| |
| sin(rot_y) 0 -cos(rot_y) |
| cos(rot_z) -sin(rot_z) 0 |
| |
Rz = | sin(rot_z) cos(rot_z) 0 | (for choice of rot_z=0 identity matrix)
| |
| 0 0 1 |
(from: https://sourceforge.net/p/guliverkli2/code/HEAD/tree/src/subtitles/RTS.cpp#l148)
Using sgn_x, sgn_y \in {-1, 1} as the sign of the respective scales
and sc_x, sc_y \in { x >= 0 | x \in \R } as the absolute scale values,
all in all we get the following tags
\fry(-360*(sgn_x-1)) \frx(-360/(sgn_y-1)) \fscx(sc_x) \fscy(sc_y) \fax(fax) \fay(fay) \org(0,0)
TODO: ^ this is wrong, use just > If scale_x is negative, pass the absolute value to \fscx and add \fry180. ||| > If scale_y is negative, pass the absolute value to \fscy and add \frx180.
which results in the ASS transform matrix:
| sgn_x 0 0 | | 1 0 0 | | sc_x sc_x fax 0 |
| | | | | |
| 0 1 0 | * | 0 sgn_y 0 | * | sc_y fay sc_y 0 |
| | | | | |
| 0 0 -sgn_x | | 0 0 -sgn_y | | 0 0 1 |
| sgn_x 0 0 | | sc_x sc_x fax 0 |
| | | |
= | 0 sgn_y 0 | * | sc_y fay sc_y 0 |
| | | |
| 0 0 sgn_x*sgn_y | | 0 0 1 |
| sgn_x*sc_x sgn_x*sc_x fax 0 |
| |
= | sgn_y*sc_y fay sgn_y*sc_y 0 |
| |
| 0 0 sgn_x*sgn_y |
using sgn_x*sc_x = scale_x and sgn_y*sc_y = scale_y
| scale_x scale_x fax 0 |
| |
= | scale_y fay scale_y 0 |
| |
| 0 0 sgn_x*sgn_y |
| a b 0 |
| |
= | c d 0 |
| |
| 0 0 sgn_x*sgn_y |
Apart from flipping the sign of the z-coordinates when sgn_x != sgn_y
this is exactly the matrix we wanted.
Since 2D-glyphs and -drawings are initially placed at z=0,
this additional sign-flip is of no concern.
---
Now coming back to the case of (a = 0 and b != 0) or (c != 0 and d = 0):
To be able to solve this we also need to apply a z-Rotation:
1) If in each column there's at least one non-zero entry or one row is all zero
(together with case condition the latter means only b xor c is non-zero):
Use \frz90, being a row swap with a sign flip:
| 0 -1 |
\frz90 = | |
| 1 0 |
So our transform-matrix equation becomes:
| -scale_y fay -scale_y | | a b |
| | = | |
| scale_x scale_x fax | | c d |
Which can be solved as before for signed scales.
The additon of a z-Rotation means our "scale sign"-roations
no longer immediately follow the actual scale.
To compensate, I'm guessing swapping the sign of scale_x
and using \frx for sgn_x and \fry for sgn_y probably works.
(Again: untested)
2) Otherwise:
(If one column is zero and the other one has only non-zero entries)
2.1) a = 0 = c and b 0 d
| scale_x*cos(rz) - scale_y*fay*sin(rz) scale_x*fax*cos(rz) - scale_y*sin(rz) | | 0 b |
| | = | |
| scale_x*sin(rz) + scale_y*fay*cos(rz) scale_x*fax*sin(tz) + scale_y*cos(rz) | | 0 d |
I: scale_x*cos(rz) - scale_y*fay*sin(rz) = 0
II: scale_x*sin(rz) + scale_y*fay*cos(rz) = 0
III: scale_x*fax*cos(rz) - scale_y*sin(rz) = b
IV: scale_x*fax*sin(tz) + scale_y*cos(rz) = d
+ II² scale_x^2 = - scale_y^2 * fay^2 scale_x = 0 and (scale_y = 0 or fay = 0)
III' : -scale_y*sin(rz) = b
IV' : scale_y*cos(rz) = d
since b 0 d scale_y 0 fay = 0
scale_y = sqrt(b^2 + d^2)
rz = sin⁻¹(-b/scale_y)
Note: we can without loss of generality choose the positive solution for scale_y,
this will just change rz by π ( cos(rz+π) = -cos(rz) and sin(rz+π) = -sin(rz) )
Since scale_y > 0 and scale_x = 0 we do not need \frx and \fry to simulate a negative scale.
2.2) b = 0 = d and a 0 c
| scale_x*cos(rz) - scale_y*fay*sin(rz) scale_x*fax*cos(rz) - scale_y*sin(rz) | | a 0 |
| | = | |
| scale_x*sin(rz) + scale_y*fay*cos(rz) scale_x*fax*sin(tz) + scale_y*cos(rz) | | c 0 |
I: scale_x*fax*cos(rz) - scale_y*sin(rz) = 0
II: scale_x*fax*sin(tz) + scale_y*cos(rz) = 0
III: scale_x*cos(rz) - scale_y*fay*sin(rz) = a
IV: scale_x*sin(rz) + scale_y*fay*cos(rz) = b
+ II² scale_y^2 = - scale_x^2 * fax^2 scale_y = 0 and (scale_x = 0 or fax = 0)
III' : scale_x*cos(rz) = a
IV' : scale_x*sin(rz) = c
since a 0 c scale_x 0 fax = 0
scale_x = sqrt(a^2 + c^2)
rz = sin⁻¹(c/scale_x)
Note: we can without loss of generality choose the positive solution for scale_x,
since this will just change rz by π ( cos(rz+π) = -cos(rz) and sin(rz+π) = -sin(rz) )
Since scale_x > 0 and scale_y = 0 we do not need \frx and \fry to simulate a negative scale.
In both 2.1) and 2.2) we get one dimension scaled to zero, and if I didin't miss something
no contradictions in the equations. Meaning the event actually vanishes and is visually identical
to choosing all parameters zero. If interpolations are involved having the mathematically accurate
transform matrix might still be good though.
*/
$isZero = function ($v){
return abs($v) < Constants::EPSILON;
$isZero = function (RealNumber $v) : bool{
return $v->isZero(new RealNumber("0.00001")); ///TODO
};
$sign = function (float $a) : int {
if($a < 0){
return -1;
}else{
return 1;
}
};
$scale_x = $scale_y = $frx = $fry = $frz = $fax = $fay = new RealNumber(0);
$a = $transform->get_a();
$b = $transform->get_b();
@ -285,113 +51,79 @@ class matrixTransformTag implements ASSPositioningTag {
throw new \Exception("Invalid transform");
}
$scale_x = $scale_y = $frx = $fry = $frz = $fax = $fay = 0;
//TODO: WiP, fix rotations on negative scales
$i = sqrt($a * $a + $c * $c);
$j = sqrt($b * $b + $d * $d);
if($i >= $j){
$n = $i;
$frz = (180 / M_PI) * atan2($c, $a);
$scale_x = $n;
$scale_y = ($a * $d - $b * $c) / $n;
$fax = ($a * $b + $c * $d) / ($n * $n);
$fay = 0;
if($scale_y < 0){
$frx = 180;
}
}else{
$n = $j;
$frz = (180 / M_PI) * atan2(-$b, $d);
$scale_x = ($a * $d - $b * $c) / $n;
$scale_y = $n;
$fax = 0;
$fay = ($a * $b + $c * $d) / ($n * $n);
if($scale_x < 0){
$fry = 180;
}
}
/*
if(
!$isZero($a) and !$isZero($d)
//!(($isZero($a) and !$isZero($b)) or (!$isZero($c) and $isZero($d)))
!(
($isZero($a) and !$isZero($b))
or
($isZero($d) and !$isZero($c))
)
){
//Trivial case
$scale_x = $a;
$scale_y = $d;
$fax = $b / $scale_x;
$fay = $c / $scale_y;
$sgn_x = $sign($scale_x);
$sgn_y = $sign($scale_y);
$fax = $isZero($a) ? new RealNumber(0) : $b->divide($a);
$fay = $isZero($d) ? new RealNumber(0) : $c->divide($d);
if($sgn_x === -1){
$fry = 180;
if($scale_x->lesser(0)){
$fry = new RealNumber(180);
}
if($sgn_y === -1){
$frx = 180;
if($scale_y->lesser(0)){
$frx = new RealNumber(180);
}
$frx = -90 * ($sign($scale_y) - 1);
$fry = -90 * ($sign($scale_x) - 1);
}else if (
(($isZero($a) or $isZero($c)) and ($isZero($c) or $isZero($d)))
or
(($isZero($a) and $isZero($b)) !== ($isZero($c) and $isZero($d)))
}elseif (
!(
($isZero($b) and !$isZero($a))
or
($isZero($c) and !$isZero($d))
)
){
//Rowswap
$frz = new RealNumber(90);
$scale_x = $c;
$scale_y = -$b;
$fax = $d / $scale_x;
$fay = $a / -$scale_y;
$scale_y = $b->negate();
$scale_x *= -1;
$fax = $isZero($c) ? new RealNumber(0) : $d->divide($c);
$fay = $isZero($b) ? new RealNumber(0) : $a->divide($b);
$sgn_x = $sign($scale_x);
$sgn_y = $sign($scale_y);
if($sgn_y === -1){
$fry = 180;
if($scale_x->lesser(0)){
$frx = new RealNumber(180);
}
if($sgn_x === -1){
$frx = 180;
if($scale_y->lesser(0)){
$fry = new RealNumber(180);
}
$frx = -90 * ($sign($scale_x) - 1);
$fry = -90 * ($sign($scale_y) - 1);
$frz = 90;
}elseif(
($isZero($a) and $isZero($b))
and
(!$isZero($c) and !$isZero($d))
}elseif (
$isZero($a) and $isZero($c) and !$isZero($b) and !$isZero($d)
){
//This cases will all be "zero" but giving parameters allows interpolations
$scale_y = sqrt($b * $b + $d * $d);
$frz = (180 / M_PI) * asin(-$b / $scale_y);
}elseif(
($isZero($b) and $isZero($d))
and
(!$isZero($a) and !$isZero($c))
//Zero Col Left
$scale_y = new RealNumber(0);
$fax = new RealNumber(0);
$fay = new RealNumber(0);
$scale_x = $b->power(2)->add($d->power(2))->sqrt();
$frz = new RealNumber(atan($b->negate()->toFloat() / $d->toFloat()) * (180 / M_PI));
if($a->lesser(0)){ // atan always yields positive cos
$frz = $frz->add(180);
}
}elseif (
!$isZero($a) and !$isZero($c) and $isZero($b) and $isZero($d)
){
//This cases will all be "zero" but giving parameters allows interpolations
$scale_x = sqrt($a * $a + $c * $c);
$frz = (180 / M_PI) * asin($c / $scale_x);
}else{
echo $transform . "\n";
throw new \Exception("Invalid transform state");
//Zero Col Right
$scale_y = new RealNumber(0);
$fax = new RealNumber(0);
$fay = new RealNumber(0);
$scale_x = $a->power(2)->add($c->power(2))->sqrt();
$frz = new RealNumber(atan($c->toFloat() / $a->toFloat()) * (180 / M_PI));
if($a->lesser(0)){ // atan always yields positive cos
$style->frz = $frz->add(180);
}
}else {
throw new \Exception("Invalid transform state. This should not happen");
}
*/
$fscx = abs($scale_x) * 100;
$fscy = abs($scale_y) * 100;
$fscx = $scale_x->absolute()->multiply(100);
$fscy = $scale_y->absolute()->multiply(100);
return new matrixTransformTag(new Vector2($fscx, $fscy), $frx, $fry, $frz, $fax, $fay);
return new matrixTransformTag($transform, new Vector2($fscx->toFloat(), $fscy->toFloat()), $frx->toFloat(), $fry->toFloat(), $frz->toFloat(), $fax->toFloat(), $fay->toFloat());
}
}

83
src/math/RealMatrix.php Normal file
View file

@ -0,0 +1,83 @@
<?php
namespace swf2ass\math;
use MathPHP\LinearAlgebra\Matrix;
use MathPHP\LinearAlgebra\ObjectMatrix;
class RealMatrix extends ObjectMatrix{
public function __construct($value){
if(!$value instanceof RealMatrix){
if($value instanceof Matrix){
$value = $value->getMatrix();
}
foreach ($value as $i => $r){
foreach ($r as $j => $v){
if(!($v instanceof RealNumber)){
$value[$i][$j] = new RealNumber($v);
}
}
}
parent::__construct($value);
}else{
parent::__construct($value->A);
}
}
public function get(int $i, int $j) : RealNumber {
return parent::get($i, $j);
}
public function multiply($B): RealMatrix {
return new RealMatrix(parent::multiply(new RealMatrix($B)));
}
public function transpose(): RealMatrix {
return new RealMatrix(parent::transpose());
}
public function submatrix(int $m₁, int $n₁, int $m₂, int $n₂): RealMatrix {
return new RealMatrix(parent::submatrix($m₁, $n₁, $m₂, $n₂));
}
public function isSquare(): bool {
return $this->m === $this->n;
}
public function isEqual(Matrix $B): bool
{
if (!$this->isEqualSizeAndType($B)) {
return false;
}
$m = $this->m;
$n = $this->n;
// All elements are the same
for ($i = 0; $i < $m; $i++) {
for ($j = 0; $j < $n; $j++) {
/** @var $B RealNumber[][] */
if (!$B[$i][$j]->equalWithEpsilon($this->A[$i][$j])) {
return false;
}
}
}
return true;
}
public function __toString(): string {
return \trim(\array_reduce(\array_map(
function ($mᵢ) {
return '[' . \implode(', ', array_map(function (RealNumber $n){return $n->toFixed(7);}, $mᵢ)) . ']';
},
$this->A
), function ($A, $mᵢ) {
return $A . \PHP_EOL . $mᵢ;
}));
}
}

213
src/math/RealNumber.php Normal file
View file

@ -0,0 +1,213 @@
<?php
namespace swf2ass\math;
use Decimal\Decimal;
use MathPHP\Number\ObjectArithmetic;
class RealNumber implements ObjectArithmetic {
private static ?RealNumber $pi = null;
private static ?RealNumber $epsilon = null;
private Decimal $decimal;
const DECIMAL_PRECISION = Decimal::DEFAULT_PRECISION;
/**
* @param RealNumber|Decimal|int|float|string $value
*/
public function __construct($value){
if($value instanceof Decimal){
$this->decimal = $value;
}else if($value instanceof RealNumber){
$this->decimal = $value->decimal;
}else if(is_float($value)){
$this->decimal = new Decimal(sprintf("%.0" . PHP_FLOAT_DIG . "F", $value));
}else if(is_string($value) or is_integer($value)){
$this->decimal = new Decimal($value, self::DECIMAL_PRECISION + 1);
}else{
$this->decimal = new Decimal($value, self::DECIMAL_PRECISION + 1);
}
}
public static function pi() : RealNumber{
return self::$pi ?? (self::$pi = new RealNumber("3.1415926535897932384626433832"));
}
public static function setDefaultEpsilon(RealNumber $number){
self::$epsilon = $number;
}
public static function epsilon() :RealNumber{
return self::$epsilon ?? (self::$epsilon = new RealNumber("0.0000000000001"));
}
public function greater($object_or_scalar) : bool{
return $this->compareTo($object_or_scalar) === 1;
}
public function equalWithEpsilon($object_or_scalar, ?RealNumber $epsilon = null) : bool{
if($epsilon === null){
$epsilon = self::epsilon();
}
return $this->subtract($object_or_scalar)->absolute()->lesserEqual($epsilon);
}
public function isZero(?RealNumber $epsilon = null) : bool{
return $this->equalWithEpsilon(0, $epsilon);//$this->decimal->isZero();
}
public function equal($object_or_scalar) : bool{
return $this->compareTo($object_or_scalar) === 0;
}
public function lesser($object_or_scalar) : bool{
return $this->compareTo($object_or_scalar) === -1;
}
public function greaterEqual($object_or_scalar) : bool{
return $this->compareTo($object_or_scalar) >= 0;
}
public function lesserEqual($object_or_scalar) : bool{
return $this->compareTo($object_or_scalar) <= 0;
}
public function compareTo($object_or_scalar) : int{
if($object_or_scalar instanceof RealNumber){
return $this->decimal->compareTo($object_or_scalar->decimal);
}else if($object_or_scalar instanceof Decimal){
return $this->decimal->compareTo($object_or_scalar);
}else{
return $this->decimal->compareTo((new RealNumber($object_or_scalar))->decimal);
}
}
public function negate() : RealNumber{
return new RealNumber($this->decimal->negate());
}
public function cosine(int $terms = 12) : RealNumber{
if($this->decimal->isZero()){
return new RealNumber(1);
}
$div = $this->divide(self::pi())->toInteger();
$x = $this->subtract(self::pi()->multiply($div));
$sign = 1;
if($div % 2 !== 0){
$sign = -1;
}
$result = new RealNumber(1);
$inter = new RealNumber(1);
$num = $x->power(2);
for($i = 1; $i <= $terms; ++$i){
$comp = 2 * $i;
$den = $comp * ($comp - 1);
$inter = $inter->multiply($num->divide($den));
if($i % 2 === 0){
$result = $result->add($inter);
}else{
$result = $result->subtract($inter);
}
}
return $result->multiply($sign);
}
public function sine(int $terms = 12) : RealNumber{
if($this->decimal->isZero()){
return new RealNumber(0);
}
return self::pi()->divide(2)->subtract($this)->cosine($terms);
}
public function getDecimal() : Decimal{
return $this->decimal;
}
public function toFloat() : float{
return $this->decimal->toFloat();
}
public function toFixed($places = self::DECIMAL_PRECISION) : string{
return $this->decimal->trim()->toFixed($places);
}
public function toInteger() : int{
return $this->decimal->toInt();
}
public function __toString(){
return $this->toFixed();
}
public function __debugInfo(){
return [
"precision" => $this->getDecimal()->precision(),
"value" => $this->toFixed()
];
}
public function add($object_or_scalar): RealNumber {
if($object_or_scalar instanceof RealNumber){
return new RealNumber($this->decimal->add($object_or_scalar->decimal));
}else if($object_or_scalar instanceof Decimal){
return new RealNumber($this->decimal->add($object_or_scalar));
}else{
return new RealNumber($this->decimal->add((new RealNumber($object_or_scalar))->decimal));
}
}
public function subtract($object_or_scalar): RealNumber {
if($object_or_scalar instanceof RealNumber){
return new RealNumber($this->decimal->sub($object_or_scalar->decimal));
}else if($object_or_scalar instanceof Decimal){
return new RealNumber($this->decimal->sub($object_or_scalar));
}else{
return new RealNumber($this->decimal->sub((new RealNumber($object_or_scalar))->decimal));
}
}
public function absolute(): RealNumber {
return new RealNumber($this->decimal->abs());
}
public function sqrt(): RealNumber {
return new RealNumber($this->decimal->sqrt());
}
public function power($object_or_scalar): RealNumber {
if($object_or_scalar instanceof RealNumber){
return new RealNumber($this->decimal->pow($object_or_scalar->decimal));
}else if($object_or_scalar instanceof Decimal){
return new RealNumber($this->decimal->pow($object_or_scalar));
}else{
return new RealNumber($this->decimal->pow((new RealNumber($object_or_scalar))->decimal));
}
}
public function multiply($object_or_scalar): RealNumber {
if($object_or_scalar instanceof RealNumber){
return new RealNumber($this->decimal->mul($object_or_scalar->decimal));
}else if($object_or_scalar instanceof Decimal){
return new RealNumber($this->decimal->mul($object_or_scalar));
}else{
return new RealNumber($this->decimal->mul((new RealNumber($object_or_scalar))->decimal));
}
}
public function divide($object_or_scalar): RealNumber {
if($object_or_scalar instanceof RealNumber){
return new RealNumber($this->decimal->div($object_or_scalar->decimal));
}else if($object_or_scalar instanceof Decimal){
return new RealNumber($this->decimal->div($object_or_scalar));
}else{
return new RealNumber($this->decimal->div((new RealNumber($object_or_scalar))->decimal));
}
}
public static function createZeroValue(): RealNumber {
return new RealNumber(0);
}
}

View file

@ -2,33 +2,98 @@
require_once __DIR__ . "/vendor/autoload.php";
$swf = new \swf\SWF(file_get_contents($argv[1]));
$settings = [
"videoScaleMultiplier" => 1, //TODO: not finished, leave at 1
"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,
];
$swfContent = file_get_contents($argv[1]);
$signature = hash("sha256", $swfContent, false);
$m = new \swf2ass\MatrixTransform(new \swf2ass\Vector2(-1, 1), new \swf2ass\Vector2(0.17115783691406, 0.16761779785156), new \swf2ass\Vector2(0, 0));
//$m = new \swf2ass\MatrixTransform(new \swf2ass\Vector2(-0.69596862792969, 0.71046447753906), new \swf2ass\Vector2(0.17115783691406, 0.16761779785156), new \swf2ass\Vector2(1374, 2174));
$t = \swf2ass\ass\matrixTransformTag::fromMatrixTransform($m);
echo $m . "\n";
var_dump($t);
var_dump($t->encode(new \swf2ass\ass\ASSLine(), 1));
//exit();
$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]),
]
]
];
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);
$assRenderer = new \swf2ass\ass\ASSRenderer($processor->getFrameRate(), $processor->getViewPort(), [
"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
]);
$assRenderer = new \swf2ass\ass\ASSRenderer($processor->getFrameRate(), $processor->getViewPort(), $settings);
$keyFrameInterval = 10 * $processor->getFrameRate(); //kf every 10 seconds TODO: make this dynamic, per-shape
$frameOffset = 0;
$lastFrame = null;
while(($frame = $processor->nextFrameOutput()) !== null){
$audio = $processor->getAudio();
@ -44,11 +109,24 @@ if ($swf->header["signature"]) {
$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());
@ -57,10 +135,48 @@ if ($swf->header["signature"]) {
++$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($rendered->getObjects()) ." :: Paths: $drawCalls draw calls, $drawItems items :: Clips: $clipCalls draw calls, $clipItems items" . PHP_EOL;
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;
foreach ($assRenderer->renderFrame($frame, $rendered) as $line){
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");
}
@ -70,12 +186,19 @@ if ($swf->header["signature"]) {
}
}
if($frameEnd !== null and $frame->getFrameNumber() >= $frameEnd){
break;
}
}
foreach ($assRenderer->flush($lastFrame) as $line){
fwrite($fp, $line . "\n");
}
}
if(count($testVectors) > 0){
file_put_contents($argv[2] . ".test.json", json_encode($testVectors, JSON_PRETTY_PRINT));
}
fclose($fp);