Skip to content
Open
Show file tree
Hide file tree
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
112 changes: 86 additions & 26 deletions src/Mpdf.php
Original file line number Diff line number Diff line change
Expand Up @@ -7087,6 +7087,83 @@ private function closeBlockBdcIfOpen()
$this->flowingBlockAttr['pdfua_bdc_elem'] = null;
}

/**
* Whether a list marker drawn now is the content of the Lbl element Li::open() made.
*
* @return bool
*/
private function listMarkerHasLbl()
{
return !$this->ColActive && isset($this->blk[$this->blklvl]['pdfua_li_lbl_elem']);
}

/**
* Open the marked content a line's chunk is drawn in: that of the inline element restoreFont()
* says it is in, or else the block's.
*
* Table cell text always has an inline element (the cell, or one inside it). An object that
* marks its own content is drawn by printLineObject() after its chunk, and opens nothing here
* unless its chunk draws a span's background or border. In an artifact every object is drawn
* after the line's text, inside the artifact's marked content.
*
* @param int $k The chunk's key in $objectbuffer
* @param bool $is_table
* @return bool Whether the chunk is an object that marks its own content
*/
private function markLineChunk($k, $is_table)
{
$object = empty($this->objectbuffer[$k]) ? null : $this->objectbuffer[$k];
$tagged = $object !== null
&& in_array($object['type'], ['image', 'barcode', 'textcircle', 'listmarker', 'input', 'textarea', 'select'], true)
&& ($object['type'] !== 'listmarker' || $this->listMarkerHasLbl())
&& empty($this->flowingBlockAttr['pdfua_artifact_open'])
&& !$this->ua->getStructureTree()->isInArtifact();
if ($tagged && !$this->spanbgcolor && empty($this->spanborddet)) {
return true;
}
$inlineElem = $object === null ? $this->ua->getAnchorState()->getInlineContentElem() : null;
if ($inlineElem !== null) {
$this->ensureInlineBdcOpen($inlineElem);
} elseif (!$is_table) {
$this->ensureBlockBdcOpen();
}

return $tagged;
}

/**
* Draw the object of a line's chunk now, so its marked content comes between the text before
* and after it, and not inside the text's. The next chunk of text begins its marked content again.
*
* What the object tags goes in the element the chunk is in, such as the Link around an image.
*
* @param int $k The chunk's key in $objectbuffer
* @param bool $is_table
* @param string|bool $blockdir
* @return void
*/
private function printLineObject($k, $is_table, $blockdir)
{
$this->closeBlockBdcIfOpen();

$parent = $this->ua->getAnchorState()->getInlineContentElem();
if ($parent === null && !$is_table && isset($this->flowingBlockAttr['pdfua_struct_elem'])) {
$parent = $this->flowingBlockAttr['pdfua_struct_elem'];
}

$line = $this->objectbuffer;
$this->objectbuffer = [$k => $line[$k]];
if ($parent !== null) {
$this->ua->getStructureTree()->pushExisting($parent);
}
$this->printobjectbuffer($is_table, $blockdir);
if ($parent !== null) {
$this->ua->getStructureTree()->close();
}
unset($line[$k]);
$this->objectbuffer = $line;
}

