Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 129 additions & 133 deletions src/Document/ContentStream/ContentStreamParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,79 @@
/** @internal */
class ContentStreamParser {
public const OPERATOR_CHARS = ['X' => true, 'x' => true, 'I' => true, 'D' => true, 'C' => true, 'T' => true, '*' => true, 'S' => true, 's' => true, 'N' => true, 'n' => true, 'G' => true, 'g' => true, 'K' => true, 'k' => true, 'm' => true, 'i' => true, 'e' => true, 'W' => true, 'w' => true, 'J' => true, 'j' => true, 'M' => true, 'd' => true, 'l' => true, 'c' => true, 'v' => true, 'y' => true, 'h' => true, 'f' => true, 'F' => true, 'B' => true, 'Q' => true, 'q' => true, '\'' => true, '"' => true, 'E' => true, 'P' => true, 'R' => true, 'r' => true, 'b' => true, 'z' => true, 'L' => true, 'o' => true, '1' => true, '0' => true];
private const OPERATOR_MAP = [
'BMC' => MarkedContentOperator::BeginMarkedContent,
'BDC' => MarkedContentOperator::BeginMarkedContentWithProperties,
'EMC' => MarkedContentOperator::EndMarkedContent,
'SCN' => ColorOperator::SetStrokingParams,
'scn' => ColorOperator::SetColorParams,
'BX' => CompatibilityOperator::BeginCompatibilitySection,
'EX' => CompatibilityOperator::EndCompatibilitySection,
'BI' => InlineImageOperator::Begin,
'ID' => InlineImageOperator::BeginImageData,
'EI' => InlineImageOperator::End,
'MD' => MarkedContentOperator::Tag,
'DP' => MarkedContentOperator::TagProperties,
'BT' => TextObjectOperator::BEGIN,
'ET' => TextObjectOperator::END,
'W*' => ClippingPathOperator::INTERSECT_EVEN_ODD,
'CS' => ColorOperator::SetName,
'cs' => ColorOperator::SetNameNonStroking,
'SC' => ColorOperator::SetStrokingColor,
'sc' => ColorOperator::SetColor,
'RG' => ColorOperator::SetStrokingColorDeviceRGB,
'rg' => ColorOperator::SetColorDeviceRGB,
'cm' => GraphicsStateOperator::ModifyCurrentTransformationMatrix,
'ri' => GraphicsStateOperator::SetIntent,
'gs' => GraphicsStateOperator::SetDictName,
're' => PathConstructionOperator::RECTANGLE,
'f*' => PathPaintingOperator::FILL_EVEN_ODD,
'B*' => PathPaintingOperator::FILL_STROKE_EVEN_ODD,
'b*' => PathPaintingOperator::CLOSE_FILL_STROKE,
'Td' => TextPositioningOperator::MOVE_OFFSET,
'TD' => TextPositioningOperator::MOVE_OFFSET_LEADING,
'Tm' => TextPositioningOperator::SET_MATRIX,
'T*' => TextPositioningOperator::NEXT_LINE,
'Tj' => TextShowingOperator::SHOW,
'TJ' => TextShowingOperator::SHOW_ARRAY,
'Tc' => TextStateOperator::CHAR_SPACE,
'Tw' => TextStateOperator::WORD_SPACE,
'Tz' => TextStateOperator::SCALE,
'TL' => TextStateOperator::LEADING,
'Tf' => TextStateOperator::FONT_SIZE,
'Tr' => TextStateOperator::RENDER,
'Ts' => TextStateOperator::RISE,
'd0' => Type3FontOperator::SetWidth,
'd1' => Type3FontOperator::SetWidthAndBoundingBox,
'Do' => XObjectOperator::Paint,
'W' => ClippingPathOperator::INTERSECT,
'G' => ColorOperator::SetStrokingColorSpace,
'g' => ColorOperator::SetColorSpace,
'K' => ColorOperator::SetStrokingColorDeviceCMYK,
'k' => ColorOperator::SetColorDeviceCMYK,
'q' => GraphicsStateOperator::SaveCurrentStateToStack,
'Q' => GraphicsStateOperator::RestoreMostRecentStateFromStack,
'w' => GraphicsStateOperator::SetLineWidth,
'J' => GraphicsStateOperator::SetLineCap,
'j' => GraphicsStateOperator::SetLineJoin,
'M' => GraphicsStateOperator::SetMiterJoin,
'd' => GraphicsStateOperator::SetLineDash,
'i' => GraphicsStateOperator::SetFlatness,
'm' => PathConstructionOperator::MOVE,
'l' => PathConstructionOperator::LINE,
'c' => PathConstructionOperator::CURVE_BEZIER_123,
'v' => PathConstructionOperator::CURVE_BEZIER_23,
'y' => PathConstructionOperator::CURVE_BEZIER_13,
'h' => PathConstructionOperator::CLOSE,
'S' => PathPaintingOperator::STROKE,
's' => PathPaintingOperator::CLOSE_STROKE,
'f' => PathPaintingOperator::FILL,
'F' => PathPaintingOperator::FILL_DEPRECATED,
'B' => PathPaintingOperator::FILL_STROKE,
'n' => PathPaintingOperator::END,
'\'' => TextShowingOperator::MOVE_SHOW,
'"' => TextShowingOperator::MOVE_SHOW_SPACING,
];

/**
* @param list<DecoratedObject> $contentsObjects
Expand Down Expand Up @@ -70,6 +143,7 @@ public static function parse(array $contentsObjects): ContentStream {
$inComment = true;
$startOfCommentOffset = $index;
$previousChar = $secondToLastChar = $thirdToLastChar = null;
continue;
} elseif ($char === '[' && $previousChar !== '\\') {
$inArrayLevel++;
} elseif ($char === '<' && $previousChar === '<' && $secondToLastChar !== '\\') {
Expand All @@ -86,52 +160,53 @@ public static function parse(array $contentsObjects): ContentStream {
} elseif ($inArrayLevel > 0 && $char === ']' && $previousChar !== '\\') {
$inArrayLevel--;
}
} elseif ($char === 'T' && $previousChar === 'B') { // TextObjectOperator::BEGIN
$startCurrentOperandIndex = $index + 1;
$textObject = new TextObject();
} elseif ($char === 'T' && $previousChar === 'E') { // TextObjectOperator::END
$startCurrentOperandIndex = $index + 1;
if ($textObject !== null) {
$content[] = $textObject;
$textObject = null;
}
} elseif ($char === 'D' && $previousChar === 'I' && $secondToLastChar !== '\\') {
$inInlineImage = true;
} elseif ($char === 'C'
&& (($secondToLastChar === 'B' && ($previousChar === 'M' || $previousChar === 'D')) || ($secondToLastChar === 'E' && $previousChar === 'M'))) { // MarkedContentOperator::BeginMarkedContent, MarkedContentOperator::EndMarkedContent, MarkedContentOperator::BeginMarkedContentWithProperties
$startCurrentOperandIndex = $index + 1;
} elseif (isset(self::OPERATOR_CHARS[$char])
&& ($operator = self::getOperator($char, $previousChar, $secondToLastChar, $thirdToLastChar)) !== null
&& (($nextChar = $rawStream[$index + 1] ?? '') === '' || self::getOperator($nextChar, $char, $previousChar, $secondToLastChar) === null)) { // Skip the current hit if the next iteration is also a valid operator
$operands = '';
if ($previousContentStream !== null && $startPreviousOperandIndex !== null && $startPreviousOperandIndex < $previousContentStream->getSizeInBytes()) {
$operands .= $previousContentStream->read($startPreviousOperandIndex, $previousContentStream->getSizeInBytes() - $startPreviousOperandIndex);
$startPreviousOperandIndex = null;
}
$operatorLength = strlen($operator->value);
$operandLength = $index + 1 - $startCurrentOperandIndex - $operatorLength;
if ($operandLength > 0) {
$operandEndOffset = $index + 1 - $operatorLength;
if ($endCommentOffset !== null
&& $endCommentOffset < $operandEndOffset
&& $startOfCommentOffset !== null
&& $startOfCommentOffset > $startCurrentOperandIndex) {
$operands .= substr($rawStream, $startCurrentOperandIndex, $startOfCommentOffset - $startCurrentOperandIndex);
$operands .= substr($rawStream, $endCommentOffset, $operandEndOffset - $endCommentOffset);
} elseif (isset(self::OPERATOR_CHARS[$char])) {
if ($char === 'T' && $previousChar === 'B') { // TextObjectOperator::BEGIN
$startCurrentOperandIndex = $index + 1;
$textObject = new TextObject();
} elseif ($char === 'T' && $previousChar === 'E') { // TextObjectOperator::END
$startCurrentOperandIndex = $index + 1;
if ($textObject !== null) {
$content[] = $textObject;
$textObject = null;
}
} elseif ($char === 'D' && $previousChar === 'I' && $secondToLastChar !== '\\') {
$inInlineImage = true;
} elseif ($char === 'C'
&& (($secondToLastChar === 'B' && ($previousChar === 'M' || $previousChar === 'D')) || ($secondToLastChar === 'E' && $previousChar === 'M'))) { // MarkedContentOperator::BeginMarkedContent, MarkedContentOperator::EndMarkedContent, MarkedContentOperator::BeginMarkedContentWithProperties
$startCurrentOperandIndex = $index + 1;
} elseif (($operator = self::getOperator($char, $previousChar, $secondToLastChar, $thirdToLastChar)) !== null
&& (($nextChar = $rawStream[$index + 1] ?? '') === '' || self::getOperator($nextChar, $char, $previousChar, $secondToLastChar) === null)) { // Skip the current hit if the next iteration is also a valid operator
$operands = '';
if ($previousContentStream !== null && $startPreviousOperandIndex !== null && $startPreviousOperandIndex < $previousContentStream->getSizeInBytes()) {
$operands .= $previousContentStream->read($startPreviousOperandIndex, $previousContentStream->getSizeInBytes() - $startPreviousOperandIndex);
$startPreviousOperandIndex = null;
}
$operatorLength = strlen($operator->value);
$operandLength = $index + 1 - $startCurrentOperandIndex - $operatorLength;
if ($operandLength > 0) {
$operandEndOffset = $index + 1 - $operatorLength;
if ($endCommentOffset !== null
&& $endCommentOffset < $operandEndOffset
&& $startOfCommentOffset !== null
&& $startOfCommentOffset > $startCurrentOperandIndex) {
$operands .= substr($rawStream, $startCurrentOperandIndex, $startOfCommentOffset - $startCurrentOperandIndex);
$operands .= substr($rawStream, $endCommentOffset, $operandEndOffset - $endCommentOffset);
} else {
$operands .= substr($rawStream, $startCurrentOperandIndex, $operandLength);
}
}

$command = new ContentStreamCommand($operator, trim($operands));
if ($textObject !== null) {
$textObject->addContentStreamCommand($command);
} else {
$operands .= substr($rawStream, $startCurrentOperandIndex, $operandLength);
$content[] = $command;
}
}

$command = new ContentStreamCommand($operator, trim($operands));
if ($textObject !== null) {
$textObject->addContentStreamCommand($command);
} else {
$content[] = $command;
$startCurrentOperandIndex = $index + 1;
$endCommentOffset = $startOfCommentOffset = null;
}

$startCurrentOperandIndex = $index + 1;
$endCommentOffset = $startOfCommentOffset = null;
}

$thirdToLastChar = $secondToLastChar;
Expand All @@ -146,102 +221,23 @@ public static function parse(array $contentsObjects): ContentStream {
return new ContentStream(...$content);
}

/**
* This method uses three maps instead of calling $enum::tryFrom for all possible enums
* as operator retrieval happens possibly millions of times in a single file
*/
public static function getOperator(string $currentChar, ?string $previousChar, ?string $secondToLastChar, ?string $thirdToLastChar): CompatibilityOperator|InlineImageOperator|MarkedContentOperator|TextObjectOperator|ClippingPathOperator|ColorOperator|GraphicsStateOperator|PathConstructionOperator|PathPaintingOperator|TextPositioningOperator|TextShowingOperator|TextStateOperator|Type3FontOperator|XObjectOperator|null {
$threeLetterMatch = match ($secondToLastChar . $previousChar . $currentChar) {
'BMC' => MarkedContentOperator::BeginMarkedContent,
'BDC' => MarkedContentOperator::BeginMarkedContentWithProperties,
'EMC' => MarkedContentOperator::EndMarkedContent,
'SCN' => ColorOperator::SetStrokingParams,
'scn' => ColorOperator::SetColorParams,
default => null,
};
if ($threeLetterMatch !== null) {
return ($thirdToLastChar === '\\' || $thirdToLastChar === '/') ? null : $threeLetterMatch;
if ($secondToLastChar !== null && isset(self::OPERATOR_CHARS[$secondToLastChar]) && $previousChar !== null && isset(self::OPERATOR_CHARS[$previousChar])) {
$key = $secondToLastChar . $previousChar . $currentChar;
if (isset(self::OPERATOR_MAP[$key])) {
return ($thirdToLastChar === '\\' || $thirdToLastChar === '/') ? null : self::OPERATOR_MAP[$key];
}
}

$twoLetterMatch = match ($previousChar . $currentChar) {
'BX' => CompatibilityOperator::BeginCompatibilitySection,
'EX' => CompatibilityOperator::EndCompatibilitySection,
'BI' => InlineImageOperator::Begin,
'ID' => InlineImageOperator::BeginImageData,
'EI' => InlineImageOperator::End,
'MD' => MarkedContentOperator::Tag,
'DP' => MarkedContentOperator::TagProperties,
'BT' => TextObjectOperator::BEGIN,
'ET' => TextObjectOperator::END,
'W*' => ClippingPathOperator::INTERSECT_EVEN_ODD,
'CS' => ColorOperator::SetName,
'cs' => ColorOperator::SetNameNonStroking,
'SC' => ColorOperator::SetStrokingColor,
'sc' => ColorOperator::SetColor,
'RG' => ColorOperator::SetStrokingColorDeviceRGB,
'rg' => ColorOperator::SetColorDeviceRGB,
'cm' => GraphicsStateOperator::ModifyCurrentTransformationMatrix,
'ri' => GraphicsStateOperator::SetIntent,
'gs' => GraphicsStateOperator::SetDictName,
're' => PathConstructionOperator::RECTANGLE,
'f*' => PathPaintingOperator::FILL_EVEN_ODD,
'B*' => PathPaintingOperator::FILL_STROKE_EVEN_ODD,
'b*' => PathPaintingOperator::CLOSE_FILL_STROKE,
'Td' => TextPositioningOperator::MOVE_OFFSET,
'TD' => TextPositioningOperator::MOVE_OFFSET_LEADING,
'Tm' => TextPositioningOperator::SET_MATRIX,
'T*' => TextPositioningOperator::NEXT_LINE,
'Tj' => TextShowingOperator::SHOW,
'TJ' => TextShowingOperator::SHOW_ARRAY,
'Tc' => TextStateOperator::CHAR_SPACE,
'Tw' => TextStateOperator::WORD_SPACE,
'Tz' => TextStateOperator::SCALE,
'TL' => TextStateOperator::LEADING,
'Tf' => TextStateOperator::FONT_SIZE,
'Tr' => TextStateOperator::RENDER,
'Ts' => TextStateOperator::RISE,
'd0' => Type3FontOperator::SetWidth,
'd1' => Type3FontOperator::SetWidthAndBoundingBox,
'Do' => XObjectOperator::Paint,
default => null,
};
if ($twoLetterMatch !== null) {
return ($secondToLastChar === '\\' || $secondToLastChar === '/') ? null : $twoLetterMatch;
if ($previousChar !== null && isset(self::OPERATOR_CHARS[$previousChar])) {
$key = $previousChar . $currentChar;
if (isset(self::OPERATOR_MAP[$key])) {
return ($secondToLastChar === '\\' || $secondToLastChar === '/') ? null : self::OPERATOR_MAP[$key];
}
}

$oneLetterMatch = match ($currentChar) {
'W' => ClippingPathOperator::INTERSECT,
'G' => ColorOperator::SetStrokingColorSpace,
'g' => ColorOperator::SetColorSpace,
'K' => ColorOperator::SetStrokingColorDeviceCMYK,
'k' => ColorOperator::SetColorDeviceCMYK,
'q' => GraphicsStateOperator::SaveCurrentStateToStack,
'Q' => GraphicsStateOperator::RestoreMostRecentStateFromStack,
'w' => GraphicsStateOperator::SetLineWidth,
'J' => GraphicsStateOperator::SetLineCap,
'j' => GraphicsStateOperator::SetLineJoin,
'M' => GraphicsStateOperator::SetMiterJoin,
'd' => GraphicsStateOperator::SetLineDash,
'i' => GraphicsStateOperator::SetFlatness,
'm' => PathConstructionOperator::MOVE,
'l' => PathConstructionOperator::LINE,
'c' => PathConstructionOperator::CURVE_BEZIER_123,
'v' => PathConstructionOperator::CURVE_BEZIER_23,
'y' => PathConstructionOperator::CURVE_BEZIER_13,
'h' => PathConstructionOperator::CLOSE,
'S' => PathPaintingOperator::STROKE,
's' => PathPaintingOperator::CLOSE_STROKE,
'f' => PathPaintingOperator::FILL,
'F' => PathPaintingOperator::FILL_DEPRECATED,
'B' => PathPaintingOperator::FILL_STROKE,
'n' => PathPaintingOperator::END,
'\'' => TextShowingOperator::MOVE_SHOW,
'"' => TextShowingOperator::MOVE_SHOW_SPACING,
default => null,
};

if ($oneLetterMatch !== null) {
return ($previousChar === '\\' || $previousChar === '/') ? null : $oneLetterMatch;
if (isset(self::OPERATOR_MAP[$currentChar])) {
return ($previousChar === '\\' || $previousChar === '/') ? null : self::OPERATOR_MAP[$currentChar];
}

return null;
Expand Down
Loading