From 603a46989655382653eba3078999b640131faf74 Mon Sep 17 00:00:00 2001 From: Christian Seiler Date: Sat, 30 May 2026 09:36:07 +0200 Subject: [PATCH] PHP: add full serialization / unserialization (binary & JSON) implementation This allows PHP scripts to read and/or generate these types of files. --- php/monoformat.php | 203 ---- php/monoformat_bithelpers.php | 43 + php/monoformat_schema.php | 210 ++++ php/monoformat_structured.php | 1925 +++++++++++++++++++++++++++++++++ 4 files changed, 2178 insertions(+), 203 deletions(-) delete mode 100644 php/monoformat.php create mode 100644 php/monoformat_bithelpers.php create mode 100644 php/monoformat_schema.php create mode 100644 php/monoformat_structured.php diff --git a/php/monoformat.php b/php/monoformat.php deleted file mode 100644 index 68f9edf..0000000 --- a/php/monoformat.php +++ /dev/null @@ -1,203 +0,0 @@ -text); - $result = pack("vvvvvCCvv", - 17, - $this->x, - $this->y, - $this->width, - $this->height, - $this->flags, - $this->scrollSpeed, - $this->fontIndex, - $len); - $result .= (string) $this->text; - if ($len % 4 != 0) { - $n = 4 - ($len % 4); - for ($i = 0; $i < $n; ++$i) { - $result .= chr(0); - } - } - return $result; - } -} - -class CurrentTimeElement extends Element { - public $x = 0; - public $y = 0; - public $width = 0; - public $height = 0; - public $fontIndex = 0; - public $utcOffset = 0; - public $flags = 0; - - public function serialize() { - $result = pack("vvvvvvvv", - 32, - $this->x, - $this->y, - $this->width, - $this->height, - $this->fontIndex, - $this->utcOffset, - $this->flags); - return $result; - } -} - -class Section { - public function serialize() { - return ""; - } -} - -class AlwaysDrawnSection extends Section { - public $drawOnFront = false; - public $drawOnBack = false; - public $clearBeforeDrawing = false; - public $elements = []; - - public function serialize() { - $flags = 0; - if ($this->drawOnFront) { - $flags |= 0x01; - } - if ($this->drawOnBack) { - $flags |= 0x02; - } - if ($this->clearBeforeDrawing) { - $flags |= 0x04; - } - $inner = pack("vv", $flags, count($this->elements)); - foreach ($this->elements as $element) { - $inner .= $element->serialize(); - } - $len = strlen($inner) + 4; - $len = substr(pack("V", $len), 0, 3); - return pack("C", 1) . $len . $inner; - } -} - -class File { - public $sections = []; - - public function serialize() { - $nSections = count($this->sections); - $result = "\xAF\x7E\x2B\x63"; - $result .= pack("Vvv", 1, $nSections, 0); - foreach ($this->sections as $section) { - $result .= $section->serialize(); - } - return $result; - } -} - -} // namespace monoformat - -namespace { - -$roomName= "BOOL"; - -$data = json_decode(file_get_contents("https://cfp.cttue.de/tdf5/schedule/export/schedule.json"), true); -$talks = []; -$now = new DateTimeImmutable("now"); -foreach ($data["schedule"]["conference"]["days"] as $day) { - foreach ($day["rooms"][$roomName] as $t) { - [$h, $m] = explode(":", $t["duration"]); - $duration = $h * 3600 + $m * 60; - $duration = DateInterval::createFromDateString("$duration sec"); - $talk = [ - "date" => new DateTimeImmutable($t["date"]), - "duration" => $duration, - "title" => $t["title"], - ]; - $talkEnd = $talk["date"]->add($talk["duration"]); - if ($talkEnd < $now) { - continue; - } - if ($talk["date"] > $now && count($talks) > 2) { - break; - } - array_push($talks, $talk); - } -} - -$elements = []; - -function putText($x, $y, $width, $height, $text) { - global $elements; - - $e = new monoformat\HScrollTextElement(); - $e->x = $x; - $e->y = $y; - $e->width = $width; - $e->height = $height; - $e->flags = 0; - $e->scrollSpeed = 15; - $e->fontIndex = 0; - $e->text = $text; - array_push($elements, $e); -} - -$i = 0; -foreach ($talks as $talk) { - putText(10, $i * 20, 25, 20, $talk["date"]->format("H:i")); - putText(35, $i * 20, 85, 20, $talk["title"]); - ++$i; -} - -$section = new monoformat\AlwaysDrawnSection(); -$section->drawOnFront = true; -$section->drawOnBack = true; -$section->clearBeforeDrawing = true; -$section->elements = $elements; - -$file = new monoformat\File(); -array_push($file->sections, $section); - -Header("Content-Type: application/octet-stream"); - -print($file->serialize()); -} // global - -?> diff --git a/php/monoformat_bithelpers.php b/php/monoformat_bithelpers.php new file mode 100644 index 0000000..dbddb64 --- /dev/null +++ b/php/monoformat_bithelpers.php @@ -0,0 +1,43 @@ += (PHP_INT_SIZE * 8)) { + return false; + } + $mask = 1 << $bit; + return ($value & $mask) == $mask; +} + +function setBit(int &$value, int $bit) { + if ($bit < 0 || $bit >= (PHP_INT_SIZE * 8)) { + return; + } + $mask = 1 << $bit; + $value |= $mask; +} + +function unsetBit(int &$value, int $bit) { + if ($bit < 0 || $bit >= (PHP_INT_SIZE * 8)) { + return; + } + $mask = 1 << $bit; + $value &= ~$mask; +} + +function maybeSetBit(int &$value, int $bit, int $set) { + if ($bit < 0 || $bit >= (PHP_INT_SIZE * 8)) { + return; + } + $mask = 1 << $bit; + if ($set) { + $value |= $mask; + } else { + $value &= ~$mask; + } +} + +} // namespace monoformat + +?> diff --git a/php/monoformat_schema.php b/php/monoformat_schema.php new file mode 100644 index 0000000..58a4b6b --- /dev/null +++ b/php/monoformat_schema.php @@ -0,0 +1,210 @@ +getProperties(\ReflectionProperty::IS_PUBLIC); + $result = new $myClass(); + foreach ($properties as $property) { + $name = $property->getName(); + if (array_key_exists($name, $json)) { + $result->$name = (bool) $json[$name]; + } + } + return $result; + } + + public function toJSON(): array { + $result = []; + $myClass = static::class; + $reflect = new \ReflectionClass($myClass); + $properties = $reflect->getProperties(\ReflectionProperty::IS_PUBLIC); + foreach ($properties as $property) { + $name = $property->getName(); + $result[$name] = $this->$name; + } + return $result; + } +} + +enum SectionType : int { + case AlwaysDrawn = 1; + case TimeBasedDrawn = 2; + case MultiTimeBasedDrawn = 3; + case ExpiryDate = 31; + case CustomFont = 32; + + public static function fromName(string $name): SectionType { + foreach (self::cases() as $case) { + if ($case->name === $name) { + return $case; + } + } + throw new \ValueError("$name is not a valid backing value for enum " . self::class); + } +} + +enum ElementType : int { + case Image = 1; + case Animation = 2; + case HScrollImage = 3; + case VScrollImage = 4; + case Line = 5; + case Box = 6; + case ClippedText = 16; + case HScrollText = 17; + //case VScrollText = 18; + case CurrentTime = 32; + + public static function fromName(string $name): SectionType { + foreach (self::cases() as $case) { + if ($case->name === $name) { + return $case; + } + } + throw new \ValueError("$name is not a valid backing value for enum " . self::class); + } +} + +class ScrollFlags { + use FlagStruct; + + public bool $endless = false; + public bool $invertDirection = false; + public bool $padBefore = false; + public bool $padAfter = false; + + public static function fromBitField(int $bitfield): ScrollFlags { + $result = new ScrollFlags(); + $result->endless = isBitSet($bitfield, 0); + $result->invertDirection = isBitSet($bitfield, 1); + $result->padBefore = isBitSet($bitfield, 2); + $result->padAfter = isBitSet($bitfield, 3); + return $result; + } + + public function toBitField(): int { + $result = 0; + maybeSetBit($result, 0, $this->endless); + maybeSetBit($result, 1, $this->invertDirection); + maybeSetBit($result, 2, $this->padBefore); + maybeSetBit($result, 3, $this->padAfter); + return $result; + } +} + +enum LineStyle : int { + case Solid = 0; + + public static function fromName(string $name): SectionType { + foreach (self::cases() as $case) { + if ($case->name === $name) { + return $case; + } + } + throw new \ValueError("$name is not a valid backing value for enum " . self::class); + } +} + +class LineFlags { + use FlagStruct; + + public bool $dark = false; + + public static function fromBitField(int $bitfield): LineFlags { + $result = new LineFlags(); + $result->dark = isBitSet($bitfield, 0); + return $result; + } + + public function toBitField(): int { + $result = 0; + maybeSetBit($result, 0, $this->dark); + return $result; + } +} + +enum FillPatternStyle : int { + case Solid = 0; + + public static function fromName(string $name): SectionType { + foreach (self::cases() as $case) { + if ($case->name === $name) { + return $case; + } + } + throw new \ValueError("$name is not a valid backing value for enum " . self::class); + } +} + +class FillFlags { + use FlagStruct; + + public bool $dark = false; + + public static function fromBitField(int $bitfield): FillFlags { + $result = new FillFlags(); + $result->dark = isBitSet($bitfield, 0); + return $result; + } + + public function toBitField(): int { + $result = 0; + maybeSetBit($result, 0, $this->dark); + return $result; + } +} + +class TextFlags { + use FlagStruct; + + public bool $dark = false; + + public static function fromBitField(int $bitfield): TextFlags { + $result = new TextFlags(); + $result->dark = isBitSet($bitfield, 0); + return $result; + } + + public function toBitField(): int { + $result = 0; + maybeSetBit($result, 0, $this->dark); + return $result; + } +} + +class TimeDisplayFlags { + use FlagStruct; + + public bool $use12h = false; + public bool $showHours = false; + public bool $showMinutes = false; + public bool $showSeconds = false; + + public static function fromBitField(int $bitfield): TimeDisplayFlags { + $result = new TimeDisplayFlags(); + $result->use12h = isBitSet($bitfield, 0); + $result->showHours = isBitSet($bitfield, 1); + $result->showMinutes = isBitSet($bitfield, 2); + $result->showSeconds = isBitSet($bitfield, 3); + return $result; + } + + public function toBitField(): int { + $result = 0; + maybeSetBit($result, 0, $this->use12h); + maybeSetBit($result, 1, $this->showHours); + maybeSetBit($result, 2, $this->showMinutes); + maybeSetBit($result, 3, $this->showSeconds); + return $result; + } +} + +} // namespace monoformat + +?> diff --git a/php/monoformat_structured.php b/php/monoformat_structured.php new file mode 100644 index 0000000..804c332 --- /dev/null +++ b/php/monoformat_structured.php @@ -0,0 +1,1925 @@ += $n) { + break; + } + maybeSetBit($byte, $j, $pixels[$k]); + } + $result .= chr($byte); + } + return $result; +} + +function unserializePixels(int $n, string $serialized): array { + $nBytes = intdiv($n + 7, 8); + if (strlen($serialized) != $nBytes) { + $actual = strlen($serializd); + throw new \ValueError("Could not unserialize a pixel bitmap: the size $actual does not equal the expected $nBytes"); + } + $result = array_fill(0, $n, false); + for ($i = 0; $i < $nBytes; ++$i) { + $byte = ord($serialized[$i]); + for ($j = 0; $j < 8; ++$j) { + $k = $i * 8 + $j; + if ($k >= $n) { + break; + } + $result[$k] = isBitSet($byte, $j); + } + } + return $result; +} + +function packU24LE(int $value) { + return substr(pack("V", $value), 0, 3); +} + +function unpackU24LE(string $parts) { + return array_values(unpack("V", $parts . "\x00"))[0]; +} + +function alignSerialized(string $data, int $alignment) { + $n = strlen($data); + if ($n % $alignment) { + $target = intdiv($n + $alignment - 1, $alignment) * $alignment; + $remaining = $target - $n; + $data .= str_repeat("\x00", $remaining); + } + return $data; +} + +abstract class Section { + abstract public function sectionType(): SectionType; + abstract public function minimumFormatVersion(): int; + abstract public function serializeBinary(int $formatVersion): string; + abstract public function serializeJSON(int $formatVersion): array; +} + +abstract class Element { + abstract public function elementType(): ElementType; + abstract public function minimumFormatVersion(): int; + abstract public function serializeBinary(int $formatVersion): string; + abstract public function serializeJSON(int $formatVersion): array; +} + +class ImageElement extends Element { + private int $m_x = 0; + private int $m_y = 0; + private int $m_width = 0; + private int $m_height = 0; + private array $m_image = []; + + public function x(): int { + return $this->m_x; + } + + public function y(): int { + return $this->m_y; + } + + public function width(): int { + return $this->m_width; + } + + public function height(): int { + return $this->m_height; + } + + public function image(): array { + return $this->m_image; + } + + public function setImage(array $image) { + $n = $m_width * $m_height; + if (count($image) != $n) { + $actual = count($image); + throw new \ValueError("Cannot update the image of an ImageElement: the number of pixels supplied, $actual, is not the expected number, $n"); + } + $this->m_image = $image; + } + + public function __construct(int $x, int $y, int $width, int $height) { + $this->m_x = $x; + $this->m_y = $y; + $this->m_width = $width; + $this->m_height = $height; + $n = $width * $height; + $this->m_image = array_fill(0, $n, false); + } + + public function elementType(): ElementType { + return ElementType::Image; + } + + public function minimumFormatVersion(): int { + return 1; + } + + public function serializeBinary(int $formatVersion): string { + $result = pack("vvvvvv", + ElementType::Image->value, + $this->m_x, + $this->m_y, + $this->m_width, + $this->m_height, + 0); + $result .= serializePixels($this->m_image); + return alignSerialized($result, 4); + } + + public function serializeJSON(int $formatVersion): array { + $result = []; + $result["type"] = ElementType::Image->name; + $result["x"] = $this->m_x; + $result["y"] = $this->m_y; + $result["width"] = $this->m_width; + $result["height"] = $this->m_height; + $result["image"] = $this->m_image; + } + + public static function parseBinary(string &$data, int $formatVersion): ImageElement { + [$type, $x, $y, $width, $height, $reserved] = array_values(unpack("v6", substr($data, 0, 12))); + $type = ElementType::from($type); + if ($type != ElementType::Image) { + throw new \ValueError("Could not unserialize an image element: type mismatch"); + } + $n = $width * $height; + $nBytes = intdiv($n + 7, 8); + $nAligned = intdiv($nBytes + 3, 4) * 4; + $pixels = unserializePixels($width * $height, substr($data, 12, $nBytes)); + $result = new ImageElement($x, $y, $width, $height); + $result->setImage($pixels); + $data = substr($data, 12 + $nAligned); + return $result; + } + + public static function parseJSON(array $element, int $formatVersion): ImageElement { + $type = ElementType::fromString($element["type"]); + if ($type != ElementType::Image) { + throw new \ValueError("Could not unserialize an image element: type mismatch"); + } + $result = new ImageElement($element["x"], $element["y"], $element["width"], $element["height"]); + $result->setImage($element["image"]); + return $result; + } +} + +class AnimationElement extends Element { + private int $m_x = 0; + private int $m_y = 0; + private int $m_width = 0; + private int $m_height = 0; + private int $m_updateInterval = 0; + private array $m_frames = []; + + public function x(): int { + return $this->m_x; + } + + public function y(): int { + return $this->m_y; + } + + public function width(): int { + return $this->m_width; + } + + public function height(): int { + return $this->m_height; + } + + public function numberOfFrames(): int { + return count($this->m_frames); + } + + public function updateInterval(): int { + return $this->m_updateInterval; + } + + public function image(int $n): array { + if ($n >= count($this->m_frames)) { + $max = count($this->m_frames) - 1; + throw new \ValueError("Cannot retrieve a frame of an AnimationElement: the frame index $n is not in the range [0..$max]"); + } + return $this->m_frames[$n]; + } + + public function setImage(int $n, array $image) { + if ($n >= count($this->m_frames)) { + $max = count($this->m_frames) - 1; + throw new \ValueError("Cannot retrieve a frame of an AnimationElement: the frame index $n is not in the range [0..$max]"); + } + $nPixels = $m_width * $m_height; + if (count($image) != $nPixels) { + $actual = count($image); + throw new \ValueError("Cannot update a frame of an AnimationElement: the number of pixels supplied, $actual, is not the expected number, $nPixels"); + } + $this->m_frames[$n] = $image; + } + + public function __construct(int $x, int $y, int $width, int $height, int $numberOfFrames, int $updateInterval) { + $this->m_x = $x; + $this->m_y = $y; + $this->m_width = $width; + $this->m_height = $height; + $this->m_updateInterval = $updateInterval; + $n = $width * $height; + for ($i = 0; $i < $numberOfFrames; ++$i) { + array_push($this->m_frames, array_fill(0, $n, false)); + } + } + + public function elementType(): ElementType { + return ElementType::Animation; + } + + public function minimumFormatVersion(): int { + return 1; + } + + public function serializeBinary(int $formatVersion): string { + $result = pack("vvvvvvvvv", + ElementType::Animation->value, + $this->m_x, + $this->m_y, + $this->m_width, + $this->m_height, + count($this->m_frames), + $this->m_updateInterval, + 0); + foreach ($this->m_frames as $frame) { + $result .= serializePixels($frame); + } + return alignSerialized($result, 4); + } + + public function serializeJSON(int $formatVersion): array { + $result = []; + $result["type"] = ElementType::Animation->name; + $result["x"] = $this->m_x; + $result["y"] = $this->m_y; + $result["width"] = $this->m_width; + $result["height"] = $this->m_height; + $result["updateInterval"] = $this->m_updateInterval; + $result["frames"] = $this->m_frames; + } + + public static function parseBinary(string &$data, int $formatVersion): AnimationElement { + [$type, $x, $y, $width, $height, $nFrames, $updateInterval, $reserved] = array_values(unpack("v8", substr($data, 0, 16))); + $type = ElementType::from($type); + if ($type != ElementType::AnimationElement) { + throw new \ValueError("Could not unserialize an animation element: type mismatch"); + } + $n = $width * $height; + $nBytes = intdiv($n + 7, 8); + $nAligned = intdiv($nBytes * $nFrames + 3, 4) * 4; + $frames = []; + $result = new AnimationElement($x, $y, $width, $height, $nFrames, $updateInterval); + for ($i = 0; $i < $nFrames; ++$i) { + $pixels = unserializePixels($width * $height, substr($data, 16 + $i * $nBytes, $nBytes)); + $result->setImage($i, $pixels); + } + $data = substr($data, 16 + $nAligned); + return $result; + } + + public static function parseJSON(array $element, int $formatVersion): AnimationElement { + $type = ElementType::fromString($element["type"]); + if ($type != ElementType::Animation) { + throw new \ValueError("Could not unserialize an animation element: type mismatch"); + } + $result = new AnimationElement($element["x"], $element["y"], $element["width"], $element["height"], count($element["frames"]), $element["updateInterval"]); + for ($i = 0; $i < count($element["frames"]); ++$i) { + $result->setImage($i, $element["frames"][$i]); + } + return $result; + } +} + +class HScrollImageElement extends Element { + private int $m_x = 0; + private int $m_y = 0; + private int $m_width = 0; + private int $m_height = 0; + private int $m_contentWidth = 0; + private ScrollFlags $m_flags; + private int $m_scrollSpeed = 0; + private array $m_image = []; + + public function x(): int { + return $this->m_x; + } + + public function y(): int { + return $this->m_y; + } + + public function width(): int { + return $this->m_width; + } + + public function height(): int { + return $this->m_height; + } + + public function contentWidth(): int { + return $this->m_contentWidth; + } + + public function flags(): ScrollFlags { + return $this->m_flags; + } + + public function scrollSpeed(): int { + return $this->m_scrollSpeed; + } + + public function image(): array { + return $this->m_image; + } + + public function setImage(array $image) { + $n = $m_contentWidth * $m_height; + if (count($image) != $n) { + $actual = count($image); + throw new \ValueError("Cannot update the image of an HScrollImageElement: the number of pixels supplied, $actual, is not the expected number, $n"); + } + $this->m_image = $image; + } + + public function __construct(int $x, int $y, int $width, int $height, int $contentWidth, ScrollFlags $flags, int $scrollSpeed) { + $this->m_x = $x; + $this->m_y = $y; + $this->m_width = $width; + $this->m_height = $height; + $this->m_contentWidth = $contentWidth; + $this->m_flags = $flags; + $this->m_scrollSpeed = $scrollSpeed; + $n = $contentWidth * $height; + $this->m_image = array_fill(0, $n, false); + } + + public function elementType(): ElementType { + return ElementType::HScrollImage; + } + + public function minimumFormatVersion(): int { + return 1; + } + + public function serializeBinary(int $formatVersion): string { + $result = pack("vvvvvvCCv", + ElementType::HScrollImage->value, + $this->m_x, + $this->m_y, + $this->m_width, + $this->m_height, + $this->m_contentWidth, + $this->m_flags->toBitField(), + $this->m_scrollSpeed, + 0); + $result .= serializePixels($this->m_image); + return alignSerialized($result, 4); + } + + public function serializeJSON(int $formatVersion): array { + $result = []; + $result["type"] = ElementType::HScrollImage->name; + $result["x"] = $this->m_x; + $result["y"] = $this->m_y; + $result["width"] = $this->m_width; + $result["height"] = $this->m_height; + $result["contentWidth"] = $this->m_contentWidth; + $result["flags"] = $this->m_flags->toJSON(); + $result["scrollSpeed"] = $this->m_scrollSpeed; + $result["image"] = $this->m_image; + } + + public static function parseBinary(string &$data, int $formatVersion): HScrollImageElement { + [$type, $x, $y, $width, $height, $contentWidth] = array_values(unpack("v6", substr($data, 0, 12))); + $type = ElementType::from($type); + if ($type != ElementType::HScrollImage) { + throw new \ValueError("Could not unserialize a horizontally scrolling image element: type mismatch"); + } + [$flags, $scrollSpeed] = array_values(unpack("C2", substr($data, 12, 2))); + $flags = TextFlags::fromBitField($flags); + $n = $contentWidth * $height; + $nBytes = intdiv($n + 7, 8); + $nAligned = intdiv($nBytes + 3, 4) * 4; + $pixels = unserializePixels($n, substr($data, 16, $nBytes)); + $result = new HScrollImageElement($x, $y, $width, $height, $contentWidth, $flags, $scrollSpeed); + $result->setImage($pixels); + $data = substr($data, 16 + $nAligned); + return $result; + } + + public static function parseJSON(array $element, int $formatVersion): HScrollImageElement { + $type = ElementType::fromString($element["type"]); + if ($type != ElementType::HScrollImage) { + throw new \ValueError("Could not unserialize a horizontally scrolling image element: type mismatch"); + } + $result = new HScrollImageElement($element["x"], $element["y"], $element["width"], $element["height"], $element["contentWidth"], + ScrollFlags::fromJSON($element["flags"]), $element["scrollSpeed"]); + $result->setImage($element["image"]); + return $result; + } +} + +class VScrollImageElement extends Element { + private int $m_x = 0; + private int $m_y = 0; + private int $m_width = 0; + private int $m_height = 0; + private int $m_contentHeight = 0; + private ScrollFlags $m_flags; + private int $m_scrollSpeed = 0; + private array $m_image = []; + + public function x(): int { + return $this->m_x; + } + + public function y(): int { + return $this->m_y; + } + + public function width(): int { + return $this->m_width; + } + + public function height(): int { + return $this->m_height; + } + + public function contentHeight(): int { + return $this->m_contentHeight; + } + + public function flags(): ScrollFlags { + return $this->m_flags; + } + + public function scrollSpeed(): int { + return $this->m_scrollSpeed; + } + + public function image(): array { + return $this->m_image; + } + + public function setImage(array $image) { + $n = $m_width * $m_contentHeight; + if (count($image) != $n) { + $actual = count($image); + throw new \ValueError("Cannot update the image of a VScrollImageElement: the number of pixels supplied, $actual, is not the expected number, $n"); + } + $this->m_image = $image; + } + + public function __construct(int $x, int $y, int $width, int $height, int $contentHeight, ScrollFlags $flags, int $scrollSpeed) { + $this->m_x = $x; + $this->m_y = $y; + $this->m_width = $width; + $this->m_height = $height; + $this->m_contentHeight = $contentHeight; + $this->m_flags = $flags; + $this->m_scrollSpeed = $scrollSpeed; + $n = $width * $contentHeight; + $this->m_image = array_fill(0, $n, false); + } + + public function elementType(): ElementType { + return ElementType::VScrollImage; + } + + public function minimumFormatVersion(): int { + return 1; + } + + public function serializeBinary(int $formatVersion): string { + $result = pack("vvvvvvCCv", + ElementType::VScrollImage->value, + $this->m_x, + $this->m_y, + $this->m_width, + $this->m_height, + $this->m_contentHeight, + $this->m_flags->toBitField(), + $this->m_scrollSpeed, + 0); + $result .= serializePixels($this->m_image); + return alignSerialized($result, 4); + } + + public function serializeJSON(int $formatVersion): array { + $result = []; + $result["type"] = ElementType::HScrollImage->name; + $result["x"] = $this->m_x; + $result["y"] = $this->m_y; + $result["width"] = $this->m_width; + $result["height"] = $this->m_height; + $result["contentHeight"] = $this->m_contentHeight; + $result["flags"] = $this->m_flags->toJSON(); + $result["scrollSpeed"] = $this->m_scrollSpeed; + $result["image"] = $this->m_image; + } + + public static function parseBinary(string &$data, int $formatVersion): VScrollImageElement { + [$type, $x, $y, $width, $height, $contentHeight] = array_values(unpack("v6", substr($data, 0, 12))); + $type = ElementType::from($type); + if ($type != ElementType::VScrollImage) { + throw new \ValueError("Could not unserialize a vertically scrolling image element: type mismatch"); + } + [$flags, $scrollSpeed] = array_values(unpack("C2", substr($data, 12, 2))); + $flags = TextFlags::fromBitField($flags); + $n = $width * $contentHeight; + $nBytes = intdiv($n + 7, 8); + $nAligned = intdiv($nBytes + 3, 4) * 4; + $pixels = unserializePixels($n, substr($data, 16, $nBytes)); + $result = new VScrollImageElement($x, $y, $width, $height, $contentHeight, $flags, $scrollSpeed); + $result->setImage($pixels); + $data = substr($data, 16 + $nAligned); + return $result; + } + + public static function parseJSON(array $element, int $formatVersion): VScrollImageElement { + $type = ElementType::fromString($element["type"]); + if ($type != ElementType::VScrollImage) { + throw new \ValueError("Could not unserialize a vertically scrolling image element: type mismatch"); + } + $result = new VScrollImageElement($element["x"], $element["y"], $element["width"], $element["height"], $element["contentHeight"], + ScrollFlags::fromJSON($element["flags"]), $element["scrollSpeed"]); + $result->setImage($element["image"]); + return $result; + } +} + +class LineElement extends Element { + private int $m_originX = 0; + private int $m_orignY = 0; + private int $m_targetX = 0; + private int $m_targetY = 0; + private LineStyle $m_lineStyle = LineStyle::Solid; + private LineFlags $m_flags; + + public function originX(): int { + return $this->m_originX; + } + + public function originY(): int { + return $this->m_originY; + } + + public function targetX(): int { + return $this->m_targetX; + } + + public function targetY(): int { + return $this->m_targetY; + } + + public function lineStyle(): LineStyle { + return $this->m_lineStyle; + } + + public function flags(): LineFlags { + return $this->m_flags; + } + + public function __construct(int $originX, int $originY, int $targetX, int $targetY, LineStyle $lineStyle, LineFlags $flags) { + $this->m_originX = $originX; + $this->m_originY = $originY; + $this->m_targetX = $targetX; + $this->m_targetY = $targetY; + $this->m_lineStyle = $lineStyle; + $this->m_flags = $flags; + } + + public function elementType(): ElementType { + return ElementType::Line; + } + + public function minimumFormatVersion(): int { + if ($this->m_flags->toBitField() != 0) { + return 2; + } else { + return 1; + } + } + + public function serializeBinary(int $formatVersion): string { + $result = pack("vvvvvCC", + ElementType::Line->value, + $this->m_originX, + $this->m_originY, + $this->m_targetX, + $this->m_targetY, + $this->m_lineStyle->value, + $this->m_flags->toBitField()); + return alignSerialized($result, 4); + } + + public function serializeJSON(int $formatVersion): array { + $result = []; + $result["type"] = ElementType::Line->name; + $result["originX"] = $this->m_originX; + $result["originY"] = $this->m_originY; + $result["targetX"] = $this->m_targetX; + $result["targetY"] = $this->m_targetY; + $result["lineStyle"] = $this->m_lineStyle->toJSON(); + $result["flags"] = $this->m_flags->toJSON(); + return $result; + } + + public static function parseBinary(string &$data, int $formatVersion): LineElement { + [$type, $originX, $originY, $targetX, $targetY] = array_values(unpack("v5", substr($data, 0, 10))); + $data = substr($data, 10); + $pos = 10; + $type = ElementType::from($type); + if ($type != ElementType::Line) { + throw new \ValueError("Could not unserialize a line element: type mismatch"); + } + [$lineStyle, $flags] = array_values(unpack("C2", substr($data, 0, 2))); + $data = substr($data, 2); + $pos += 2; + $lineStyle = LineStyle::from($lineStyle); + $flags = LineFlags::fromBitField($flags); + $alignedPos = intdiv($pos + 3, 4) * 4; + $data = substr($data, $alignedPos - $pos); + + $result = new LineElement($originX, $originY, $targetX, $targetY, $lineStyle, $flags); + return $result; + } + + public static function parseJSON(array $element, int $formatVersion): LineElement { + $type = ElementType::fromString($element["type"]); + if ($type != ElementType::Line) { + throw new \ValueError("Could not unserialize a line element: type mismatch"); + } + $flags = new LineFlags(); + $lineStyle = LineStyle::fromName($element["lineStyle"]); + if ($formatVersion >= 2) { + $flags = LineFlags::fromJSON($element["flags"]); + } + $result = new LineElement($element["originX"], $element["originY"], $element["targetX"], $element["targetY"], $lineStyle, $flags); + return $result; + } +} + +class BoxElement extends Element { + private int $m_x = 0; + private int $m_y = 0; + private int $m_width = 0; + private int $m_height = 0; + private FillPattern $m_fillPattern = FillPattern::Solid; + private FillFlags $m_flags; + + public function x(): int { + return $this->m_x; + } + + public function y(): int { + return $this->m_y; + } + + public function width(): int { + return $this->m_width; + } + + public function height(): int { + return $this->m_height; + } + + public function fillPattern(): FillPattern { + return $this->m_fillPattern; + } + + public function flags(): FillFlags { + return $this->m_flags; + } + + public function __construct(int $x, int $y, int $width, int $height, FillPattern $fillPattern, FillFlags $flags) { + $this->m_x = $x; + $this->m_y = $y; + $this->m_width = $width; + $this->m_height = $height; + $this->m_fillPattern = $fillPattern; + $this->m_flags = $flags; + } + + public function elementType(): ElementType { + return ElementType::Box; + } + + public function minimumFormatVersion(): int { + return 2; + } + + public function serializeBinary(int $formatVersion): string { + $result = pack("vvvvvCC", + ElementType::Box->value, + $this->m_x, + $this->m_y, + $this->m_width, + $this->m_height, + $this->m_fillPattern->value, + $this->m_flags->toBitField()); + return alignSerialized($result, 4); + } + + public function serializeJSON(int $formatVersion): array { + $result = []; + $result["type"] = ElementType::Box->name; + $result["x"] = $this->m_x; + $result["y"] = $this->m_y; + $result["width"] = $this->m_width; + $result["height"] = $this->m_height; + $result["fillPattern"] = $this->m_fillPattern->toJSON(); + $result["flags"] = $this->m_flags->toJSON(); + return $result; + } + + public static function parseBinary(string &$data, int $formatVersion): BoxElement { + [$type, $x, $y, $width, $height] = array_values(unpack("v5", substr($data, 0, 10))); + $data = substr($data, 10); + $pos = 10; + $type = ElementType::from($type); + if ($type != ElementType::Box) { + throw new \ValueError("Could not unserialize a box element: type mismatch"); + } + [$fillPattern, $flags] = array_values(unpack("C2", substr($data, 0, 2))); + $data = substr($data, 2); + $pos += 2; + $fillPattern = FillPattern::from($fillPattern); + $flags = FillFlags::fromBitField($flags); + + $alignedPos = intdiv($pos + $len + 3, 4) * 4; + $data = substr($data, $alignedPos - $pos); + + $result = new BoxElement($x, $y, $width, $height, $fillPattern, $flags); + return $result; + } + + public static function parseJSON(array $element, int $formatVersion): BoxElement { + $type = ElementType::fromString($element["type"]); + if ($type != ElementType::Box) { + throw new \ValueError("Could not unserialize a box element: type mismatch"); + } + $fillPattern = FillPattern::fromName($element["fillPattern"]); + $flags = FillFlags::fromJSON($element["flags"]); + $result = new BoxElement($element["x"], $element["y"], $element["width"], $element["height"], $fillPattern, $flags); + return $result; + } +} + +class ClippedTextElement extends Element { + private int $m_x = 0; + private int $m_y = 0; + private int $m_width = 0; + private int $m_height = 0; + private TextFlags $m_textFlags; + private int $m_fontIndex = 0; + private string $m_text = ""; + + public function x(): int { + return $this->m_x; + } + + public function y(): int { + return $this->m_y; + } + + public function width(): int { + return $this->m_width; + } + + public function height(): int { + return $this->m_height; + } + + public function textFlags(): TextFlags { + return $this->m_textFlags; + } + + public function fontIndex(): int { + return $this->m_fontIndex; + } + + public function text(): string { + return $this->m_text; + } + + public function __construct(int $x, int $y, int $width, int $height, TextFlags $textFlags, int $fontIndex, string $text) { + $this->m_x = $x; + $this->m_y = $y; + $this->m_width = $width; + $this->m_height = $height; + $this->m_textFlags = $textFlags; + $this->m_fontIndex = $fontIndex; + $this->m_text = $text; + } + + public function elementType(): ElementType { + return ElementType::ClippedText; + } + + public function minimumFormatVersion(): int { + if ($this->m_textFlags->toBitField() != 0) { + return 2; + } else { + return 1; + } + } + + public function serializeBinary(int $formatVersion): string { + $result = pack("vvvvv", + ElementType::ClippedText->value, + $this->m_x, + $this->m_y, + $this->m_width, + $this->m_height); + if ($formatVersion >= 2) { + $result .= pack("CC", + $this->m_textFlags->toBitField(), + 0); + } + $result .= pack("vv", + $this->m_fontIndex, + strlen($this->m_text)); + $result .= $this->m_text; + return alignSerialized($result, 4); + } + + public function serializeJSON(int $formatVersion): array { + $result = []; + $result["type"] = ElementType::ClippedText->name; + $result["x"] = $this->m_x; + $result["y"] = $this->m_y; + $result["width"] = $this->m_width; + $result["height"] = $this->m_height; + if ($formatVersion >= 2) { + $result["textFlags"] = $this->m_textFlags->toJSON(); + } + $result["fontIndex"] = $this->m_fontIndex; + $result["text"] = $this->m_text; + return $result; + } + + public static function parseBinary(string &$data, int $formatVersion): ClippedTextElement { + [$type, $x, $y, $width, $height] = array_values(unpack("v5", substr($data, 0, 10))); + $data = substr($data, 10); + $pos = 10; + $type = ElementType::from($type); + if ($type != ElementType::ClippedText) { + throw new \ValueError("Could not unserialize a clipped text element: type mismatch"); + } + $textFlags = 0; + if ($formatVersion >= 2) { + [$textFlags, $reserved] = array_values(unpack("C2", substr($data, 0, 2))); + $data = substr($data, 2); + $pos += 2; + } + $textFlags = TextFlags::fromBitField($textFlags); + [$fontIndex, $len] = array_values(unpack("v2", substr($data, 0, 4))); + $data = substr($data, 4); + $text = substr($data, 0, $len); + $pos += 4; + $alignedPos = intdiv($pos + $len + 3, 4) * 4; + $data = substr($data, $alignedPos - $pos); + + $result = new ClippedTextElement($x, $y, $width, $height, $textFlags, $fontIndex, $text); + return $result; + } + + public static function parseJSON(array $element, int $formatVersion): ClippedTextElement { + $type = ElementType::fromString($element["type"]); + if ($type != ElementType::ClippedText) { + throw new \ValueError("Could not unserialize a clipped text element: type mismatch"); + } + $textFlags = new TextFlags(); + if ($formatVersion >= 2) { + $textFlags = TextFlags::fromJSON($element["textFlags"]); + } + $result = new ClippedTextElement($element["x"], $element["y"], $element["width"], $element["height"], $textFlags, $element["fontIndex"], $element["text"]); + return $result; + } +} + +class HScrollTextElement extends Element { + private int $m_x = 0; + private int $m_y = 0; + private int $m_width = 0; + private int $m_height = 0; + private TextFlags $m_textFlags; + private ScrollFlags $m_scrollFlags; + private int $m_scrollSpeed = 0; + private int $m_fontIndex = 0; + private string $m_text = ""; + + public function x(): int { + return $this->m_x; + } + + public function y(): int { + return $this->m_y; + } + + public function width(): int { + return $this->m_width; + } + + public function height(): int { + return $this->m_height; + } + + public function textFlags(): TextFlags { + return $this->m_textFlags; + } + + public function flags(): ScrollFlags { + return $this->m_textFlags; + } + + public function scrollSpeed(): int { + return $this->m_scrollSpeed; + } + + public function fontIndex(): int { + return $this->m_fontIndex; + } + + public function text(): string { + return $this->m_text; + } + + public function __construct(int $x, int $y, int $width, int $height, TextFlags $textFlags, ScrollFlags $flags, int $scrollSpeed, int $fontIndex, string $text) { + $this->m_x = $x; + $this->m_y = $y; + $this->m_width = $width; + $this->m_height = $height; + $this->m_textFlags = $textFlags; + $this->m_flags = $flags; + $this->m_scrollSpeed = $scrollSpeed; + $this->m_fontIndex = $fontIndex; + $this->m_text = $text; + } + + public function elementType(): ElementType { + return ElementType::HScrollText; + } + + public function minimumFormatVersion(): int { + if ($this->m_textFlags->toBitField() != 0) { + return 2; + } else { + return 1; + } + } + + public function serializeBinary(int $formatVersion): string { + $result = pack("vvvvv", + ElementType::HScrollText->value, + $this->m_x, + $this->m_y, + $this->m_width, + $this->m_height); + if ($formatVersion >= 2) { + $result .= pack("CC", + $this->m_textFlags->toBitField(), + 0); + } + $result .= pack("CC", + $this->m_flags->toBitField(), + $this->m_scrollSpeed); + if ($formatVersion >= 2) { + $result .= pack("v", 0); + } + $result .= pack("vv", + $this->m_fontIndex, + strlen($this->m_text)); + $result .= $this->m_text; + return alignSerialized($result, 4); + } + + public function serializeJSON(int $formatVersion): array { + $result = []; + $result["type"] = ElementType::HScrollText->name; + $result["x"] = $this->m_x; + $result["y"] = $this->m_y; + $result["width"] = $this->m_width; + $result["height"] = $this->m_height; + if ($formatVersion >= 2) { + $result["textFlags"] = $this->m_textFlags->toJSON(); + } + $result["flags"] = $this->m_flags; + $result["scrollSpeed"] = $this->m_scrollSpeed; + $result["fontIndex"] = $this->m_fontIndex; + $result["text"] = $this->m_text; + return $result; + } + + public static function parseBinary(string &$data, int $formatVersion): HScrollTextElement { + [$type, $x, $y, $width, $height] = array_values(unpack("v5", substr($data, 0, 10))); + $data = substr($data, 10); + $pos = 10; + $type = ElementType::from($type); + if ($type != ElementType::HScrollText) { + throw new \ValueError("Could not unserialize a clipped text element: type mismatch"); + } + $textFlags = 0; + if ($formatVersion >= 2) { + [$textFlags, $reserved] = array_values(unpack("C2", substr($data, 0, 2))); + $data = substr($data, 2); + $pos += 2; + } + $textFlags = TextFlags::fromBitField($textFlags); + [$flags, $scrollSpeed] = array_values(unpack("C2", substr($data, 0, 2))); + $flags = ScrollFlags::fromBitField($flags); + $data = substr($data, 2); + $pos += 2; + if ($formatVersion >= 2) { + $data = substr($data, 2); + $pos += 2; + } + [$fontIndex, $len] = array_values(unpack("v2", substr($data, 0, 4))); + $data = substr($data, 4); + $text = substr($data, 0, $len); + $pos += 4; + $alignedPos = intdiv($pos + $len + 3, 4) * 4; + $data = substr($data, $alignedPos - $pos); + + $result = new HScrollTextElement($x, $y, $width, $height, $textFlags, $flags, $scrollSpeed, $fontIndex, $text); + return $result; + } + + public static function parseJSON(array $element, int $formatVersion): HScrollTextElement { + $type = ElementType::fromString($element["type"]); + if ($type != ElementType::HScrollText) { + throw new \ValueError("Could not unserialize a horizontally scrolling text element: type mismatch"); + } + $textFlags = new TextFlags(); + if ($formatVersion >= 2) { + $textFlags = TextFlags::fromJSON($element["textFlags"]); + } + $flags = ScrollFlags::fromJSON($element["flags"]); + $result = new HScrollTextElement($element["x"], $element["y"], $element["width"], $element["height"], $textFlags, $flags, $element["scrollSpeed"], $element["fontIndex"], $element["text"]); + return $result; + } +} + +class CurrentTimeElement extends Element { + private int $m_x = 0; + private int $m_y = 0; + private int $m_width = 0; + private int $m_height = 0; + private TextFlags $m_textFlags; + private int $m_fontIndex = 0; + private int $m_utcOffset = 0; + private TimeDisplayFlags $m_flags; + + public function x(): int { + return $this->m_x; + } + + public function y(): int { + return $this->m_y; + } + + public function width(): int { + return $this->m_width; + } + + public function height(): int { + return $this->m_height; + } + + public function textFlags(): TextFlags { + return $this->m_textFlags; + } + + public function fontIndex(): int { + return $this->m_fontIndex; + } + + public function utcOffset(): int { + return $this->m_utcOffset; + } + + public function flags(): TimeDisplayFlags { + return $this->m_flags; + } + + public function __construct(int $x, int $y, int $width, int $height, TextFlags $textFlags, int $fontIndex, int $utcOffset, TimeDisplayFlags $flags) { + $this->m_x = $x; + $this->m_y = $y; + $this->m_width = $width; + $this->m_height = $height; + $this->m_textFlags = $textFlags; + $this->m_fontIndex = $fontIndex; + $this->m_utcOffset = $utcOffset; + $this->m_flags = $flags; + } + + public function elementType(): ElementType { + return ElementType::CurrentTime; + } + + public function minimumFormatVersion(): int { + if ($this->m_textFlags->toBitField() != 0) { + return 2; + } else { + return 1; + } + } + + public function serializeBinary(int $formatVersion): string { + $result = pack("vvvvv", + ElementType::CurrentTime->value, + $this->m_x, + $this->m_y, + $this->m_width, + $this->m_height); + if ($formatVersion >= 2) { + $result .= pack("CC", + $this->m_textFlags->toBitField(), + 0); + } + $result .= pack("vvv", + $this->m_fontIndex, + $this->m_utcOffset, + $this->m_flags->toBitField()); + if ($formatVersion >= 2) { + $result .= pack("v", 0); + } + return alignSerialized($result, 4); + } + + public function serializeJSON(int $formatVersion): array { + $result = []; + $result["type"] = ElementType::CurrentTime->name; + $result["x"] = $this->m_x; + $result["y"] = $this->m_y; + $result["width"] = $this->m_width; + $result["height"] = $this->m_height; + if ($formatVersion >= 2) { + $result["textFlags"] = $this->m_textFlags->toJSON(); + } + $result["fontIndex"] = $this->m_fontIndex; + $result["utcOffset"] = $this->m_utcOffset; + $result["flags"] = $this->m_flags->toJSON(); + return $result; + } + + public static function parseBinary(string &$data, int $formatVersion): CurrentTimeElement { + [$type, $x, $y, $width, $height] = array_values(unpack("v5", substr($data, 0, 10))); + $data = substr($data, 10); + $pos = 10; + $type = ElementType::from($type); + if ($type != ElementType::CurrentTime) { + throw new \ValueError("Could not unserialize a current time element: type mismatch"); + } + $textFlags = 0; + if ($formatVersion >= 2) { + [$textFlags, $reserved] = array_values(unpack("C2", substr($data, 0, 2))); + $data = substr($data, 2); + $pos += 2; + } + $textFlags = TextFlags::fromBitField($textFlags); + [$fontIndex, $utcOffset, $flags] = array_values(unpack("v3", substr($data, 0, 6))); + $flags = TimeDisplayFlags::fromBitField($flags); + $data = substr($data, 6); + $pos += 6; + if ($formatVersion >= 2) { + $data = substr($data, 2); + $pos += 2; + } + $alignedPos = intdiv($pos + 3, 4) * 4; + $data = substr($data, $alignedPos - $pos); + + $result = new CurrentTimeElement($x, $y, $width, $height, $textFlags, $fontIndex, $utcOffset, $flags); + return $result; + } + + public static function parseJSON(array $element, int $formatVersion): CurrentTime { + $type = ElementType::fromString($element["type"]); + if ($type != ElementType::CurrentTime) { + throw new \ValueError("Could not unserialize a current time element: type mismatch"); + } + $textFlags = new TextFlags(); + if ($formatVersion >= 2) { + $textFlags = TextFlags::fromJSON($element["textFlags"]); + } + $flags = TimeDisplayFlags::fromJSON($element["flags"]); + $result = new CurrentTimeElement($element["x"], $element["y"], $element["width"], $element["height"], $textFlags, $element["fontIndex"], $element["utcOffset"], $flags); + return $result; + } +} + +abstract class ElementContainingSection extends Section { + protected array $m_elements = []; + + public function elementCount(): int { + return count($this->m_elements); + } + + public function elementAt(int $i) { + if ($i >= count($this->m_elements)) { + $max = count($this->m_elements) - 1; + throw new \IndexError("Index $i out of range [0 ..$max]"); + } + return $this->m_elements[$i]; + } + + public function appendElement(Element $element) { + array_push($this->m_elements, $element); + } + + public function insertElement(int $index, Element $element) { + if ($index < count($this->m_elements)) { + array_splice($this->m_elements, $index, 0, $element); + } else { + array_push($this->m_elements, $element); + } + } + + public function replaceElement(int $index, Element $element): Element { + $result = null; + if ($index < count($this->m_elements)) { + $result = $this->m_elements[$index]; + $this->m_elements[$index] = $element; + } + return $result; + } + + public function eraseElement(int $index): Element { + $result = null; + if ($index < count($this->m_elements)) { + $result = $this->m_elements[$index]; + array_splice($this->m_elements, $index, 1); + } + return $result; + } + + protected static function parseElementsBinary(string &$elementData, int $nElements, int $formatVersion): array { + $elements = []; + for ($i = 0; $i < $nElements; ++$i) { + $type = ElementType::from(unpack("v", substr($elementData, 0, 2))[1]); + $element = null; + switch ($type) { + case ElementType::Image: $element = ImageElement::parseBinary($elementData, $formatVersion); break; + case ElementType::Animation: $element = AnimationElement::parseBinary($elementData, $formatVersion); break; + case ElementType::HScrollImage: $element = HScrollImageElement::parseBinary($elementData, $formatVersion); break; + case ElementType::VScrollImage: $element = VScrollImageElement::parseBinary($elementData, $formatVersion); break; + case ElementType::Line: $element = LineElement::parseBinary($elementData, $formatVersion); break; + case ElementType::Box: $element = BoxElement::parseBinary($elementData, $formatVersion); break; + case ElementType::ClippedText: $element = ClippedTextElement::parseBinary($elementData, $formatVersion); break; + case ElementType::HScrollText: $element = HScrollTextElement::parseBinary($elementData, $formatVersion); break; + case ElementType::CurrentTime: $element = CurrenttimeElement::parseBinary($elementData, $formatVersion); break; + default: throw new \ValueError("Unsupported element type " . $type->name . " encountered in file"); + } + array_push($elements, $element); + } + return $elements; + } + + protected static function parseElementsJSON(array $elementData, int $formatVersion): array { + $elements = []; + foreach ($elementData as $serializedElement) { + $type = ElementType::fromName($serializedElement["type"]); + $element = null; + switch ($type) { + case ElementType::Image: $element = ImageElement::parseJSON($serializedElement, $formatVersion); break; + case ElementType::Animation: $element = AnimationElement::parseJSON($serializedElement, $formatVersion); break; + case ElementType::HScrollImage: $element = HScrollImageElement::parseJSON($serializedElement, $formatVersion); break; + case ElementType::VScrollImage: $element = VScrollImageElement::parseJSON($serializedElement, $formatVersion); break; + case ElementType::Line: $element = LineElement::parseJSON($serializedElement, $formatVersion); break; + case ElementType::Box: $element = BoxElement::parseJSON($serializedElement, $formatVersion); break; + case ElementType::ClippedText: $element = ClippedTextElement::parseJSON($serializedElement, $formatVersion); break; + case ElementType::HScrollText: $element = HScrollTextElement::parseJSON($serializedElement, $formatVersion); break; + case ElementType::CurrentTime: $element = CurrenttimeElement::parseJSON($serializedElement, $formatVersion); break; + default: throw new \ValueError("Unsupported element type " . $type->name . " encountered in file"); + } + array_push($elements, $element); + } + return $elements; + } +} + +class AlwaysDrawnSection extends ElementContainingSection { + private bool $m_drawOnFront = true; + private bool $m_drawOnBack = true; + private bool $m_clearBeforeDrawing = true; + + public function doesDrawOnFront(): bool { + return $this->m_drawOnFront; + } + + public function doesDrawOnBack(): bool { + return $this->m_drawOnBack; + } + + public function doesClearBeforeDrawing(): bool { + return $this->m_clearBeforeDrawing; + } + + public function setDrawOnFront(bool $value) { + $this->m_drawOnFront = $value; + } + + public function setDrawOnBack(bool $value) { + $this->m_drawOnBack = $value; + } + + public function setClearBeforeDrawing(bool $value) { + $this->m_clearBeforeDrawing = $value; + } + + public function sectionType(): SectionType { + return SectionType::AlwaysDrawn; + } + + public function minimumFormatVersion(): int { + $result = 1; + foreach ($this->m_elements as $element) { + $result = max($result, $element->minimumFormatVersion()); + } + return $result; + } + + public function serializeBinary(int $formatVersion): string { + $result = pack("C", SectionType::AlwaysDrawn->value); + // Will be replaced later + $result .= packU24LE(0); + $flags = 0; + maybeSetBit($flags, 0, $this->m_drawOnFront); + maybeSetBit($flags, 1, $this->m_drawOnBack); + maybeSetBit($flags, 2, $this->m_clearBeforeDrawing); + $result .= pack("vv", $flags, count($this->m_elements)); + foreach ($this->m_elements as $element) { + $result .= $element->serializeBinary($formatVersion); + } + $result = alignSerialized($result, 4); + $len = strlen($result); + $len = packU24LE($len); + $result[1] = $len[0]; + $result[2] = $len[1]; + $result[3] = $len[2]; + return $result; + } + + public function serializeJSON(int $formatVersion): array { + $result = []; + $result["type"] = SectionType::AlwaysDrawn->name; + $result["flags"] = []; + $result["flags"]["drawOnFront"] = $this->m_drawOnFront; + $result["flags"]["drawOnBack"] = $this->m_drawOnBack; + $result["flags"]["clearBeforeDrawing"] = $this->m_clearBeforeDrawing; + $result["elements"] = []; + foreach ($this->m_elements as $element) { + array_push($result["elements"], $element->serializeJSON($formatVersion)); + } + return $result; + } + + public static function parseBinary(string $section, int $formatVersion): AlwaysDrawnSection { + $type = SectionType::from(unpack("C", substr($section, 0, 1))[1]); + if ($type != SectionType::AlwaysDrawn) { + throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a AlwaysDrawnSection"); + } + $len = unpackU24LE(substr($section, 1, 3)); + [$flags, $nElements] = array_values(unpack("v2", substr($section, 4, 4))); + $elementData = substr($section, 8); + $elements = static::parseElementsBinary($elementData, $nElements, $formatVersion); + $result = new AlwaysDrawnSection(); + $result->m_drawOnFront = isBitSet($flags, 0); + $result->m_drawOnBack = isBitSet($flags, 1); + $result->m_clearBeforeDrawing = isBitSet($flags, 2); + $result->m_elements = $elements; + return $result; + } + + public static function parseJSON(array $section, int $formatVersion): AlwaysDrawnSection { + $type = SectionType::fromString($section["type"]); + if ($type != SectionType::AlwaysDrawn) { + throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a AlwaysDrawnSection"); + } + $drawOnFront = $section["flags"]["drawOnFront"]; + $drawOnBack = $section["flags"]["drawOnBack"]; + $clearBeforeDrawing = $section["flags"]["clearBeforeDrawing"]; + $elements = static::parseElementsJSON($section["elements"], $formatVersion); + $result = new AlwaysDrawnSection(); + $result->m_drawOnFront = $drawOnFront; + $result->m_drawOnBack = $drawOnBack; + $result->m_clearBeforeDrawing = $clearBeforeDrawing; + $result->m_elements = $elements; + return $result; + } +} + +class TimeBasedDrawnSection extends ElementContainingSection { + private int $m_startTimestamp = 0; + private int $m_endTimestamp = 0; + private bool $m_drawOnFront = true; + private bool $m_drawOnBack = true; + private bool $m_clearBeforeDrawing = true; + + public function doesDrawOnFront(): bool { + return $this->m_drawOnFront; + } + + public function doesDrawOnBack(): bool { + return $this->m_drawOnBack; + } + + public function doesClearBeforeDrawing(): bool { + return $this->m_clearBeforeDrawing; + } + + public function setDrawOnFront(bool $value) { + $this->m_drawOnFront = $value; + } + + public function setDrawOnBack(bool $value) { + $this->m_drawOnBack = $value; + } + + public function setClearBeforeDrawing(bool $value) { + $this->m_clearBeforeDrawing = $value; + } + + public function startTimestamp(): int { + return $this->m_startTimestamp; + } + + public function endTimestamp(): int { + return $this->m_endTimestamp; + } + + public function setStartTimestamp(int $value) { + $this->m_startTimestamp = $value; + } + + public function setEndTimestamp(int $value) { + $this->m_endTimestamp = $value; + } + + public function sectionType(): SectionType { + return SectionType::TimeBasedDrawn; + } + + public function minimumFormatVersion(): int { + $result = 1; + foreach ($this->m_elements as $element) { + $result = max($result, $element->minimumFormatVersion()); + } + return $result; + } + + public function serializeBinary(int $formatVersion): string { + $result = pack("C", SectionType::TimeBasedDrawn->value); + // Will be replaced later + $result .= packU24LE(0); + $flags = 0; + maybeSetBit($flags, 0, $this->m_drawOnFront); + maybeSetBit($flags, 1, $this->m_drawOnBack); + maybeSetBit($flags, 2, $this->m_clearBeforeDrawing); + $result .= pack("vv", $flags, count($this->m_elements)); + $result .= pack("PP", $this->m_startTimestamp, $this->m_endTimestamp); + foreach ($this->m_elements as $element) { + $result .= $element->serializeBinary($formatVersion); + } + $result = alignSerialized($result, 4); + $len = strlen($result); + $len = packU24LE($len); + $result[1] = $len[0]; + $result[2] = $len[1]; + $result[3] = $len[2]; + return $result; + } + + public function serializeJSON(int $formatVersion): array { + $result = []; + $result["type"] = SectionType::TimeBasedDrawn->name; + $result["flags"] = []; + $result["flags"]["drawOnFront"] = $this->m_drawOnFront; + $result["flags"]["drawOnBack"] = $this->m_drawOnBack; + $result["flags"]["clearBeforeDrawing"] = $this->m_clearBeforeDrawing; + $result["startTimestamp"] = $this->m_startTimestamp; + $result["endTimestamp"] = $this->m_endTimestamp; + $result["elements"] = []; + foreach ($this->m_elements as $element) { + array_push($result["elements"], $element->serializeJSON($formatVersion)); + } + return $result; + } + + public static function parseBinary(string $section, int $formatVersion): TimeBasedDrawnSection { + $type = SectionType::from(unpack("C", substr($section, 0, 1))[1]); + if ($type != SectionType::TimeBasedDrawn) { + throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a TimeBasedDrawnSection"); + } + $len = unpackU24LE(substr($section, 1, 3)); + [$flags, $nElements] = array_values(unpack("v2", substr($section, 4, 4))); + [$startTimestamp, $endTimestamp] = array_values(unpack("P2", substr($section, 8, 16))); + $elementData = substr($section, 24); + $elements = static::parseElementsBinary($elementData, $nElements, $formatVersion); + $result = new TimeBasedDrawnSection(); + $result->m_drawOnFront = isBitSet($flags, 0); + $result->m_drawOnBack = isBitSet($flags, 1); + $result->m_clearBeforeDrawing = isBitSet($flags, 2); + $result->m_startTimestamp = $startTimestamp; + $result->m_endTimestamp = $endTimestamp; + $result->m_elements = $elements; + return $result; + } + + public static function parseJSON(array $section, int $formatVersion): TimeBasedDrawnSection { + $type = SectionType::fromString($section["type"]); + if ($type != SectionType::TimeBasedDrawn) { + throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a TimeBasedDrawnSection"); + } + $drawOnFront = $section["flags"]["drawOnFront"]; + $drawOnBack = $section["flags"]["drawOnBack"]; + $clearBeforeDrawing = $section["flags"]["clearBeforeDrawing"]; + $startTimestamp = $section["startTimestamp"]; + $endTimestamp = $section["endTimestamp"]; + $elements = static::parseElementsJSON($section["elements"], $formatVersion); + $result = new TimeBasedDrawnSection(); + $result->m_drawOnFront = $drawOnFront; + $result->m_drawOnBack = $drawOnBack; + $result->m_clearBeforeDrawing = $clearBeforeDrawing; + $result->m_startTimestamp = $startTimestamp; + $result->m_endTimestamp = $endTimestamp; + $result->m_elements = $elements; + return $result; + } +} + +class MultiTimeBasedDrawnSection extends ElementContainingSection { + private array $m_timeRanges = []; + private bool $m_drawOnFront = true; + private bool $m_drawOnBack = true; + private bool $m_clearBeforeDrawing = true; + + public function doesDrawOnFront(): bool { + return $this->m_drawOnFront; + } + + public function doesDrawOnBack(): bool { + return $this->m_drawOnBack; + } + + public function doesClearBeforeDrawing(): bool { + return $this->m_clearBeforeDrawing; + } + + public function setDrawOnFront(bool $value) { + $this->m_drawOnFront = $value; + } + + public function setDrawOnBack(bool $value) { + $this->m_drawOnBack = $value; + } + + public function setClearBeforeDrawing(bool $value) { + $this->m_clearBeforeDrawing = $value; + } + + public function timeRanges(): array { + return $this->m_timeRanges; + } + + public function setTimeRanges(array $timeRanges) { + foreach ($timeRanges as $timeRange) { + if (!is_array($timeRange) || count($timeRange) != 2 || !isset($timeRange[0]) || !isset($timeRange[1])) { + throw new \ValueError("Invalid time range list supplied"); + } + } + $this->m_timeRanges = $timeRanges; + } + + public function sectionType(): SectionType { + return SectionType::MultiTimeBasedDrawn; + } + + public function minimumFormatVersion(): int { + $result = 2; + foreach ($this->m_elements as $element) { + $result = max($result, $element->minimumFormatVersion()); + } + return $result; + } + + public function serializeBinary(int $formatVersion): string { + $result = pack("C", SectionType::MultiTimeBasedDrawn->value); + // Will be replaced later + $result .= packU24LE(0); + $flags = 0; + maybeSetBit($flags, 0, $this->m_drawOnFront); + maybeSetBit($flags, 1, $this->m_drawOnBack); + maybeSetBit($flags, 2, $this->m_clearBeforeDrawing); + $result .= pack("vvv", $flags, count($this->m_elements), count($this->m_timeRanges), 0); + foreach ($this->m_timeRanges as $timeRange) { + $result .= pack("PP", $timeRange[0], $timeRange[1]); + } + foreach ($this->m_elements as $element) { + $result .= $element->serializeBinary($formatVersion); + } + $result = alignSerialized($result, 4); + $len = strlen($result); + $len = packU24LE($len); + $result[1] = $len[0]; + $result[2] = $len[1]; + $result[3] = $len[2]; + return $result; + } + + public function serializeJSON(int $formatVersion): array { + $result = []; + $result["type"] = SectionType::TimeBasedDrawn->name; + $result["flags"] = []; + $result["flags"]["drawOnFront"] = $this->m_drawOnFront; + $result["flags"]["drawOnBack"] = $this->m_drawOnBack; + $result["flags"]["clearBeforeDrawing"] = $this->m_clearBeforeDrawing; + $result["timeRanges"] = $this->m_timeRanges; + $result["elements"] = []; + foreach ($this->m_elements as $element) { + array_push($result["elements"], $element->serializeJSON($formatVersion)); + } + return $result; + } + + public static function parseBinary(string $section, int $formatVersion): MultiTimeBasedDrawnSection { + $type = SectionType::from(unpack("C", substr($section, 0, 1))[1]); + if ($type != SectionType::MultiTimeBasedDrawn) { + throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a MultiTimeBasedDrawnSection"); + } + $len = unpackU24LE(substr($section, 1, 3)); + [$flags, $nElements, $nRanges, $reserved] = array_values(unpack("v4", substr($section, 4, 8))); + $section = substr($section, 12); + $timeRanges = []; + for ($i = 0; $i < $nRanges; ++$i) { + [$startTimestamp, $endTimestamp] = array_values(unpack("P2", substr($section, 0, 16))); + $section = substr($section, 16); + array_push($timeRanges, [$startTimestamp, $endTimestamp]); + } + + $elementData = $section; + $elements = static::parseElementsBinary($elementData, $nElements, $formatVersion); + $result = new MultiTimeBasedDrawnSection(); + $result->m_drawOnFront = isBitSet($flags, 0); + $result->m_drawOnBack = isBitSet($flags, 1); + $result->m_clearBeforeDrawing = isBitSet($flags, 2); + $result->m_timeRanges = $timeRanges; + return $result; + } + + public static function parseJSON(array $section, int $formatVersion): MultiTimeBasedDrawnSection { + $type = SectionType::fromString($section["type"]); + if ($type != SectionType::MultiTimeBasedDrawn) { + throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a MultiTimeBasedDrawnSection"); + } + $drawOnFront = $section["flags"]["drawOnFront"]; + $drawOnBack = $section["flags"]["drawOnBack"]; + $clearBeforeDrawing = $section["flags"]["clearBeforeDrawing"]; + $timeRanges = $section["timeRanges"]; + $elements = static::parseElementsJSON($section["elements"], $formatVersion); + $result = new MultiTimeBasedDrawnSection(); + $result->m_drawOnFront = $drawOnFront; + $result->m_drawOnBack = $drawOnBack; + $result->m_clearBeforeDrawing = $clearBeforeDrawing; + // Use setter here to validate the structure + $result->setTimeRanges($timeRanges); + $result->m_elements = $elements; + return $result; + } +} + +class ExpiryDateSection extends Section { + private int $m_expiryDate = 0; + + public function expiryDate(): int { + return $this->m_expiryDate; + } + + public function setExpiryDate(int $value) { + $this->m_expiryDate = $value; + } + + public function sectionType(): SectionType { + return SectionType::ExpiryDate; + } + + public function minimumFormatVersion(): int { + return 2; + } + + public function serializeBinary(int $formatVersion): string { + $result = pack("C", SectionType::ExpiryDate->value); + // Will be replaced later + $result .= packU24LE(0); + $result .= pack("P", $this->m_expiryDate); + $result = alignSerialized($result, 4); + $len = strlen($result); + $len = packU24LE($len); + $result[1] = $len[0]; + $result[2] = $len[1]; + $result[3] = $len[2]; + return $result; + } + + public function serializeJSON(int $formatVersion): array { + $result = []; + $result["type"] = SectionType::ExpiryDate->name; + $result["expiryDate"] = $this->m_expiryDate; + return $result; + } + + public static function parseBinary(string $section, int $formatVersion): ExpiryDateSection { + $type = SectionType::from(unpack("C", substr($section, 0, 1))[1]); + if ($type != SectionType::ExpiryDate) { + throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a ExpiryDateSection"); + } + $len = unpackU24LE(substr($section, 1, 3)); + [$expiryDate] = array_values(unpack("P", substr($section, 4, 8))); + $result = new ExpiryDateSection(); + $result->m_expiryDate = $expiryDate; + return $result; + } + + public static function parseJSON(array $section, int $formatVersion): ExpiryDateSection { + $type = SectionType::fromString($section["type"]); + if ($type != SectionType::ExpiryDate) { + throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a ExpiryDateSection"); + } + $expiryDate = $section["expiryDate"]; + $result = new ExpiryDateSection(); + $result->m_expiryDate = $expiryDate; + return $result; + } +} + +class CustomFontSection extends Section { + private string $m_fontData = ""; + + public function fontData(): string { + return $this->m_fontData; + } + + public function setFontData(string $value) { + $this->m_fontData = $value; + } + + public function sectionType(): SectionType { + return SectionType::CustomFont; + } + + public function minimumFormatVersion(): int { + return 1; + } + + public function serializeBinary(int $formatVersion): string { + $result = pack("C", SectionType::CustomFont->value); + // Will be replaced later + $result .= packU24LE(0); + $result .= packU24LE(strlen($this->m_fontData)); + $result .= pack("C", 0); + $result .= $this->m_fontData; + $result = alignSerialized($result, 4); + $len = strlen($result); + $len = packU24LE($len); + $result[1] = $len[0]; + $result[2] = $len[1]; + $result[3] = $len[2]; + return $result; + } + + public function serializeJSON(int $formatVersion): array { + $result = []; + $result["type"] = SectionType::CustomFont->name; + $result["data"] = base64_encode($this->m_fontData); + return $result; + } + + public static function parseBinary(string $section, int $formatVersion): CustomFontSection { + $type = SectionType::from(unpack("C", substr($section, 0, 1))[1]); + if ($type != SectionType::CustomFont) { + throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a CustomFontSection"); + } + $len = unpackU24LE(substr($section, 1, 3)); + $actualLen = unpackU24LE(substr($section, 4, 3)); + $fontData = substr($section, 8, $actualLen); + $result = new CustomFontSection(); + $result->m_fontData = $fontData; + return $result; + } + + public static function parseJSON(array $section, int $formatVersion): CustomFontSection { + $type = SectionType::fromString($section["type"]); + if ($type != SectionType::CustomFont) { + throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a CustomFontSection"); + } + $fontData = base64_decode($section["data"]); + $result = new CustomFontSection(); + $result->m_fontData = $fontData; + return $result; + } +} + +class File { + public array $sections = []; +} + +function serializeFile(File $file): string { + $result = "\xAF\x7E\x2B\x63"; + $formatVersion = 1; + foreach ($file->sections as $section) { + $formatVersion = max($formatVersion, $section->minimumFormatVersion()); + } + $result .= pack("Vvv", $formatVersion, count($file->sections), 0); + $result = alignSerialized($result, 4); + foreach ($file->sections as $section) { + $result .= $section->serializeBinary($formatVersion); + } + return $result; +} + +function serializeFileJSON(File $file): array { + $result = []; + $formatVersion = 1; + foreach ($file->sections as $section) { + $formatVersion = max($formatVersion, $section->minimumFormatVersion()); + } + $result["version"] = $formatVersion; + $result["sections"] = []; + foreach ($file->sections as $section) { + array_push($result["sections"], $section->serializeJSON($formatVersion)); + } + return $result; +} + +function parseFile(string $data): File { + if (substr($data, 0, 4) != "\xAF\x7E\x2B\x63") { + throw new \ValueError("The file contains an invalid magic sequence"); + } + $version = unpack("V", substr($data, 4, 4))[1]; + [$nSections, $reserved] = array_values(unpack("v2", substr($data, 8, 4))); + $data = substr($data, 12); + $result = new File(); + for ($i = 0; $i < $nSections; ++$i) { + $type = SectionType::from(unpack("C", substr($data, 0, 1))[1]); + $size = unpackU24LE(substr($data, 1, 3)); + $sectionData = substr($data, 0, $size); + $data = substr($data, $size); + $section = null; + switch ($type) { + case SectionType::AlwaysDrawn: $section = AlwaysDrawnSection::parseBinary($sectionData, $version); break; + case SectionType::TimeBasedDrawn: $section = TimeBasedDrawnSection::parseBinary($sectionData, $version); break; + case SectionType::MultiTimeBasedDrawn: $section = MultiTimeBasedDrawnSection::parseBinary($sectionData, $version); break; + case SectionType::ExpiryDate: $section = ExpiryDateSection::parseBinary($sectionData, $version); break; + case SectionType::CustomFont: $section = CustomFontSection::parseBinary($sectionData, $version); break; + default: throw new \ValueError("Unsupported section type " . $type->name . " encountered in file"); + } + array_push($result->sections, $section); + } + return $result; +} + +function parseFileJSON(array $data): File { + $version = $data["version"]; + if ($version < 1 || $version > 2) { + throw new \ValueError("Unsupported file format version $version encountered while parsing a file"); + } + $result = new File(); + foreach ($data["sections"] as $serializedSection) { + $type = SectionType::fromString($serializedSection["type"]); + $section = null; + switch ($type) { + case SectionType::AlwaysDrawn: $section = AlwaysDrawnSection::parseJSON($serializedSection, $version); break; + case SectionType::TimeBasedDrawn: $section = TimeBasedDrawnSection::parseJSON($serializedSection, $version); break; + case SectionType::MultiTimeBasedDrawn: $section = MultiTimeBasedDrawnSection::parseJSON($serializedSection, $version); break; + case SectionType::ExpiryDate: $section = ExpiryDateSection::parseJSON($serializedSection, $version); break; + case SectionType::CustomFont: $section = CustomFontSection::parseJSON($serializedSection, $version); break; + default: throw new \ValueError("Unsupported section type " . $type->name . " encountered in file"); + } + array_push($result->sections, $section); + } + return $result; +} + +} // namespace monoformat + +?>