/**
* Mark where a table cell's block frames begin, once the TD or TH element is open.
*
Expand Down Expand Up @@ -7701,19 +7778,7 @@ function finishFlowingBlock($endofblock = false, $next = '')
$this->restoreFont($font[$k]); // mPDF 5.7


// Mark the chunk against the element restoreFont() says it is in. Table cell text
// always has one (the cell, or an element inside it); a cell chunk without one is
// an image or widget, which printobjectbuffer() marks itself.
if ($this->PDFUA) {
$pdfuaInlineElem = (!isset($this->objectbuffer[$k]) || !$this->objectbuffer[$k])
? $this->ua->getAnchorState()->getInlineContentElem()
: null;
if ($pdfuaInlineElem !== null) {
$this->ensureInlineBdcOpen($pdfuaInlineElem);
} elseif (!$is_table) {
$this->ensureBlockBdcOpen();
}
}
$pdfuaLineObject = $this->PDFUA && $this->markLineChunk($k, $is_table);

if ($is_table && substr($align, 0, 1) == 'D' && $aord == 0) {
$dp = $this->decimal_align[substr($align, 0, 2)];
Expand Down Expand Up @@ -7793,6 +7858,9 @@ function finishFlowingBlock($endofblock = false, $next = '')
} else {
$this->Cell($stringWidth, $stackHeight, $chunk, '', 0, '', $fill, $this->HREF, 0, 0, 0, 'M', $fill, true, (isset($cOTLdata[$aord]) ? $cOTLdata[$aord] : false), $this->textvar, (isset($lineBox[$k]) ? $lineBox[$k] : false)); // first or middle part // mPDF 5.7.1
}
if ($pdfuaLineObject) {
$this->printLineObject($k, $is_table, $blockdir);
}


if (!empty($this->spanborddet)) {
Expand Down Expand Up @@ -8534,9 +8602,8 @@ function ($col) {
// on the structure stack
$pdfuaLblMcid = null;
if ($this->PDFUA
&& !$this->ColActive
&& !$this->ua->getStructureTree()->isInArtifact()
&& isset($this->blk[$this->blklvl]['pdfua_li_lbl_elem'])
&& $this->listMarkerHasLbl()
) {
$structParents = $this->pdfuaStructParents();
$pdfuaLblMcid = $this->ua->getStructureTree()->addContentForElement(
Expand Down Expand Up @@ -9585,17 +9652,7 @@ function WriteFlowingBlock($s, $sOTLdata)
$this->restoreFont($font[$k]); // mPDF 5.7


// As in finishFlowingBlock()
if ($this->PDFUA) {
$pdfuaInlineElem = (!isset($this->objectbuffer[$k]) || !$this->objectbuffer[$k])
? $this->ua->getAnchorState()->getInlineContentElem()
: null;
if ($pdfuaInlineElem !== null) {
$this->ensureInlineBdcOpen($pdfuaInlineElem);
} elseif (!$is_table) {
$this->ensureBlockBdcOpen();
}
}
$pdfuaLineObject = $this->PDFUA && $this->markLineChunk($k, $is_table);

$this->SetSpacing(($this->fixedlSpacing * Mpdf::SCALE) + $jcharspacing, ($this->fixedlSpacing + $this->minwSpacing) * Mpdf::SCALE + $jws);
// Now unset these values so they don't influence GetStringwidth below or in fn. Cell
Expand Down Expand Up @@ -9666,6 +9723,9 @@ function WriteFlowingBlock($s, $sOTLdata)
} else {
$this->Cell($stringWidth, $stackHeight, $chunk, '', 0, '', $fill, $this->HREF, 0, 0, 0, 'M', $fill, true, (isset($cOTLdata[$aord]) ? $cOTLdata[$aord] : false), $this->textvar, (isset($lineBox[$k]) ? $lineBox[$k] : false)); // first or middle part
}
if ($pdfuaLineObject) {
$this->printLineObject($k, $is_table, $blockdir);
}


if (!empty($this->spanborddet)) {
Expand Down
139 changes: 139 additions & 0 deletions tests/Mpdf/Ua/InlineObjectReadingOrderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php

namespace Mpdf\Ua;

/**
* An image, barcode, list marker or form field in a line is marked where it falls among the
* line's text, and its marked content does not sit inside the text's.
*/
class InlineObjectReadingOrderTest extends PdfUaTestCase
{

/**
* @var string
*/
private $image = __DIR__ . '/../../data/img/bayeux2.jpg';

/**
* An image's Figure is read between the text before and after it.
*
* @return void
*/
public function testImageIsReadBetweenTheTextAroundIt()
{
$pdf = $this->getOutput(
$this->makeMpdf(),
'<p>Before <img src="' . $this->image . '" alt="A picture" width="20"> after</p>'
);

$this->assertSame(['MCID 0', 'Figure', 'MCID 2'], $this->kidsOf($pdf, 'P'));
$this->assertNoNestedMarkedContent($pdf);
}

/**
* A line broken off in the middle of a paragraph draws its image in order too.
*
* @return void
*/
public function testImageOnAWrappedLineIsReadBetweenTheTextAroundIt()
{
$pdf = $this->getOutput(
$this->makeMpdf(),
'<p>Before <img src="' . $this->image . '" alt="A picture" width="20"> after '
. str_repeat('and then some more words ', 20) . '</p>'
);

$kids = $this->kidsOf($pdf, 'P');
$this->assertSame(['MCID 0', 'Figure', 'MCID 2'], array_slice($kids, 0, 3));
$this->assertNoNestedMarkedContent($pdf);
}

/**
* An image in a link is the Link's content, beside its annotation, so a Link with text after it
* is not left empty.
*
* @return void
*/
public function testImageInALinkIsTheLinksContent()
{
$pdf = $this->getOutput(
$this->makeMpdf(),
'<p>x <a href="https://example.com"><img src="' . $this->image . '" alt="Home" width="20"></a> y</p>'
);

$this->assertSame(['MCID 0', 'Link', 'MCID 2'], $this->kidsOf($pdf, 'P'));
$this->assertSame(['Figure', 'OBJR'], $this->kidsOf($pdf, 'Link'));
$this->assertNoNestedMarkedContent($pdf);
}

/**
* A checkbox before its label's text is read before it.
*
* @return void
*/
public function testFormFieldIsReadBetweenTheTextAroundIt()
{
$pdf = $this->getOutput(
$this->makeMpdf(['useActiveForms' => true]),
'<p>Start <label><input type="checkbox" name="c" value="y" title="Agree"> Check</label>'
. ' and <input type="text" name="t" title="Name"> end</p>'
);

$this->assertSame(['MCID 0', 'Form', 'MCID 1', 'Form', 'MCID 2'], $this->kidsOf($pdf, 'P'));
}

/**
* Images in a table cell, a barcode and list markers open their marked content outside the text's.
*
* @return void
*/
public function testObjectsOpenNoMarkedContentInsideTheTexts()
{
$pdf = $this->getOutput(
$this->makeMpdf(),
'<table><tr><td>cell <img src="' . $this->image . '" alt="C" width="10"> text</td></tr></table>'
. '<ul><li>item <barcode code="123" type="C39" /> more</li><li>two</li></ul>'
);

$this->assertSame(['MCID 0', 'Figure', 'MCID 2'], $this->kidsOf($pdf, 'TD'));
$this->assertNoNestedMarkedContent($pdf);
}

/**
* The kids of the first structure element of a type, each named by the type of the element it
* refers to, 'MCID n' or 'OBJR'.
*
* @param string $pdf
* @param string $type
*
* @return string[]
*/
private function kidsOf($pdf, $type)
{
preg_match_all('@(\d+) 0 obj\s*<</Type /StructElem\s+/S /(\w+)(.*?)endobj@s', $pdf, $m, PREG_SET_ORDER);
$types = [];
$body = null;
foreach ($m as $elem) {
$types[$elem[1]] = $elem[2];
if ($body === null && $elem[2] === $type) {
$body = $elem[3];
}
}
$this->assertNotNull($body, 'no /' . $type . ' element');
$this->assertSame(1, preg_match('@/K (\[.*\]|<<[^>]*>>|\d+ 0 R|\d+)\s*>>\s*$@s', $body, $k));

preg_match_all('@<</Type /MCR[^>]*/MCID (\d+)>>|<</Type /OBJR[^>]*>>|(\d+) 0 R|\d+@', $k[1], $refs, PREG_SET_ORDER);
$kids = [];
foreach ($refs as $ref) {
if (strpos($ref[0], '/OBJR') !== false) {
$kids[] = 'OBJR';
} elseif (isset($ref[2]) && $ref[2] !== '') {
$kids[] = $types[$ref[2]];
} else {
$kids[] = 'MCID ' . ($ref[1] !== '' ? $ref[1] : $ref[0]);
}
}

return $kids;
}
}
26 changes: 26 additions & 0 deletions tests/Mpdf/Ua/PdfUaTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,30 @@ protected function getOutput(\Mpdf\Mpdf $mpdf, $html)
$mpdf->WriteHTML($html);
return $mpdf->Output(null, 'S');
}

/**
* Marked content with an MCID may be inside no other marked content (ISO 32000-1 §14.7.4.2).
*
* @param string $pdf
*
* @return void
*/
protected function assertNoNestedMarkedContent($pdf)
{
preg_match_all('@stream\r?\n(.*?)endstream@s', $pdf, $streams);
foreach ($streams[1] as $stream) {
preg_match_all('@/(\w+) <</MCID \d+>> BDC|\bBMC\b|\bEMC\b@', $stream, $ops, PREG_SET_ORDER);
$depth = 0;
foreach ($ops as $op) {
if ($op[0] === 'EMC') {
$depth--;
continue;
}
if ($op[0] !== 'BMC') {
$this->assertSame(0, $depth, '/' . $op[1] . ' opened inside other marked content');
}
$depth++;
}
}
}
}
Binary file modified tests/data/snapshots/pdfua.pdf
Binary file not shown.
Loading