| /* |
| * Copyright (C) 2007 Alp Toker <alp@atoker.com> |
| * Copyright (C) 2007-2019 Apple Inc. |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Library General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Library General Public License for more details. |
| * |
| * You should have received a copy of the GNU Library General Public License |
| * along with this library; see the file COPYING.LIB. If not, write to |
| * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| */ |
| |
| #include "config.h" |
| #include "PrintContext.h" |
| |
| #include "ElementTraversal.h" |
| #include "GraphicsContext.h" |
| #include "Frame.h" |
| #include "FrameView.h" |
| #include "LengthBox.h" |
| #include "RenderView.h" |
| #include "RuntimeEnabledFeatures.h" |
| #include "StyleInheritedData.h" |
| #include "StyleResolver.h" |
| #include "StyleScope.h" |
| #include <wtf/text/StringConcatenateNumbers.h> |
| |
| namespace WebCore { |
| |
| PrintContext::PrintContext(Frame* frame) |
| : FrameDestructionObserver(frame) |
| { |
| } |
| |
| PrintContext::~PrintContext() |
| { |
| if (m_isPrinting) |
| end(); |
| } |
| |
| void PrintContext::computePageRects(const FloatRect& printRect, float headerHeight, float footerHeight, float userScaleFactor, float& outPageHeight, bool allowHorizontalTiling) |
| { |
| if (!frame()) |
| return; |
| |
| auto& frame = *this->frame(); |
| m_pageRects.clear(); |
| outPageHeight = 0; |
| |
| if (!frame.document() || !frame.view() || !frame.document()->renderView()) |
| return; |
| |
| if (userScaleFactor <= 0) { |
| LOG_ERROR("userScaleFactor has bad value %.2f", userScaleFactor); |
| return; |
| } |
| |
| RenderView* view = frame.document()->renderView(); |
| const IntRect& documentRect = view->documentRect(); |
| FloatSize pageSize = frame.resizePageRectsKeepingRatio(FloatSize(printRect.width(), printRect.height()), FloatSize(documentRect.width(), documentRect.height())); |
| float pageWidth = pageSize.width(); |
| float pageHeight = pageSize.height(); |
| |
| outPageHeight = pageHeight; // this is the height of the page adjusted by margins |
| pageHeight -= headerHeight + footerHeight; |
| |
| if (pageHeight <= 0) { |
| LOG_ERROR("pageHeight has bad value %.2f", pageHeight); |
| return; |
| } |
| |
| computePageRectsWithPageSizeInternal(FloatSize(pageWidth / userScaleFactor, pageHeight / userScaleFactor), allowHorizontalTiling); |
| } |
| |
| FloatBoxExtent PrintContext::computedPageMargin(FloatBoxExtent printMargin) |
| { |
| if (!frame() || !frame()->document()) |
| return printMargin; |
| if (!RuntimeEnabledFeatures::sharedFeatures().pageAtRuleSupportEnabled()) |
| return printMargin; |
| // FIXME Currently no pseudo class is supported. |
| auto style = frame()->document()->styleScope().resolver().styleForPage(0); |
| |
| float pixelToPointScaleFactor = 1 / CSSPrimitiveValue::conversionToCanonicalUnitsScaleFactor(CSSUnitType::CSS_PT).value(); |
| return { style->marginTop().isAuto() ? printMargin.top() : style->marginTop().value() * pixelToPointScaleFactor, |
| style->marginRight().isAuto() ? printMargin.right() : style->marginRight().value() * pixelToPointScaleFactor, |
| style->marginBottom().isAuto() ? printMargin.bottom() : style->marginBottom().value() * pixelToPointScaleFactor, |
| style->marginLeft().isAuto() ? printMargin.left() : style->marginLeft().value() * pixelToPointScaleFactor }; |
| } |
| |
| FloatSize PrintContext::computedPageSize(FloatSize pageSize, FloatBoxExtent printMargin) |
| { |
| auto computedMargin = computedPageMargin(printMargin); |
| if (computedMargin == printMargin) |
| return pageSize; |
| |
| auto horizontalMarginDelta = (printMargin.left() - computedMargin.left()) + (printMargin.right() - computedMargin.right()); |
| auto verticalMarginDelta = (printMargin.top() - computedMargin.top()) + (printMargin.bottom() - computedMargin.bottom()); |
| return { pageSize.width() + horizontalMarginDelta, pageSize.height() + verticalMarginDelta }; |
| } |
| |
| void PrintContext::computePageRectsWithPageSize(const FloatSize& pageSizeInPixels, bool allowHorizontalTiling) |
| { |
| m_pageRects.clear(); |
| computePageRectsWithPageSizeInternal(pageSizeInPixels, allowHorizontalTiling); |
| } |
| |
| void PrintContext::computePageRectsWithPageSizeInternal(const FloatSize& pageSizeInPixels, bool allowInlineDirectionTiling) |
| { |
| if (!frame()) |
| return; |
| |
| auto& frame = *this->frame(); |
| if (!frame.document() || !frame.view() || !frame.document()->renderView()) |
| return; |
| |
| RenderView* view = frame.document()->renderView(); |
| |
| IntRect docRect = view->documentRect(); |
| |
| int pageWidth = pageSizeInPixels.width(); |
| int pageHeight = pageSizeInPixels.height(); |
| |
| bool isHorizontal = view->style().isHorizontalWritingMode(); |
| |
| int docLogicalHeight = isHorizontal ? docRect.height() : docRect.width(); |
| int pageLogicalHeight = isHorizontal ? pageHeight : pageWidth; |
| int pageLogicalWidth = isHorizontal ? pageWidth : pageHeight; |
| |
| int inlineDirectionStart; |
| int inlineDirectionEnd; |
| int blockDirectionStart; |
| int blockDirectionEnd; |
| if (isHorizontal) { |
| if (view->style().isFlippedBlocksWritingMode()) { |
| blockDirectionStart = docRect.maxY(); |
| blockDirectionEnd = docRect.y(); |
| } else { |
| blockDirectionStart = docRect.y(); |
| blockDirectionEnd = docRect.maxY(); |
| } |
| inlineDirectionStart = view->style().isLeftToRightDirection() ? docRect.x() : docRect.maxX(); |
| inlineDirectionEnd = view->style().isLeftToRightDirection() ? docRect.maxX() : docRect.x(); |
| } else { |
| if (view->style().isFlippedBlocksWritingMode()) { |
| blockDirectionStart = docRect.maxX(); |
| blockDirectionEnd = docRect.x(); |
| } else { |
| blockDirectionStart = docRect.x(); |
| blockDirectionEnd = docRect.maxX(); |
| } |
| inlineDirectionStart = view->style().isLeftToRightDirection() ? docRect.y() : docRect.maxY(); |
| inlineDirectionEnd = view->style().isLeftToRightDirection() ? docRect.maxY() : docRect.y(); |
| } |
| |
| unsigned pageCount = ceilf((float)docLogicalHeight / pageLogicalHeight); |
| for (unsigned i = 0; i < pageCount; ++i) { |
| int pageLogicalTop = blockDirectionEnd > blockDirectionStart ? |
| blockDirectionStart + i * pageLogicalHeight : |
| blockDirectionStart - (i + 1) * pageLogicalHeight; |
| if (allowInlineDirectionTiling) { |
| for (int currentInlinePosition = inlineDirectionStart; |
| inlineDirectionEnd > inlineDirectionStart ? currentInlinePosition < inlineDirectionEnd : currentInlinePosition > inlineDirectionEnd; |
| currentInlinePosition += (inlineDirectionEnd > inlineDirectionStart ? pageLogicalWidth : -pageLogicalWidth)) { |
| int pageLogicalLeft = inlineDirectionEnd > inlineDirectionStart ? currentInlinePosition : currentInlinePosition - pageLogicalWidth; |
| IntRect pageRect(pageLogicalLeft, pageLogicalTop, pageLogicalWidth, pageLogicalHeight); |
| if (!isHorizontal) |
| pageRect = pageRect.transposedRect(); |
| m_pageRects.append(pageRect); |
| } |
| } else { |
| int pageLogicalLeft = inlineDirectionEnd > inlineDirectionStart ? inlineDirectionStart : inlineDirectionStart - pageLogicalWidth; |
| IntRect pageRect(pageLogicalLeft, pageLogicalTop, pageLogicalWidth, pageLogicalHeight); |
| if (!isHorizontal) |
| pageRect = pageRect.transposedRect(); |
| m_pageRects.append(pageRect); |
| } |
| } |
| } |
| |
| void PrintContext::begin(float width, float height) |
| { |
| if (!frame()) |
| return; |
| |
| Ref frame = *this->frame(); |
| // This function can be called multiple times to adjust printing parameters without going back to screen mode. |
| m_isPrinting = true; |
| |
| FloatSize originalPageSize = FloatSize(width, height); |
| FloatSize minLayoutSize = frame->resizePageRectsKeepingRatio(originalPageSize, FloatSize(width * minimumShrinkFactor(), height * minimumShrinkFactor())); |
| |
| // This changes layout, so callers need to make sure that they don't paint to screen while in printing mode. |
| frame->setPrinting(true, minLayoutSize, originalPageSize, maximumShrinkFactor() / minimumShrinkFactor(), AdjustViewSize); |
| } |
| |
| float PrintContext::computeAutomaticScaleFactor(const FloatSize& availablePaperSize) |
| { |
| if (!frame()) |
| return 1; |
| |
| auto& frame = *this->frame(); |
| if (!frame.view()) |
| return 1; |
| |
| bool useViewWidth = true; |
| if (frame.document() && frame.document()->renderView()) |
| useViewWidth = frame.document()->renderView()->style().isHorizontalWritingMode(); |
| |
| float viewLogicalWidth = useViewWidth ? frame.view()->contentsWidth() : frame.view()->contentsHeight(); |
| if (viewLogicalWidth < 1) |
| return 1; |
| |
| float maxShrinkToFitScaleFactor = 1 / maximumShrinkFactor(); |
| float shrinkToFitScaleFactor = (useViewWidth ? availablePaperSize.width() : availablePaperSize.height()) / viewLogicalWidth; |
| return std::max(maxShrinkToFitScaleFactor, shrinkToFitScaleFactor); |
| } |
| |
| void PrintContext::spoolPage(GraphicsContext& ctx, int pageNumber, float width) |
| { |
| if (!frame()) |
| return; |
| |
| auto& frame = *this->frame(); |
| if (!frame.view()) |
| return; |
| |
| // FIXME: Not correct for vertical text. |
| IntRect pageRect = m_pageRects[pageNumber]; |
| float scale = width / pageRect.width(); |
| |
| ctx.save(); |
| ctx.scale(scale); |
| ctx.translate(-pageRect.x(), -pageRect.y()); |
| ctx.clip(pageRect); |
| frame.view()->paintContents(ctx, pageRect); |
| outputLinkedDestinations(ctx, *frame.document(), pageRect); |
| ctx.restore(); |
| } |
| |
| void PrintContext::spoolRect(GraphicsContext& ctx, const IntRect& rect) |
| { |
| if (!frame()) |
| return; |
| |
| auto& frame = *this->frame(); |
| if (!frame.view()) |
| return; |
| |
| // FIXME: Not correct for vertical text. |
| ctx.save(); |
| ctx.translate(-rect.x(), -rect.y()); |
| ctx.clip(rect); |
| frame.view()->paintContents(ctx, rect); |
| outputLinkedDestinations(ctx, *frame.document(), rect); |
| ctx.restore(); |
| } |
| |
| void PrintContext::end() |
| { |
| if (!frame()) |
| return; |
| |
| auto& frame = *this->frame(); |
| ASSERT(m_isPrinting); |
| m_isPrinting = false; |
| frame.setPrinting(false, FloatSize(), FloatSize(), 0, AdjustViewSize); |
| m_linkedDestinations = nullptr; |
| } |
| |
| static inline RenderBoxModelObject* enclosingBoxModelObject(RenderElement* renderer) |
| { |
| while (renderer && !is<RenderBoxModelObject>(*renderer)) |
| renderer = renderer->parent(); |
| return downcast<RenderBoxModelObject>(renderer); |
| } |
| |
| int PrintContext::pageNumberForElement(Element* element, const FloatSize& pageSizeInPixels) |
| { |
| // Make sure the element is not freed during the layout. |
| RefPtr<Element> elementRef(element); |
| element->document().updateLayout(); |
| |
| auto* box = enclosingBoxModelObject(element->renderer()); |
| if (!box) |
| return -1; |
| |
| Frame* frame = element->document().frame(); |
| FloatRect pageRect(FloatPoint(0, 0), pageSizeInPixels); |
| PrintContext printContext(frame); |
| printContext.begin(pageRect.width(), pageRect.height()); |
| FloatSize scaledPageSize = pageSizeInPixels; |
| scaledPageSize.scale(frame->view()->contentsSize().width() / pageRect.width()); |
| printContext.computePageRectsWithPageSize(scaledPageSize, false); |
| |
| int top = roundToInt(box->offsetTop()); |
| int left = roundToInt(box->offsetLeft()); |
| size_t pageNumber = 0; |
| for (; pageNumber < printContext.pageCount(); pageNumber++) { |
| const IntRect& page = printContext.pageRect(pageNumber); |
| if (page.x() <= left && left < page.maxX() && page.y() <= top && top < page.maxY()) |
| return pageNumber; |
| } |
| return -1; |
| } |
| |
| void PrintContext::collectLinkedDestinations(Document& document) |
| { |
| for (Element* child = document.documentElement(); child; child = ElementTraversal::next(*child)) { |
| String outAnchorName; |
| if (Element* element = child->findAnchorElementForLink(outAnchorName)) |
| m_linkedDestinations->add(outAnchorName, *element); |
| } |
| } |
| |
| void PrintContext::outputLinkedDestinations(GraphicsContext& graphicsContext, Document& document, const IntRect& pageRect) |
| { |
| if (!graphicsContext.supportsInternalLinks()) |
| return; |
| |
| if (!m_linkedDestinations) { |
| m_linkedDestinations = makeUnique<HashMap<String, Ref<Element>>>(); |
| collectLinkedDestinations(document); |
| } |
| |
| for (const auto& it : *m_linkedDestinations) { |
| RenderElement* renderer = it.value->renderer(); |
| if (!renderer) |
| continue; |
| |
| FloatPoint point = renderer->absoluteAnchorRect().minXMinYCorner(); |
| point = point.expandedTo(FloatPoint()); |
| |
| if (!pageRect.contains(roundedIntPoint(point))) |
| continue; |
| |
| graphicsContext.addDestinationAtPoint(it.key, point); |
| } |
| } |
| |
| String PrintContext::pageProperty(Frame* frame, const char* propertyName, int pageNumber) |
| { |
| ASSERT(frame); |
| ASSERT(frame->document()); |
| |
| Ref<Frame> protectedFrame(*frame); |
| |
| auto& document = *frame->document(); |
| PrintContext printContext(frame); |
| printContext.begin(800); // Any width is OK here. |
| document.updateLayout(); |
| auto style = document.styleScope().resolver().styleForPage(pageNumber); |
| |
| // Implement formatters for properties we care about. |
| if (!strcmp(propertyName, "margin-left")) { |
| if (style->marginLeft().isAuto()) |
| return "auto"_s; |
| return String::number(style->marginLeft().value()); |
| } |
| if (!strcmp(propertyName, "line-height")) |
| return String::number(style->lineHeight().value()); |
| if (!strcmp(propertyName, "font-size")) |
| return String::number(style->fontDescription().computedPixelSize()); |
| if (!strcmp(propertyName, "font-family")) |
| return style->fontDescription().firstFamily(); |
| if (!strcmp(propertyName, "size")) |
| return makeString(style->pageSize().width.value(), ' ', style->pageSize().height.value()); |
| |
| return makeString("pageProperty() unimplemented for: ", propertyName); |
| } |
| |
| bool PrintContext::isPageBoxVisible(Frame* frame, int pageNumber) |
| { |
| return frame->document()->isPageBoxVisible(pageNumber); |
| } |
| |
| String PrintContext::pageSizeAndMarginsInPixels(Frame* frame, int pageNumber, int width, int height, int marginTop, int marginRight, int marginBottom, int marginLeft) |
| { |
| IntSize pageSize(width, height); |
| frame->document()->pageSizeAndMarginsInPixels(pageNumber, pageSize, marginTop, marginRight, marginBottom, marginLeft); |
| |
| return makeString('(', pageSize.width(), ", ", pageSize.height(), ") ", marginTop, ' ', marginRight, ' ', marginBottom, ' ', marginLeft); |
| } |
| |
| bool PrintContext::beginAndComputePageRectsWithPageSize(Frame& frame, const FloatSize& pageSizeInPixels) |
| { |
| if (!frame.document() || !frame.view() || !frame.document()->renderView()) |
| return false; |
| |
| frame.document()->updateLayout(); |
| |
| begin(pageSizeInPixels.width(), pageSizeInPixels.height()); |
| // Account for shrink-to-fit. |
| FloatSize scaledPageSize = pageSizeInPixels; |
| scaledPageSize.scale(frame.view()->contentsSize().width() / pageSizeInPixels.width()); |
| computePageRectsWithPageSize(scaledPageSize, false); |
| |
| return true; |
| } |
| |
| int PrintContext::numberOfPages(Frame& frame, const FloatSize& pageSizeInPixels) |
| { |
| Ref<Frame> protectedFrame(frame); |
| |
| PrintContext printContext(&frame); |
| if (!printContext.beginAndComputePageRectsWithPageSize(frame, pageSizeInPixels)) |
| return -1; |
| |
| return printContext.pageCount(); |
| } |
| |
| void PrintContext::spoolAllPagesWithBoundaries(Frame& frame, GraphicsContext& graphicsContext, const FloatSize& pageSizeInPixels) |
| { |
| Ref<Frame> protectedFrame(frame); |
| |
| PrintContext printContext(&frame); |
| if (!printContext.beginAndComputePageRectsWithPageSize(frame, pageSizeInPixels)) |
| return; |
| |
| const float pageWidth = pageSizeInPixels.width(); |
| const Vector<IntRect>& pageRects = printContext.pageRects(); |
| int totalHeight = pageRects.size() * (pageSizeInPixels.height() + 1) - 1; |
| |
| // Fill the whole background by white. |
| graphicsContext.setFillColor(Color::white); |
| graphicsContext.fillRect(FloatRect(0, 0, pageWidth, totalHeight)); |
| |
| graphicsContext.save(); |
| |
| int currentHeight = 0; |
| for (size_t pageIndex = 0; pageIndex < pageRects.size(); pageIndex++) { |
| // Draw a line for a page boundary if this isn't the first page. |
| if (pageIndex > 0) { |
| #if PLATFORM(COCOA) |
| int boundaryLineY = currentHeight; |
| #else |
| int boundaryLineY = currentHeight - 1; |
| #endif |
| graphicsContext.save(); |
| graphicsContext.setStrokeColor(Color::blue); |
| graphicsContext.setFillColor(Color::blue); |
| graphicsContext.drawLine(IntPoint(0, boundaryLineY), IntPoint(pageWidth, boundaryLineY)); |
| graphicsContext.restore(); |
| } |
| |
| graphicsContext.save(); |
| graphicsContext.translate(0, currentHeight); |
| printContext.spoolPage(graphicsContext, pageIndex, pageWidth); |
| graphicsContext.restore(); |
| |
| currentHeight += pageSizeInPixels.height() + 1; |
| } |
| |
| graphicsContext.restore(); |
| } |
| |
| } |