diff --git a/.gitignore b/.gitignore index 8da2e81..391036c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ /*.zip /*.mp4 /*.mp3 +/*.flac +/*.xml /*.webm /*.mkv /*.swf \ No newline at end of file diff --git a/composer.json b/composer.json index 937541c..453d164 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "weebdatahoarder/swf2ass", "type": "project", - "license": "MIT", + "license": "GPL-3.0-or-later", "autoload": { "psr-4": { "swf2ass\\": "src/" diff --git a/image2ass.php b/image2ass.php index 9266af5..6e60571 100644 --- a/image2ass.php +++ b/image2ass.php @@ -3,13 +3,12 @@ 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); + $lastCursor = new \swf2ass\Vector2(0, 0); foreach ($path as $e) { /** @var \swf2ass\LineRecord|\swf2ass\MoveRecord $e */ @@ -127,12 +126,12 @@ function cleanupPath(array $path) { } /** - * @param \swf2ass\Coordinate $corner + * @param \swf2ass\Vector2 $corner * @param \swf2ass\LineRecord[] $path * * @return ?\swf2ass\LineRecord */ -function findNextCorner(\swf2ass\Coordinate $corner, array &$path, array &$cornerMap): ?\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]); @@ -147,7 +146,7 @@ function findNextCorner(\swf2ass\Coordinate $corner, array &$path, array &$corne return null; } -function offsetPath($path, \swf2ass\Coordinate $offset): array { +function offsetPath($path, \swf2ass\Vector2 $offset): array { $newPath = []; foreach ($path as $e) { @@ -164,7 +163,7 @@ function offsetPath($path, \swf2ass\Coordinate $offset): array { } 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)); + $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){ @@ -197,9 +196,9 @@ function getBoundingBox($path) { function calculateAreaOfPath($path) { $totalArea = 0; - /** @var \swf2ass\Coordinate[][] $subPolygons */ + /** @var \swf2ass\Vector2[][] $subPolygons */ $subPolygons = []; - /** @var \swf2ass\Coordinate[] $currentPolygon */ + /** @var \swf2ass\Vector2[] $currentPolygon */ $currentPolygon = []; foreach ($path as $n) { if ($n instanceof \swf2ass\MoveRecord) { @@ -264,7 +263,7 @@ function renderFrame(array $pixelMatrix, int $resX, int $resY, bool $doFullFrame $colors[$k] = $color; $paths[$k] = []; } - foreach ((new \swf2ass\Square(new \swf2ass\Coordinate($x, $y), 1))->draw() as $p) { + foreach ((new \swf2ass\Square(new \swf2ass\Vector2($x, $y), 1))->draw() as $p) { $paths[$k][] = $p; } } @@ -288,7 +287,7 @@ function renderFrame(array $pixelMatrix, int $resX, int $resY, bool $doFullFrame $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) { + foreach ((new \swf2ass\Rectangle(new \swf2ass\Vector2(0, 0), new \swf2ass\Vector2($resX, $resY)))->draw() as $p) { $optimizedPaths[$k0][] = $p; } } @@ -296,7 +295,7 @@ function renderFrame(array $pixelMatrix, int $resX, int $resY, bool $doFullFrame $assLines = []; foreach ($optimizedPaths as $k => $path) { - $pos = new \swf2ass\Coordinate(0, 0); + $pos = new \swf2ass\Vector2(0, 0); $bb = getBoundingBox($path); $pos = $bb->topLeft; /*$first = reset($path); diff --git a/src/ActionList.php b/src/ActionList.php new file mode 100644 index 0000000..514b62b --- /dev/null +++ b/src/ActionList.php @@ -0,0 +1,9 @@ +style = $styleId; $this->segment = new PathSegment($start); } - public function add_point(VisitedPoint $point){ + public function add_point(VisitedPoint $point) { $this->segment->add_point($point); } diff --git a/src/BitmapConverter.php b/src/BitmapConverter.php index b02564d..fa23bdf 100644 --- a/src/BitmapConverter.php +++ b/src/BitmapConverter.php @@ -6,18 +6,18 @@ namespace swf2ass; class BitmapConverter { /** @var Color[][] */ private array $pixelMatrix; - private Coordinate $resolution; + private Vector2 $resolution; public function __construct(array $pixelMatrix) { $this->pixelMatrix = $pixelMatrix; - $this->resolution = new Coordinate(count($pixelMatrix[0]), count($pixelMatrix)); + $this->resolution = new Vector2(count($pixelMatrix[0]), count($pixelMatrix)); } private static function calculateAreaOfPath($path) { $totalArea = 0; - /** @var Coordinate[][] $subPolygons */ + /** @var Vector2[][] $subPolygons */ $subPolygons = []; - /** @var Coordinate[] $currentPolygon */ + /** @var Vector2[] $currentPolygon */ $currentPolygon = []; foreach ($path as $n) { if ($n instanceof MoveRecord) { @@ -65,7 +65,7 @@ class BitmapConverter { private static function normalizePath($path) { $newPath = []; - $lastCursor = new Coordinate(0, 0); + $lastCursor = new Vector2(0, 0); foreach ($path as $e) { /** @var LineRecord|MoveRecord $e */ @@ -178,12 +178,12 @@ class BitmapConverter { } /** - * @param Coordinate $corner + * @param Vector2 $corner * @param LineRecord[] $path * * @return ?LineRecord */ - private static function findNextCorner(Coordinate $corner, array &$path, array &$cornerMap): ?LineRecord { + private static function findNextCorner(Vector2 $corner, array &$path, array &$cornerMap): ?LineRecord { foreach ($cornerMap[$corner->x . "_" . $corner->y] as $i => $p) { unset($path[$i]); unset($cornerMap[$p->start->x . "_" . $p->start->y][$i]); @@ -200,7 +200,7 @@ class BitmapConverter { private static function getBoundingBox($path) { - $bb = new Rectangle(new \swf2ass\Coordinate(PHP_INT_MAX, PHP_INT_MAX), new Coordinate(PHP_INT_MIN, PHP_INT_MIN)); + $bb = new Rectangle(new \swf2ass\Vector2(PHP_INT_MAX, PHP_INT_MAX), new Vector2(PHP_INT_MIN, PHP_INT_MIN)); /*if(count($path) > 0){ $first = reset($path); if($first instanceof MoveRecord or $first instanceof LineRecord){ @@ -241,7 +241,7 @@ class BitmapConverter { $colors[$k] = $color; $paths[$k] = []; } - foreach ((new Square(new Coordinate($x, $y), 1))->draw() as $p) { + foreach ((new Square(new Vector2($x, $y), 1))->draw() as $p) { $paths[$k][] = $p; } } @@ -265,7 +265,7 @@ class BitmapConverter { $k0 = array_key_first($optimizedPaths); $path = $optimizedPaths[$k0]; $optimizedPaths[$k0] = []; - foreach ((new Rectangle(new Coordinate(0, 0), $this->resolution))->draw() as $p) { + foreach ((new Rectangle(new Vector2(0, 0), $this->resolution))->draw() as $p) { $optimizedPaths[$k0][] = $p; } } @@ -273,19 +273,19 @@ class BitmapConverter { /** @var DrawPath[] $commands */ $commands = []; - foreach ($optimizedPaths as $k => $path){ + foreach ($optimizedPaths as $k => $path) { $edges = []; $bb = self::getBoundingBox($path); - $pos = $bb->topLeft->multiply(TWIP_SIZE); + $pos = $bb->topLeft->multiply(Constants::TWIP_SIZE); - $edges[] = new MoveRecord($pos, new Coordinate(0, 0)); + $edges[] = new MoveRecord($pos, new Vector2(0, 0)); - foreach ($path as $edge){ - if($edge instanceof MoveRecord){ - $edges[] = new MoveRecord($edge->coord->multiply(TWIP_SIZE), $edge->start->multiply(TWIP_SIZE)); - }else if($edge instanceof LineRecord){ - $edges[] = new LineRecord($edge->coord->multiply(TWIP_SIZE), $edge->start->multiply(TWIP_SIZE)); - }else{ + foreach ($path as $edge) { + if ($edge instanceof MoveRecord) { + $edges[] = new MoveRecord($edge->coord->multiply(Constants::TWIP_SIZE), $edge->start->multiply(Constants::TWIP_SIZE)); + } else if ($edge instanceof LineRecord) { + $edges[] = new LineRecord($edge->coord->multiply(Constants::TWIP_SIZE), $edge->start->multiply(Constants::TWIP_SIZE)); + } else { var_dump($edge); throw new \Exception(); } diff --git a/src/BitmapDefinition.php b/src/BitmapDefinition.php index c67d1e4..ff4815d 100644 --- a/src/BitmapDefinition.php +++ b/src/BitmapDefinition.php @@ -4,13 +4,13 @@ namespace swf2ass; class BitmapDefinition implements ObjectDefinition { - public $id; - public Coordinate $size; + public int $id; + public Vector2 $size; /** @var Color[][] */ public $pixels; private DrawPathList $drawPathList; - public function __construct($id, Coordinate $size, array $pixels) { + public function __construct(int $id, Vector2 $size, array $pixels) { $this->id = $id; $this->size = $size; $this->pixels = $pixels; @@ -19,16 +19,17 @@ class BitmapDefinition implements ObjectDefinition { $this->drawPathList = $converter->render(true); } - public function getId(){ + public function getObjectId(): int { return $this->id; } - public function getShapeList() : DrawPathList{ + + public function getShapeList(): DrawPathList { return $this->drawPathList; } static function fromXML(\DOMElement $element): BitmapDefinition { //Utils::dump_element($element); - $size = new Coordinate((int)$element->getAttribute("width"), (int)$element->getAttribute("height")); + $size = new Vector2((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) { @@ -70,7 +71,7 @@ class BitmapDefinition implements ObjectDefinition { return new BitmapDefinition($element->getAttribute("objectID"), $size, $pixels); } - public function getPixel(Coordinate $c) { + public function getPixel(Vector2 $c) { return $this->pixels[$c->y][$c->x]; } @@ -82,7 +83,7 @@ class BitmapDefinition implements ObjectDefinition { $iterator = $im->getPixelIterator(); foreach ($iterator as $row => $pixels) { foreach ($pixels as $col => $pixel) { - $c = $this->getPixel(new Coordinate($col, $row)); + $c = $this->getPixel(new Vector2($col, $row)); $colorStr = "#" . bin2hex(chr($c->r)) . bin2hex(chr($c->g)) . bin2hex(chr($c->b)) . bin2hex(chr(255 - ($c->a ?? 0))); $pixel->setColor($colorStr); } diff --git a/src/ClippingViewLayout.php b/src/ClippingViewLayout.php new file mode 100644 index 0000000..1867f62 --- /dev/null +++ b/src/ClippingViewLayout.php @@ -0,0 +1,17 @@ +clipDepth = $clipDepth; + parent::__construct($objectId, $object, $parent); + } + + public function getClipDepth(): int { + return $this->clipDepth; + } +} \ No newline at end of file diff --git a/src/Color.php b/src/Color.php index ef8755d..ae19ffd 100644 --- a/src/Color.php +++ b/src/Color.php @@ -8,24 +8,24 @@ class Color { public $b; public $alpha; - public function __construct($r, $g, $b, $alpha = null) { + public function __construct($r, $g, $b, $alpha = 0) { $this->r = $r; $this->g = $g; $this->b = $b; $this->alpha = $alpha; } - public function equals(?Color $other): bool { - return $other !== null and $other->r === $this->r and $other->g === $this->g and $other->b === $this->b and $other->alpha == $this->alpha; + public function equals(?Color $other, $alpha = true): bool { + return $other !== null and $other->r === $this->r and $other->g === $this->g and $other->b === $this->b and (!$alpha or $other->alpha == $this->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 distance(Color $color, $alpha = false): float { + return sqrt(pow($color->r - $this->r, 2) + pow($color->g - $this->g, 2) + pow($color->b - $this->b, 2) + ($alpha ? pow($color->alpha - $this->alpha, 2) : 0)); } 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) { + if (($this->alpha === 0) and $nullAlpha) { } else { $c .= "\\1a&H" . strtoupper(Utils::padHex(dechex($this->alpha ?? 0))) . "&"; @@ -34,11 +34,7 @@ class Color { 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); + return new Color((int)$element->getAttribute("red"), (int)$element->getAttribute("green"), (int)$element->getAttribute("blue"), $element->hasAttribute("alpha") ? (int)$element->getAttribute("alpha") : 0); } } \ No newline at end of file diff --git a/src/ColorTransform.php b/src/ColorTransform.php index 6b05513..74ed31d 100644 --- a/src/ColorTransform.php +++ b/src/ColorTransform.php @@ -4,14 +4,18 @@ namespace swf2ass; class ColorTransform { - public $mult; - public $add; + public Color $mult; + public Color $add; - public function __construct(Color $mult = null, Color $add = null) { + public function __construct(Color $mult, Color $add) { $this->mult = $mult; $this->add = $add; } + public static function identity(): ColorTransform { + return new ColorTransform(new Color(256, 256, 256, 256), new Color(0, 0, 0)); + } + public function applyToStyleContainer(StyleContainer $styles): StyleContainer { if ($styles->original_color1 instanceof Color) { $styles->color1 = $styles->original_color1->applyTransform($this); @@ -23,9 +27,30 @@ class ColorTransform { return $styles; } + public function applyMultiplyToColor(Color $color): Color { + return new Color(max(0, min(($this->mult->r * $color->r) / 256, 255)), max(0, min(($this->mult->g * $color->g) / 256, 255)), max(0, min(($this->mult->b * $color->b) / 256, 255)), max(0, min(($this->mult->alpha * $color->alpha) / 256, 255)),); + } + + public function applyAdditionToColor(Color $color): Color { + return new Color(max(0, min($this->add->r + $color->r, 255)), max(0, min($this->add->g + $color->g, 255)), max(0, min($this->add->b + $color->b, 255)), max(0, min($this->add->alpha + $color->alpha, 255)),); + } + + public function applyToColor(Color $color): Color { + return new Color(max(0, min((($this->mult->r * $color->r) / 256) + $this->add->r, 255)), max(0, min((($this->mult->g * $color->g) / 256) + $this->add->g, 255)), max(0, min((($this->mult->b * $color->b) / 256) + $this->add->b, 255)), max(0, min((($this->mult->alpha * $color->alpha) / 256) + $this->add->alpha, 255)),); + } + + public function combine(ColorTransform $transform) { + //TODO: maybe these get altered all at once? + return new ColorTransform($this->applyMultiplyToColor($transform->mult), $this->applyAdditionToColor($transform->add)); + } + + public function equals(ColorTransform $other, $epsilon = Constants::EPSILON): bool { + return ($this->mult === $other or ($this->mult !== null and $this->mult->equals($other->mult))) and ($this->add === $other or ($this->add !== null and $this->add->equals($other->add))); + } + public static function fromXML(\DOMElement $element): ColorTransform { - $add = new Color(null, null, null, null); - $mult = new Color(null, null, null, null); + $add = new Color(0, 0, 0, 0); + $mult = new Color(256, 256, 256, 256); if ($element->hasAttribute("factorRed")) { $mult->r = (int)$element->getAttribute("factorRed"); $mult->g = (int)$element->getAttribute("factorGreen"); diff --git a/src/Constants.php b/src/Constants.php new file mode 100644 index 0000000..8ad734c --- /dev/null +++ b/src/Constants.php @@ -0,0 +1,8 @@ +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 multiply($size): Coordinate { - return new Coordinate($this->x * $size, $this->y * $size); - } - - 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); - } -} \ No newline at end of file diff --git a/src/CubicCurveRecord.php b/src/CubicCurveRecord.php index 9fd1765..17ab796 100644 --- a/src/CubicCurveRecord.php +++ b/src/CubicCurveRecord.php @@ -3,19 +3,19 @@ namespace swf2ass; class CubicCurveRecord implements Record { - public Coordinate $start; - public Coordinate $control1; - public Coordinate $control2; - public Coordinate $anchor; + public Vector2 $start; + public Vector2 $control1; + public Vector2 $control2; + public Vector2 $anchor; - public function __construct(Coordinate $control1, Coordinate $control2, Coordinate $anchor, Coordinate $start) { + public function __construct(Vector2 $control1, Vector2 $control2, Vector2 $anchor, Vector2 $start) { $this->control1 = $control1; $this->control2 = $control2; $this->anchor = $anchor; $this->start = $start; } - public function getStart(): Coordinate { + public function getStart(): Vector2 { return $this->start; } @@ -23,12 +23,11 @@ class CubicCurveRecord implements Record { return new CubicCurveRecord($this->control2, $this->control1, $this->start, $this->anchor); } + public function applyMatrixTransform(MatrixTransform $transform): CubicCurveRecord { + return new CubicCurveRecord($transform->applyToVector($this->control1), $transform->applyToVector($this->control2), $transform->applyToVector($this->anchor), $transform->applyToVector($this->start)); + } + public static function fromQuadraticRecord(QuadraticCurveRecord $q): CubicCurveRecord { - return new CubicCurveRecord( - $q->start->add($q->control->multiply(2))->divide(3), - $q->anchor->add($q->control->multiply(2))->divide(3), - $q->anchor, - $q->start - ); + return new CubicCurveRecord($q->start->add($q->control->multiply(2))->divide(3), $q->anchor->add($q->control->multiply(2))->divide(3), $q->anchor, $q->start); } } \ No newline at end of file diff --git a/src/CubicSplineCurveRecord.php b/src/CubicSplineCurveRecord.php new file mode 100644 index 0000000..cdce2ff --- /dev/null +++ b/src/CubicSplineCurveRecord.php @@ -0,0 +1,46 @@ +control = $control; + $this->anchor = $anchor; + $this->start = $start; + } + + public function getStart(): Vector2 { + return $this->start; + } + + public function reverse(): CubicSplineCurveRecord { + return new CubicSplineCurveRecord(array_reverse($this->control), $this->start, $this->anchor); + } + + public function applyMatrixTransform(MatrixTransform $transform): CubicSplineCurveRecord { + $control = []; + foreach ($this->control as $c) { + $control[] = $transform->applyToVector($c); + } + + return new CubicSplineCurveRecord($control, $transform->applyToVector($this->anchor), $transform->applyToVector($this->start)); + } + + public function append(Record $record): ?CubicSplineCurveRecord { + if ($record instanceof CubicCurveRecord) { + + } + + return null; + } +} \ No newline at end of file diff --git a/src/CurrentState.php b/src/CurrentState.php index 38195af..b763bb2 100644 --- a/src/CurrentState.php +++ b/src/CurrentState.php @@ -24,7 +24,7 @@ class CurrentState { $this->frameRate = 60; $this->frameCount = 100; $this->frameNumber = 0; - $this->viewPort = new Rectangle(new Coordinate(0, 0), new Coordinate(480, 360)); + $this->viewPort = new Rectangle(new Vector2(0, 0), new Vector2(480, 360)); $this->objects = []; $this->displayList = []; $this->clipList = []; diff --git a/src/DisplayEntry.php b/src/DisplayEntry.php deleted file mode 100644 index cd6a669..0000000 --- a/src/DisplayEntry.php +++ /dev/null @@ -1,166 +0,0 @@ -clipDepth > 0) { - return []; - } - if ($this->depth != 2) { - //return []; - } - - if($currentState->frameOffset === null){ - return []; - } - $dialogue = []; - - $startFrame = $this->startFrame - $currentState->frameOffset; - - $frameList = []; - foreach ($this->frames as $frame => $info){ - $fn = $frame - $currentState->frameOffset; - if($fn < 0){ - continue; - } - $frameList[$fn] = clone $info; - $frameList[$fn]->frame = $fn; - } - - if(count($frameList) === 0){ - return []; - } - - $startTime = $startFrame * (1 / $currentState->frameRate); - $endTime = ($startFrame + 1) * (1 / $currentState->frameRate); - - - if ($this->object instanceof ObjectDefinition) { - $shapeDef = $this->object; - - foreach ($shapeDef->getShapeList()->commands as $path){ - $currentFrameStyles = [new StyleContainer()]; - - if($path->style instanceof LineStyleRecord){ //Stroke - $currentFrameStyles[0]->original_color3 = $currentFrameStyles[0]->color3 = $path->style->color; - $currentFrameStyles[0]->bord = round($path->style->width / TWIP_SIZE, 1); - }else if($path->style instanceof FillStyleRecord){ //Fill - $currentFrameStyles[0]->original_color1 = $currentFrameStyles[0]->color1 = $path->style->fill instanceof Gradient ? $path->style->fill->getItems()[0]->color : $path->style->fill; //TODO: gradient? - } - - $currentFrameStyles[0]->shad = 0; - - $assLine = "{\\an7\\shad0\\p1}"; - - foreach ($path->commands->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 = $startFrame; - - $debugInfo = "oid:".$shapeDef->getId(); - - $savedFrameStyles = $currentFrameStyles; - - $af = 0; - $frames = 0; - foreach ($frameList 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); - } - - $lastSavedStyle = end($savedFrameStyles); - $savedStyle = clone $lastSavedStyle; - $savedStyle->resetTransformedStyles(); - if ($frameInformation->transform instanceof MatrixTransform) { - $frameInformation->transform->applyToStyleContainer($savedStyle); - } - if ($frameInformation->colorTransform instanceof ColorTransform) { - $frameInformation->colorTransform->applyToStyleContainer($savedStyle); - } - - 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,$debugInfo,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 = [$savedStyle]; - } else if ($transition instanceof StyleContainer) { - $currentFrameStyles[] = $transition; - $savedFrameStyles[] = $savedStyle; - } - } - ++$frames; - } - - $assHeader = "Dialogue: {$this->depth}," . Utils::timeToStamp($lastLineStart) . "," . Utils::timeToStamp($endTime) . ",f,$debugInfo,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; - } - -} \ No newline at end of file diff --git a/src/FillStyleRecord.php b/src/FillStyleRecord.php index 183a287..721e08b 100644 --- a/src/FillStyleRecord.php +++ b/src/FillStyleRecord.php @@ -7,7 +7,7 @@ class FillStyleRecord implements StyleRecord { /** @var Gradient|Color */ public $fill; - public function __construct($fill){ + public function __construct($fill) { $this->fill = $fill; } } \ No newline at end of file diff --git a/src/FrameInformation.php b/src/FrameInformation.php index 780c8af..e6acc37 100644 --- a/src/FrameInformation.php +++ b/src/FrameInformation.php @@ -3,11 +3,62 @@ namespace swf2ass; class FrameInformation { - public int $frame; + private int $frameNumber; + private int $frameOffset = 0; - /** @var ?ColorTransform */ - public ?ColorTransform $colorTransform = null; + private float $frameRate; - /** @var ?MatrixTransform */ - public ?MatrixTransform $transform = null; + private ?ViewFrame $frame; + + + public function __construct(int $frameNumber, float $frameRate, ?ViewFrame $frame) { + $this->frameNumber = $frameNumber; + $this->frameRate = $frameRate; + $this->frame = $frame; + } + + public function setFrameOffset(int $frameOffset) { + $this->frameOffset = $frameOffset; + } + + public function getFrameOffset(): int { + return $this->frameOffset; + } + + public function getFrame(): ?ViewFrame { + return $this->frame; + } + + public function getFrameNumber(): int { + return $this->frameNumber - $this->frameOffset; + } + + public function getStartTimeSeconds(): float { + return $this->getFrameNumber() / $this->frameRate; + } + + public function getStartTimeMilliSeconds(): int { + return $this->getStartTimeSeconds() * 1000; + } + + public function getEndTimeSeconds(): float { + return $this->getStartTimeSeconds() + $this->getFrameDurationSeconds(); + } + + public function getEndTimeMilliSeconds(): int { + return $this->getEndTimeSeconds() * 1000; + } + + public function getFrameDurationSeconds(): float { + return 1 / $this->frameRate; + } + + public function getFrameDurationMilliSeconds(): int { + return $this->getFrameDurationSeconds() * 1000; + } + + + public function diff(FrameInformation $other): FrameInformation { + return new FrameInformation($this->getFrameNumber() - $other->getFrameNumber(), $this->frameRate, null); + } } \ No newline at end of file diff --git a/src/Gradient.php b/src/Gradient.php index 07f593b..88217e0 100644 --- a/src/Gradient.php +++ b/src/Gradient.php @@ -7,6 +7,7 @@ interface Gradient { /** * @return GradientItem[] */ - public function getItems() : array; - public function getMatrixTransform() : ?MatrixTransform; + public function getItems(): array; + + public function getMatrixTransform(): ?MatrixTransform; } \ No newline at end of file diff --git a/src/JPEGBitmapDefinition.php b/src/JPEGBitmapDefinition.php index 5a98079..9b48c65 100644 --- a/src/JPEGBitmapDefinition.php +++ b/src/JPEGBitmapDefinition.php @@ -31,7 +31,7 @@ class JPEGBitmapDefinition extends BitmapDefinition { $im->transformImageColorspace(\Imagick::COLORSPACE_SRGB); $resolution = $im->getImageGeometry(); - $size = new Coordinate($resolution["width"], $resolution["height"]); + $size = new Vector2($resolution["width"], $resolution["height"]); $pixels = array_fill(0, $size->y, array_fill(0, $size->x, new Color(0, 0, 0))); diff --git a/src/LineRecord.php b/src/LineRecord.php index deea32f..ebc2412 100644 --- a/src/LineRecord.php +++ b/src/LineRecord.php @@ -3,15 +3,15 @@ namespace swf2ass; class LineRecord implements Record { - public Coordinate $start; - public Coordinate $coord; + public Vector2 $start; + public Vector2 $coord; - public function __construct(Coordinate $coord, Coordinate $start) { + public function __construct(Vector2 $coord, Vector2 $start) { $this->coord = $coord; $this->start = $start; } - public function getStart(): Coordinate { + public function getStart(): Vector2 { return $this->start; } @@ -19,7 +19,11 @@ class LineRecord implements Record { return new LineRecord($this->start, $this->coord); } - public static function fromXML(\DOMElement $element, Coordinate $cursor): LineRecord { - return new LineRecord($cursor->add(new Coordinate((int)$element->getAttribute("x"), (int)$element->getAttribute("y"))), $cursor); + public static function fromXML(\DOMElement $element, Vector2 $cursor): LineRecord { + return new LineRecord($cursor->add(new Vector2((int)$element->getAttribute("x"), (int)$element->getAttribute("y"))), $cursor); + } + + public function applyMatrixTransform(MatrixTransform $transform): LineRecord { + return new LineRecord($transform->applyToVector($this->coord), $transform->applyToVector($this->start)); } } \ No newline at end of file diff --git a/src/LineStyleRecord.php b/src/LineStyleRecord.php index acf6cf7..23cf6f1 100644 --- a/src/LineStyleRecord.php +++ b/src/LineStyleRecord.php @@ -3,7 +3,7 @@ namespace swf2ass; -class LineStyleRecord implements StyleRecord { +class LineStyleRecord implements StyleRecord { public int $width; public Color $color; @@ -12,7 +12,7 @@ class LineStyleRecord implements StyleRecord { //TODO: join/cap/etc style public bool $allow_close = true; - public function __construct(int $width, Color $color){ + public function __construct(int $width, Color $color) { $this->width = $width; $this->color = $color; diff --git a/src/LinearGradient.php b/src/LinearGradient.php index c3671f6..766e930 100644 --- a/src/LinearGradient.php +++ b/src/LinearGradient.php @@ -12,10 +12,12 @@ class LinearGradient implements Gradient { $this->colors = $colors; $this->transform = $transform; } - public function getItems() : array { + + public function getItems(): array { return $this->colors; } - public function getMatrixTransform() : ?MatrixTransform{ + + public function getMatrixTransform(): ?MatrixTransform { return $this->transform; } diff --git a/src/Matrix2D.php b/src/Matrix2D.php new file mode 100644 index 0000000..c1df713 --- /dev/null +++ b/src/Matrix2D.php @@ -0,0 +1,203 @@ + */ + private \SplFixedArray $data; + private Vector2 $size; + + + /** + * @param Vector2 $size + * @param \SplFixedArray $values + * @throws \Exception + */ + public function __construct(Vector2 $size, ?\SplFixedArray $initialize = null) { + $this->size = $size; + $this->data = $initialize !== null ? $initialize : \SplFixedArray::fromArray(array_fill(0, $size->x * $size->y, 0)); + if ($this->data->getSize() !== $size->x * $size->y) { + throw new \InvalidArgumentException("Wrong sized Matrix initialization"); + } + } + + public static function from2DArray(array $data): Matrix2D { + $size = new Vector2(count($data[0]), count($data)); + $newData = new \SplFixedArray($size->x * $size->y); + foreach ($data as $y => $row) { + foreach ($row as $x => $value) { + $i = $size->x * $y + $x; + $newData->offsetSet($i, $value); + } + } + + return new Matrix2D($size, $newData); + } + + /** + * @param Vector2 $position + * @param int|float $value + * @throws \Exception + */ + public function set(Vector2 $position, $value) { + $i = $this->size->x * $position->y + $position->x; + if (!$this->data->offsetExists($i)) { + throw new \OutOfRangeException("Index [{$position->x}, {$position->y}] out of range"); + } + $this->data->offsetSet($i, $value); + } + + + /** + * @param Vector2 $position + * @return float|int + */ + public function get(Vector2 $position) { + $i = $this->size->x * $position->y + $position->x; + if (!$this->data->offsetExists($i)) { + throw new \OutOfRangeException("Index [{$position->x}, {$position->y}] out of range"); + } + return $this->data->offsetGet($i); + } + + private function internalGet($x, $y) { + return $this->data->offsetGet($this->size->x * $y + $x); + } + + private function internalSet($x, $y, $value) { + $this->data->offsetSet($this->size->x * $y + $x, $value); + } + + public function isSquare(): bool { + return $this->size->equals($this->size->invert()); + } + + public function getSize(): Vector2 { + return $this->size; + } + + public function __clone() { + $this->data = clone $this->data; + } + + public function add(Matrix2D $matrix): Matrix2D { + if (!$this->size->equals($matrix->size)) { + throw new \LogicException("Matrix sizes not equal"); + } + + $result = clone $this; + foreach ($result->data as $i => $value) { + $result->data->offsetSet($i, $value + $matrix->data->offsetGet($i)); + } + + return $result; + } + + public function substract(Matrix2D $matrix): Matrix2D { + if (!$this->size->equals($matrix->size)) { + throw new \LogicException("Matrix sizes not equal"); + } + + $result = clone $this; + foreach ($result->data as $i => $value) { + $result->data->offsetSet($i, $value - $matrix->data->offsetGet($i)); + } + + return $result; + } + + public function transpose(): Matrix2D { + $result = new Matrix2D($this->size->invert()); + foreach ($result->data as $i => $value) { + $x = $i % $this->size->x; + $y = intdiv($i, $this->size->x); + $result->internalSet($y, $x, $value); + } + + return $result; + } + + private function isMatrixTransform(): bool { + return $this->size->x === 3 and $this->size->y === 3 and $this->internalGet(0, 2) === 0 and $this->internalGet(1, 2) === 0 and $this->internalGet(2, 2) === 1; + } + + private function isVector2(): bool { + return $this->size->x === 3 and $this->size->y === 1 and $this->internalGet(1, 0) === 1; + } + + /** + * Naive Matrix product, O(n^3), with some special cases optimizations + * + * @param Matrix2D $matrix + * @return Matrix2D + * @throws \Exception + */ + public function product(Matrix2D $matrix): Matrix2D { + if ($this->size->x !== $matrix->size->y) { + throw new \LogicException("Matrix A columns != B rows"); + } + + if ($matrix->isMatrixTransform()) { + // http://www.senocular.com/flash/tutorials/transformmatrix/ + if ($this->isMatrixTransform()) { + $a1 = $this->internalGet(0, 0); + $b1 = $this->internalGet(1, 0); + $c1 = $this->internalGet(0, 1); + $d1 = $this->internalGet(1, 1); + $tx1 = $this->internalGet(0, 2); + $ty1 = $this->internalGet(1, 2); + + $a2 = $matrix->internalGet(0, 0); + $b2 = $matrix->internalGet(1, 0); + $c2 = $matrix->internalGet(0, 1); + $d2 = $matrix->internalGet(1, 1); + $tx2 = $matrix->internalGet(0, 2); + $ty2 = $matrix->internalGet(1, 2); + + return Matrix2D::from2DArray([[$a1 * $a2 + $b1 * $c2, $a1 * $b2 + $b1 * $d2, 0], [$c1 * $a2 + $d1 * $c2, $c1 + $b2 + $d1 * $d2, 0], [$tx1 * $a2 + $ty1 * $c2 + $tx2, $tx1 * $b2 + $ty1 * $d2 + $ty2, 1]]); + } elseif ($this->isVector2()) { + $x = $this->internalGet(0, 0); + $y = $this->internalGet(1, 0); + + $a = $matrix->internalGet(0, 0); + $b = $matrix->internalGet(1, 0); + $c = $matrix->internalGet(0, 1); + $d = $matrix->internalGet(1, 1); + $tx = $matrix->internalGet(0, 2); + $ty = $matrix->internalGet(1, 2); + + return Matrix2D::from2DArray([[$x * $a + $y * $c + $tx, $x * $b + $y * $d + $ty, 1]]); + } + } + + //Fallback for all other types + + $result = new Matrix2D(new Vector2($this->size->x, $matrix->size->y)); + for ($i = 0; $i < $result->size->x; ++$i) { + for ($j = 0; $j < $result->size->y; ++$j) { + $sum = 0; + for ($k = 0; $k < $this->size->y; ++$k) { + $sum += $this->internalGet($i, $k) * $matrix->internalGet($k, $j); + } + $result->internalSet($i, $j, $sum); + } + } + return $result; + } + + public function equals(Matrix2D $other, $epsilon = Constants::EPSILON): bool { + if (!$this->size->equals($other->size)) { + return false; + } + + foreach ($this->data as $i => $value) { + if (abs($value - $other->data->offsetGet($i)) > $epsilon) { + return false; + } + } + + return true; + } + +} \ No newline at end of file diff --git a/src/MatrixTransform.php b/src/MatrixTransform.php index c50a38e..58cf6b8 100644 --- a/src/MatrixTransform.php +++ b/src/MatrixTransform.php @@ -4,53 +4,124 @@ namespace swf2ass; class MatrixTransform { - public $scaleX; - public $scaleY; - public $skewX; - public $skewY; - public $transX; - public $transY; + private const IDENTITY = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; - 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; + private Matrix2D $matrix; + + public function __construct(?Vector2 $scale, ?Vector2 $rotateSkew, ?Vector2 $translation) { + $matrix = self::IDENTITY; + + if ($scale !== null) { + $matrix[0][0] = $scale->x; + $matrix[1][1] = $scale->y; + } + if ($rotateSkew !== null) { + $matrix[1][0] = $rotateSkew->x; + $matrix[0][1] = $rotateSkew->y; + } + if ($translation !== null) { + $matrix[2][0] = $translation->x; + $matrix[2][1] = $translation->y; + } + $this->matrix = Matrix2D::from2DArray($matrix); + } + + public static function identity(): MatrixTransform { + return new MatrixTransform(new Vector2(1, 1), new Vector2(0, 0), new Vector2(0, 0)); + } + + public function combine(MatrixTransform $other): MatrixTransform { + $result = clone $this; + $result->matrix = $this->matrix->product($other->matrix); + return $result; + } + + public function getScaleX() { + return $this->matrix->get(new Vector2(0, 0)); + } + + public function getScaleY() { + return $this->matrix->get(new Vector2(1, 1)); + } + + public function getScale(): Vector2 { + return new Vector2($this->getScaleX(), $this->getScaleY()); + } + + public function getRotateSkewX() { + return $this->matrix->get(new Vector2(0, 1)); + } + + public function getRotateSkewY() { + return $this->matrix->get(new Vector2(1, 0)); + } + + public function getRotateSkew(): Vector2 { + return new Vector2($this->getRotateSkewX(), $this->getRotateSkewY()); + } + + public function getTranslationX() { + return $this->matrix->get(new Vector2(0, 2)); + } + + public function getTranslationY() { + return $this->matrix->get(new Vector2(1, 2)); + } + + public function getTranslation(): Vector2 { + return new Vector2($this->getTranslationX(), $this->getTranslationY()); + } + + public function applyToVector(Vector2 $vector): Vector2 { + $result = Matrix2D::from2DArray([[$vector->x, $vector->y, 1]])->product($this->matrix); + return new Vector2($result->get(new Vector2(0, 0)), $result->get(new Vector2(1, 0))); + /* + return + $vector->vectorMultiply($this->getScale()) //scale + ->add($vector->vectorMultiply($this->getRotateSkew())->invert()) //skew + ->add($this->getTranslation()); //translate + */ + } + + public function applyToShape(Shape $shape): Shape { + $newShape = new Shape(); + foreach ($shape->edges as $edge) { + $newShape->edges[] = $edge->applyMatrixTransform($this); + } + + return $newShape; } public function applyToStyleContainer(StyleContainer $styles): StyleContainer { - if ($this->scaleX !== null) { - if ($this->scaleX < 0) { + if ($this->scale !== null) { + + if ($this->scale->x < 0) { $styles->fry = 180; } - $styles->fscx = round(abs($this->scaleX) * 100, 2); + $styles->fscx = round(abs($this->scale->x) * 100, 2); + + if ($this->scale->y < 0) { + $styles->frx = 180; + } + $styles->fscy = round(abs($this->scale->y) * 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); + + if ($this->skew !== null) { + $styles->fax = round($this->skew->x, 4); + $styles->fay = round($this->skew->y, 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); + + if ($this->translation !== null) { + $styles->pos = ($styles->pos ?? new Vector2(0, 0))->add($this->translation); } else { $styles->pos = null; } @@ -58,8 +129,12 @@ class MatrixTransform { return $styles; } + public function equals(MatrixTransform $other, $epsilon = Constants::EPSILON): bool { + return $this->matrix->equals($other->matrix, $epsilon); + } + 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); + return new MatrixTransform($element->hasAttribute("scaleX") ? new Vector2(floatval($element->getAttribute("scaleX")), floatval($element->getAttribute("scaleY"))) : null, $element->hasAttribute("skewX") ? new Vector2(floatval($element->getAttribute("skewX")), floatval($element->getAttribute("skewY"))) : null, $element->hasAttribute("transX") ? new Vector2(intval($element->getAttribute("transX")), intval($element->getAttribute("transY"))) : null,); } } \ No newline at end of file diff --git a/src/MoveRecord.php b/src/MoveRecord.php index df519ae..34af996 100644 --- a/src/MoveRecord.php +++ b/src/MoveRecord.php @@ -3,15 +3,15 @@ namespace swf2ass; class MoveRecord implements Record { - public Coordinate $start; - public Coordinate $coord; + public Vector2 $start; + public Vector2 $coord; - public function __construct(Coordinate $coord, Coordinate $start) { + public function __construct(Vector2 $coord, Vector2 $start) { $this->coord = $coord; $this->start = $start; } - public function getStart(): Coordinate { + public function getStart(): Vector2 { return $this->start; } @@ -19,7 +19,11 @@ class MoveRecord implements Record { return new MoveRecord($this->start, $this->coord); } - public static function fromXML(\DOMElement $element, Coordinate $cursor): MoveRecord { - return new MoveRecord(new Coordinate((int)$element->getAttribute("x"), (int)$element->getAttribute("y")), $cursor); + public static function fromXML(\DOMElement $element, Vector2 $cursor): MoveRecord { + return new MoveRecord(new Vector2((int)$element->getAttribute("x"), (int)$element->getAttribute("y")), $cursor); + } + + public function applyMatrixTransform(MatrixTransform $transform): MoveRecord { + return new MoveRecord($transform->applyToVector($this->coord), $transform->applyToVector($this->start)); } } \ No newline at end of file diff --git a/src/MultiFrameObjectDefinition.php b/src/MultiFrameObjectDefinition.php new file mode 100644 index 0000000..146ecae --- /dev/null +++ b/src/MultiFrameObjectDefinition.php @@ -0,0 +1,9 @@ +points = [new VisitedPoint($start, false)]; } @@ -18,15 +15,15 @@ class PathSegment { $this->points = array_reverse($this->points, false); } - public function add_point(VisitedPoint $point){ + public function add_point(VisitedPoint $point) { $this->points[] = $point; } - public function start(): Coordinate { + public function start(): Vector2 { return reset($this->points)->pos; } - public function end(): Coordinate { + public function end(): Vector2 { return end($this->points)->pos; } @@ -38,27 +35,27 @@ class PathSegment { return $this->start()->equals($this->end()); } - public function swap(PathSegment $other){ + public function swap(PathSegment $other) { [$this->points, $other->points] = [$other->points, $this->points]; } - public function merge(PathSegment $other){ + public function merge(PathSegment $other) { $this->points = array_merge($this->points, array_slice($other->points, 1)); } - public function try_merge(PathSegment $other, bool $directed) : bool{ - if($other->end()->equals($this->start())){ + public function try_merge(PathSegment $other, bool $directed): bool { + if ($other->end()->equals($this->start())) { $this->swap($other); $this->merge($other); return true; - }else if($this->end()->equals($other->start())){ + } else if ($this->end()->equals($other->start())) { $this->merge($other); return true; - }else if(!$directed and $this->end()->equals($other->end())){ + } else if (!$directed and $this->end()->equals($other->end())) { $other->flip(); $this->merge($other); return true; - }else if(!$directed and $this->start()->equals($other->start())){ + } else if (!$directed and $this->start()->equals($other->start())) { $other->flip(); $this->swap($other); $this->merge($other); @@ -68,25 +65,25 @@ class PathSegment { return false; } - public function getShape() : Shape { - if($this->is_empty()){ - throw new Exception(); + public function getShape(): Shape { + if ($this->is_empty()) { + throw new \Exception(); } $shape = new Shape(); $first = reset($this->points); - $pos = new Coordinate(0, 0); + $pos = new Vector2(0, 0); $shape->edges[] = new MoveRecord($first->pos, $pos); $pos = $first->pos; - while (($point = next($this->points)) !== false){ - if(!$point->is_bezier_control){ + while (($point = next($this->points)) !== false) { + if (!$point->is_bezier_control) { $shape->edges[] = new LineRecord($point->pos, $pos); $pos = $point->pos; - }else{ + } else { $end = next($this->points); - if($end === false){ + if ($end === false) { throw new \Exception("Bezier without endpoint"); } diff --git a/src/PendingPath.php b/src/PendingPath.php index 674198e..95eeac9 100644 --- a/src/PendingPath.php +++ b/src/PendingPath.php @@ -5,7 +5,7 @@ namespace swf2ass; class PendingPath { - /** @var PathSegment[] */ + /** @var PathSegment[] */ public array $segments = []; @@ -17,28 +17,28 @@ class PendingPath { * @param PathSegment $new_segment * @param bool $directed */ - public function merge_path(PathSegment $new_segment, bool $directed){ - if(!$new_segment->is_empty()){ + public function merge_path(PathSegment $new_segment, bool $directed) { + if (!$new_segment->is_empty()) { $merged = null; - foreach ($this->segments as $i => $segment){ - if($segment->try_merge($new_segment, $directed)){ + foreach ($this->segments as $i => $segment) { + if ($segment->try_merge($new_segment, $directed)) { unset($this->segments[$i]); $merged = $segment; break; } } - if($merged === null){ + if ($merged === null) { $this->segments[] = $new_segment; - }else{ + } else { $this->merge_path($merged, $directed); } } } - public function getShape() : Shape { + public function getShape(): Shape { $shape = new Shape(); - foreach ($this->segments as $segment){ + foreach ($this->segments as $segment) { $shape->edges = array_merge($shape->edges, $segment->getShape()->edges); } diff --git a/src/PendingPathMap.php b/src/PendingPathMap.php index 49b1b33..11f3829 100644 --- a/src/PendingPathMap.php +++ b/src/PendingPathMap.php @@ -12,8 +12,8 @@ class PendingPathMap { } - public function merge_path(ActivePath $path, bool $directed){ - if(!isset($this->map[$path->style])){ + public function merge_path(ActivePath $path, bool $directed) { + if (!isset($this->map[$path->style])) { $this->map[$path->style] = new PendingPath(); } diff --git a/src/QuadraticCurveRecord.php b/src/QuadraticCurveRecord.php index 38362b6..b9aac01 100644 --- a/src/QuadraticCurveRecord.php +++ b/src/QuadraticCurveRecord.php @@ -3,17 +3,17 @@ namespace swf2ass; class QuadraticCurveRecord implements Record { - public Coordinate $start; - public Coordinate $control; - public Coordinate $anchor; + public Vector2 $start; + public Vector2 $control; + public Vector2 $anchor; - public function __construct(Coordinate $control, Coordinate $anchor, Coordinate $start) { + public function __construct(Vector2 $control, Vector2 $anchor, Vector2 $start) { $this->control = $control; $this->anchor = $anchor; $this->start = $start; } - public function getStart(): Coordinate { + public function getStart(): Vector2 { return $this->start; } @@ -21,9 +21,13 @@ class QuadraticCurveRecord implements Record { return new QuadraticCurveRecord($this->control, $this->start, $this->anchor); } - public static function fromXML(\DOMElement $element, Coordinate $cursor): QuadraticCurveRecord { - $control = $cursor->add(new Coordinate((int)$element->getAttribute("x1"), (int)$element->getAttribute("y1"))); - $anchor = $control->add(new Coordinate((int)$element->getAttribute("x2"), (int)$element->getAttribute("y2"))); + public function applyMatrixTransform(MatrixTransform $transform): QuadraticCurveRecord { + return new QuadraticCurveRecord($transform->applyToVector($this->control), $transform->applyToVector($this->anchor), $transform->applyToVector($this->start)); + } + + public static function fromXML(\DOMElement $element, Vector2 $cursor): QuadraticCurveRecord { + $control = $cursor->add(new Vector2((int)$element->getAttribute("x1"), (int)$element->getAttribute("y1"))); + $anchor = $control->add(new Vector2((int)$element->getAttribute("x2"), (int)$element->getAttribute("y2"))); return new QuadraticCurveRecord($control, $anchor, $cursor); } diff --git a/src/RadialGradient.php b/src/RadialGradient.php index f7c47ab..e7ac7d2 100644 --- a/src/RadialGradient.php +++ b/src/RadialGradient.php @@ -12,10 +12,12 @@ class RadialGradient implements Gradient { $this->colors = $colors; $this->transform = $transform; } - public function getItems() : array { + + public function getItems(): array { return $this->colors; } - public function getMatrixTransform() : ?MatrixTransform{ + + public function getMatrixTransform(): ?MatrixTransform { return $this->transform; } diff --git a/src/Record.php b/src/Record.php index 0878ab1..7d031cf 100644 --- a/src/Record.php +++ b/src/Record.php @@ -3,7 +3,9 @@ namespace swf2ass; interface Record { - public function getStart(): Coordinate; + public function getStart(): Vector2; public function reverse(): Record; + + public function applyMatrixTransform(MatrixTransform $transform): Record; } \ No newline at end of file diff --git a/src/Rectangle.php b/src/Rectangle.php index a2620fd..6d1af5f 100644 --- a/src/Rectangle.php +++ b/src/Rectangle.php @@ -6,15 +6,15 @@ namespace swf2ass; class Rectangle implements ComplexShape { - public Coordinate $topLeft; - public Coordinate $bottomRight; + public Vector2 $topLeft; + public Vector2 $bottomRight; - public function __construct(Coordinate $topLeft, Coordinate $bottomRight) { + public function __construct(Vector2 $topLeft, Vector2 $bottomRight) { $this->topLeft = $topLeft; $this->bottomRight = $bottomRight; } - public function inBounds(Coordinate $pos): bool { + public function inBounds(Vector2 $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; } @@ -31,27 +31,28 @@ class Rectangle implements ComplexShape { } 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)),]; + return [new LineRecord(new Vector2($this->topLeft->x, $this->bottomRight->y), $this->topLeft), new LineRecord($this->bottomRight, new Vector2($this->topLeft->x, $this->bottomRight->y)), new LineRecord(new Vector2($this->bottomRight->x, $this->topLeft->y), $this->bottomRight), new LineRecord($this->topLeft, new Vector2($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),]; + return [new LineRecord(new Vector2($this->topLeft->x, $this->bottomRight->y), $this->topLeft), new LineRecord($this->bottomRight, new Vector2($this->topLeft->x, $this->bottomRight->y)), new LineRecord(new Vector2($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 multiply($size): Rectangle { return new Rectangle($this->topLeft->multiply($size), $this->bottomRight->multiply($size)); } - public function toPixel($twipSize = TWIP_SIZE): Rectangle { + public function toPixel($twipSize = Constants::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"))); + return new Rectangle(new Vector2((int)$element->getAttribute("left"), (int)$element->getAttribute("top")), new Vector2((int)$element->getAttribute("right"), (int)$element->getAttribute("bottom"))); } public static function fromData($bitdata, &$offset): Rectangle { @@ -71,6 +72,6 @@ class Rectangle implements ComplexShape { $yMax = Utils::binary2dec(substr($bitdata, $offset, $nbits)); $offset += $nbits; - return new Rectangle(new Coordinate($xMin, $yMin), new Coordinate($xMax, $yMax)); + return new Rectangle(new Vector2($xMin, $yMin), new Vector2($xMax, $yMax)); } } diff --git a/src/RenderedFrame.php b/src/RenderedFrame.php new file mode 100644 index 0000000..d4364f9 --- /dev/null +++ b/src/RenderedFrame.php @@ -0,0 +1,24 @@ +list[] = $ob; + } + + /** + * @return RenderedObject[] + */ + public function getObjects(): array { + return $this->list; + } +} \ No newline at end of file diff --git a/src/RenderedObject.php b/src/RenderedObject.php new file mode 100644 index 0000000..320cf3a --- /dev/null +++ b/src/RenderedObject.php @@ -0,0 +1,23 @@ +depth = $depth; + $this->objectId = $objectId; + $this->drawPathList = $drawPathList; + $this->colorTransform = $colorTransform; + $this->matrixTransform = $matrixTransform; + $this->clip = $clip; + } +} diff --git a/src/SWFProcessor.php b/src/SWFProcessor.php new file mode 100644 index 0000000..dd34321 --- /dev/null +++ b/src/SWFProcessor.php @@ -0,0 +1,91 @@ +background = new FillStyleRecord(new Color(255, 255, 255)); + + $rect = $root->getElementsByTagName("size")->item(0)->getElementsByTagName("Rectangle")->item(0); + if ($rect instanceof \DOMElement and $rect->nodeName === "Rectangle") { + $this->viewPort = Rectangle::fromXML($rect); + } else { + throw new \Exception("Could not find viewport"); + } + + $this->frameRate = $root->getAttribute("framerate"); + } + + public function getFrameRate(): float { + return $this->frameRate; + } + + public function getViewPort(): Rectangle { + return $this->viewPort; + } + + public function getAudio() { + return $this->audio; + } + + protected function process(): ?string { + $node = $this->current(); + if ($node === null) { + return null; + } + + switch ($node->nodeName) { + case "SetBackgroundColor": + //TODO: implement Gradient + $this->background = new FillStyleRecord(Color::fromXML($node->getElementsByTagName("color")->item(0)->getElementsByTagName("Color")->item(0))); + return $node->nodeName; + case "SoundStreamHead": + case "SoundStreamHead2": + $this->audio = (object)["node" => $node, "start" => null, "content" => [],]; + return $node->nodeName; + case "DefineSound": + $this->audio = (object)["node" => $node, "start" => $this->getFrame(), "content" => []]; + return $node->nodeName; + case "SoundStreamBlock": + if ($this->audio !== null) { + if ($this->audio->start === null) { + $this->audio->start = $this->getFrame(); + } + //$audio .= substr($data, 2); + $this->audio->content[] = [$this->getFrame(), base64_decode(trim($node->textContent))]; + } + return $node->nodeName; + } + + return parent::process(); + } + + public function nextFrameOutput(): ?FrameInformation { + $actions = $actions ?? new ActionList(); + $frame = $this->nextFrame($actions); + + if ($frame === null) { + return null; + } + + //TODO: actions? + + $frame->addChild(self::BACKGROUND_OBJECT_DEPTH, new ViewFrame(self::BACKGROUND_OBJECT_ID, new DrawPathList([DrawPath::fill($this->background, new Shape($this->getViewPort()->draw()))]))); + return new FrameInformation($this->frame - 1, $this->frameRate, $frame); + } +} \ No newline at end of file diff --git a/src/SWFTreeProcessor.php b/src/SWFTreeProcessor.php new file mode 100644 index 0000000..c0f16bb --- /dev/null +++ b/src/SWFTreeProcessor.php @@ -0,0 +1,198 @@ +frame = 0; + $this->root = $root; + $this->layout = new ViewLayout($objectId, null); + + $e = $this->root->getElementsByTagName("tags")->item(0); + if ($e instanceof \DOMElement) { + $e = $e->firstChild; + do { + $e = $e->nextSibling; + } while ($e !== null and !($e instanceof \DOMElement)); + $this->currentElement = $e; + } else { + throw new \Exception("Could not find elements"); + } + } + + public function getFrame(): int { + return $this->frame; + } + + protected function next() { + $e = $this->currentElement; + do { + $e = $e->nextSibling; + } while ($e !== null and !($e instanceof \DOMElement)); + $this->currentElement = $e; + } + + protected function current(): ?\DOMElement { + return $this->currentElement; + } + + /** + * @return ObjectDefinition[] + */ + public function getObjects(): array { + return $this->objects; + } + + protected function process(): ?string { + $node = $this->current(); + if ($node === null) { + return null; + } + + switch ($node->nodeName) { + case "DefineShape": + case "DefineShape2": + case "DefineShape3": + case "DefineShape4": + case "DefineShape5": + $shape = ShapeDefinition::fromXML($node); + $this->objects[$shape->getObjectId()] = $shape; + break; + case "DefineSprite": + + $objectID = (int)$node->getAttribute("objectID"); + $framesCount = (int)$node->getAttribute("frames"); + $spriteTree = new SWFTreeProcessor($objectID, $node); + $actions = new ActionList(); + /** @var ViewFrame[] $frames */ + $frames = []; + while (($frame = $spriteTree->nextFrame($actions)) !== null) { + $frames[] = $frame; + } + + $sprite = new SpriteDefinition($objectID, $frames); + + $this->objects[$sprite->getObjectId()] = $sprite; + break; + case "DefineBitsLossless": + case "DefineBitsLossless2": + $bitmap = BitmapDefinition::fromXML($node); + $this->objects[$bitmap->getObjectId()] = $bitmap; + break; + case "DefineBitsJPEG2": + case "DefineBitsJPEG3": + $bitmap = JPEGBitmapDefinition::fromXML($node); + $this->objects[$bitmap->getObjectId()] = $bitmap; + break; + case "RemoveObject": + case "RemoveObject2": + $this->layout->remove((int)$node->getAttribute("depth")); + break; + case "PlaceObject2": + case "PlaceObject3": + $depth = (int)$node->getAttribute("depth"); + + $objectID = $node->hasAttribute("objectID") ? (int)$node->getAttribute("objectID") : null; + $clipDepth = $node->hasAttribute("clipDepth") ? (int)$node->getAttribute("clipDepth") : null; + + $replace = $node->getAttribute("replace") === "1"; + + $object = $objectID === null ? $this->layout->get($depth) : ($this->objects[$objectID] ?? null); + if ($object === null) { + var_dump("Object oid:$objectID depth:$depth not found"); + /*if($replace){ + $this->layout->remove($depth); + }*/ + //TODO: insert bogus one + $this->layout->remove($depth); + break; + } + + + $transform = null; + $transformNodes = $node->getElementsByTagName("transform"); + if ($transformNodes->count() > 0) { + $transform = MatrixTransform::fromXML($transformNodes->item(0)->getElementsByTagName("Transform")->item(0)); + //TODO: multiple transforms!??? + } + + + $colorTransform = null; + $colorTransformNodes = $node->getElementsByTagName("colorTransform"); + if ($colorTransformNodes->count() > 0) { + //TODO: add more modes? + $colorTransform = ColorTransform::fromXML($colorTransformNodes->item(0)->getElementsByTagName("ColorTransform2")->item(0)); + } + + + $currentObject = $this->layout->get($depth); + + + if ($replace and $currentObject !== null) { + if ($currentObject->getObjectId() === $object->getObjectId()) { + if ($transform !== null) { + $currentObject->setMatrixTransform($transform); + } + if ($colorTransform !== null) { + $currentObject->setColorTransform($colorTransform); + } + break; + } + } + + $view = $clipDepth !== null ? new ClippingViewLayout($clipDepth, $objectID, $object, $this->layout) : new ViewLayout($objectID, $object, $this->layout); + $view->setMatrixTransform($transform); + $view->setColorTransform($colorTransform); + + if ($replace) { + $this->layout->replace($depth, $view); + } else { + $this->layout->place($depth, $view); + } + + break; + case "ShowFrame": + break; + case "End": + break; + default: + //TODO + var_dump($node->nodeName); + } + + return $node->nodeName; + } + + public function getViewLayout(): ViewLayout { + return $this->layout; + } + + public function nextFrame(ActionList $actions): ?ViewFrame { + while (($nodeName = $this->process()) !== null) { + $this->next(); + + if ($nodeName === "ShowFrame") { + break; + } else if ($nodeName === "End" and $this->frame === 0) { + break; + } + } + + if ($nodeName === null) { + return null; + } + + ++$this->frame; + //TODO $this->layout->hasFrame(); + return $this->layout->nextFrame($actions); + } +} \ No newline at end of file diff --git a/src/Shape.php b/src/Shape.php index b2a2a92..a5ec7e6 100644 --- a/src/Shape.php +++ b/src/Shape.php @@ -13,4 +13,8 @@ class Shape { public function __construct(array $edges = []) { $this->edges = $edges; } + + public function merge(Shape $shape): Shape { + return new Shape(array_merge($this->edges, $shape->edges)); + } } \ No newline at end of file diff --git a/src/ShapeConverter.php b/src/ShapeConverter.php index 9431df2..bbc5969 100644 --- a/src/ShapeConverter.php +++ b/src/ShapeConverter.php @@ -11,7 +11,7 @@ class ShapeConverter { private ?ActivePath $fill_style1 = null; private ?ActivePath $line_style = null; - private Coordinate $position; + private Vector2 $position; private PendingPathMap $fills; private PendingPathMap $strokes; @@ -19,10 +19,9 @@ class ShapeConverter { public DrawPathList $commands; - public function __construct(\DOMElement $element, StyleList $currentStyles) { $this->styles = $currentStyles; - $this->position = new Coordinate(0, 0); + $this->position = new Vector2(0, 0); $this->fills = new PendingPathMap(); $this->strokes = new PendingPathMap(); $this->commands = new DrawPathList([]); @@ -45,7 +44,7 @@ class ShapeConverter { if ($node->hasAttribute("fillStyle1")) { - if($this->fill_style1 !== null){ + if ($this->fill_style1 !== null) { $this->fills->merge_path($this->fill_style1, true); } @@ -56,8 +55,8 @@ class ShapeConverter { } if ($node->hasAttribute("fillStyle0")) { - if($this->fill_style0 !== null){ - if(!$this->fill_style0->segment->is_empty()){ + if ($this->fill_style0 !== null) { + if (!$this->fill_style0->segment->is_empty()) { $this->fill_style0->flip(); $this->fills->merge_path($this->fill_style0, true); } @@ -68,7 +67,7 @@ class ShapeConverter { } if ($node->hasAttribute("lineStyle")) { - if($this->line_style !== null){ + if ($this->line_style !== null) { $this->strokes->merge_path($this->line_style, false); } @@ -77,13 +76,13 @@ class ShapeConverter { $this->line_style = $id > 0 ? new ActivePath($id, $this->position) : null; } - }else if ($node->nodeName === "LineTo") { - $curve = LineRecord::fromXML($node, $this->position); + } else if ($node->nodeName === "LineTo") { + $line = LineRecord::fromXML($node, $this->position); - $this->visit_point($curve->coord, false); + $this->visit_point($line->coord, false); - $this->position = $curve->coord; - }else if ($node->nodeName === "CurveTo") { + $this->position = $line->coord; + } else if ($node->nodeName === "CurveTo") { $curve = QuadraticCurveRecord::fromXML($node, $this->position); $this->visit_point($curve->control, true); @@ -96,51 +95,51 @@ class ShapeConverter { $this->flush_layer(); } - public function visit_point(Coordinate $coordinate, bool $isBezierControlPoint){ + public function visit_point(Vector2 $coordinate, bool $isBezierControlPoint) { $point = new VisitedPoint($coordinate, $isBezierControlPoint); - if($this->fill_style0 !== null){ + if ($this->fill_style0 !== null) { $this->fill_style0->add_point($point); } - if($this->fill_style1 !== null){ + if ($this->fill_style1 !== null) { $this->fill_style1->add_point($point); } - if($this->line_style !== null){ + if ($this->line_style !== null) { $this->line_style->add_point($point); } } - public function flush_paths(){ - if($this->fill_style1 !== null){ - $this->fills->merge_path($this->fill_style1, true); - $this->fill_style1 = new ActivePath($this->fill_style1->style, $this->position); + public function flush_paths() { + if ($this->fill_style1 !== null) { + $this->fills->merge_path($this->fill_style1, true); + $this->fill_style1 = new ActivePath($this->fill_style1->style, $this->position); } - if($this->fill_style0 !== null){ - if(!$this->fill_style0->segment->is_empty()){ + if ($this->fill_style0 !== null) { + if (!$this->fill_style0->segment->is_empty()) { $this->fill_style0->flip(); $this->fills->merge_path($this->fill_style0, true); } $this->fill_style0 = new ActivePath($this->fill_style0->style, $this->position); } - if($this->line_style !== null){ + if ($this->line_style !== null) { $this->strokes->merge_path($this->line_style, false); $this->line_style = new ActivePath($this->line_style->style, $this->position); } } - public function flush_layer(){ + public function flush_layer() { $this->flush_paths(); $this->fill_style0 = null; $this->fill_style1 = null; $this->line_style = null; - foreach ($this->fills->map as $styleId => $path){ + foreach ($this->fills->map as $styleId => $path) { assert($styleId > 0 && $styleId < count($this->styles->fillStyles)); // ????? $style = $this->styles->getFillStyle($styleId - 1); - if($style === null){ + if ($style === null) { var_dump($this->styles); var_dump($styleId); } @@ -149,16 +148,16 @@ class ShapeConverter { } $this->fills->map = []; - foreach ($this->strokes->map as $styleId => $path){ + foreach ($this->strokes->map as $styleId => $path) { assert($styleId > 0 && $styleId < count($this->styles->lineStyles)); // ????? $style = $this->styles->getLineStyle($styleId - 1); - foreach ($path->segments as $segment){ + foreach ($path->segments as $segment) { $segmentStyle = $style; //Close non-closed segments by double drawing backwards - if(!$segment->is_closed()){ + if (!$segment->is_closed()) { $other = clone $segment; $other->flip(); $segment->merge($other); diff --git a/src/ShapeDefinition.php b/src/ShapeDefinition.php index beadc46..13b85d9 100644 --- a/src/ShapeDefinition.php +++ b/src/ShapeDefinition.php @@ -3,25 +3,24 @@ namespace swf2ass; class ShapeDefinition implements ObjectDefinition { - public $id; + public int $id; public Rectangle $bounds; public DrawPathList $shapeList; - public function __construct($id, Rectangle $bounds, DrawPathList $shapes) { + public function __construct(int $id, Rectangle $bounds, DrawPathList $shapes) { $this->id = $id; $this->bounds = $bounds; $this->shapeList = $shapes; } - public function getId(){ + public function getObjectId(): int { return $this->id; } - public function getShapeList() : DrawPathList{ + + public function getShapeList(): DrawPathList { return $this->shapeList; } - - static function fromXML(\DOMElement $element): ShapeDefinition { $styles = StyleList::fromXML($element->getElementsByTagName("styles")->item(0)->getElementsByTagName("StyleList")->item(0)); //Utils::dump_element($element->getElementsByTagName("styles")->item(0)); @@ -32,6 +31,6 @@ class ShapeDefinition implements ObjectDefinition { $drawPathList = $drawPathList->merge(DrawPathList::fromXML($node, $styles)); } } - return new ShapeDefinition($element->getAttribute("objectID"), Rectangle::fromXML($element->getElementsByTagName("bounds")->item(0)->getElementsByTagName("Rectangle")->item(0)), $drawPathList); + return new ShapeDefinition((int)$element->getAttribute("objectID"), Rectangle::fromXML($element->getElementsByTagName("bounds")->item(0)->getElementsByTagName("Rectangle")->item(0)), $drawPathList); } } \ No newline at end of file diff --git a/src/ShiftedRadialGradient.php b/src/ShiftedRadialGradient.php index 20a3dce..97337de 100644 --- a/src/ShiftedRadialGradient.php +++ b/src/ShiftedRadialGradient.php @@ -15,10 +15,12 @@ class ShiftedRadialGradient implements Gradient { $this->transform = $transform; $this->attributes = $attributes; } - public function getItems() : array { + + public function getItems(): array { return $this->colors; } - public function getMatrixTransform() : ?MatrixTransform{ + + public function getMatrixTransform(): ?MatrixTransform { return $this->transform; } @@ -28,10 +30,6 @@ class ShiftedRadialGradient implements Gradient { $colors[] = GradientItem::fromXML($item); } $matrix = $element->getElementsByTagName("matrix"); - return new ShiftedRadialGradient($colors, $matrix->count() > 0 ? MatrixTransform::fromXML($matrix->item(0)) : null, [ - "spreadMode" => $element->hasAttribute("spreadMode") ? $element->getAttribute("spreadMode") : null, - "interpolationMode" => $element->hasAttribute("interpolationMode") ? $element->getAttribute("interpolationMode") : null, - "shift" => $element->hasAttribute("shift") ? $element->getAttribute("shift") : null, - ]); + return new ShiftedRadialGradient($colors, $matrix->count() > 0 ? MatrixTransform::fromXML($matrix->item(0)) : null, ["spreadMode" => $element->hasAttribute("spreadMode") ? $element->getAttribute("spreadMode") : null, "interpolationMode" => $element->hasAttribute("interpolationMode") ? $element->getAttribute("interpolationMode") : null, "shift" => $element->hasAttribute("shift") ? $element->getAttribute("shift") : null,]); } } \ No newline at end of file diff --git a/src/SpriteDefinition.php b/src/SpriteDefinition.php new file mode 100644 index 0000000..b469a96 --- /dev/null +++ b/src/SpriteDefinition.php @@ -0,0 +1,56 @@ +id = $id; + $this->frames = $frames; + $this->frameCounter = $frameCounter % count($this->frames); + } + + public function getObjectId(): int { + return $this->id; + } + + public function getShapeList(): DrawPathList { + $list = new DrawPathList(); + foreach ($this->frames[$this->frameCounter]->render(0, [], ColorTransform::identity(), MatrixTransform::identity())->getObjects() as $object) { + $list = $list->merge($object->drawPathList); + } + return $list; + } + + public function hasFrame(): bool { + return $this->hasFrames; + } + + public function getFrameCounter(): int { + return $this->frameCounter; + } + + public function getFrame(int $frameNumber): ?ViewFrame { + return $this->frames[$frameNumber] ?? null; + } + + public function nextFrame(): ViewFrame { + $f = $this->frames[$this->frameCounter]; + ++$this->frameCounter; + if ($this->frameCounter >= count($this->frames)) { + $this->frameCounter = count($this->frames) - 1; + $this->hasFrames = false; //TODO LOOP + } + return $f; + } +} \ No newline at end of file diff --git a/src/Square.php b/src/Square.php index c48b75d..7c61115 100644 --- a/src/Square.php +++ b/src/Square.php @@ -4,7 +4,7 @@ namespace swf2ass; class Square extends Rectangle { - public function __construct(Coordinate $topLeft, $size = 1) { - parent::__construct($topLeft, $topLeft->add(new Coordinate($size, $size))); + public function __construct(Vector2 $topLeft, $size = 1) { + parent::__construct($topLeft, $topLeft->add(new Vector2($size, $size))); } } diff --git a/src/StyleContainer.php b/src/StyleContainer.php index e409d95..6c0ec40 100644 --- a/src/StyleContainer.php +++ b/src/StyleContainer.php @@ -8,8 +8,8 @@ class StyleContainer { public $fscx = null, $fscy = null; public $fax = null, $fay = null; - public ?Coordinate $pos = null; - /** @var ?Coordinate[] */ + public ?Vector2 $pos = null; + /** @var ?Vector2[] */ public $move = null; public ?Color $color1 = null; diff --git a/src/StyleList.php b/src/StyleList.php index 56fff0c..d5b485a 100644 --- a/src/StyleList.php +++ b/src/StyleList.php @@ -43,7 +43,7 @@ class StyleList { //TODO Utils::dump_element($node); $fillStyles[] = new FillStyleRecord(new Color(0, 0, 0, 255 / 2)); - }else{ + } else { Utils::dump_element($node); throw new \Exception("Unknown style " . $node->nodeName); } @@ -61,18 +61,18 @@ class StyleList { if ($colors->count() > 0) { $color = Color::fromXML($colors->item(0)->getElementsByTagName("Color")->item(0)); - }else if ($colors2->count() > 0) { + } else if ($colors2->count() > 0) { $color = Color::fromXML($colors2->item(0)); } else if ($grads->count() > 0) { //TODO: other gradients? //$fill = new FillStyleRecord(LinearGradient::fromXML($grads->item(0)->getElementsByTagName("LinearGradient")->item(0))); } - if($color === null){ + if ($color === null) { Utils::dump_element($node); } - $lineStyles[] = new LineStyleRecord(max(TWIP_SIZE, (int)$node->getAttribute("width")), $color, null); + $lineStyles[] = new LineStyleRecord(max(Constants::TWIP_SIZE, (int)$node->getAttribute("width")), $color, null); } } return new StyleList($fillStyles, $lineStyles); diff --git a/src/Utils.php b/src/Utils.php index df59518..0b59cb5 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -26,220 +26,6 @@ abstract class Utils { 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; - - $width = $currentState->viewPort->getWidth(); - $height = $currentState->viewPort->getHeight(); - $ar = $width/$height; - $durationSeconds = (int) ceil($currentState->frameRate * $currentState->frameCount); - -$header = <<frameRate}:{$durationSeconds}:{$width}:{$height}:160:160:160:c -Video AR Value: {$ar} - -[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; - - foreach (explode("\n", $header) as $line){ - yield $line . "\n"; - } - - - $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 === "DefineSound") { - if($currentState->frameOffset === null){ - $currentState->frameOffset = $currentState->frameNumber; - } - } else if ($node->nodeName === "SoundStreamBlock" and $sound !== null) { - if($currentState->frameOffset === null){ - $currentState->frameOffset = $currentState->frameNumber; - } - $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)); - $currentState->displayList[0] = new DisplayEntry(); - $currentState->displayList[0]->object = new ShapeDefinition(0, $currentState->viewPort->multiply(TWIP_SIZE), new DrawPathList([DrawPath::fill(new FillStyleRecord($currentState->backgroundColor), new Shape($currentState->viewPort->multiply(TWIP_SIZE)->draw()))])); - $currentState->displayList[0]->startFrame = $currentState->frameNumber; - $currentState->displayList[0]->depth = 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])) { - var_dump("Unknown object id $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); - var_dump("Frame {$currentState->frameNumber} ~{$currentState->frameOffset} (" . count($currentState->displayList) .")"); - - 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 === "DefineSprite") { - //TODO do actual animation - var_dump("TODO DefineSprite"); - //Utils::dump_element($node); - - foreach ($node->getElementsByTagName("tags")->item(0)->childNodes as $node2) { - if ($node2 instanceof \DOMElement) { - if($node2->nodeName === "PlaceObject2" and isset($currentState->objects[$node2->getAttribute("objectID")])){ - //TODO - $currentState->objects[$node->getAttribute("objectID")] = $currentState->objects[$node2->getAttribute("objectID")]; - var_dump($node->getAttribute("objectID") . " === " . $node2->getAttribute("objectID") . " " . get_class($currentState->objects[$node->getAttribute("objectID")])); - break; - } - } - } - } 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 if ($node->nodeName === "DoAction") { - - Utils::dump_element($node); - } else if ($node->nodeName === "FileAttributes") { - - Utils::dump_element($node); - } else { - - echo $node->nodeName . PHP_EOL; - } - } - } - } - - foreach ($currentState->displayList as $depth => $entry){ - foreach ($entry->getLines($currentState) as $line) { - yield $line . PHP_EOL; - } - } - - return $currentState; - } - static function dump_element(\DOMElement $element) { var_dump($element->ownerDocument->saveXML($element)); } diff --git a/src/Vector2.php b/src/Vector2.php new file mode 100644 index 0000000..7da1802 --- /dev/null +++ b/src/Vector2.php @@ -0,0 +1,64 @@ +x = $x; + $this->y = $y; + } + + + public function equals(Vector2 $b, $epsilon = Constants::EPSILON): bool { + return ($this->x === $b->x or abs($b->x - $this->x) <= $epsilon) and ($this->y === $b->y or abs($b->y - $this->y) <= $epsilon); + } + + public function distance(Vector2 $b): float { + return sqrt(pow($this->x - $b->x, 2) + pow($this->y - $b->y, 2)); + } + + public function invert(): Vector2 { + return new Vector2($this->y, $this->x); + } + + public function add(Vector2 $b): Vector2 { + return new Vector2($this->x + $b->x, $this->y + $b->y); + } + + public function sub(Vector2 $b): Vector2 { + return new Vector2($this->x - $b->x, $this->y - $b->y); + } + + public function abs(): Vector2 { + return new Vector2(abs($this->x), abs($this->y)); + } + + /** + * @param Vector2 $b + * @return numeric + */ + public function dot(Vector2 $b) { + return $this->x * $b->x + $this->y * $b->y; + } + + public function vectorMultiply(Vector2 $b): Vector2 { + return new Vector2($this->x * $b->x, $this->y * $b->y); + } + + public function multiply($size): Vector2 { + return new Vector2($this->x * $size, $this->y * $size); + } + + public function divide($size): Vector2 { + return new Vector2($this->x / $size, $this->y / $size); + } + + public function toPixel($twipSize = Constants::TWIP_SIZE): Vector2 { + return $this->divide($twipSize); + } +} \ No newline at end of file diff --git a/src/ViewFrame.php b/src/ViewFrame.php new file mode 100644 index 0000000..50cc229 --- /dev/null +++ b/src/ViewFrame.php @@ -0,0 +1,113 @@ +objectId = $objectId; + $this->drawPathList = $drawPathList; + } + + public function getObjectId(): int { + return $this->objectId; + } + + public function setClipDepthMap(array $clipDepthMap) { + $this->clipDepthMap = $clipDepthMap; + //TODO: process this + } + + /** + * @return ViewFrame[]|null + */ + public function getClipDepthMap(): ?array { + return $this->clipDepthMap; + } + + public function addChild(int $depth, ViewFrame $frame) { + if ($this->drawPathList !== null) { + throw new \Exception(); + } + $this->depthMap[$depth] = $frame; + } + + /** + * @return ViewFrame[] + */ + public function getDepthMap(): array { + return $this->depthMap; + } + + public function setColorTransform(?ColorTransform $transform) { + $this->colorTransform = $transform; + } + + public function setMatrixTransform(?MatrixTransform $transform) { + $this->matrixTransform = $transform; + } + + public function getColorTransform(): ?ColorTransform { + return $this->colorTransform; + } + + public function getMatrixTransform(): ?MatrixTransform { + return $this->matrixTransform; + } + + public function render(int $baseDepth, array $depthChain, ColorTransform $parentColor, MatrixTransform $parentMatrix): RenderedFrame { + $depthChain[] = $baseDepth; + + $matrixTransform = $this->matrixTransform !== null ? $parentMatrix->combine($this->matrixTransform) : $parentMatrix; + $colorTransform = $this->colorTransform !== null ? $parentColor->combine($this->colorTransform) : $parentColor; + + + $renderedFrame = new RenderedFrame(); + + $clipShape = null; + if ($this->clipDepthMap !== null) { + $colorIdentity = ColorTransform::identity(); + $matrixIdentity = MatrixTransform::identity(); + $clipShape = new Shape(); + foreach ($this->clipDepthMap as $clipDepth => $clipFrame) { + //TODO: detect rectangle clips? for \iclip + //TODO: add \clip for bounds + foreach ($clipFrame->render($clipDepth, $depthChain, $colorIdentity, $matrixIdentity)->getObjects() as $clipObject) { + foreach ($clipObject->drawPathList->commands as $clipPath) { + //TODO: is transform here needed? + $clipShape = $clipShape->merge($clipObject->matrixTransform->applyToShape($clipPath->commands)); + } + } + } + } + + if ($this->drawPathList !== null) { + $renderedFrame->add(new RenderedObject($depthChain, $this->getObjectId(), $this->drawPathList, $colorTransform, $matrixTransform, $clipShape)); + } else { + foreach ($this->depthMap as $depth => $frame) { + $objects = $frame->render($depth, $depthChain, $colorTransform, $matrixTransform)->getObjects(); + foreach ($objects as $object) { + if ($object->clip !== null and $clipShape !== null) { + $object->clip = $object->clip->merge($clipShape); + } else if ($clipShape !== null) { + $object->clip = $clipShape; + } + + $renderedFrame->add($object); + } + } + } + + return $renderedFrame; + } +} \ No newline at end of file diff --git a/src/ViewLayout.php b/src/ViewLayout.php new file mode 100644 index 0000000..bfab6a7 --- /dev/null +++ b/src/ViewLayout.php @@ -0,0 +1,150 @@ +objectId = $objectId; + if ($object !== null and $objectId !== $object->getObjectId()) { + throw new \LogicException(); + } + $this->parent = $parent; + $this->object = $object; + } + + public function getObjectId(): int { + return $this->object !== null ? $this->object->getObjectId() : -1; + } + + public function getObject(): ?ObjectDefinition { + return $this->object; + } + + /** + * @return ViewLayout[] + */ + public function getDepthMap(): array { + return $this->depthMap; + } + + public function get(int $depth): ?ViewLayout { + return $this->depthMap[$depth] ?? null; + } + + public function place(int $depth, ViewLayout $ob) { + if ($this->object !== null) { + throw new \LogicException("Cannot have ObjectDefinition and children at the same time"); + } + if (isset($this->depthMap[$depth]) and $this->depthMap[$depth] !== $ob) { + throw new \Exception("Depth $depth already exists: tried replacing object " . $this->depthMap[$depth]->getObjectId() . " with " . $ob->getObjectId()); + } + $this->depthMap[$depth] = $ob; + } + + public function replace(int $depth, ViewLayout $ob) { + if ($this->object !== null) { + throw new \LogicException("Cannot have ObjectDefinition and children at the same time"); + } + $oldObject = $this->get($depth); + if ($oldObject !== null) { + $ob->colorTransform = $ob->colorTransform ?? $oldObject->colorTransform; + $ob->matrixTransform = $ob->matrixTransform ?? $oldObject->matrixTransform; + } + $this->depthMap[$depth] = $ob; + } + + public function remove(int $depth) { + unset($this->depthMap[$depth]); + } + + public function hasFrame(): bool { + if ($this->object !== null) { + if ($this->object instanceof MultiFrameObjectDefinition) { + if ($this->object->hasFrame()) { + return true; + } + } + } else { + foreach ($this->depthMap as $ob) { + if ($ob->hasFrame()) { + return true; + } + } + } + + //TODO + + return false; + } + + public function getMatrixTransform(): ?MatrixTransform { + return $this->matrixTransform; + } + + public function getColorTransform(): ?ColorTransform { + return $this->colorTransform; + } + + public function setMatrixTransform(?MatrixTransform $transform) { + $this->matrixTransform = $transform; + } + + public function setColorTransform(?ColorTransform $transform) { + $this->colorTransform = $transform; + } + + 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()); + } + } else { + $frame = new ViewFrame($this->getObjectId(), null); + /** @var ClippingViewLayout[] $clipMap */ + $clipMap = []; + + ksort($this->depthMap); + + foreach ($this->depthMap as $depth => $child) { + if ($child instanceof ClippingViewLayout) { + $clipMap[$depth] = $child; + } else { + /** @var ViewFrame[] $clips */ + $clips = []; //TODO: make something else? + foreach ($clipMap as $clipDepth => $clip) { + //$targetDepth = $clip->getClipDepth() + $clipDepth; + if ($clip->getClipDepth() > $depth and $clipDepth < $depth) { + $clips[$clipDepth] = $clip->nextFrame($actionList); + } + } + + $f = $child->nextFrame($actionList); + if (count($clips) > 0) { + $f->setClipDepthMap($clips); + } + $frame->addChild($depth, $f); + } + } + } + + $frame->setColorTransform($this->colorTransform); + $frame->setMatrixTransform($this->matrixTransform); + + return $frame; + } +} \ No newline at end of file diff --git a/src/VisitedPoint.php b/src/VisitedPoint.php index 2d285cd..82da48b 100644 --- a/src/VisitedPoint.php +++ b/src/VisitedPoint.php @@ -4,10 +4,10 @@ namespace swf2ass; class VisitedPoint { - public Coordinate $pos; + public Vector2 $pos; public bool $is_bezier_control; - public function __construct(Coordinate $pos, bool $is_bezier_control = false) { + public function __construct(Vector2 $pos, bool $is_bezier_control = false) { $this->pos = $pos; $this->is_bezier_control = $is_bezier_control; } diff --git a/src/ass/ASSColorTag.php b/src/ass/ASSColorTag.php new file mode 100644 index 0000000..790236b --- /dev/null +++ b/src/ass/ASSColorTag.php @@ -0,0 +1,10 @@ +text = $text; + } + + public static function encodeTime(int $ms, $msPrecision = 2): string { + if ($ms < 0) { + throw new \LogicException("ms less than 0: $ms"); + } + + $hours = intdiv($ms, self::HOURS_MS); + $ms -= $hours * self::HOURS_MS; + $minutes = intdiv($ms, self::MINUTES_MS); + $ms -= $minutes * self::MINUTES_MS; + + $s = explode(".", strval(round($ms / 1000, $msPrecision))); + if (!isset($s[1])) { + $s[1] = 0; + } + + return ((string)$hours) . ":" . str_pad((string)$minutes, 2, "0", STR_PAD_LEFT) . ':' . str_pad($s[0], 2, "0", STR_PAD_LEFT) . "." . str_pad($s[1], $msPrecision, "0", STR_PAD_RIGHT); + } + + public function encode(): string { + return ($this->isComment ? "Comment" : "Dialogue") . ": " . $this->layer . "," . self::encodeTime($this->start) . "," . self::encodeTime($this->end) . "," . $this->style . "," . $this->name . "," . $this->marginLeft . "," . $this->marginRight . "," . $this->marginVertical . "," . $this->effect . "," . $this->text; + } +} \ No newline at end of file diff --git a/src/ass/ASSPositioningTag.php b/src/ass/ASSPositioningTag.php new file mode 100644 index 0000000..7fab5e5 --- /dev/null +++ b/src/ass/ASSPositioningTag.php @@ -0,0 +1,11 @@ +toPixel(); + $width = $display->getWidth(); + $height = $display->getHeight(); + $ar = $width / $height; + + + $this->header = <<header !== null) { + foreach (explode("\n", $this->header) as $line) { + yield $line; + } + $this->header = null; + } + + $objects = $frame->getObjects(); + usort($objects, [self::class, "depthSort"]); + + $dupeBuffer = []; + + foreach ($objects as $object) { + foreach ($this->renderObject($object) as $line) { + $line->layer = $object->depth[0] === 0 ? $object->depth[1] : $object->depth[0]; + $line->start = $information->getStartTimeMilliSeconds(); + $line->end = $information->getEndTimeMilliSeconds(); + $line->frames = 1; + $line->style = "f"; + + foreach ($this->dupeBuffer as $i => $dup) { + if ($dup->layer === $line->layer and $dup->text === $line->text) { + $line->start = $dup->start; + $line->frames += $dup->frames; + unset($this->dupeBuffer[$i]); + break; + } + } + + + $dupeBuffer[] = $line; + } + } + + //Flush non dupes + foreach ($this->dupeBuffer as $line) { + $line->name .= " dup:{$line->frames}"; + yield $line->encode(); + } + + $this->dupeBuffer = $dupeBuffer; + } + + public function flush(): \Generator { + foreach ($this->dupeBuffer as $line) { + $line->name .= " dup:{$line->frames}"; + yield $line->encode(); + } + $this->dupeBuffer = []; + } + + public static function depthSort(RenderedObject $a, RenderedObject $b) { + if (count($b->depth) > count($a->depth)) { + foreach ($b->depth as $i => $depth) { + $otherDepth = $a->depth[$i] ?? 0; + if ($depth !== $otherDepth) { + return $otherDepth - $depth; + } + } + } else { + foreach ($a->depth as $i => $depth) { + $otherDepth = $b->depth[$i] ?? 0; + if ($depth !== $otherDepth) { + return $depth - $otherDepth; + } + } + } + + return 0; + } + + /** + * @param RenderedObject $object + * @return ASSLine[] + */ + public function renderObject(RenderedObject $object): array { + $lines = []; + foreach ($object->drawPathList->commands as $drawPath) { + $container = containerTag::fromPathEntry($drawPath, $object->clip, $object->colorTransform, $object->matrixTransform); + $line = new ASSLine("{" . $container->encode() . "}"); + $line->name = "o:{$object->objectId} d:" . implode(".", array_slice($object->depth, 1)); + $lines[] = $line; + } + + return $lines; + } +} \ No newline at end of file diff --git a/src/ass/ASSStyleTag.php b/src/ass/ASSStyleTag.php new file mode 100644 index 0000000..dd43bce --- /dev/null +++ b/src/ass/ASSStyleTag.php @@ -0,0 +1,11 @@ +strength = $strength; + } + + public function transitionStyleRecord(StyleRecord $record): ?blurEdgesGaussianTag { + return self::fromStyleRecord($record); + } + + public function encode(): string { + return "\\blur{$this->strength}"; + } + + public static function fromStyleRecord(StyleRecord $record): ?blurEdgesGaussianTag { + //TODO? + return new blurEdgesGaussianTag(0); + } + + public function equals(ASSTag $tag): bool { + return $tag instanceof $this and abs($this->strength - $tag->strength) <= Constants::EPSILON; + } +} \ No newline at end of file diff --git a/src/ass/blurEdgesTag.php b/src/ass/blurEdgesTag.php new file mode 100644 index 0000000..0f547f5 --- /dev/null +++ b/src/ass/blurEdgesTag.php @@ -0,0 +1,32 @@ +strength = $strength; + } + + public function transitionStyleRecord(StyleRecord $record): ?blurEdgesTag { + return self::fromStyleRecord($record); + } + + public function encode(): string { + return "\\be{$this->strength}"; + } + + public static function fromStyleRecord(StyleRecord $record): ?blurEdgesTag { + //TODO? + return new blurEdgesTag(0); + } + + public function equals(ASSTag $tag): bool { + return $tag instanceof $this and abs($this->strength - $tag->strength) <= Constants::EPSILON; + } +} \ No newline at end of file diff --git a/src/ass/borderTag.php b/src/ass/borderTag.php new file mode 100644 index 0000000..5909c61 --- /dev/null +++ b/src/ass/borderTag.php @@ -0,0 +1,36 @@ +size = $size; + } + + public function transitionStyleRecord(StyleRecord $record): ?borderTag { + return self::fromStyleRecord($record); + } + + public function encode(): string { + return "\\bord{$this->size}"; + } + + public static function fromStyleRecord(StyleRecord $record): ?borderTag { + if ($record instanceof LineStyleRecord) { + return new borderTag($record->width / Constants::TWIP_SIZE); + } + return new borderTag(); + } + + public function equals(ASSTag $tag): bool { + return $tag instanceof $this and abs($this->size - $tag->size) <= Constants::EPSILON; + } +} \ No newline at end of file diff --git a/src/ass/clipTag.php b/src/ass/clipTag.php new file mode 100644 index 0000000..714a76c --- /dev/null +++ b/src/ass/clipTag.php @@ -0,0 +1,10 @@ +getCommands()) . ")"; + } +} \ No newline at end of file diff --git a/src/ass/colorTag.php b/src/ass/colorTag.php new file mode 100644 index 0000000..9354526 --- /dev/null +++ b/src/ass/colorTag.php @@ -0,0 +1,33 @@ +color = $color; + } + + public static abstract function fromStyleRecord(StyleRecord $record): ?colorTag; + + public function transitionStyleRecord(StyleRecord $record): ?ASSStyleTag { + return $this::fromStyleRecord($record); + } + + public function equals(ASSTag $tag): bool { + return $tag instanceof $this and $this->color->equals($tag->color); + } + + public function transitionColor(ColorTransform $transform): ?colorTag { + return new $this($transform->applyToColor($this->color)); + } +} \ No newline at end of file diff --git a/src/ass/containerTag.php b/src/ass/containerTag.php new file mode 100644 index 0000000..a5322d7 --- /dev/null +++ b/src/ass/containerTag.php @@ -0,0 +1,136 @@ +tags as $tag) { + if ($tag instanceof ASSColorTag) { + $newTag = $tag->transitionColor($transform); + if ($newTag === null) { + return null; + } + $container->tags[] = $newTag; + } else { + $container->tags[] = $tag; + } + } + return $container; + } + + public function transitionMatrixTransform(MatrixTransform $transform): ?containerTag { + $container = new containerTag(); + + foreach ($this->tags as $tag) { + if ($tag instanceof ASSPositioningTag) { + $newTag = $tag->transitionMatrixTransform($transform); + if ($newTag === null) { + return null; + } + $container->tags[] = $newTag; + } else { + $container->tags[] = $tag; + } + } + return $container; + } + + public function transitionStyleRecord(StyleRecord $record): ?containerTag { + $container = new containerTag(); + + foreach ($this->tags as $tag) { + if ($tag instanceof ASSStyleTag) { + $newTag = $tag->transitionStyleRecord($record); + if ($newTag === null) { + return null; + } + $container->tags[] = $newTag; + } else { + $container->tags[] = $tag; + } + } + return $container; + } + + protected function try_append(?ASSTag $tag, $throw_on_null = false) { + if ($tag !== null) { + $this->tags[] = $tag; + return; + } + + if ($throw_on_null) { + throw new \Exception(); + } + } + + public static function fromPathEntry(DrawPath $path, ?Shape $clip, ?ColorTransform $colorTransform, ?MatrixTransform $matrixTransform): containerTag { + $container = new containerTag(); + $container->try_append(borderTag::fromStyleRecord($path->style)); + $container->try_append(shadowTag::fromStyleRecord($path->style)); + $container->try_append(lineColorTag::fromStyleRecord($path->style)); + $container->try_append(fillColorTag::fromStyleRecord($path->style)); + if ($matrixTransform !== null) { + $container->try_append(matrixTransformTag::fromMatrixTransform($matrixTransform)); + } + if ($colorTransform !== null) { + $container = $container->transitionColor($colorTransform); + } + if ($clip !== null) { + //TODO: matrix transform? + //$container->try_append(new clipTag($clip)); + } + $container->try_append(new drawTag($path->commands)); + + return $container; + } + + public static function fromMatrixTransform(MatrixTransform $transform): ?ASSPositioningTag { + throw new \Exception(); + } + + public static function fromStyleRecord(StyleRecord $record): ?ASSStyleTag { + throw new \Exception(); + } + + public function equals(ASSTag $tag): bool { + if ($tag instanceof $this and count($this->tags) === count($tag->tags)) { + $tags = $this->tags; + $otherTags = $tag->tags; + foreach ($tags as $i => $t) { + foreach ($otherTags as $j => $t2) { + if ($t->equals($t2)) { + unset($tags[$i]); + unset($otherTags[$j]); + } + break; + } + if (isset($tags[$i])) { + break; + } + } + return count($tags) === 0 and count($otherTags) === 0; + } + return false; + } + + public function encode(): string { + $ret = ""; + foreach ($this->tags as $tag) { + $ret .= $tag->encode(); + } + return $ret; + } +} \ No newline at end of file diff --git a/src/ass/drawTag.php b/src/ass/drawTag.php new file mode 100644 index 0000000..c78b246 --- /dev/null +++ b/src/ass/drawTag.php @@ -0,0 +1,10 @@ +getCommands()) . "{\\p0"; + } +} \ No newline at end of file diff --git a/src/ass/drawingTag.php b/src/ass/drawingTag.php new file mode 100644 index 0000000..c1659e0 --- /dev/null +++ b/src/ass/drawingTag.php @@ -0,0 +1,83 @@ +shape = $shape; + } + + public function applyMatrixTransform(MatrixTransform $transform): drawingTag { + return new $this($transform->applyToShape($this->shape)); + } + + /** + * @return string[] + */ + protected function getCommands(): array { + $commands = []; + /** @var ?Record $lastEdge */ + $lastEdge = null; + foreach ($this->shape->edges as $edge) { + if ($edge instanceof MoveRecord) { + $coords = $edge->coord->toPixel(); + $commands[] = "m " . round($coords->x, self::PRECISION) . " " . round($coords->y, self::PRECISION) . " "; + } else if ($edge instanceof LineRecord) { + $coords = $edge->coord->toPixel(); + $commands[] = "l " . round($coords->x, self::PRECISION) . " " . round($coords->y, self::PRECISION) . " "; + } else if ($edge instanceof QuadraticCurveRecord or $edge instanceof CubicCurveRecord or $edge instanceof CubicSplineCurveRecord) { + if ($edge instanceof QuadraticCurveRecord) { + $edge = CubicCurveRecord::fromQuadraticRecord($edge); + } + + if ($edge instanceof CubicCurveRecord) { + $control1 = $edge->control1->toPixel(); + $control2 = $edge->control2->toPixel(); + $anchor = $edge->anchor->toPixel(); + $commands[] = "b " . round($control1->x, self::PRECISION) . " " . round($control1->y, self::PRECISION) . " " . round($control2->x, self::PRECISION) . " " . round($control2->y, self::PRECISION) . " " . round($anchor->x, self::PRECISION) . " " . round($anchor->y, self::PRECISION) . " "; + } + + //TODO + if ($edge instanceof CubicSplineCurveRecord) { + $anchor = $edge->anchor->toPixel(); + $controlPoints = []; + foreach ($edge->control as $control) { + $control = $control->toPixel(); + $controlPoints[] = round($control->x, self::PRECISION) . " " . round($control->y, self::PRECISION); + } + + $commands[] = "s " . implode(" ", $controlPoints) . " " . round($anchor->x, self::PRECISION) . " " . round($anchor->y, self::PRECISION) . " "; + } + + } else { + var_dump($edge); + throw new \Exception("Invalid edge " . get_class($edge)); + } + + $lastEdge = $edge; + } + + return $commands; + } + + public function equals(ASSTag $tag): bool { + return $tag instanceof $this and $this->encode() === $tag->encode(); + } + +} \ No newline at end of file diff --git a/src/ass/fillColorTag.php b/src/ass/fillColorTag.php new file mode 100644 index 0000000..b6f27a9 --- /dev/null +++ b/src/ass/fillColorTag.php @@ -0,0 +1,31 @@ +fill instanceof Color) { + $color = $record->fill; + } else if ($record->fill instanceof Gradient) { //TODO: split this elsewhere + $color = $record->fill->getItems()[0]->color; + } else { + throw new \Exception("Invalid Fill record"); + } + return new fillColorTag($color); + } + return new fillColorTag(new Color(0, 0, 0, 255)); + } + + public function encode(): string { + return "\\1c&H" . strtoupper(Utils::padHex(dechex($this->color->b))) . strtoupper(Utils::padHex(dechex($this->color->g))) . strtoupper(Utils::padHex(dechex($this->color->r))) . "&\\1a&H" . strtoupper(Utils::padHex(dechex($this->color->alpha))) . "&"; + } +} \ No newline at end of file diff --git a/src/ass/insideClipTag.php b/src/ass/insideClipTag.php new file mode 100644 index 0000000..4e369e4 --- /dev/null +++ b/src/ass/insideClipTag.php @@ -0,0 +1,10 @@ +getCommands()) . ")"; + } +} \ No newline at end of file diff --git a/src/ass/lineColorTag.php b/src/ass/lineColorTag.php new file mode 100644 index 0000000..26954e4 --- /dev/null +++ b/src/ass/lineColorTag.php @@ -0,0 +1,21 @@ +color); + } + return new lineColorTag(new Color(0, 0, 0, 255)); + } + + public function encode(): string { + return "\\3c&H" . strtoupper(Utils::padHex(dechex($this->color->b))) . strtoupper(Utils::padHex(dechex($this->color->g))) . strtoupper(Utils::padHex(dechex($this->color->r))) . "&\\3a&H" . strtoupper(Utils::padHex(dechex($this->color->alpha))) . "&"; + } +} \ No newline at end of file diff --git a/src/ass/matrixTransformTag.php b/src/ass/matrixTransformTag.php new file mode 100644 index 0000000..ba2a68a --- /dev/null +++ b/src/ass/matrixTransformTag.php @@ -0,0 +1,65 @@ +position !== null and $this->position->transitionMatrixTransform($transform) === null) { + return null; + } + if ($this->scale !== null and $this->scale->transitionMatrixTransform($transform) === null) { + return null; + } + if ($this->rotation !== null and $this->rotation->transitionMatrixTransform($transform) === null) { + return null; + } + if ($this->shear !== null and $this->shear->transitionMatrixTransform($transform) === null) { + return null; + } + + return self::fromMatrixTransform($transform); + } + + public function encode(): string { + $tag = ""; + if ($this->scale !== null) { + $tag .= $this->scale->encode(); + } + if ($this->rotation !== null) { + $tag .= $this->rotation->encode(); + } + if ($this->shear !== null) { + $tag .= $this->shear->encode(); + } + if ($this->position !== null) { + $tag .= $this->position->encode(); + } + return $tag; + } + + public function equals(ASSTag $tag): bool { + return $tag instanceof $this and $this->position->equals($tag->position); + } + + public static function fromMatrixTransform(MatrixTransform $transform): ?matrixTransformTag { + $tag = new matrixTransformTag(); + $tag->scale = scaleTag::fromMatrixTransform($transform); + $tag->rotation = rotationTag::fromMatrixTransform($transform); + $tag->shear = shearingTag::fromMatrixTransform($transform); + $tag->position = positionTag::fromMatrixTransform($transform); + + return $tag; + } +} \ No newline at end of file diff --git a/src/ass/positionTag.php b/src/ass/positionTag.php new file mode 100644 index 0000000..25337a8 --- /dev/null +++ b/src/ass/positionTag.php @@ -0,0 +1,35 @@ +position = $position; + } + + public function transitionMatrixTransform(MatrixTransform $transform): ?positionTag { + $translation = $transform->getTranslation()->divide(Constants::TWIP_SIZE); + return $this->position->equals($translation) ? self::fromMatrixTransform($transform) : null; + } + + public function encode(): string { + return "\\pos({$this->position->x},{$this->position->y})"; + } + + public function equals(ASSTag $tag): bool { + return $tag instanceof $this and $this->position->equals($tag->position); + } + + public static function fromMatrixTransform(MatrixTransform $transform): ?positionTag { + $translation = $transform->getTranslation()->divide(Constants::TWIP_SIZE); + return new positionTag($translation); + } +} \ No newline at end of file diff --git a/src/ass/rotationTag.php b/src/ass/rotationTag.php new file mode 100644 index 0000000..de8f7b4 --- /dev/null +++ b/src/ass/rotationTag.php @@ -0,0 +1,36 @@ +rotate = $rotate; + } + + public function transitionMatrixTransform(MatrixTransform $transform): ?rotationTag { + return self::fromMatrixTransform($transform); + } + + public function encode(): string { + return "\\frx{$this->rotate->x}\\fry{$this->rotate->y}"; + } + + public function equals(ASSTag $tag): bool { + return $tag instanceof $this and $this->rotate->equals($tag->rotate); + } + + public static function fromMatrixTransform(MatrixTransform $transform): ?rotationTag { + $scale = $transform->getScale(); + //$rotation = $transform->getRotateSkew(); ???? + + return new rotationTag(new Vector2($scale->y < 0 ? 180 : 0, $scale->x < 0 ? 180 : 0)); + } +} \ No newline at end of file diff --git a/src/ass/scaleTag.php b/src/ass/scaleTag.php new file mode 100644 index 0000000..8c292bc --- /dev/null +++ b/src/ass/scaleTag.php @@ -0,0 +1,35 @@ +scale = $scale; + } + + public function transitionMatrixTransform(MatrixTransform $transform): ?scaleTag { + return self::fromMatrixTransform($transform); + } + + public function encode(): string { + return "\\fscx{$this->scale->x}\\fscy{$this->scale->y}"; + } + + public function equals(ASSTag $tag): bool { + return $tag instanceof $this and $this->scale->equals($tag->scale); + } + + public static function fromMatrixTransform(MatrixTransform $transform): ?scaleTag { + $scale = $transform->getScale(); + + return new scaleTag($scale->abs()->multiply(100)); + } +} \ No newline at end of file diff --git a/src/ass/shadowTag.php b/src/ass/shadowTag.php new file mode 100644 index 0000000..ce7e1c9 --- /dev/null +++ b/src/ass/shadowTag.php @@ -0,0 +1,33 @@ +depth = $depth; + } + + public function transitionStyleRecord(StyleRecord $record): ?shadowTag { + return self::fromStyleRecord($record); + } + + public function encode(): string { + return "\\shad{$this->depth}"; + } + + public static function fromStyleRecord(StyleRecord $record): ?shadowTag { + //TODO? + return new shadowTag(0); + } + + public function equals(ASSTag $tag): bool { + return $tag instanceof $this and abs($this->depth - $tag->depth) <= Constants::EPSILON; + } +} \ No newline at end of file diff --git a/src/ass/shearingTag.php b/src/ass/shearingTag.php new file mode 100644 index 0000000..5c91c0a --- /dev/null +++ b/src/ass/shearingTag.php @@ -0,0 +1,36 @@ +shear = $shear; + } + + public function transitionMatrixTransform(MatrixTransform $transform): ?shearingTag { + return self::fromMatrixTransform($transform); + } + + public function encode(): string { + return "\\fax{$this->shear->x}\\fay{$this->shear->y}"; + } + + public function equals(ASSTag $tag): bool { + return $tag instanceof $this and $this->shear->equals($tag->shear); + } + + public static function fromMatrixTransform(MatrixTransform $transform): ?shearingTag { + $skew = $transform->getRotateSkew(); + //TODO? maybe have to split this + + return new shearingTag($skew); + } +} \ No newline at end of file diff --git a/swf2ass.php b/swf2ass.php index c18e22b..a77799b 100644 --- a/swf2ass.php +++ b/swf2ass.php @@ -22,9 +22,57 @@ $fp = fopen($argv[2], "w+"); $headerTag = $swf->getElementsByTagName("Header")->item(0); if ($headerTag instanceof DOMElement) { - foreach (\swf2ass\Utils::processSWF($headerTag, $soundStream) as $line) { + $processor = new \swf2ass\SWFProcessor($headerTag); + $assRenderer = new \swf2ass\ass\ASSRenderer($processor->getFrameRate(), $processor->getViewPort()); + + $keyFrameInterval = 10 * $processor->getFrameRate(); //kf every 10 seconds + $frameOffset = 0; + while(($frame = $processor->nextFrameOutput()) !== null){ + $audio = $processor->getAudio(); + if($audio !== null and $frameOffset === 0){ + if($audio->start === null){ + continue; + } + $frameOffset = $audio->start; + } + + $frame->setFrameOffset($frameOffset); + + $rendered = $frame->getFrame()->render(0, [], \swf2ass\ColorTransform::identity(), \swf2ass\MatrixTransform::identity()); + $drawCalls = 0; + $drawItems = 0; + $clipCalls = 0; + $clipItems = 0; + foreach ($rendered->getObjects() as $object){ + if($object->clip !== null){ + ++$clipCalls; + $clipItems += count($object->clip->edges); + } + foreach ($object->drawPathList->commands as $path){ + ++$drawCalls; + $drawItems += count($path->commands->edges); + } + } + echo "=== frame ".$frame->getFrameNumber()." ~ $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; + + foreach ($assRenderer->renderFrame($frame, $rendered) as $line){ + fwrite($fp, $line . "\n"); + } + + if($frame->getFrameNumber() > 0 and $frame->getFrameNumber() % $keyFrameInterval === 0){ + foreach ($assRenderer->flush() as $line){ + fwrite($fp, $line . "\n"); + } + } + + } + foreach ($assRenderer->flush() as $line){ + fwrite($fp, $line . "\n"); + } + + /*foreach (\swf2ass\Utils::processSWF($headerTag, $soundStream) as $line) { fwrite($fp, $line); - }; + };*/ } /* @@ -32,6 +80,6 @@ if($soundStream !== ""){ file_put_contents($argv[2] . ".mp3", $soundStream); }*/ - +fclose($fp);