PrintedSheetFrame.cpp (17783B)
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 "mozilla/PresShell.h" 12 #include "mozilla/StaticPrefs_print.h" 13 #include "nsCSSFrameConstructor.h" 14 #include "nsPageContentFrame.h" 15 #include "nsPageFrame.h" 16 #include "nsPageSequenceFrame.h" 17 18 using namespace mozilla; 19 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) 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. 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 65 void PrintedSheetFrame::ClaimPageFrameFromPrevInFlow() { 66 MoveOverflowToChildList(); 67 if (!GetPrevContinuation()) { 68 // The first page content frame of each document will not yet have its page 69 // style set yet. This is because normally page style is set either from 70 // the previous page content frame, or using the new page name when named 71 // pages cause a page break in block reflow. Ensure that, for the first 72 // page, it is set here so that all nsPageContentFrames have their page 73 // style set before reflow. 74 auto* firstChild = PrincipalChildList().FirstChild(); 75 MOZ_ASSERT(firstChild && firstChild->IsPageFrame(), 76 "PrintedSheetFrame only has nsPageFrame children"); 77 auto* pageFrame = static_cast<nsPageFrame*>(firstChild); 78 pageFrame->PageContentFrame()->EnsurePageName(); 79 } 80 } 81 82 void PrintedSheetFrame::Reflow(nsPresContext* aPresContext, 83 ReflowOutput& aReflowOutput, 84 const ReflowInput& aReflowInput, 85 nsReflowStatus& aStatus) { 86 MarkInReflow(); 87 DO_GLOBAL_REFLOW_COUNT("PrintedSheetFrame"); 88 MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); 89 90 // If we have a prev-in-flow, take its overflowing content: 91 MoveOverflowToChildList(); 92 93 const WritingMode wm = aReflowInput.GetWritingMode(); 94 95 // See the comments for GetSizeForChildren. 96 // Note that nsPageFrame::ComputeSinglePPSPageSizeScale depends on this value 97 // and is currently called while reflowing a single nsPageFrame child (i.e. 98 // before we've finished reflowing ourself). Ideally our children wouldn't be 99 // accessing our dimensions until after we've finished reflowing ourself - 100 // see bug 1835782. 101 mSizeForChildren = 102 nsSize(aReflowInput.AvailableISize(), aReflowInput.AvailableBSize()); 103 if (mPD->PagesPerSheetInfo()->mNumPages == 1) { 104 auto* firstChild = PrincipalChildList().FirstChild(); 105 MOZ_ASSERT(firstChild && firstChild->IsPageFrame(), 106 "PrintedSheetFrame only has nsPageFrame children"); 107 if (static_cast<nsPageFrame*>(firstChild) 108 ->GetPageOrientationRotation(mPD) != 0.0) { 109 std::swap(mSizeForChildren.width, mSizeForChildren.height); 110 } 111 } 112 113 // Count the number of pages that are displayed on this sheet (i.e. how many 114 // child frames we end up laying out, excluding any pages that are skipped 115 // due to not being in the user's page-range selection). 116 uint32_t numPagesOnThisSheet = 0; 117 118 // Target for numPagesOnThisSheet. 119 const uint32_t desiredPagesPerSheet = mPD->PagesPerSheetInfo()->mNumPages; 120 121 if (desiredPagesPerSheet > 1) { 122 ComputePagesPerSheetGridMetrics(mSizeForChildren); 123 } 124 125 // NOTE: I'm intentionally *not* using a range-based 'for' loop here, since 126 // we potentially mutate the frame list (appending to the end) during the 127 // list, which is not generally safe with range-based 'for' loops. 128 for (auto* childFrame = mFrames.FirstChild(); childFrame; 129 childFrame = childFrame->GetNextSibling()) { 130 MOZ_ASSERT(childFrame->IsPageFrame(), 131 "we're only expecting page frames as children"); 132 auto* pageFrame = static_cast<nsPageFrame*>(childFrame); 133 134 // Be sure our child has a pointer to the nsSharedPageData and knows its 135 // page number: 136 pageFrame->SetSharedPageData(mPD); 137 pageFrame->DeterminePageNum(); 138 139 if (!TagIfSkippedByCustomRange(pageFrame, pageFrame->GetPageNum(), mPD)) { 140 // The page is going to be displayed on this sheet. Tell it its index 141 // among the displayed pages, so we can use that to compute its "cell" 142 // when painting. 143 pageFrame->SetIndexOnSheet(numPagesOnThisSheet); 144 numPagesOnThisSheet++; 145 } 146 147 // This is the app-unit size of the page (in physical & logical units). 148 // Note: The page sizes come from CSS or else from the user selected size; 149 // pages are never reflowed to fit their sheet - if/when necessary they are 150 // scaled to fit their sheet. Hence why we get the page's own dimensions to 151 // use as its "available space"/"container size" here. 152 const nsSize physPageSize = pageFrame->ComputePageSize(); 153 const LogicalSize pageSize(wm, physPageSize); 154 155 ReflowInput pageReflowInput(aPresContext, aReflowInput, pageFrame, 156 pageSize); 157 158 // For layout purposes, we position *all* our nsPageFrame children at our 159 // origin. Then, if we have multiple pages-per-sheet, we'll shrink & shift 160 // each one into the right position as a paint-time effect, in 161 // BuildDisplayList. 162 LogicalPoint pagePos(wm); 163 164 // Outparams for reflow: 165 ReflowOutput pageReflowOutput(pageReflowInput); 166 nsReflowStatus status; 167 168 ReflowChild(pageFrame, aPresContext, pageReflowOutput, pageReflowInput, wm, 169 pagePos, physPageSize, ReflowChildFlags::Default, status); 170 171 FinishReflowChild(pageFrame, aPresContext, pageReflowOutput, 172 &pageReflowInput, wm, pagePos, physPageSize, 173 ReflowChildFlags::Default); 174 175 // Since we don't support incremental reflow in printed documents (see the 176 // early-return in nsPageSequenceFrame::Reflow), we can assume that this 177 // was the first time that pageFrame has been reflowed, and so there's no 178 // way that it could already have a next-in-flow. If it *did* have a 179 // next-in-flow, we would need to handle it in the 'status' logic below. 180 NS_ASSERTION(!pageFrame->GetNextInFlow(), "bad child flow list"); 181 182 // Did this page complete the document, or do we need to generate 183 // another page frame? 184 if (status.IsFullyComplete()) { 185 // The page we just reflowed is the final page! Record its page number 186 // as the number of pages: 187 mPD->mRawNumPages = pageFrame->GetPageNum(); 188 } else { 189 // Create a continuation for our page frame. We add the continuation to 190 // our child list, and then potentially push it to our overflow list, if 191 // it really belongs on the next sheet. 192 nsIFrame* continuingPage = 193 PresShell()->FrameConstructor()->CreateContinuingFrame(pageFrame, 194 this); 195 mFrames.InsertFrame(nullptr, pageFrame, continuingPage); 196 const bool isContinuingPageSkipped = 197 TagIfSkippedByCustomRange(static_cast<nsPageFrame*>(continuingPage), 198 pageFrame->GetPageNum() + 1, mPD); 199 200 // If we've already reached the target number of pages for this sheet, 201 // and this continuation page that we just created is meant to be 202 // displayed (i.e. it's in the chosen page range), then we need to push it 203 // to our overflow list so that it'll go onto a subsequent sheet. 204 // Otherwise we leave it on this sheet. This ensures we *only* generate 205 // another sheet IFF there's a displayable page that will end up on it. 206 if (numPagesOnThisSheet >= desiredPagesPerSheet && 207 !isContinuingPageSkipped) { 208 PushChildrenToOverflow(continuingPage, pageFrame); 209 aStatus.SetIncomplete(); 210 } 211 } 212 } 213 214 // This should hold for the first sheet, because our UI should prevent the 215 // user from creating a 0-length page range; and it should hold for 216 // subsequent sheets because we should only create an additional sheet when 217 // we discover a displayable (i.e. non-skipped) page that we need to push 218 // to that new sheet. 219 220 // XXXdholbert In certain edge cases (e.g. after a page-orientation-flip that 221 // reduces the page count), it's possible for us to be given a page range 222 // that is *entirely out-of-bounds* (with "from" & "to" both being larger 223 // than our actual page-number count). This scenario produces a single 224 // PrintedSheetFrame with zero displayable pages on it, which is a weird 225 // state to be in. This is hopefully a scenario that the frontend code can 226 // detect and recover from (e.g. by clamping the range to our reported 227 // `rawNumPages`), but it can't do that until *after* we've completed this 228 // problematic reflow and can reported an up-to-date `rawNumPages` to the 229 // frontend. So: to give the frontend a chance to intervene and apply some 230 // correction/clamping to its print-range parameters, we soften this 231 // assertion *specifically for the first printed sheet*. 232 if (!GetPrevContinuation()) { 233 NS_WARNING_ASSERTION(numPagesOnThisSheet > 0, 234 "Shouldn't create a sheet with no displayable pages " 235 "on it"); 236 } else { 237 MOZ_ASSERT(numPagesOnThisSheet > 0, 238 "Shouldn't create a sheet with no displayable pages on it"); 239 } 240 241 MOZ_ASSERT(numPagesOnThisSheet <= desiredPagesPerSheet, 242 "Shouldn't have more than desired number of displayable pages " 243 "on this sheet"); 244 mNumPages = numPagesOnThisSheet; 245 246 // Populate our ReflowOutput outparam -- just use up all the 247 // available space, for both our desired size & overflow areas. 248 aReflowOutput.ISize(wm) = aReflowInput.AvailableISize(); 249 if (aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE) { 250 aReflowOutput.BSize(wm) = aReflowInput.AvailableBSize(); 251 } 252 aReflowOutput.SetOverflowAreasToDesiredBounds(); 253 254 FinishAndStoreOverflow(&aReflowOutput); 255 } 256 257 nsSize PrintedSheetFrame::ComputeSheetSize(const nsPresContext* aPresContext) { 258 // We use the user selected page (sheet) dimensions, and default to the 259 // orientation as specified by the user. 260 nsSize sheetSize = aPresContext->GetPageSize(); 261 262 // Don't waste cycles changing the orientation of a square. 263 if (sheetSize.width == sheetSize.height) { 264 return sheetSize; 265 } 266 267 if (!StaticPrefs:: 268 print_save_as_pdf_use_page_rule_size_as_paper_size_enabled()) { 269 if (mPD->mPrintSettings->HasOrthogonalPagesPerSheet()) { 270 std::swap(sheetSize.width, sheetSize.height); 271 } 272 return sheetSize; 273 } 274 275 auto* firstChild = PrincipalChildList().FirstChild(); 276 MOZ_ASSERT(firstChild->IsPageFrame(), 277 "PrintedSheetFrame only has nsPageFrame children"); 278 auto* sheetsFirstPageFrame = static_cast<nsPageFrame*>(firstChild); 279 280 nsSize pageSize = sheetsFirstPageFrame->ComputePageSize(); 281 282 // Don't waste cycles changing the orientation of a square. 283 if (pageSize.width == pageSize.height) { 284 return sheetSize; 285 } 286 287 const bool pageIsRotated = 288 sheetsFirstPageFrame->GetPageOrientationRotation(mPD) != 0.0; 289 290 if (pageIsRotated && pageSize.width == pageSize.height) { 291 // Straighforward rotation without needing sheet orientation optimization. 292 std::swap(sheetSize.width, sheetSize.height); 293 return sheetSize; 294 } 295 296 // Try to orient the sheet optimally based on the physical orientation of the 297 // first/sole page on the sheet. (In the multiple pages-per-sheet case, the 298 // first page is the only one that exists at this point in the code, so it is 299 // the only one we can reason about. Any other pages may, or may not, have 300 // the same physical orientation.) 301 302 if (pageIsRotated) { 303 // Fix up for its physical orientation: 304 std::swap(pageSize.width, pageSize.height); 305 } 306 307 const bool pageIsPortrait = pageSize.width < pageSize.height; 308 const bool sheetIsPortrait = sheetSize.width < sheetSize.height; 309 310 // Switch the sheet orientation if the page orientation is different, or 311 // if we need to switch it because the number of pages-per-sheet demands 312 // orthogonal sheet layout, but not if both are true since then we'd 313 // actually need to double switch. 314 if ((sheetIsPortrait != pageIsPortrait) != 315 mPD->mPrintSettings->HasOrthogonalPagesPerSheet()) { 316 std::swap(sheetSize.width, sheetSize.height); 317 } 318 319 return sheetSize; 320 } 321 322 void PrintedSheetFrame::ComputePagesPerSheetGridMetrics( 323 const nsSize& aSheetSize) { 324 MOZ_ASSERT(mPD->PagesPerSheetInfo()->mNumPages > 1, 325 "Unnecessary to call this in a regular 1-page-per-sheet scenario; " 326 "the computed values won't ever be used in that case"); 327 328 // Compute the space available for the pages-per-sheet "page grid" (just 329 // subtract the sheet's unwriteable margin area): 330 nsSize availSpaceOnSheet = aSheetSize; 331 nsMargin uwm = mPD->mPrintSettings->GetIgnoreUnwriteableMargins() 332 ? nsMargin{} 333 : nsPresContext::CSSTwipsToAppUnits( 334 mPD->mPrintSettings->GetUnwriteableMarginInTwips()); 335 336 // XXXjwatt Once we support heterogeneous sheet orientations, we'll also need 337 // to rotate uwm if this sheet is not the primary orientation. 338 if (mPD->mPrintSettings->HasOrthogonalPagesPerSheet()) { 339 // aSheetSize already takes account of the switch of *sheet* orientation 340 // that we do in this case (the orientation implied by the page size 341 // dimensions in the nsIPrintSettings applies to *pages*). That is not the 342 // case for the unwriteable margins since we got them from the 343 // nsIPrintSettings object ourself, so we need to adjust `uwm` here. 344 // 345 // Note: In practice, sheets with an orientation that is orthogonal to the 346 // physical orientation of sheets output by a printer must be rotated 90 347 // degrees for/by the printer. In that case the convention seems to be that 348 // the "left" edge of the orthogonally oriented sheet becomes the "top", 349 // and so forth. The rotation direction will matter in the case that the 350 // top and bottom unwriteable margins are different, or the left and right 351 // unwriteable margins are different. So we need to match this behavior, 352 // which means we must rotate the `uwm` 90 degrees *counter-clockwise*. 353 nsMargin rotated(uwm.right, uwm.bottom, uwm.left, uwm.top); 354 uwm = rotated; 355 } 356 357 availSpaceOnSheet.width -= uwm.LeftRight(); 358 availSpaceOnSheet.height -= uwm.TopBottom(); 359 360 if (MOZ_UNLIKELY(availSpaceOnSheet.IsEmpty())) { 361 // This sort of thing should be rare, but it can happen if there are 362 // bizarre page sizes, and/or if there's an unexpectedly large unwriteable 363 // margin area. 364 NS_WARNING("Zero area for pages-per-sheet grid, or zero-sized grid"); 365 mGridOrigin = nsPoint(0, 0); 366 mGridNumCols = 1; 367 return; 368 } 369 370 // If there are a different number of rows vs. cols, we'll aim to put 371 // the larger number of items in the longer axis. 372 const auto* ppsInfo = mPD->PagesPerSheetInfo(); 373 uint32_t smallerNumTracks = ppsInfo->mNumPages / ppsInfo->mLargerNumTracks; 374 bool sheetIsPortraitLike = aSheetSize.width < aSheetSize.height; 375 auto numCols = 376 sheetIsPortraitLike ? smallerNumTracks : ppsInfo->mLargerNumTracks; 377 auto numRows = 378 sheetIsPortraitLike ? ppsInfo->mLargerNumTracks : smallerNumTracks; 379 380 mGridOrigin = nsPoint(uwm.left, uwm.top); 381 mGridNumCols = numCols; 382 mGridCellWidth = availSpaceOnSheet.width / nscoord(numCols); 383 mGridCellHeight = availSpaceOnSheet.height / nscoord(numRows); 384 } 385 386 gfx::IntSize PrintedSheetFrame::GetPrintTargetSizeInPoints( 387 const int32_t aAppUnitsPerPhysicalInch) const { 388 const auto size = GetSize(); 389 MOZ_ASSERT(size.width > 0 && size.height > 0); 390 const float pointsPerAppUnit = 391 POINTS_PER_INCH_FLOAT / float(aAppUnitsPerPhysicalInch); 392 return IntSize::Ceil(float(size.width) * pointsPerAppUnit, 393 float(size.height) * pointsPerAppUnit); 394 } 395 396 #ifdef DEBUG_FRAME_DUMP 397 nsresult PrintedSheetFrame::GetFrameName(nsAString& aResult) const { 398 return MakeFrameName(u"PrintedSheet"_ns, aResult); 399 } 400 #endif 401 402 } // namespace mozilla