Keep sales PDF item rows from splitting across a page break - #304
Open
TuVanDev wants to merge 1 commit into
Open
Keep sales PDF item rows from splitting across a page break#304TuVanDev wants to merge 1 commit into
TuVanDev wants to merge 1 commit into
Conversation
Core item renderers pass a hardcoded 'shift' => 5 on their line block, which makes the block-level page-break check in AbstractPdf::drawLineBlocks() fire only in the last 20pt of a page. An item block that does not fit in the space left on the current page is started anyway and then broken mid-block by correctText(), so one item row is split across the page boundary: name and option lines on one page, SKU, qty, price, tax and subtotal on the next, where the numeric columns line up against the wrong name fragment. Remove the hardcoded shift from the five renderers that still carry it so drawLineBlocks() computes the real block height, and guard the check with fitsOnEmptyPage() so it cannot fire for a block that would not fit on an empty page either, which would otherwise leave a page holding nothing but its item-table header row. createPage() records the Y coordinate a new page actually starts drawing at, so that guard compares against the real top of the current page. Extends ACP2E-4630, which removed the hardcoded shift from the credit memo renderer only.
Member
|
In the future, can you keep notes shorter/more concise? Appreciate all the context and test data, but it's a lot to parse. Thanks |
Member
|
Thanks for the thorough PR. One issue: Mage-OS
The rest of |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description (*)
AbstractPdf::drawLineBlocks()has a block-level page-break check that is meant to move a whole item block to the next page when it no longer fits in the space left on the current one:shiftis documented on the method as "full line height (optional)", and when it is absentdrawLineBlocks()computes it from the block's own lines. Every core item renderer, however, passes a hardcoded'shift' => 5:Magento\Sales\Model\Order\Pdf\Items\Invoice\DefaultInvoiceMagento\Sales\Model\Order\Pdf\Items\Shipment\DefaultShipmentMagento\Bundle\Model\Sales\Order\Pdf\Items\InvoiceMagento\Bundle\Model\Sales\Order\Pdf\Items\ShipmentMagento\Bundle\Model\Sales\Order\Pdf\Items\CreditmemoWith
shift = 5the check is effectively dead: it only fires in the last 20pt of a page. An item block that does not fit in the remaining space is therefore started anyway and broken mid-block bycorrectText(), so a single item row is split across the page boundary. The product-name and option lines end up on one page while that same item's SKU, qty, price, tax and subtotal columns are drawn on the next one, where the numeric columns then line up against the wrong name fragment.Magento\Sales\Model\Order\Pdf\Items\Creditmemo\DefaultCreditmemono longer has the hardcoded value: it was removed as part of ACP2E-4630 (commit 545981c), which fixed the related overlapping-text defect by returning the new page out ofcorrectText()/correctLines(). This pull request finishes that work for the remaining five renderers.Removing
'shift' => 5on its own is not enough, because a block taller than one usable page then makes the check true even at the top of a fresh page:newPage()starts drawing at y=800 and the guard is< 15, so a block over ~785pt (less the table-header row) can never satisfy it. The break fires, the page that was just created is left with nothing but its table header, and the block overflows across the boundary anyway. Two additions prevent that:fitsOnEmptyPage()suppresses the break when the block cannot fit on an empty page. Breaking gains nothing there, it only leaves the rest of the current page empty.createPage()records the Y coordinate each new page starts drawing at, sofitsOnEmptyPage()compares against the real top of the current page (a table header lowers it) rather than an assumed one. For a page created elsewhere, such as the first page of each document built ingetPdf(), only the un-lowered coordinatenewPage()sets is known, and that is used instead.Together these make an empty page structurally impossible: on a page that has nothing drawn on it yet
$this->yequals that page's recorded top, so$this->y - $shift < 15and$shift <= $pageTop - 15are exact negations of each other and the break cannot fire.That also removes a latent empty-page case in the credit memo renderer, which has had the computed
shiftsince ACP2E-4630 but no such guard: an item whose option block is taller than a page currently gets pushed to a fresh page, leaving the credit memo's first page empty below its table header, and still overflows. Measured here at 3 pages before this change and 2 after.The two literals the check depends on (
800fromnewPage()and15) are now named constants, since the new guard has to agree with both.Related Pull Requests
This is the same change I opened upstream as magento/magento2#41061 (base
2.4-develop), where it is still open and unreviewed.To be straightforward about that: Mage-OS syncs
magento/magento2daily through.github/workflows/merge-upstream-changes.yml, so if Adobe merges #41061 this fix will arrive here on its own and this pull request would be redundant. It is offered now only because that outcome is not in my hands and Adobe's queue is long. Nothing here is novel relative to #41061 - the diff is the same change re-applied to Mage-OSmainfile content and independently re-verified against it. Maintainers should feel free to close this in favour of the upstream sync if that is the preference; taking it now only buys the fix earlier and, if Adobe later merges #41061, the sync of that commit should be a no-op or a trivial conflict since the content is identical.It also extends ACP2E-4630 (commit 545981c), already present on
main, which fixed the overlapping-text half of this code path and removed the hardcodedshiftfrom the credit memo renderer only.Fixed Issues (if relevant)
None. I searched
mage-os/mageos-magento2issues and pull requests (open and closed) for a matching report and found none. Found while investigating multi-page invoice PDFs.Manual testing scenarios (*)
Verification
Verified against Mage-OS
mainfile content, not just the upstream branch. The seven target files onmainare byte-identical to the2.4-developrevision the change was originally written against, so the patch applies with no offset, but the render verification was re-run from scratch with the Mage-OS copies of the classes overlaid over a real store, over real invoices, packing slips, credit memos, order prints and a 5-invoice mass action, plus synthetic option blocks sized just under and just over one page.Raw PDF bytes are not reproducible run to run, so comparison is on extracted per-page draw runs (page, y, x, string) rather than file hashes.
Also checked across every case above: zero text runs drawn on top of a page's document-header block, before or after.
The remaining split on a block taller than a page is unavoidable: it cannot fit anywhere, so it is drawn from where it stands instead of costing an extra page. Single-page documents draw exactly the same text at exactly the same coordinates as before, verified as an identical draw-run dump. Page counts rise on documents whose items were previously being cut in half, which is what the check is for: the extra pages buy back the item rows that were being broken.
Order print rows are a project subclass of
Magento\Sales\Model\Order\Pdf\Invoicethat routes through the same coreDefaultInvoicerenderer anddrawLineBlocks(); they are an extra data point on the same code path, not a separate core document type.Not render-verified: the three
Magento\Bundle\Model\Sales\Order\Pdf\Items\*edits. The catalog available for verification has no bundle products, so those renderers were never instantiated in any of the runs above. Each of the three is the identical single-line removal of'shift' => 5from the$drawItems[]/$draw[]array that was verified on the non-bundle renderers, and they call the samedrawLineBlocks(). Worth a look from anyone with a bundle catalog to hand.Questions or comments
fitsOnEmptyPage()andcreatePage()areprivate, which keeps them out of the public surface but also means a third-party subclass ofAbstractPdfthat creates pages by callingnewPage()directly gets thePAGE_TOP_Yfallback rather than a recorded coordinate. That is the conservative direction (it can only under-estimate how much room a page has, never over-estimate) but if aprotectedseam is preferred here I am happy to change it.Unit coverage:
Magento\Sales\Test\Unit\Model\Order\Pdf\AbstractTest::testDrawLineBlocksBreaksPageOnlyForABlockThatFitscovers both halves of the new guard. Against unmodifiedmaincontent it fails withZend_Pdf::newPage('595:842:', null) was not expected to be called.on the "block taller than an empty page stays where it is" data set, and passes with this change. The three pre-existing tests in that file still pass.phpcs --standard=Magento2 --warning-severity=1reports 0 errors and 0 warnings on all seven changed files.CI status
Coding Standard, Unit Tests and Integration Tests are all sitting at
action_requiredon this pull request: as a fork PR they need a maintainer to approve the workflow runs. Happy to push again if anything comes back red once they do.The Sansec eComscan check reports
failure, but that is not this change. It is apull_request_targetworkflow whoseactions/checkoutstep refuses to check out fork code (Refusing to check out fork pull request code from a 'pull_request_target' workflow), so it fails on every fork PR - including recently merged ones such as #297.Contribution checklist (*)
mainfile content:phpcs --standard=Magento2 --warning-severity=1is 0/0 on all seven files, and the fullMagento\Sales\Test\Unit\Model\Order\Pdf\AbstractTestsuite passes (5 tests, 24 assertions) on PHPUnit 9.6.