diff options
Diffstat (limited to 'layout/generic/PrintedSheetFrame.cpp')
-rw-r--r-- | layout/generic/PrintedSheetFrame.cpp | 404 |
1 files changed, 404 insertions, 0 deletions
diff --git a/layout/generic/PrintedSheetFrame.cpp b/layout/generic/PrintedSheetFrame.cpp new file mode 100644 index 0000000000..5b8151bbd0 --- /dev/null +++ b/layout/generic/PrintedSheetFrame.cpp @@ -0,0 +1,404 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +/* Rendering object for a printed or print-previewed sheet of paper */ + +#include "mozilla/PrintedSheetFrame.h" + +#include <tuple> + +#include "mozilla/StaticPrefs_print.h" +#include "nsCSSFrameConstructor.h" +#include "nsPageContentFrame.h" +#include "nsPageFrame.h" +#include "nsPageSequenceFrame.h" + +using namespace mozilla; + +PrintedSheetFrame* NS_NewPrintedSheetFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) + PrintedSheetFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_QUERYFRAME_HEAD(PrintedSheetFrame) + NS_QUERYFRAME_ENTRY(PrintedSheetFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +NS_IMPL_FRAMEARENA_HELPERS(PrintedSheetFrame) + +void PrintedSheetFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + if (PresContext()->IsScreen()) { + // Draw the background/shadow/etc. of a blank sheet of paper, for + // print-preview. + DisplayBorderBackgroundOutline(aBuilder, aLists); + } + + for (auto* frame : mFrames) { + if (!frame->HasAnyStateBits(NS_PAGE_SKIPPED_BY_CUSTOM_RANGE)) { + BuildDisplayListForChild(aBuilder, frame, aLists); + } + } +} + +// If the given page is included in the user's page range, this function +// returns false. Otherwise, it tags the page with the +// NS_PAGE_SKIPPED_BY_CUSTOM_RANGE state bit and returns true. +static bool TagIfSkippedByCustomRange(nsPageFrame* aPageFrame, int32_t aPageNum, + nsSharedPageData* aPD) { + if (!nsIPrintSettings::IsPageSkipped(aPageNum, aPD->mPageRanges)) { + MOZ_ASSERT(!aPageFrame->HasAnyStateBits(NS_PAGE_SKIPPED_BY_CUSTOM_RANGE), + "page frames NS_PAGE_SKIPPED_BY_CUSTOM_RANGE state should " + "only be set if we actually want to skip the page"); + return false; + } + + aPageFrame->AddStateBits(NS_PAGE_SKIPPED_BY_CUSTOM_RANGE); + return true; +} + +void PrintedSheetFrame::ClaimPageFrameFromPrevInFlow() { + MoveOverflowToChildList(); + if (!GetPrevContinuation()) { + // The first page content frame of each document will not yet have its page + // style set yet. This is because normally page style is set either from + // the previous page content frame, or using the new page name when named + // pages cause a page break in block reflow. Ensure that, for the first + // page, it is set here so that all nsPageContentFrames have their page + // style set before reflow. + auto* firstChild = PrincipalChildList().FirstChild(); + MOZ_ASSERT(firstChild && firstChild->IsPageFrame(), + "PrintedSheetFrame only has nsPageFrame children"); + auto* pageFrame = static_cast<nsPageFrame*>(firstChild); + pageFrame->PageContentFrame()->EnsurePageName(); + } +} + +void PrintedSheetFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aReflowOutput, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("PrintedSheetFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aReflowOutput, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + + // If we have a prev-in-flow, take its overflowing content: + MoveOverflowToChildList(); + + const WritingMode wm = aReflowInput.GetWritingMode(); + + // See the comments for GetSizeForChildren. + // Note that nsPageFrame::ComputeSinglePPSPageSizeScale depends on this value + // and is currently called while reflowing a single nsPageFrame child (i.e. + // before we've finished reflowing ourself). Ideally our children wouldn't be + // accessing our dimensions until after we've finished reflowing ourself - + // see bug 1835782. + mSizeForChildren = + nsSize(aReflowInput.AvailableISize(), aReflowInput.AvailableBSize()); + if (mPD->PagesPerSheetInfo()->mNumPages == 1) { + auto* firstChild = PrincipalChildList().FirstChild(); + MOZ_ASSERT(firstChild && firstChild->IsPageFrame(), + "PrintedSheetFrame only has nsPageFrame children"); + if (static_cast<nsPageFrame*>(firstChild) + ->GetPageOrientationRotation(mPD) != 0.0) { + std::swap(mSizeForChildren.width, mSizeForChildren.height); + } + } + + // Count the number of pages that are displayed on this sheet (i.e. how many + // child frames we end up laying out, excluding any pages that are skipped + // due to not being in the user's page-range selection). + uint32_t numPagesOnThisSheet = 0; + + // Target for numPagesOnThisSheet. + const uint32_t desiredPagesPerSheet = mPD->PagesPerSheetInfo()->mNumPages; + + if (desiredPagesPerSheet > 1) { + ComputePagesPerSheetGridMetrics(mSizeForChildren); + } + + // NOTE: I'm intentionally *not* using a range-based 'for' loop here, since + // we potentially mutate the frame list (appending to the end) during the + // list, which is not generally safe with range-based 'for' loops. + for (auto* childFrame = mFrames.FirstChild(); childFrame; + childFrame = childFrame->GetNextSibling()) { + MOZ_ASSERT(childFrame->IsPageFrame(), + "we're only expecting page frames as children"); + auto* pageFrame = static_cast<nsPageFrame*>(childFrame); + + // Be sure our child has a pointer to the nsSharedPageData and knows its + // page number: + pageFrame->SetSharedPageData(mPD); + pageFrame->DeterminePageNum(); + + if (!TagIfSkippedByCustomRange(pageFrame, pageFrame->GetPageNum(), mPD)) { + // The page is going to be displayed on this sheet. Tell it its index + // among the displayed pages, so we can use that to compute its "cell" + // when painting. + pageFrame->SetIndexOnSheet(numPagesOnThisSheet); + numPagesOnThisSheet++; + } + + // This is the app-unit size of the page (in physical & logical units). + // Note: The page sizes come from CSS or else from the user selected size; + // pages are never reflowed to fit their sheet - if/when necessary they are + // scaled to fit their sheet. Hence why we get the page's own dimensions to + // use as its "available space"/"container size" here. + const nsSize physPageSize = pageFrame->ComputePageSize(); + const LogicalSize pageSize(wm, physPageSize); + + ReflowInput pageReflowInput(aPresContext, aReflowInput, pageFrame, + pageSize); + + // For layout purposes, we position *all* our nsPageFrame children at our + // origin. Then, if we have multiple pages-per-sheet, we'll shrink & shift + // each one into the right position as a paint-time effect, in + // BuildDisplayList. + LogicalPoint pagePos(wm); + + // Outparams for reflow: + ReflowOutput pageReflowOutput(pageReflowInput); + nsReflowStatus status; + + ReflowChild(pageFrame, aPresContext, pageReflowOutput, pageReflowInput, wm, + pagePos, physPageSize, ReflowChildFlags::Default, status); + + FinishReflowChild(pageFrame, aPresContext, pageReflowOutput, + &pageReflowInput, wm, pagePos, physPageSize, + ReflowChildFlags::Default); + + // Since we don't support incremental reflow in printed documents (see the + // early-return in nsPageSequenceFrame::Reflow), we can assume that this + // was the first time that pageFrame has been reflowed, and so there's no + // way that it could already have a next-in-flow. If it *did* have a + // next-in-flow, we would need to handle it in the 'status' logic below. + NS_ASSERTION(!pageFrame->GetNextInFlow(), "bad child flow list"); + + // Did this page complete the document, or do we need to generate + // another page frame? + if (status.IsFullyComplete()) { + // The page we just reflowed is the final page! Record its page number + // as the number of pages: + mPD->mRawNumPages = pageFrame->GetPageNum(); + } else { + // Create a continuation for our page frame. We add the continuation to + // our child list, and then potentially push it to our overflow list, if + // it really belongs on the next sheet. + nsIFrame* continuingPage = + PresShell()->FrameConstructor()->CreateContinuingFrame(pageFrame, + this); + mFrames.InsertFrame(nullptr, pageFrame, continuingPage); + const bool isContinuingPageSkipped = + TagIfSkippedByCustomRange(static_cast<nsPageFrame*>(continuingPage), + pageFrame->GetPageNum() + 1, mPD); + + // If we've already reached the target number of pages for this sheet, + // and this continuation page that we just created is meant to be + // displayed (i.e. it's in the chosen page range), then we need to push it + // to our overflow list so that it'll go onto a subsequent sheet. + // Otherwise we leave it on this sheet. This ensures we *only* generate + // another sheet IFF there's a displayable page that will end up on it. + if (numPagesOnThisSheet >= desiredPagesPerSheet && + !isContinuingPageSkipped) { + PushChildrenToOverflow(continuingPage, pageFrame); + aStatus.SetIncomplete(); + } + } + } + + // This should hold for the first sheet, because our UI should prevent the + // user from creating a 0-length page range; and it should hold for + // subsequent sheets because we should only create an additional sheet when + // we discover a displayable (i.e. non-skipped) page that we need to push + // to that new sheet. + + // XXXdholbert In certain edge cases (e.g. after a page-orientation-flip that + // reduces the page count), it's possible for us to be given a page range + // that is *entirely out-of-bounds* (with "from" & "to" both being larger + // than our actual page-number count). This scenario produces a single + // PrintedSheetFrame with zero displayable pages on it, which is a weird + // state to be in. This is hopefully a scenario that the frontend code can + // detect and recover from (e.g. by clamping the range to our reported + // `rawNumPages`), but it can't do that until *after* we've completed this + // problematic reflow and can reported an up-to-date `rawNumPages` to the + // frontend. So: to give the frontend a chance to intervene and apply some + // correction/clamping to its print-range parameters, we soften this + // assertion *specifically for the first printed sheet*. + if (!GetPrevContinuation()) { + NS_WARNING_ASSERTION(numPagesOnThisSheet > 0, + "Shouldn't create a sheet with no displayable pages " + "on it"); + } else { + MOZ_ASSERT(numPagesOnThisSheet > 0, + "Shouldn't create a sheet with no displayable pages on it"); + } + + MOZ_ASSERT(numPagesOnThisSheet <= desiredPagesPerSheet, + "Shouldn't have more than desired number of displayable pages " + "on this sheet"); + mNumPages = numPagesOnThisSheet; + + // Populate our ReflowOutput outparam -- just use up all the + // available space, for both our desired size & overflow areas. + aReflowOutput.ISize(wm) = aReflowInput.AvailableISize(); + if (aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE) { + aReflowOutput.BSize(wm) = aReflowInput.AvailableBSize(); + } + aReflowOutput.SetOverflowAreasToDesiredBounds(); + + FinishAndStoreOverflow(&aReflowOutput); +} + +nsSize PrintedSheetFrame::ComputeSheetSize(const nsPresContext* aPresContext) { + // We use the user selected page (sheet) dimensions, and default to the + // orientation as specified by the user. + nsSize sheetSize = aPresContext->GetPageSize(); + + // Don't waste cycles changing the orientation of a square. + if (sheetSize.width == sheetSize.height) { + return sheetSize; + } + + if (!StaticPrefs:: + print_save_as_pdf_use_page_rule_size_as_paper_size_enabled()) { + if (mPD->mPrintSettings->HasOrthogonalPagesPerSheet()) { + std::swap(sheetSize.width, sheetSize.height); + } + return sheetSize; + } + + auto* firstChild = PrincipalChildList().FirstChild(); + MOZ_ASSERT(firstChild->IsPageFrame(), + "PrintedSheetFrame only has nsPageFrame children"); + auto* sheetsFirstPageFrame = static_cast<nsPageFrame*>(firstChild); + + nsSize pageSize = sheetsFirstPageFrame->ComputePageSize(); + + // Don't waste cycles changing the orientation of a square. + if (pageSize.width == pageSize.height) { + return sheetSize; + } + + const bool pageIsRotated = + sheetsFirstPageFrame->GetPageOrientationRotation(mPD) != 0.0; + + if (pageIsRotated && pageSize.width == pageSize.height) { + // Straighforward rotation without needing sheet orientation optimization. + std::swap(sheetSize.width, sheetSize.height); + return sheetSize; + } + + // Try to orient the sheet optimally based on the physical orientation of the + // first/sole page on the sheet. (In the multiple pages-per-sheet case, the + // first page is the only one that exists at this point in the code, so it is + // the only one we can reason about. Any other pages may, or may not, have + // the same physical orientation.) + + if (pageIsRotated) { + // Fix up for its physical orientation: + std::swap(pageSize.width, pageSize.height); + } + + const bool pageIsPortrait = pageSize.width < pageSize.height; + const bool sheetIsPortrait = sheetSize.width < sheetSize.height; + + // Switch the sheet orientation if the page orientation is different, or + // if we need to switch it because the number of pages-per-sheet demands + // orthogonal sheet layout, but not if both are true since then we'd + // actually need to double switch. + if ((sheetIsPortrait != pageIsPortrait) != + mPD->mPrintSettings->HasOrthogonalPagesPerSheet()) { + std::swap(sheetSize.width, sheetSize.height); + } + + return sheetSize; +} + +void PrintedSheetFrame::ComputePagesPerSheetGridMetrics( + const nsSize& aSheetSize) { + MOZ_ASSERT(mPD->PagesPerSheetInfo()->mNumPages > 1, + "Unnecessary to call this in a regular 1-page-per-sheet scenario; " + "the computed values won't ever be used in that case"); + + // Compute the space available for the pages-per-sheet "page grid" (just + // subtract the sheet's unwriteable margin area): + nsSize availSpaceOnSheet = aSheetSize; + nsMargin uwm = mPD->mPrintSettings->GetIgnoreUnwriteableMargins() + ? nsMargin{} + : nsPresContext::CSSTwipsToAppUnits( + mPD->mPrintSettings->GetUnwriteableMarginInTwips()); + + // XXXjwatt Once we support heterogeneous sheet orientations, we'll also need + // to rotate uwm if this sheet is not the primary orientation. + if (mPD->mPrintSettings->HasOrthogonalPagesPerSheet()) { + // aSheetSize already takes account of the switch of *sheet* orientation + // that we do in this case (the orientation implied by the page size + // dimensions in the nsIPrintSettings applies to *pages*). That is not the + // case for the unwriteable margins since we got them from the + // nsIPrintSettings object ourself, so we need to adjust `uwm` here. + // + // Note: In practice, sheets with an orientation that is orthogonal to the + // physical orientation of sheets output by a printer must be rotated 90 + // degrees for/by the printer. In that case the convention seems to be that + // the "left" edge of the orthogonally oriented sheet becomes the "top", + // and so forth. The rotation direction will matter in the case that the + // top and bottom unwriteable margins are different, or the left and right + // unwriteable margins are different. So we need to match this behavior, + // which means we must rotate the `uwm` 90 degrees *counter-clockwise*. + nsMargin rotated(uwm.right, uwm.bottom, uwm.left, uwm.top); + uwm = rotated; + } + + availSpaceOnSheet.width -= uwm.LeftRight(); + availSpaceOnSheet.height -= uwm.TopBottom(); + + if (MOZ_UNLIKELY(availSpaceOnSheet.IsEmpty())) { + // This sort of thing should be rare, but it can happen if there are + // bizarre page sizes, and/or if there's an unexpectedly large unwriteable + // margin area. + NS_WARNING("Zero area for pages-per-sheet grid, or zero-sized grid"); + mGridOrigin = nsPoint(0, 0); + mGridNumCols = 1; + return; + } + + // If there are a different number of rows vs. cols, we'll aim to put + // the larger number of items in the longer axis. + const auto* ppsInfo = mPD->PagesPerSheetInfo(); + uint32_t smallerNumTracks = ppsInfo->mNumPages / ppsInfo->mLargerNumTracks; + bool sheetIsPortraitLike = aSheetSize.width < aSheetSize.height; + auto numCols = + sheetIsPortraitLike ? smallerNumTracks : ppsInfo->mLargerNumTracks; + auto numRows = + sheetIsPortraitLike ? ppsInfo->mLargerNumTracks : smallerNumTracks; + + mGridOrigin = nsPoint(uwm.left, uwm.top); + mGridNumCols = numCols; + mGridCellWidth = availSpaceOnSheet.width / nscoord(numCols); + mGridCellHeight = availSpaceOnSheet.height / nscoord(numRows); +} + +gfx::IntSize PrintedSheetFrame::GetPrintTargetSizeInPoints( + const int32_t aAppUnitsPerPhysicalInch) const { + const auto size = GetSize(); + MOZ_ASSERT(size.width > 0 && size.height > 0); + const float pointsPerAppUnit = + POINTS_PER_INCH_FLOAT / float(aAppUnitsPerPhysicalInch); + return IntSize::Ceil(float(size.width) * pointsPerAppUnit, + float(size.height) * pointsPerAppUnit); +} + +#ifdef DEBUG_FRAME_DUMP +nsresult PrintedSheetFrame::GetFrameName(nsAString& aResult) const { + return MakeFrameName(u"PrintedSheet"_ns, aResult); +} +#endif + +} // namespace mozilla |