= $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 = $this->m_width * $this->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 = $this->m_width * $this->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("vvvvvvvv", 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::Animation) { 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 = $this->m_contentWidth * $this->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 = $this->m_width * $this->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_flags; } 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 ?>