Repository: PrinsFrank/pdfparser (php)

Issue:
"Invalid font operand "1.0 1" for Tf Operator" when font dictionary key contains other font operator
Simple reproduction:
```
/Tc1.0 1 Tf
```

As "Tc" is also a valid operator, it is parsed here as the operator, removing it from the operand buffer.

Pull request:
Don't continue matching operators when in dictionary key or escaped string
Fixes #93 

Patch:
diff --git a/src/Document/ContentStream/ContentStreamParser.php b/src/Document/ContentStream/ContentStreamParser.php
index 0d4c7f6..d3c1de8 100644
--- a/src/Document/ContentStream/ContentStreamParser.php
+++ b/src/Document/ContentStream/ContentStreamParser.php
@@ -91,8 +91,8 @@ class ContentStreamParser {
             'scn' => ColorOperator::SetColorParams,
             default => null,
         };
-        if ($threeLetterMatch !== null && !in_array($thirdToLastChar, ['\\', '/'], true)) {
-            return $threeLetterMatch;
+        if ($threeLetterMatch !== null) {
+            return in_array($thirdToLastChar, ['\\', '/'], true) ? null : $threeLetterMatch;
         }
 
         $twoLetterMatch = match ($previousChar . $currentChar) {
@@ -137,8 +137,8 @@ class ContentStreamParser {
             'Do' => XObjectOperator::Paint,
             default => null,
         };
-        if ($twoLetterMatch !== null && !in_array($secondToLastChar, ['\\', '/'], true)) {
-            return $twoLetterMatch;
+        if ($twoLetterMatch !== null) {
+            return in_array($secondToLastChar, ['\\', '/'], true) ? null : $twoLetterMatch;
         }
 
         $oneLetterMatch = match ($currentChar) {
@@ -172,8 +172,8 @@ class ContentStreamParser {
             default => null,
         };
 
-        if ($oneLetterMatch !== null && !in_array($previousChar, ['\\', '/'], true)) {
-            return $oneLetterMatch;
+        if ($oneLetterMatch !== null) {
+            return in_array($previousChar, ['\\', '/'], true) ? null : $oneLetterMatch;
         }
 
         return null;


Test patch:
diff --git a/tests/Unit/Document/Text/ContentStreamParserTest.php b/tests/Unit/Document/Text/ContentStreamParserTest.php
index a5ffb40..537f100 100644
--- a/tests/Unit/Document/Text/ContentStreamParserTest.php
+++ b/tests/Unit/Document/Text/ContentStreamParserTest.php
@@ -48,6 +48,22 @@ class ContentStreamParserTest extends TestCase {
         );
     }
 
+    public function testParseTextWithOperatorInTextOperand(): void {
+        static::assertEquals(
+            new ContentStream(
+                (new TextObject())
+                    ->addContentStreamCommand(new ContentStreamCommand(TextStateOperator::FONT_SIZE, '/Tc1.0 1'))
+            ),
+            ContentStreamParser::parse(
+                <<<EOD
+                BT
+                /Tc1.0 1 Tf
+                ET
+                EOD
+            )
+        );
+    }
+
     #[DataProvider('provideOperators')]
     public function testGetOperator(CompatibilityOperator|InlineImageOperator|MarkedContentOperator|TextObjectOperator|ClippingPathOperator|ColorOperator|GraphicsStateOperator|PathConstructionOperator|PathPaintingOperator|TextPositioningOperator|TextShowingOperator|TextStateOperator|Type3FontOperator|XObjectOperator $enumCase): void {
         static::assertSame(
@@ -62,43 +78,13 @@ class ContentStreamParserTest extends TestCase {
 
     #[DataProvider('provideOperators')]
     public function testGetOperatorWithLeadingEscapeValue(CompatibilityOperator|InlineImageOperator|MarkedContentOperator|TextObjectOperator|ClippingPathOperator|ColorOperator|GraphicsStateOperator|PathConstructionOperator|PathPaintingOperator|TextPositioningOperator|TextShowingOperator|TextStateOperator|Type3FontOperator|XObjectOperator $enumCase): void {
-        if (in_array($enumCase, [TextPositioningOperator::MOVE_OFFSET, TextShowingOperator::SHOW, TextShowingOperator::SHOW_ARRAY, TextStateOperator::WORD_SPACE, GraphicsStateOperator::SetIntent, ColorOperator::SetStrokingColorDeviceRGB, ColorOperator::SetColorDeviceRGB, ColorOperator::SetName, ColorOperator::SetNameNonStroking, ColorOperator::SetColor, ColorOperator::SetColorParams, GraphicsStateOperator::ModifyCurrentTransformationMatrix, GraphicsStateOperator::SetDictName, TextPositioningOperator::SET_MATRIX, TextStateOperator::CHAR_SPACE, TextStateOperator::FONT_SIZE, TextStateOperator::RISE], true)) {
-            // If a enum case matches, but there is an escape character in front, it will match partially a different enum case or none at all
-            static::assertSame(
-                match ($enumCase) {
-                    TextPositioningOperator::MOVE_OFFSET => GraphicsStateOperator::SetLineDash,
-                    TextShowingOperator::SHOW => GraphicsStateOperator::SetLineJoin,
-                    TextShowingOperator::SHOW_ARRAY => GraphicsStateOperator::SetLineCap,
-                    TextStateOperator::WORD_SPACE => GraphicsStateOperator::SetLineWidth,
-                    GraphicsStateOperator::SetIntent => GraphicsStateOperator::SetFlatness,
-                    ColorOperator::SetStrokingColorDeviceRGB => ColorOperator::SetStrokingColorSpace,
-                    ColorOperator::SetColorDeviceRGB => ColorOperator::SetColorSpace,
-                    ColorOperator::SetName => PathPaintingOperator::STROKE,
-                    ColorOperator::SetNameNonStroking => PathPaintingOperator::CLOSE_STROKE,
-                    ColorOperator::SetColor => PathConstructionOperator::CURVE_BEZIER_123,
-                    ColorOperator::SetColorParams => PathPaintingOperator::END,
-                    GraphicsStateOperator::ModifyCurrentTransformationMatrix => PathConstructionOperator::MOVE,
-                    GraphicsStateOperator::SetDictName => PathPaintingOperator::CLOSE_STROKE,
-                    TextPositioningOperator::SET_MATRIX => PathConstructionOperator::MOVE,
-                    TextStateOperator::CHAR_SPACE => PathConstructionOperator::CURVE_BEZIER_123,
-                    TextStateOperator::FONT_SIZE => PathPaintingOperator::FILL,
-                    TextStateOperator::RISE => PathPaintingOperator::CLOSE_STROKE,
-                },
-                match (strlen($enumCase->value)) {
-                    1 => ContentStreamParser::getOperator($enumCase->value, '\\', null, null),
-                    2 => ContentStreamParser::getOperator(substr($enumCase->value, 1, 1), substr($enumCase->value, 0, 1), '\\', null),
-                    3 => ContentStreamParser::getOperator(substr($enumCase->value, 2, 1), substr($enumCase->value, 1, 1), substr($enumCase->value, 0, 1), '\\'),
-                }
-            );
-        } else {
-            static::assertNull(
-                match (strlen($enumCase->value)) {
-                    1 => ContentStreamParser::getOperator($enumCase->value, '\\', null, null),
-                    2 => ContentStreamParser::getOperator(substr($enumCase->value, 1, 1), substr($enumCase->value, 0, 1), '\\', null),
-                    3 => ContentStreamParser::getOperator(substr($enumCase->value, 2, 1), substr($enumCase->value, 1, 1), substr($enumCase->value, 0, 1), '\\'),
-                }
-            );
-        }
+        static::assertNull(
+            match (strlen($enumCase->value)) {
+                1 => ContentStreamParser::getOperator($enumCase->value, '\\', null, null),
+                2 => ContentStreamParser::getOperator(substr($enumCase->value, 1, 1), substr($enumCase->value, 0, 1), '\\', null),
+                3 => ContentStreamParser::getOperator(substr($enumCase->value, 2, 1), substr($enumCase->value, 1, 1), substr($enumCase->value, 0, 1), '\\'),
+            }
+        );
     }
 
     /** @return iterable<string, array{0: CompatibilityOperator|InlineImageOperator|MarkedContentOperator|TextObjectOperator|ClippingPathOperator|ColorOperator|GraphicsStateOperator|PathConstructionOperator|PathPaintingOperator|TextPositioningOperator|TextShowingOperator|TextStateOperator|Type3FontOperator|XObjectOperator}> */