Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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 http://mozilla.org/MPL/2.0/. */
#include "nsPageFrame.h"
#include "mozilla/AppUnits.h"
#include "mozilla/PresShell.h"
#include "mozilla/StaticPrefs_layout.h"
#include "mozilla/StaticPrefs_print.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/intl/Segmenter.h"
#include "gfxContext.h"
#include "nsDeviceContext.h"
#include "nsFontMetrics.h"
#include "nsIFrame.h"
#include "nsLayoutUtils.h"
#include "nsPresContext.h"
#include "nsGkAtoms.h"
#include "nsFieldSetFrame.h"
#include "nsPageContentFrame.h"
#include "nsDisplayList.h"
#include "nsPageSequenceFrame.h" // for nsSharedPageData
#include "nsTextFormatter.h" // for page number localization formatting
#include "nsBidiUtils.h"
#include "nsIPrintSettings.h"
#include "PrintedSheetFrame.h"
#include "mozilla/Logging.h"
extern mozilla::LazyLogModule gLayoutPrintingLog;
#define PR_PL(_p1) MOZ_LOG(gLayoutPrintingLog, mozilla::LogLevel::Debug, _p1)
using namespace mozilla;
using namespace mozilla::gfx;
nsPageFrame* NS_NewPageFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
return new (aPresShell) nsPageFrame(aStyle, aPresShell->GetPresContext());
}
NS_IMPL_FRAMEARENA_HELPERS(nsPageFrame)
NS_QUERYFRAME_HEAD(nsPageFrame)
NS_QUERYFRAME_ENTRY(nsPageFrame)
NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
nsPageFrame::nsPageFrame(ComputedStyle* aStyle, nsPresContext* aPresContext)
: nsContainerFrame(aStyle, aPresContext, kClassID) {}
nsPageFrame::~nsPageFrame() = default;
nsReflowStatus nsPageFrame::ReflowPageContent(
nsPresContext* aPresContext, const ReflowInput& aPageReflowInput) {
nsPageContentFrame* const frame = PageContentFrame();
// If this is the first page, it won't have its page name and computed style
// set yet. Before reflow, make sure that page name and computed style have
// been applied.
frame->EnsurePageName();
// XXX Pay attention to the page's border and padding...
//
// Reflow our ::-moz-page-content frame, allowing it only to be as big as we
// are (minus margins).
const nsSize pageSize = ComputePageSize();
// Scaling applied to the page in the single page-per-sheet case (used for
// down-scaling when the page is too large to fit on the sheet we are printing
// on). In the single page-per-sheet case, we need this here to preemptively
// increase the margins by the same amount that the scaling will reduce them
// in order to make sure that their physical size is unchanged (particularly
// important for the unwriteable margins).
const auto* ppsInfo = GetSharedPageData()->PagesPerSheetInfo();
const float pageSizeScale =
ppsInfo->mNumPages == 1 ? ComputeSinglePPSPageSizeScale(pageSize) : 1.0f;
// Scaling applied to content, as given by the print UI.
// This is an additional scale factor that is applied to the content in the
// nsPageContentFrame.
const float extraContentScale = aPresContext->GetPageScale();
// Size for the page content. This will be scaled by extraContentScale, and
// is used to calculate the computed size of the nsPageContentFrame content
// by subtracting margins.
nsSize availableSpace = pageSize;
// When the reflow size is NS_UNCONSTRAINEDSIZE it means we are reflowing
// a single page to print selection. So this means we want to use
// NS_UNCONSTRAINEDSIZE without altering it.
//
// FIXME(emilio): Is this still true?
availableSpace.width =
NSToCoordCeil(availableSpace.width / extraContentScale);
if (availableSpace.height != NS_UNCONSTRAINEDSIZE) {
availableSpace.height =
NSToCoordCeil(availableSpace.height / extraContentScale);
}
// Get the number of Twips per pixel from the PresContext
const nscoord onePixel = AppUnitsPerCSSPixel();
// insurance against infinite reflow, when reflowing less than a pixel
// XXX Shouldn't we do something more friendly when invalid margins
// are set?
if (availableSpace.width < onePixel || availableSpace.height < onePixel) {
NS_WARNING("Reflow aborted; no space for content");
return {};
}
ReflowInput kidReflowInput(
aPresContext, aPageReflowInput, frame,
LogicalSize(frame->GetWritingMode(), availableSpace));
kidReflowInput.mFlags.mIsTopOfPage = true;
kidReflowInput.mFlags.mTableIsSplittable = true;
nsMargin defaultMargins = aPresContext->GetDefaultPageMargin();
// The default margins are in the coordinate space of the physical paper.
// Scale them by the pageSizeScale to convert them to the content coordinate
// space.
for (const auto side : mozilla::AllPhysicalSides()) {
defaultMargins.Side(side) =
NSToCoordRound((float)defaultMargins.Side(side) / pageSizeScale);
}
mPageContentMargin = defaultMargins;
// Use the margins given in the @page rule if told to do so.
// We clamp to the paper's unwriteable margins to avoid clipping, *except*
// that we will respect a margin of zero if specified, assuming this means
// the document is intended to fit the paper size exactly, and the client is
// taking full responsibility for what happens around the edges.
if (mPD->mPrintSettings->GetHonorPageRuleMargins()) {
const auto& margin = kidReflowInput.mStyleMargin->mMargin;
for (const auto side : mozilla::AllPhysicalSides()) {
if (!margin.Get(side).IsAuto()) {
// Computed margins are already in the coordinate space of the content,
// do not scale.
const nscoord computed =
kidReflowInput.ComputedPhysicalMargin().Side(side);
// Respecting a zero margin is particularly important when the client
// is PDF.js where the PDF already contains the margins.
// User could also be asking to ignore unwriteable margins (Though
// currently, it is impossible through the print UI to set both
// `HonorPageRuleMargins` and `IgnoreUnwriteableMargins`).
if (computed == 0 ||
mPD->mPrintSettings->GetIgnoreUnwriteableMargins()) {
mPageContentMargin.Side(side) = computed;
} else {
// Unwriteable margins are in the coordinate space of the physical
// paper. Scale them by the pageSizeScale to convert them to the
// content coordinate space.
const int32_t unwriteableTwips =
mPD->mPrintSettings->GetUnwriteableMarginInTwips().Side(side);
const nscoord unwriteable = nsPresContext::CSSTwipsToAppUnits(
(float)unwriteableTwips / pageSizeScale);
mPageContentMargin.Side(side) = std::max(
kidReflowInput.ComputedPhysicalMargin().Side(side), unwriteable);
}
}
}
}
// TODO: This seems odd that we need to scale the margins by the extra
// scale factor, but this is needed for correct margins.
// Why are the margins already scaled? Shouldn't they be stored so that this
// scaling factor would be redundant?
nscoord computedWidth =
availableSpace.width - mPageContentMargin.LeftRight() / extraContentScale;
nscoord computedHeight;
if (availableSpace.height == NS_UNCONSTRAINEDSIZE) {
computedHeight = NS_UNCONSTRAINEDSIZE;
} else {
computedHeight = availableSpace.height -
mPageContentMargin.TopBottom() / extraContentScale;
}
// Check the width and height, if they're too small we reset the margins
// back to the default.
if (computedWidth < onePixel || computedHeight < onePixel) {
mPageContentMargin = defaultMargins;
computedWidth = availableSpace.width -
mPageContentMargin.LeftRight() / extraContentScale;
if (computedHeight != NS_UNCONSTRAINEDSIZE) {
computedHeight = availableSpace.height -
mPageContentMargin.TopBottom() / extraContentScale;
}
// And if they're still too small, we give up.
if (computedWidth < onePixel || computedHeight < onePixel) {
NS_WARNING("Reflow aborted; no space for content");
return {};
}
}
kidReflowInput.SetComputedWidth(computedWidth);
kidReflowInput.SetComputedHeight(computedHeight);
// calc location of frame
const nscoord xc = mPageContentMargin.left;
const nscoord yc = mPageContentMargin.top;
// Get the child's desired size
ReflowOutput kidOutput(kidReflowInput);
nsReflowStatus kidStatus;
ReflowChild(frame, aPresContext, kidOutput, kidReflowInput, xc, yc,
ReflowChildFlags::Default, kidStatus);
// Place and size the child
FinishReflowChild(frame, aPresContext, kidOutput, &kidReflowInput, xc, yc,
ReflowChildFlags::Default);
NS_ASSERTION(!kidStatus.IsFullyComplete() || !frame->GetNextInFlow(),
"bad child flow list");
return kidStatus;
}
void nsPageFrame::Reflow(nsPresContext* aPresContext,
ReflowOutput& aReflowOutput,
const ReflowInput& aReflowInput,
nsReflowStatus& aStatus) {
MarkInReflow();
DO_GLOBAL_REFLOW_COUNT("nsPageFrame");
MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
MOZ_ASSERT(mPD, "Need a pointer to nsSharedPageData before reflow starts");
// Our status is the same as our child's.
aStatus = ReflowPageContent(aPresContext, aReflowInput);
PR_PL(("PageFrame::Reflow %p ", this));
PR_PL(("[%d,%d][%d,%d]\n", aReflowOutput.Width(), aReflowOutput.Height(),
aReflowInput.AvailableWidth(), aReflowInput.AvailableHeight()));
// Return our desired size
WritingMode wm = aReflowInput.GetWritingMode();
aReflowOutput.ISize(wm) = aReflowInput.AvailableISize();
if (aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE) {
aReflowOutput.BSize(wm) = aReflowInput.AvailableBSize();
}
aReflowOutput.SetOverflowAreasToDesiredBounds();
FinishAndStoreOverflow(&aReflowOutput);
PR_PL(("PageFrame::Reflow %p ", this));
PR_PL(("[%d,%d]\n", aReflowInput.AvailableWidth(),
aReflowInput.AvailableHeight()));
}
#ifdef DEBUG_FRAME_DUMP
nsresult nsPageFrame::GetFrameName(nsAString& aResult) const {
return MakeFrameName(u"Page"_ns, aResult);
}
#endif
void nsPageFrame::ProcessSpecialCodes(const nsString& aStr, nsString& aNewStr) {
aNewStr = aStr;
// Search to see if the &D code is in the string
// then subst in the current date/time
constexpr auto kDate = u"&D"_ns;
if (aStr.Find(kDate) != kNotFound) {
aNewStr.ReplaceSubstring(kDate, mPD->mDateTimeStr);
}
// NOTE: Must search for &PT before searching for &P
//
// Search to see if the "page number and page" total code are in the string
// and replace the page number and page total code with the actual
// values
constexpr auto kPageAndTotal = u"&PT"_ns;
if (aStr.Find(kPageAndTotal) != kNotFound) {
nsAutoString uStr;
nsTextFormatter::ssprintf(uStr, mPD->mPageNumAndTotalsFormat.get(),
mPageNum, mPD->mRawNumPages);
aNewStr.ReplaceSubstring(kPageAndTotal, uStr);
}
// Search to see if the page number code is in the string
// and replace the page number code with the actual value
constexpr auto kPage = u"&P"_ns;
if (aStr.Find(kPage) != kNotFound) {
nsAutoString uStr;
nsTextFormatter::ssprintf(uStr, mPD->mPageNumFormat.get(), mPageNum);
aNewStr.ReplaceSubstring(kPage, uStr);
}
constexpr auto kTitle = u"&T"_ns;
if (aStr.Find(kTitle) != kNotFound) {
aNewStr.ReplaceSubstring(kTitle, mPD->mDocTitle);
}
constexpr auto kDocURL = u"&U"_ns;
if (aStr.Find(kDocURL) != kNotFound) {
aNewStr.ReplaceSubstring(kDocURL, mPD->mDocURL);
}
constexpr auto kPageTotal = u"&L"_ns;
if (aStr.Find(kPageTotal) != kNotFound) {
nsAutoString uStr;
nsTextFormatter::ssprintf(uStr, mPD->mPageNumFormat.get(),
mPD->mRawNumPages);
aNewStr.ReplaceSubstring(kPageTotal, uStr);
}
}
//------------------------------------------------------------------------------
nscoord nsPageFrame::GetXPosition(gfxContext& aRenderingContext,
nsFontMetrics& aFontMetrics,
const nsRect& aRect, int32_t aJust,
const nsString& aStr) {
nscoord width = nsLayoutUtils::AppUnitWidthOfStringBidi(
aStr, this, aFontMetrics, aRenderingContext);
nscoord x = aRect.x;
switch (aJust) {
case nsIPrintSettings::kJustLeft:
x += mPD->mEdgePaperMargin.left;
break;
case nsIPrintSettings::kJustCenter:
x += (aRect.width - width) / 2;
break;
case nsIPrintSettings::kJustRight:
x += aRect.width - width - mPD->mEdgePaperMargin.right;
break;
} // switch
return x;
}
// Draw a header or footer
// @param aRenderingContext - rendering content to draw into
// @param aHeaderFooter - indicates whether it is a header or footer
// @param aStrLeft - string for the left header or footer; can be empty
// @param aStrCenter - string for the center header or footer; can be empty
// @param aStrRight - string for the right header or footer; can be empty
// @param aRect - the rect of the page
// @param aAscent - the ascent of the font
// @param aHeight - the height of the font
void nsPageFrame::DrawHeaderFooter(
gfxContext& aRenderingContext, nsFontMetrics& aFontMetrics,
nsHeaderFooterEnum aHeaderFooter, const nsString& aStrLeft,
const nsString& aStrCenter, const nsString& aStrRight, const nsRect& aRect,
nscoord aAscent, nscoord aHeight) {
int32_t numStrs = 0;
if (!aStrLeft.IsEmpty()) numStrs++;
if (!aStrCenter.IsEmpty()) numStrs++;
if (!aStrRight.IsEmpty()) numStrs++;
if (numStrs == 0) return;
const nscoord contentWidth =
aRect.width - (mPD->mEdgePaperMargin.left + mPD->mEdgePaperMargin.right);
const nscoord strSpace = contentWidth / numStrs;
if (!aStrLeft.IsEmpty()) {
DrawHeaderFooter(aRenderingContext, aFontMetrics, aHeaderFooter,
nsIPrintSettings::kJustLeft, aStrLeft, aRect, aAscent,
aHeight, strSpace);
}
if (!aStrCenter.IsEmpty()) {
DrawHeaderFooter(aRenderingContext, aFontMetrics, aHeaderFooter,
nsIPrintSettings::kJustCenter, aStrCenter, aRect, aAscent,
aHeight, strSpace);
}
if (!aStrRight.IsEmpty()) {
DrawHeaderFooter(aRenderingContext, aFontMetrics, aHeaderFooter,
nsIPrintSettings::kJustRight, aStrRight, aRect, aAscent,
aHeight, strSpace);
}
}
// Draw a header or footer string
// @param aRenderingContext - rendering context to draw into
// @param aHeaderFooter - indicates whether it is a header or footer
// @param aJust - indicates where the string is located within the header/footer
// @param aStr - the string to be drawn
// @param aRect - the rect of the page
// @param aHeight - the height of the font
// @param aAscent - the ascent of the font
// @param aWidth - available width for the string
void nsPageFrame::DrawHeaderFooter(gfxContext& aRenderingContext,
nsFontMetrics& aFontMetrics,
nsHeaderFooterEnum aHeaderFooter,
int32_t aJust, const nsString& aStr,
const nsRect& aRect, nscoord aAscent,
nscoord aHeight, nscoord aWidth) {
DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
if ((aHeaderFooter == eHeader && aHeight < mPageContentMargin.top) ||
(aHeaderFooter == eFooter && aHeight < mPageContentMargin.bottom)) {
nsAutoString str;
ProcessSpecialCodes(aStr, str);
int32_t len = (int32_t)str.Length();
if (len == 0) {
return; // bail is empty string
}
int32_t index;
int32_t textWidth = 0;
const char16_t* text = str.get();
// find how much text fits, the "position" is the size of the available area
if (nsLayoutUtils::BinarySearchForPosition(drawTarget, aFontMetrics, text,
0, 0, 0, len, int32_t(aWidth),
index, textWidth)) {
if (index < len - 1) {
// we can't fit in all the text, try to remove 3 glyphs and append
// three "." charactrers.
// TODO: This might not actually remove three glyphs in cases where
// ZWJ sequences, regional indicators, etc are used.
// We also have guarantee that removing three glyphs will make enough
// space for the ellipse, if they are zero-width or even just narrower
// than the "." character.
mozilla::intl::GraphemeClusterBreakReverseIteratorUtf16 revIter(str);
// Start iteration at the point where the text does properly fit.
revIter.Seek(index);
// Step backwards 3 times, checking if we have any string left by the
// end.
revIter.Next();
revIter.Next();
if (const Maybe<uint32_t> maybeIndex = revIter.Next()) {
// TODO: We should consider checking for the ellipse character, or
// possibly for another continuation indicator based on
// localization.
str.Truncate(*maybeIndex);
str.AppendLiteral("...");
} else {
// We can only fit 3 or fewer chars. Just show nothing
str.Truncate();
}
}
} else {
return; // bail if couldn't find the correct length
}
if (HasRTLChars(str)) {
PresContext()->SetBidiEnabled();
}
// calc the x and y positions of the text
nscoord x =
GetXPosition(aRenderingContext, aFontMetrics, aRect, aJust, str);
nscoord y;
if (aHeaderFooter == eHeader) {
y = aRect.y + mPD->mEdgePaperMargin.top;
} else {
y = aRect.YMost() - aHeight - mPD->mEdgePaperMargin.bottom;
}
// set up new clip and draw the text
aRenderingContext.Save();
aRenderingContext.Clip(NSRectToSnappedRect(
aRect, PresContext()->AppUnitsPerDevPixel(), *drawTarget));
aRenderingContext.SetColor(sRGBColor::OpaqueBlack());
nsLayoutUtils::DrawString(this, aFontMetrics, &aRenderingContext, str.get(),
str.Length(), nsPoint(x, y + aAscent), nullptr,
DrawStringFlags::ForceHorizontal);
aRenderingContext.Restore();
}
}
class nsDisplayHeaderFooter final : public nsPaintedDisplayItem {
public:
nsDisplayHeaderFooter(nsDisplayListBuilder* aBuilder, nsPageFrame* aFrame)
: nsPaintedDisplayItem(aBuilder, aFrame) {
MOZ_COUNT_CTOR(nsDisplayHeaderFooter);
}
MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayHeaderFooter)
virtual void Paint(nsDisplayListBuilder* aBuilder,
gfxContext* aCtx) override {
#ifdef DEBUG
nsPageFrame* pageFrame = do_QueryFrame(mFrame);
MOZ_ASSERT(pageFrame, "We should have an nsPageFrame");
#endif
static_cast<nsPageFrame*>(mFrame)->PaintHeaderFooter(
*aCtx, ToReferenceFrame(), false);
}
NS_DISPLAY_DECL_NAME("HeaderFooter", TYPE_HEADER_FOOTER)
virtual nsRect GetComponentAlphaBounds(
nsDisplayListBuilder* aBuilder) const override {
bool snap;
return GetBounds(aBuilder, &snap);
}
};
static void PaintMarginGuides(nsIFrame* aFrame, DrawTarget* aDrawTarget,
const nsRect& aDirtyRect, nsPoint aPt) {
// Set up parameters needed to draw the guides: we draw them in blue,
// using 2px-long dashes with 2px separation and a line width of 0.5px.
// Drawing is antialiased, so on a non-hidpi screen where the line width is
// less than one device pixel, it doesn't disappear but renders fainter
// than a solid 1px-wide line would be.
// (In many cases, the entire preview is scaled down so that the guides
// will be nominally less than 1 dev px even on a hidpi screen, resulting
// in lighter antialiased rendering so they don't dominate the page.)
ColorPattern pattern(ToDeviceColor(sRGBColor(0.0f, 0.0f, 1.0f)));
Float dashes[] = {2.0f, 2.0f};
StrokeOptions stroke(/* line width (in CSS px) */ 0.5f,
JoinStyle::MITER_OR_BEVEL, CapStyle::BUTT,
/* mitre limit (default, not used) */ 10.0f,
/* set dash pattern of 2px stroke, 2px gap */
ArrayLength(dashes), dashes,
/* dash offset */ 0.0f);
DrawOptions options;
MOZ_RELEASE_ASSERT(aFrame->IsPageFrame());
const nsMargin& margin =
static_cast<nsPageFrame*>(aFrame)->GetUsedPageContentMargin();
int32_t appUnitsPerDevPx = aFrame->PresContext()->AppUnitsPerDevPixel();
// Get the frame's rect and inset by the margins to get the edges of the
// content area, where we want to draw the guides.
// We draw in two stages, first applying the top/bottom margins and drawing
// the horizontal guides across the full width of the page.
nsRect rect(aPt, aFrame->GetSize());
rect.Deflate(nsMargin(margin.top, 0, margin.bottom, 0));
Rect r = NSRectToRect(rect, appUnitsPerDevPx);
aDrawTarget->StrokeLine(r.TopLeft(), r.TopRight(), pattern, stroke, options);
aDrawTarget->StrokeLine(r.BottomLeft(), r.BottomRight(), pattern, stroke,
options);
// Then reset rect, apply the left/right margins, and draw vertical guides
// extending the full height of the page.
rect = nsRect(aPt, aFrame->GetSize());
rect.Deflate(nsMargin(0, margin.right, 0, margin.left));
r = NSRectToRect(rect, appUnitsPerDevPx);
aDrawTarget->StrokeLine(r.TopLeft(), r.BottomLeft(), pattern, stroke,
options);
aDrawTarget->StrokeLine(r.TopRight(), r.BottomRight(), pattern, stroke,
options);
}
static std::tuple<uint32_t, uint32_t> GetRowAndColFromIdx(uint32_t aIdxOnSheet,
uint32_t aNumCols) {
// Compute the row index by *dividing* the item's ordinal position by how
// many items fit in each row (i.e. the number of columns), and flooring.
// Compute the column index by getting the remainder of that division:
// Notably, mNumRows is irrelevant to this computation; that's because
// we're adding new items column-by-column rather than row-by-row.
return {aIdxOnSheet / aNumCols, aIdxOnSheet % aNumCols};
}
// The minimum ratio for which we will center the page on the sheet when using
// auto-detect logic.
// Note that this ratio is of the content's size to the sheet size scaled to be
// in content space, and so the actual ratio will always be from 0.0 to 1.0,
// with this marking the smallest ratio we consider a near-miss.
// The ratio of A4 on Letter is 0.915034. A threshold of 0.9 will ensure that
// A4 on Letter works, as well as other near-misses.
//
// The ratio is computed as so:
// scale = min(1, sheetHeight / pageHeight, sheetWidth / pageWidth)
//
// Where pageSize is pageWidth or pageHeight, and sheetSize is sheetWidth or
// sheetHeight, respectively:
// scaledPageSize = pageSize * scale
// ratio = scaledPageSize / sheetSize
//
// A4 (210mm x 297mm) on US Letter (215.9mm x 279.4mm) is derived as so:
// scale = min(1, 215.9 / 210, 279.4 / 297) = 0.9407407407..
//
// Using the widths:
// scaledPageSize = (210 * 0.940741) = 197.556
// ratio = 197.556 / 215.9 = 0.915034
//
// See nsPageFrame::ComputeSinglePPSPageSizeScale for scale calculation, and
// OffsetToCenterPage for ratio calculation.
constexpr float kCenterPageRatioThreshold = 0.9f;
// Numeric values for the pref "print.center_page_on_sheet"
enum {
kPrintCenterPageOnSheetNever = 0,
kPrintCenterPageOnSheetAlways = 1,
kPrintCenterPageOnSheetAuto = 2
};
// Returns an offset to center the page on the sheet, with a given scale.
// When no centering can/should happen, this will avoid extra calculations and
// return 0.0f.
// This takes into account the value of the pref "print.center_page_on_sheet".
static float OffsetToCenterPage(nscoord aContentSize, nscoord aSheetSize,
float aScale, float aAppUnitsPerPixel) {
MOZ_ASSERT(aScale <= 1.0f && aScale > 0.0f,
"Scale must be in the range (0,1]");
const unsigned centerPagePref = StaticPrefs::print_center_page_on_sheet();
if (centerPagePref == kPrintCenterPageOnSheetNever) {
return 0.0f;
}
// Determine the ratio of scaled page to the sheet size.
const float sheetSize =
NSAppUnitsToFloatPixels(aSheetSize, aAppUnitsPerPixel);
const float scaledContentSize =
NSAppUnitsToFloatPixels(aContentSize, aAppUnitsPerPixel) * aScale;
const float ratio = scaledContentSize / sheetSize;
// If the ratio is within the threshold, or the pref indicates we should
// always center the page, return half the difference to form the offset.
if (centerPagePref == kPrintCenterPageOnSheetAlways ||
ratio >= kCenterPageRatioThreshold) {
return (sheetSize - scaledContentSize) * 0.5f;
}
return 0.0f;
}
// Helper for BuildDisplayList:
static gfx::Matrix4x4 ComputePagesPerSheetAndPageSizeTransform(
const nsIFrame* aFrame, float aAppUnitsPerPixel) {
MOZ_ASSERT(aFrame->IsPageFrame());
auto* pageFrame = static_cast<const nsPageFrame*>(aFrame);
const nsSize contentPageSize = pageFrame->ComputePageSize();
MOZ_ASSERT(contentPageSize.width > 0 && contentPageSize.height > 0);
nsSharedPageData* pd = pageFrame->GetSharedPageData();
const auto* ppsInfo = pd->PagesPerSheetInfo();
const nsContainerFrame* const parentFrame = pageFrame->GetParent();
MOZ_ASSERT(parentFrame->IsPrintedSheetFrame(),
"Parent of nsPageFrame should be PrintedSheetFrame");
const auto* sheetFrame = static_cast<const PrintedSheetFrame*>(parentFrame);
const double rotation =
pageFrame->GetPageOrientationRotation(pageFrame->GetSharedPageData());
gfx::Matrix4x4 transform;
if (ppsInfo->mNumPages == 1) {
const nsSize sheetSize = sheetFrame->GetSizeForChildren();
if (rotation != 0.0) {
const bool sheetIsPortrait = sheetSize.width < sheetSize.height;
const bool rotatingClockwise = rotation > 0.0;
// rotation point:
int32_t x, y;
if (rotatingClockwise != sheetIsPortrait) {
// rotating portrait clockwise, or landscape counterclockwise
x = y = std::min(sheetSize.width, sheetSize.height) / 2;
} else {
// rotating portrait counterclockwise, or landscape clockwise
x = y = std::max(sheetSize.width, sheetSize.height) / 2;
}
transform = gfx::Matrix4x4::Translation(
NSAppUnitsToFloatPixels(x, aAppUnitsPerPixel),
NSAppUnitsToFloatPixels(y, aAppUnitsPerPixel), 0);
transform.RotateZ(rotation);
transform.PreTranslate(NSAppUnitsToFloatPixels(-x, aAppUnitsPerPixel),
NSAppUnitsToFloatPixels(-y, aAppUnitsPerPixel), 0);
}
// If the difference in horizontal size, after scaling, is relatively small
// then center the page on the sheet.
const float scale =
pageFrame->ComputeSinglePPSPageSizeScale(contentPageSize);
const float centeringOffset = OffsetToCenterPage(
contentPageSize.width, sheetSize.width, scale, aAppUnitsPerPixel);
// Only bother with the translation if it is at least one pixel.
// It's possible for a mismatch in the paper size reported by the print
// server and the paper size from Gecko to lead to small offsets, or
// even (in combination with floating point error) a very small negative
// offset. Do not apply an offset in those cases.
if (centeringOffset >= 1.0f) {
transform.PreTranslate(centeringOffset, 0, 0);
}
transform.PreScale(scale, scale, 1);
return transform;
}
// The multiple pages-per-sheet case.
// Begin with the translation of the page to its pages-per-sheet grid "cell"
// (the grid origin accounts for the sheet's unwriteable margins):
const nsPoint gridOrigin = sheetFrame->GetGridOrigin();
const nscoord cellWidth = sheetFrame->GetGridCellWidth();
const nscoord cellHeight = sheetFrame->GetGridCellHeight();
uint32_t rowIdx, colIdx;
std::tie(rowIdx, colIdx) = GetRowAndColFromIdx(pageFrame->IndexOnSheet(),
sheetFrame->GetGridNumCols());
transform = gfx::Matrix4x4::Translation(
NSAppUnitsToFloatPixels(gridOrigin.x + nscoord(colIdx) * cellWidth,
aAppUnitsPerPixel),
NSAppUnitsToFloatPixels(gridOrigin.y + nscoord(rowIdx) * cellHeight,
aAppUnitsPerPixel),
0.0f);
// Scale the page to fit, centered, in the grid cell:
float scaleX = float(cellWidth) / float(contentPageSize.width);
float scaleY = float(cellHeight) / float(contentPageSize.height);
MOZ_ASSERT(scaleX > 0.0f && scaleX <= 1.0f && scaleY > 0.0f &&
scaleY <= 1.0f);
float scale;
float dx = 0.0f, dy = 0.0f;
if (scaleX < scaleY) {
scale = scaleX;
// We need to scale down more for the width than the height, so we'll have
// some spare space in the page's vertical direction. We offset the page
// to share that space equally above and below the page to center it.
nscoord extraSpace =
cellHeight - NSToCoordRound(float(contentPageSize.height) * scale);
dy = NSAppUnitsToFloatPixels(extraSpace / 2, aAppUnitsPerPixel);
} else {
scale = scaleY;
nscoord extraSpace =
cellWidth - NSToCoordRound(float(contentPageSize.width) * scale);
dx = NSAppUnitsToFloatPixels(extraSpace / 2, aAppUnitsPerPixel);
}
transform.PreTranslate(dx, dy, 0.0f);
transform.PreScale(scale, scale, 1.0f);
// Apply 'page-orientation' rotation, if applicable:
if (rotation != 0.0) {
// We've already translated and scaled the page to fit the cell, ignoring
// rotation. Here we rotate the page around its center and, if necessary,
// also scale it to fit it to its cell for its orientation change.
float fitScale = 1.0f;
if (MOZ_LIKELY(cellWidth != cellHeight &&
contentPageSize.width != contentPageSize.height)) {
// If neither the cell nor the page are square, the scale must change.
float cellRatio = float(cellWidth) / float(cellHeight);
float pageRatio =
float(contentPageSize.width) / float(contentPageSize.height);
const bool orientationWillMatchAfterRotation =
floor(cellRatio) != floor(pageRatio);
if (cellRatio > 1.0f) {
cellRatio = 1.0f / cellRatio; // normalize
}
if (pageRatio > 1.0f) {
pageRatio = 1.0f / pageRatio; // normalize
}
fitScale = std::max(cellRatio, pageRatio);
if (orientationWillMatchAfterRotation) {
// Scale up, not down
fitScale = 1.0f / fitScale;
}
}
transform.PreTranslate(
NSAppUnitsToFloatPixels(contentPageSize.width / 2, aAppUnitsPerPixel),
NSAppUnitsToFloatPixels(contentPageSize.height / 2, aAppUnitsPerPixel),
0);
if (MOZ_LIKELY(fitScale != 1.0f)) {
transform.PreScale(fitScale, fitScale, 1.0f);
}
transform.RotateZ(rotation);
transform.PreTranslate(
NSAppUnitsToFloatPixels(-contentPageSize.width / 2, aAppUnitsPerPixel),
NSAppUnitsToFloatPixels(-contentPageSize.height / 2, aAppUnitsPerPixel),
0);
}
return transform;
}
nsIFrame::ComputeTransformFunction nsPageFrame::GetTransformGetter() const {
return ComputePagesPerSheetAndPageSizeTransform;
}
nsPageContentFrame* nsPageFrame::PageContentFrame() const {
nsIFrame* const frame = mFrames.FirstChild();
MOZ_ASSERT(frame, "pageFrame must have one child");
MOZ_ASSERT(frame->IsPageContentFrame(),
"pageFrame must have pageContentFrame as the first child");
return static_cast<nsPageContentFrame*>(frame);
}
nsSize nsPageFrame::ComputePageSize() const {
// Compute the expected page-size.
const nsPageFrame* const frame =
StaticPrefs::layout_css_allow_mixed_page_sizes()
? this
: static_cast<nsPageFrame*>(FirstContinuation());
const StylePageSize& pageSize = frame->PageContentFrame()->StylePage()->mSize;
nsSize size = PresContext()->GetPageSize();
if (pageSize.IsSize()) {
// Use the specified size,
// ignoring sizes that include a zero width or height.
// These are also ignored in ServoStyleSet::GetPageSizeForPageName()
// when getting the paper size.
nscoord cssPageWidth = pageSize.AsSize().width.ToAppUnits();
nscoord cssPageHeight = pageSize.AsSize().height.ToAppUnits();
if (cssPageWidth > 0 && cssPageHeight > 0) {
return nsSize{cssPageWidth, cssPageHeight};
}
// Invalid size; just return the default
return size;
}
if (pageSize.IsOrientation()) {
// Ensure the correct orientation is applied.
if (pageSize.AsOrientation() == StylePageSizeOrientation::Portrait) {
if (size.width > size.height) {
std::swap(size.width, size.height);
}
} else {
MOZ_ASSERT(pageSize.AsOrientation() ==
StylePageSizeOrientation::Landscape);
if (size.width < size.height) {
std::swap(size.width, size.height);
}
}
} else {
MOZ_ASSERT(pageSize.IsAuto(), "Impossible page-size value?");
}
return size;
}
float nsPageFrame::ComputeSinglePPSPageSizeScale(
const nsSize aContentPageSize) const {
MOZ_ASSERT(GetSharedPageData()->PagesPerSheetInfo()->mNumPages == 1,
"Only intended for the pps==1 case");
MOZ_ASSERT(aContentPageSize == ComputePageSize(),
"Incorrect content page size");
// Check for the simplest case first, an auto page-size which requires no
// scaling at all.
{
const nsPageFrame* const frame =
StaticPrefs::layout_css_allow_mixed_page_sizes()
? this
: static_cast<nsPageFrame*>(FirstContinuation());
const StylePageSize& pageSize =
frame->PageContentFrame()->StylePage()->mSize;
if (pageSize.IsAuto()) {
return 1.0f;
}
}
const nsContainerFrame* const parent = GetParent();
MOZ_ASSERT(parent && parent->IsPrintedSheetFrame(),
"Parent of nsPageFrame should be PrintedSheetFrame");
const auto* sheet = static_cast<const PrintedSheetFrame*>(parent);
// Compute scaling due to a possible mismatch in the paper size we are
// printing to (from the pres context) and the specified page size when the
// content uses "@page {size: ...}" to specify a page size for the content.
float scale = 1.0f;
const nsSize sheetSize = sheet->GetSizeForChildren();
nscoord contentPageHeight = aContentPageSize.height;
// Scale down if the target is too wide.
if (aContentPageSize.width > sheetSize.width) {
scale *= float(sheetSize.width) / float(aContentPageSize.width);
contentPageHeight = NSToCoordRound(contentPageHeight * scale);
}
// Scale down if the target is too tall.
if (contentPageHeight > sheetSize.height) {
scale *= float(sheetSize.height) / float(contentPageHeight);
}
MOZ_ASSERT(
scale <= 1.0f,
"Page-size mismatches should only have caused us to scale down, not up.");
return scale;
}
double nsPageFrame::GetPageOrientationRotation(nsSharedPageData* aPD) const {
if (!StaticPrefs::layout_css_page_orientation_enabled()) {
return 0.0;
}
if (aPD->PagesPerSheetInfo()->mNumPages == 1 && !PresContext()->IsScreen() &&
aPD->mPrintSettings->GetOutputFormat() !=
nsIPrintSettings::kOutputFormatPDF) {
// In the single page-per-sheet case we rotate the page by essentially
// rotating the entire sheet. But we can't do that when the output device
// doesn't support mixed sheet orientations.
return 0.0;
}
const StylePageOrientation& orientation =
PageContentFrame()->StylePage()->mPageOrientation;
if (orientation == StylePageOrientation::RotateLeft) {
return -M_PI / 2.0;
}
if (orientation == StylePageOrientation::RotateRight) {
return M_PI / 2.0;
}
return 0.0;
}
void nsPageFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
const nsDisplayListSet& aLists) {
nsDisplayList content(aBuilder);
nsDisplayListSet set(&content, &content, &content, &content, &content,
&content);
{
DisplayListClipState::AutoSaveRestore clipState(aBuilder);
clipState.Clear();
nsPresContext* const pc = PresContext();
{
// We need to extend the building rect to include the specified page size
// (scaled by the print scaling factor), in case it is larger than the
// physical page size. In that case the nsPageFrame will be the size of
// the physical page, but the child nsPageContentFrame will be the larger
// specified page size. The more correct way to do this would be to fully
// reverse the result of ComputePagesPerSheetAndPageSizeTransform to
// handle this scaling, but this should have the same result and is
// easier.
const float scale = pc->GetPageScale();
const nsSize pageSize = ComputePageSize();
const nsRect scaledPageRect{0, 0, NSToCoordCeil(pageSize.width / scale),
NSToCoordCeil(pageSize.height / scale)};
nsDisplayListBuilder::AutoBuildingDisplayList buildingForPageContentFrame(
aBuilder, this, scaledPageRect, scaledPageRect);
nsContainerFrame::BuildDisplayList(aBuilder, set);
}
if (pc->IsRootPaginatedDocument()) {
content.AppendNewToTop<nsDisplayHeaderFooter>(aBuilder, this);
// For print-preview, show margin guides if requested in the settings.
if (pc->Type() == nsPresContext::eContext_PrintPreview &&
mPD->mPrintSettings->GetShowMarginGuides()) {
content.AppendNewToTop<nsDisplayGeneric>(
aBuilder, this, PaintMarginGuides, "MarginGuides",
DisplayItemType::TYPE_MARGIN_GUIDES);
}
}
}
// We'll be drawing the page with a (usually-trivial)
// N-pages-per-sheet transform applied, so our passed-in visible rect
// isn't meaningful while we're drawing our children, because the
// transform could scale down content whose coordinates are off-screen
// such that it ends up on-screen. So: we temporarily update the visible
// rect to be the child nsPageFrame's whole frame-rect (represented in
// this PrintedSheetFrame's coordinate space.
content.AppendNewToTop<nsDisplayTransform>(
aBuilder, this, &content, content.GetBuildingRect(),
nsDisplayTransform::WithTransformGetter);
set.MoveTo(aLists);
}
//------------------------------------------------------------------------------
void nsPageFrame::DeterminePageNum() {
// If we have no previous continuation, we're page 1. Otherwise, we're
// just one more than our previous continuation's page number.
auto* prevContinuation = static_cast<nsPageFrame*>(GetPrevContinuation());
mPageNum = prevContinuation ? prevContinuation->GetPageNum() + 1 : 1;
}
void nsPageFrame::PaintHeaderFooter(gfxContext& aRenderingContext, nsPoint aPt,
bool aDisableSubpixelAA) {
nsPresContext* pc = PresContext();
nsRect rect(aPt, ComputePageSize());
aRenderingContext.SetColor(sRGBColor::OpaqueBlack());
DrawTargetAutoDisableSubpixelAntialiasing disable(
aRenderingContext.GetDrawTarget(), aDisableSubpixelAA);
// Get the FontMetrics to determine width.height of strings
nsFontMetrics::Params params;
params.userFontSet = pc->GetUserFontSet();
params.textPerf = pc->GetTextPerfMetrics();
params.featureValueLookup = pc->GetFontFeatureValuesLookup();
RefPtr<nsFontMetrics> fontMet = pc->GetMetricsFor(mPD->mHeadFootFont, params);
nscoord ascent = fontMet->MaxAscent();
nscoord visibleHeight = fontMet->MaxHeight();
// print document headers and footers
nsString headerLeft, headerCenter, headerRight;
mPD->mPrintSettings->GetHeaderStrLeft(headerLeft);
mPD->mPrintSettings->GetHeaderStrCenter(headerCenter);
mPD->mPrintSettings->GetHeaderStrRight(headerRight);
DrawHeaderFooter(aRenderingContext, *fontMet, eHeader, headerLeft,
headerCenter, headerRight, rect, ascent, visibleHeight);
nsString footerLeft, footerCenter, footerRight;
mPD->mPrintSettings->GetFooterStrLeft(footerLeft);
mPD->mPrintSettings->GetFooterStrCenter(footerCenter);
mPD->mPrintSettings->GetFooterStrRight(footerRight);
DrawHeaderFooter(aRenderingContext, *fontMet, eFooter, footerLeft,
footerCenter, footerRight, rect, ascent, visibleHeight);
}
void nsPageFrame::SetSharedPageData(nsSharedPageData* aPD) {
mPD = aPD;
// Set the shared data into the page frame before reflow
PageContentFrame()->SetSharedPageData(mPD);
}
nsIFrame* NS_NewPageBreakFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
MOZ_ASSERT(aPresShell, "null PresShell");
// check that we are only creating page break frames when printing
NS_ASSERTION(aPresShell->GetPresContext()->IsPaginated(),
"created a page break frame while not printing");
return new (aPresShell)
nsPageBreakFrame(aStyle, aPresShell->GetPresContext());
}
NS_IMPL_FRAMEARENA_HELPERS(nsPageBreakFrame)
nsPageBreakFrame::nsPageBreakFrame(ComputedStyle* aStyle,
nsPresContext* aPresContext)
: nsLeafFrame(aStyle, aPresContext, kClassID) {}
nsPageBreakFrame::~nsPageBreakFrame() = default;
nscoord nsPageBreakFrame::GetIntrinsicISize() {
return nsPresContext::CSSPixelsToAppUnits(1);
}
nscoord nsPageBreakFrame::GetIntrinsicBSize() { return 0; }
void nsPageBreakFrame::Reflow(nsPresContext* aPresContext,
ReflowOutput& aReflowOutput,
const ReflowInput& aReflowInput,
nsReflowStatus& aStatus) {
DO_GLOBAL_REFLOW_COUNT("nsPageBreakFrame");
MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
// Override reflow, since we don't want to deal with what our
// computed values are.
const WritingMode wm = aReflowInput.GetWritingMode();
nscoord bSize = aReflowInput.AvailableBSize();
if (aReflowInput.AvailableBSize() == NS_UNCONSTRAINEDSIZE) {
bSize = nscoord(0);
} else if (GetContent()->IsHTMLElement(nsGkAtoms::legend)) {
// If this is a page break frame for a _rendered legend_ then it should be
// ignored since these frames are inserted inside the fieldset's inner
// frame and thus "misplaced". nsFieldSetFrame::Reflow deals with these
// forced breaks explicitly instead.
const nsContainerFrame* parent = GetParent();
if (parent &&
parent->Style()->GetPseudoType() == PseudoStyleType::fieldsetContent) {
while ((parent = parent->GetParent())) {
if (const nsFieldSetFrame* const fieldset = do_QueryFrame(parent)) {
const auto* const legend = fieldset->GetLegend();
if (legend && legend->GetContent() == GetContent()) {
bSize = nscoord(0);
}
break;
}
}
}
}
LogicalSize finalSize(wm, GetIntrinsicISize(), bSize);
// round the height down to the nearest pixel
// XXX(mats) why???
finalSize.BSize(wm) -=
finalSize.BSize(wm) % nsPresContext::CSSPixelsToAppUnits(1);
aReflowOutput.SetSize(wm, finalSize);
}
#ifdef DEBUG_FRAME_DUMP
nsresult nsPageBreakFrame::GetFrameName(nsAString& aResult) const {
return MakeFrameName(u"PageBreak"_ns, aResult);
}
#endif