Repository: PrinsFrank/pdfparser (php)

Issue:
Missing support for multiple codespaceranges (Unrecognized codespacerange format)
```
begincodespacerange
<0003> <005a>
<0102> <01b5>
<0357> <03ed>
endcodespacerange
```

results in 
PrinsFrank\PdfParser\Exception\ParseFailureException: Unrecognized codespacerange format
For reproduction, see the file from https://github.com/k00ni/pdfparser/commit/3c1372ca36b2de666e3af752ec85dd69a27a445a on the latest commit in main.

Pull request:
Add support for multiple codespaceranges
Fixes #63 

Patch:
diff --git a/src/Document/CMap/Registry/Adobe/Identity0.php b/src/Document/CMap/Registry/Adobe/Identity0.php
index 6667c7b..6e919c7 100644
--- a/src/Document/CMap/Registry/Adobe/Identity0.php
+++ b/src/Document/CMap/Registry/Adobe/Identity0.php
@@ -5,14 +5,14 @@ namespace PrinsFrank\PdfParser\Document\CMap\Registry\Adobe;
 use Override;
 use PrinsFrank\PdfParser\Document\CMap\Registry\CMapResource;
 use PrinsFrank\PdfParser\Document\CMap\ToUnicode\BFRange;
+use PrinsFrank\PdfParser\Document\CMap\ToUnicode\CodeSpaceRange;
 use PrinsFrank\PdfParser\Document\CMap\ToUnicode\ToUnicodeCMap;
 
 class Identity0 implements CMapResource {
     #[Override]
     public function getToUnicodeCMap(): ToUnicodeCMap {
         return new ToUnicodeCMap(
-            0x0000,
-            0xFFFF,
+            [new CodeSpaceRange(0x0000, 0xFFFF)],
             2,
             new BFRange(0x0000, 0xFFFF, ['0000'])
         );
diff --git a/src/Document/CMap/ToUnicode/CodeSpaceRange.php b/src/Document/CMap/ToUnicode/CodeSpaceRange.php
new file mode 100644
index 0000000..e1cb89a
--- /dev/null
+++ b/src/Document/CMap/ToUnicode/CodeSpaceRange.php
@@ -0,0 +1,11 @@
+<?php declare(strict_types=1);
+
+namespace PrinsFrank\PdfParser\Document\CMap\ToUnicode;
+
+class CodeSpaceRange {
+    public function __construct(
+        public readonly int $codeSpaceStart,
+        public readonly int $codeSpaceEnd,
+    ) {
+    }
+}
diff --git a/src/Document/CMap/ToUnicode/ToUnicodeCMap.php b/src/Document/CMap/ToUnicode/ToUnicodeCMap.php
index bb95c8e..d49a2ec 100644
--- a/src/Document/CMap/ToUnicode/ToUnicodeCMap.php
+++ b/src/Document/CMap/ToUnicode/ToUnicodeCMap.php
@@ -12,12 +12,12 @@ class ToUnicodeCMap {
     /**
      * @no-named-arguments
      *
+     * @param list<CodeSpaceRange> $codeSpaceRanges
      * @param int<1, max> $byteSize
      * @throws InvalidArgumentException
      */
     public function __construct(
-        public readonly int $codeSpaceStart,
-        public readonly int $codeSpaceEnd,
+        public readonly array $codeSpaceRanges,
         public readonly int $byteSize,
         BFRange|BFChar ...$bfCharRangeInfo,
     ) {
diff --git a/src/Document/CMap/ToUnicode/ToUnicodeCMapParser.php b/src/Document/CMap/ToUnicode/ToUnicodeCMapParser.php
index b413d63..e20417f 100644
--- a/src/Document/CMap/ToUnicode/ToUnicodeCMapParser.php
+++ b/src/Document/CMap/ToUnicode/ToUnicodeCMapParser.php
@@ -15,12 +15,29 @@ class ToUnicodeCMapParser {
         $beginCodeSpaceRangePos += strlen(ToUnicodeCMapOperator::BeginCodeSpaceRange->value);
         $endCodeSpaceRangePos = $stream->firstPos(ToUnicodeCMapOperator::EndCodeSpaceRange, $beginCodeSpaceRangePos, $startOffset + $nrOfBytes)
             ?? throw new ParseFailureException();
-        if (preg_match('/^\s*<(?P<start>[0-9a-fA-F ]+)>\s*<(?P<end>[0-9a-fA-F ]+)>\s*$/', $stream->read($beginCodeSpaceRangePos, $endCodeSpaceRangePos - $beginCodeSpaceRangePos), $matchesSpaceRange) !== 1) {
-            throw new ParseFailureException('Unrecognized codespacerange format');
-        }
+        $codeSpaceRangeSectionString = $stream->read($beginCodeSpaceRangePos, $endCodeSpaceRangePos - $beginCodeSpaceRangePos);
+        $codeSpaceRanges = [];
+        $byteSize = null;
+        foreach (explode("\n", $codeSpaceRangeSectionString) as $codeSpaceRangeSectionStringLine) {
+            if (trim($codeSpaceRangeSectionStringLine) === '') {
+                continue;
+            }
+
+            if (preg_match('/^\s*<(?P<start>[0-9a-fA-F ]+)>\s*<(?P<end>[0-9a-fA-F ]+)>\s*$/', $codeSpaceRangeSectionStringLine, $matchesSpaceRange) !== 1) {
+                throw new ParseFailureException('Unrecognized codespacerange format');
+            }
+
+            if (strlen($matchesSpaceRange['start']) !== strlen($matchesSpaceRange['end'])) {
+                throw new ParseFailureException(sprintf('Start(%s) and end(%s) of codespacerange don\'t have the same number of bytes', $matchesSpaceRange['start'], $matchesSpaceRange['end']));
+            }
+
+            $byteSizeRange = strlen(trim($matchesSpaceRange['start'])) / 2;
+            if ($byteSize !== null && $byteSizeRange !== $byteSize) {
+                throw new ParseFailureException(sprintf('Byte size of codespaceranges is inconsistent, expected %d, got %d', $byteSize, $byteSizeRange));
+            }
 
-        if (strlen($matchesSpaceRange['start']) !== strlen($matchesSpaceRange['end'])) {
-            throw new ParseFailureException(sprintf('Start(%s) and end(%s) of codespacerange don\'t have the same number of bytes', $matchesSpaceRange['start'], $matchesSpaceRange['end']));
+            $byteSize = $byteSizeRange;
+            $codeSpaceRanges[] = new CodeSpaceRange((int) hexdec($matchesSpaceRange['start']), (int) hexdec($matchesSpaceRange['end']));
         }
 
         /** @var array<int, list<BFRange|BFChar>> $bfCharRangeInfo where the first index is used to track the position of the element in the CMap */
@@ -63,9 +80,8 @@ class ToUnicodeCMapParser {
 
         ksort($bfCharRangeInfo); // Make sure that Char and Range are in order they occur in the CMap
         return new ToUnicodeCMap(
-            (int) hexdec(trim($matchesSpaceRange['start'])),
-            (int) hexdec(trim($matchesSpaceRange['end'])),
-            ($byteSize = (strlen(trim($matchesSpaceRange['start'])) / 2)) >= 1 && is_int($byteSize) ? $byteSize : throw new ParseFailureException(sprintf('Byte size should be an integer of 1 or higher, got %s', $byteSize)),
+            $codeSpaceRanges,
+            is_int($byteSize) && $byteSize > 0 ? $byteSize : 2,
             ...array_merge(...$bfCharRangeInfo)
         );
     }


Test patch:
diff --git a/tests/Unit/Document/CMap/Registry/Adobe/Identity0Test.php b/tests/Unit/Document/CMap/Registry/Adobe/Identity0Test.php
index 8c6aa7f..6497663 100644
--- a/tests/Unit/Document/CMap/Registry/Adobe/Identity0Test.php
+++ b/tests/Unit/Document/CMap/Registry/Adobe/Identity0Test.php
@@ -6,6 +6,7 @@ use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 use PrinsFrank\PdfParser\Document\CMap\Registry\Adobe\Identity0;
 use PrinsFrank\PdfParser\Document\CMap\ToUnicode\BFRange;
+use PrinsFrank\PdfParser\Document\CMap\ToUnicode\CodeSpaceRange;
 use PrinsFrank\PdfParser\Document\CMap\ToUnicode\ToUnicodeCMap;
 
 #[CoversClass(Identity0::class)]
@@ -13,8 +14,7 @@ class Identity0Test extends TestCase {
     public function testGetToUnicodeCMap(): void {
         static::assertEquals(
             new ToUnicodeCMap(
-                0x0000,
-                0xFFFF,
+                [new CodeSpaceRange(0x0000, 0xFFFF)],
                 2,
                 new BFRange(0x0000, 0xFFFF, ['0000'])
             ),
diff --git a/tests/Unit/Document/CMap/ToUnicode/ToUnicodeCMapParserTest.php b/tests/Unit/Document/CMap/ToUnicode/ToUnicodeCMapParserTest.php
index bc79e65..63556da 100644
--- a/tests/Unit/Document/CMap/ToUnicode/ToUnicodeCMapParserTest.php
+++ b/tests/Unit/Document/CMap/ToUnicode/ToUnicodeCMapParserTest.php
@@ -6,6 +6,7 @@ use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\TestCase;
 use PrinsFrank\PdfParser\Document\CMap\ToUnicode\BFChar;
 use PrinsFrank\PdfParser\Document\CMap\ToUnicode\BFRange;
+use PrinsFrank\PdfParser\Document\CMap\ToUnicode\CodeSpaceRange;
 use PrinsFrank\PdfParser\Document\CMap\ToUnicode\ToUnicodeCMap;
 use PrinsFrank\PdfParser\Document\CMap\ToUnicode\ToUnicodeCMapParser;
 use PrinsFrank\PdfParser\Stream\InMemoryStream;
@@ -44,8 +45,7 @@ class ToUnicodeCMapParserTest extends TestCase {
         );
         static::assertEquals(
             new ToUnicodeCMap(
-                0x0000,
-                0xFFFF,
+                [new CodeSpaceRange(0x0000, 0xFFFF)],
                 2,
                 new BFRange(0x0000, 0x005E, ['0020']),
                 new BFRange(0x005F, 0x0061, ['00660066', '00660069', '00660066006C']),
@@ -54,4 +54,24 @@ class ToUnicodeCMapParserTest extends TestCase {
             ToUnicodeCMapParser::parse($stream, 0, $stream->getSizeInBytes())
         );
     }
+
+    public function testParseToUnicodeCMapWithMultipleCodeSpaceRanges(): void {
+        $stream = new InMemoryStream(
+            <<<EOD
+            begincmap
+            1 begincodespacerange
+            < 0000 > < BBBB >
+            < DDDD > < FFFF >
+            endcodespacerange
+            endcmap
+            EOD
+        );
+        static::assertEquals(
+            new ToUnicodeCMap(
+                [new CodeSpaceRange(0x0000, 0xBBBB), new CodeSpaceRange(0xDDDD, 0xFFFF)],
+                2,
+            ),
+            ToUnicodeCMapParser::parse($stream, 0, $stream->getSizeInBytes())
+        );
+    }
 }