1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
6 
7 /* Rendering object for a printed or print-previewed sheet of paper */
8 
9 #include "mozilla/PrintedSheetFrame.h"
10 
11 #include <tuple>
12 
13 #include "mozilla/StaticPrefs_print.h"
14 #include "nsCSSFrameConstructor.h"
15 #include "nsPageFrame.h"
16 #include "nsPageSequenceFrame.h"
17 
18 using namespace mozilla;
19 
NS_NewPrintedSheetFrame(PresShell * aPresShell,ComputedStyle * aStyle)20 PrintedSheetFrame* NS_NewPrintedSheetFrame(PresShell* aPresShell,
21                                            ComputedStyle* aStyle) {
22   return new (aPresShell)
23       PrintedSheetFrame(aStyle, aPresShell->GetPresContext());
24 }
25 
26 namespace mozilla {
27 
28 NS_QUERYFRAME_HEAD(PrintedSheetFrame)
NS_QUERYFRAME_ENTRY(PrintedSheetFrame)29   NS_QUERYFRAME_ENTRY(PrintedSheetFrame)
30 NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
31 
32 NS_IMPL_FRAMEARENA_HELPERS(PrintedSheetFrame)
33 
34 void PrintedSheetFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
35                                          const nsDisplayListSet& aLists) {
36   if (PresContext()->IsScreen()) {
37     // Draw the background/shadow/etc. of a blank sheet of paper, for
38     // print-preview.
39     DisplayBorderBackgroundOutline(aBuilder, aLists);
40   }
41 
42   for (auto* frame : mFrames) {
43     if (!frame->HasAnyStateBits(NS_PAGE_SKIPPED_BY_CUSTOM_RANGE)) {
44       BuildDisplayListForChild(aBuilder, frame, aLists);
45     }
46   }
47 }
48 
49 // If the given page is included in the user's page range, this function
50 // returns false. Otherwise, it tags the page with the
51 // NS_PAGE_SKIPPED_BY_CUSTOM_RANGE state bit and returns true.
TagIfSkippedByCustomRange(nsPageFrame * aPageFrame,int32_t aPageNum,nsSharedPageData * aPD)52 static bool TagIfSkippedByCustomRange(nsPageFrame* aPageFrame, int32_t aPageNum,
53                                       nsSharedPageData* aPD) {
54   if (!nsIPrintSettings::IsPageSkipped(aPageNum, aPD->mPageRanges)) {
55     MOZ_ASSERT(!aPageFrame->HasAnyStateBits(NS_PAGE_SKIPPED_BY_CUSTOM_RANGE),
56                "page frames NS_PAGE_SKIPPED_BY_CUSTOM_RANGE state should "
57                "only be set if we actually want to skip the page");
58     return false;
59   }
60 
61   aPageFrame->AddStateBits(NS_PAGE_SKIPPED_BY_CUSTOM_RANGE);
62   return true;
63 }
64 
Reflow(nsPresContext * aPresContext,ReflowOutput & aReflowOutput,const ReflowInput & aReflowInput,nsReflowStatus & aStatus)65 void PrintedSheetFrame::Reflow(nsPresContext* aPresContext,
66                                ReflowOutput& aReflowOutput,
67                                const ReflowInput& aReflowInput,
68                                nsReflowStatus& aStatus) {
69   MarkInReflow();
70   DO_GLOBAL_REFLOW_COUNT("PrintedSheetFrame");
71   DISPLAY_REFLOW(aPresContext, this, aReflowInput, aReflowOutput, aStatus);
72   MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
73 
74   // If we have a prev-in-flow, take its overflowing content:
75   MoveOverflowToChildList();
76 
77   const WritingMode wm = aReflowInput.GetWritingMode();
78 
79   // This is the app-unit size of each page (in physical & logical units):
80   const nsSize physPageSize = aPresContext->GetPageSize();
81   const LogicalSize pageSize(wm, physPageSize);
82 
83   // Count the number of pages that are displayed on this sheet (i.e. how many
84   // child frames we end up laying out, excluding any pages that are skipped
85   // due to not being in the user's page-range selection).
86   uint32_t numPagesOnThisSheet = 0;
87 
88   // Target for numPagesOnThisSheet.
89   const uint32_t desiredPagesPerSheet = mPD->PagesPerSheetInfo()->mNumPages;
90 
91   // If we're the first continuation and we're doing >1 pages per sheet,
92   // precompute some metrics that we'll use when painting the pages:
93   if (desiredPagesPerSheet > 1 && !GetPrevContinuation()) {
94     ComputePagesPerSheetOriginAndScale();
95   }
96 
97   // NOTE: I'm intentionally *not* using a range-based 'for' loop here, since
98   // we potentially mutate the frame list (appending to the end) during the
99   // list, which is not generally safe with range-based 'for' loops.
100   for (auto* childFrame = mFrames.FirstChild(); childFrame;
101        childFrame = childFrame->GetNextSibling()) {
102     MOZ_ASSERT(childFrame->IsPageFrame(),
103                "we're only expecting page frames as children");
104     auto* pageFrame = static_cast<nsPageFrame*>(childFrame);
105 
106     // Be sure our child has a pointer to the nsSharedPageData and knows its
107     // page number:
108     pageFrame->SetSharedPageData(mPD);
109     pageFrame->DeterminePageNum();
110 
111     if (!TagIfSkippedByCustomRange(pageFrame, pageFrame->GetPageNum(), mPD)) {
112       // The page is going to be displayed on this sheet. Tell it its index
113       // among the displayed pages, so we can use that to compute its "cell"
114       // when painting.
115       pageFrame->SetIndexOnSheet(numPagesOnThisSheet);
116       numPagesOnThisSheet++;
117     }
118 
119     ReflowInput pageReflowInput(aPresContext, aReflowInput, pageFrame,
120                                 pageSize);
121 
122     // For layout purposes, we position *all* our nsPageFrame children at our
123     // origin. Then, if we have multiple pages-per-sheet, we'll shrink & shift
124     // each one into the right position as a paint-time effect, in
125     // BuildDisplayList.
126     LogicalPoint pagePos(wm);
127 
128     // Outparams for reflow:
129     ReflowOutput pageReflowOutput(pageReflowInput);
130     nsReflowStatus status;
131 
132     ReflowChild(pageFrame, aPresContext, pageReflowOutput, pageReflowInput, wm,
133                 pagePos, physPageSize, ReflowChildFlags::Default, status);
134 
135     FinishReflowChild(pageFrame, aPresContext, pageReflowOutput,
136                       &pageReflowInput, wm, pagePos, physPageSize,
137                       ReflowChildFlags::Default);
138 
139     // Since we don't support incremental reflow in printed documents (see the
140     // early-return in nsPageSequenceFrame::Reflow), we can assume that this
141     // was the first time that pageFrame has been reflowed, and so there's no
142     // way that it could already have a next-in-flow. If it *did* have a
143     // next-in-flow, we would need to handle it in the 'status' logic below.
144     NS_ASSERTION(!pageFrame->GetNextInFlow(), "bad child flow list");
145 
146     // Did this page complete the document, or do we need to generate
147     // another page frame?
148     if (status.IsFullyComplete()) {
149       // The page we just reflowed is the final page! Record its page number
150       // as the number of pages:
151       mPD->mRawNumPages = pageFrame->GetPageNum();
152     } else {
153       // Create a continuation for our page frame. We add the continuation to
154       // our child list, and then potentially push it to our overflow list, if
155       // it really belongs on the next sheet.
156       nsIFrame* continuingPage =
157           PresShell()->FrameConstructor()->CreateContinuingFrame(pageFrame,
158                                                                  this);
159       mFrames.InsertFrame(nullptr, pageFrame, continuingPage);
160       const bool isContinuingPageSkipped =
161           TagIfSkippedByCustomRange(static_cast<nsPageFrame*>(continuingPage),
162                                     pageFrame->GetPageNum() + 1, mPD);
163 
164       // If we've already reached the target number of pages for this sheet,
165       // and this continuation page that we just created is meant to be
166       // displayed (i.e. it's in the chosen page range), then we need to push it
167       // to our overflow list so that it'll go onto a subsequent sheet.
168       // Otherwise we leave it on this sheet. This ensures we *only* generate
169       // another sheet IFF there's a displayable page that will end up on it.
170       if (numPagesOnThisSheet >= desiredPagesPerSheet &&
171           !isContinuingPageSkipped) {
172         PushChildrenToOverflow(continuingPage, pageFrame);
173         aStatus.SetIncomplete();
174       }
175     }
176   }
177 
178   // This should hold for the first sheet, because our UI should prevent the
179   // user from creating a 0-length page range; and it should hold for
180   // subsequent sheets because we should only create an additional sheet when
181   // we discover a displayable (i.e. non-skipped) page that we need to push
182   // to that new sheet.
183 
184   // XXXdholbert In certain edge cases (e.g. after a page-orientation-flip that
185   // reduces the page count), it's possible for us to be given a page range
186   // that is *entirely out-of-bounds* (with "from" & "to" both being larger
187   // than our actual page-number count).  This scenario produces a single
188   // PrintedSheetFrame with zero displayable pages on it, which is a weird
189   // state to be in. This is hopefully a scenario that the frontend code can
190   // detect and recover from (e.g. by clamping the range to our reported
191   // `rawNumPages`), but it can't do that until *after* we've completed this
192   // problematic reflow and can reported an up-to-date `rawNumPages` to the
193   // frontend.  So: to give the frontend a chance to intervene and apply some
194   // correction/clamping to its print-range parameters, we soften this
195   // assertion *specifically for the first printed sheet*.
196   if (!GetPrevContinuation()) {
197     NS_WARNING_ASSERTION(numPagesOnThisSheet > 0,
198                          "Shouldn't create a sheet with no displayable pages "
199                          "on it");
200   } else {
201     MOZ_ASSERT(numPagesOnThisSheet > 0,
202                "Shouldn't create a sheet with no displayable pages on it");
203   }
204 
205   MOZ_ASSERT(numPagesOnThisSheet <= desiredPagesPerSheet,
206              "Shouldn't have more than desired number of displayable pages "
207              "on this sheet");
208   mNumPages = numPagesOnThisSheet;
209 
210   // Populate our ReflowOutput outparam -- just use up all the
211   // available space, for both our desired size & overflow areas.
212   aReflowOutput.ISize(wm) = aReflowInput.AvailableISize();
213   if (aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE) {
214     aReflowOutput.BSize(wm) = aReflowInput.AvailableBSize();
215   }
216   aReflowOutput.SetOverflowAreasToDesiredBounds();
217 
218   FinishAndStoreOverflow(&aReflowOutput);
219   NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aReflowOutput);
220 }
221 
ComputePagesPerSheetOriginAndScale()222 void PrintedSheetFrame::ComputePagesPerSheetOriginAndScale() {
223   MOZ_ASSERT(mPD->PagesPerSheetInfo()->mNumPages > 1,
224              "Unnecessary to call this in a regular 1-page-per-sheet scenario; "
225              "the computed values won't ever be used in that case");
226   MOZ_ASSERT(!GetPrevContinuation(),
227              "Only needs to be called once, so 1st continuation handles it");
228 
229   // The "full-scale" size of a page (if it weren't shrunk down into a grid):
230   const nsSize pageSize = PresContext()->GetPageSize();
231 
232   // Compute the space available for the pages-per-sheet "page grid" (just
233   // subtract the sheet's unwriteable margin area):
234   nsSize availSpaceOnSheet = pageSize;
235   nsMargin uwm = nsPresContext::CSSTwipsToAppUnits(
236       mPD->mPrintSettings->GetUnwriteableMarginInTwips());
237 
238   if (mPD->mPrintSettings->HasOrthogonalSheetsAndPages()) {
239     // The pages will be rotated to be orthogonal to the physical sheet.  To
240     // account for that, we rotate the components of availSpaceOnSheet and uwm,
241     // so that we can reason about them here from the perspective of a
242     // "pageSize"-oriented *page*.
243     std::swap(availSpaceOnSheet.width, availSpaceOnSheet.height);
244 
245     // Note that the pages are rotated 90 degrees clockwise when placed onto a
246     // sheet (so that, e.g. in a scenario with two side-by-side portait pages
247     // that are rotated & placed onto a sheet, the "left" edge of the first
248     // page is at the "top" of the sheet and hence comes out of the printer
249     // first, etc).  So: given `nsMargin uwm` whose sides correspond to the
250     // physical sheet's sides, we have to rotate 90 degrees *counter-clockwise*
251     // in order to "cancel out" the page rotation and to represent it in the
252     // page's perspective. From a page's perspective, its own "top" side
253     // corresponds to the physical sheet's right side, which is why we're
254     // passing "uwm.right" as the "top" component here; and so on.
255     nsMargin rotated(uwm.right, uwm.bottom, uwm.left, uwm.top);
256     uwm = rotated;
257   }
258 
259   availSpaceOnSheet.width -= uwm.LeftRight();
260   availSpaceOnSheet.height -= uwm.TopBottom();
261   nsPoint pageGridOrigin(uwm.left, uwm.top);
262 
263   // If there are a different number of rows vs. cols, we'll aim to put
264   // the larger number of items in the longer axis.
265   const auto* ppsInfo = mPD->PagesPerSheetInfo();
266   uint32_t smallerNumTracks = ppsInfo->mNumPages / ppsInfo->mLargerNumTracks;
267   bool pageSizeIsPortraitLike = pageSize.width > pageSize.height;
268   auto numCols =
269       pageSizeIsPortraitLike ? smallerNumTracks : ppsInfo->mLargerNumTracks;
270   auto numRows =
271       pageSizeIsPortraitLike ? ppsInfo->mLargerNumTracks : smallerNumTracks;
272 
273   // Compute the full size of the "page grid" that we'll be scaling down &
274   // placing onto a given sheet:
275   nsSize pageGridFullSize(numCols * pageSize.width, numRows * pageSize.height);
276 
277   if (MOZ_UNLIKELY(availSpaceOnSheet.IsEmpty() || pageGridFullSize.IsEmpty())) {
278     // Either we have a 0-sized available area, or we have a 0-sized page-grid
279     // to draw into the available area. This sort of thing should be rare, but
280     // it can happen if there are bizarre page sizes, and/or if there's an
281     // unexpectedly large unwritable margin area. Regardless: if we get here,
282     // we shouldn't be drawing anything onto the sheet; so let's just use a
283     // scale factor of 0, and bail early to avoid division by 0 in hScale &
284     // vScale computations below.
285     NS_WARNING("Zero area for pages-per-sheet grid, or zero-sized grid");
286     mPD->mPagesPerSheetGridOrigin = pageGridOrigin;
287     mPD->mPagesPerSheetNumCols = 1;
288     mPD->mPagesPerSheetScale = 0.0f;
289     return;
290   }
291 
292   // Compute the scale factors required in each axis:
293   float hScale =
294       availSpaceOnSheet.width / static_cast<float>(pageGridFullSize.width);
295   float vScale =
296       availSpaceOnSheet.height / static_cast<float>(pageGridFullSize.height);
297 
298   // Choose the more restrictive scale factor (so that we don't overflow the
299   // sheet's printable area in either axis). And center the page-grid in the
300   // other axis (since it probably ends up with extra space).
301   float scale = std::min(hScale, vScale);
302   if (hScale < vScale) {
303     // hScale is the more restrictive scale-factor, so we'll be using that.
304     // Nudge the grid in the vertical axis to center it:
305     nscoord extraSpace = availSpaceOnSheet.height -
306                          NSToCoordFloor(scale * pageGridFullSize.height);
307     if (MOZ_LIKELY(extraSpace > 0)) {
308       pageGridOrigin.y += extraSpace / 2;
309     }
310   } else if (vScale < hScale) {
311     // vScale is the more restrictive scale-factor, so we'll be using that.
312     // Nudge the grid in the vertical axis to center it:
313     nscoord extraSpace = availSpaceOnSheet.width -
314                          NSToCoordFloor(scale * pageGridFullSize.width);
315     if (MOZ_LIKELY(extraSpace > 0)) {
316       pageGridOrigin.x += extraSpace / 2;
317     }
318   }
319   // else, we fit exactly in both axes, with the same scale factor, so there's
320   // no extra space in either axis, i.e. no need to center.
321 
322   // Update the nsSharedPageData member data:
323   mPD->mPagesPerSheetGridOrigin = pageGridOrigin;
324   mPD->mPagesPerSheetNumCols = numCols;
325   mPD->mPagesPerSheetScale = scale;
326 }
327 
328 #ifdef DEBUG_FRAME_DUMP
GetFrameName(nsAString & aResult) const329 nsresult PrintedSheetFrame::GetFrameName(nsAString& aResult) const {
330   return MakeFrameName(u"PrintedSheet"_ns, aResult);
331 }
332 #endif
333 
334 }  // namespace mozilla
335