[iOS] PDFDocumentImage should not create a cached image larger than 4M pixels
[WebKit-https.git] / Source / WebCore / platform / graphics / cg / PDFDocumentImage.cpp
1 /*
2  * Copyright (C) 2004, 2005, 2006, 2013 Apple Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
24  */
25
26 #include "config.h"
27 #include "PDFDocumentImage.h"
28
29 #if USE(CG)
30
31 #if PLATFORM(IOS)
32 #include <CoreGraphics/CoreGraphics.h>
33 #include <ImageIO/ImageIO.h>
34 #endif
35
36 #include "GraphicsContext.h"
37 #include "ImageBuffer.h"
38 #include "ImageObserver.h"
39 #include "IntRect.h"
40 #include "Length.h"
41 #include "SharedBuffer.h"
42 #include "TextStream.h"
43 #include <CoreGraphics/CGContext.h>
44 #include <CoreGraphics/CGPDFDocument.h>
45 #include <wtf/MathExtras.h>
46 #include <wtf/RAMSize.h>
47 #include <wtf/RetainPtr.h>
48 #include <wtf/StdLibExtras.h>
49
50 #if !PLATFORM(COCOA)
51 #include "ImageSourceCG.h"
52 #endif
53
54 namespace WebCore {
55
56 PDFDocumentImage::PDFDocumentImage(ImageObserver* observer)
57     : Image(observer)
58     , m_cachedBytes(0)
59     , m_rotationDegrees(0)
60     , m_hasPage(false)
61 {
62 }
63
64 PDFDocumentImage::~PDFDocumentImage()
65 {
66 }
67
68 String PDFDocumentImage::filenameExtension() const
69 {
70     return "pdf";
71 }
72
73 FloatSize PDFDocumentImage::size() const
74 {
75     FloatSize expandedCropBoxSize = FloatSize(expandedIntSize(m_cropBox.size()));
76
77     if (m_rotationDegrees == 90 || m_rotationDegrees == 270)
78         return expandedCropBoxSize.transposedSize();
79     return expandedCropBoxSize;
80 }
81
82 void PDFDocumentImage::computeIntrinsicDimensions(Length& intrinsicWidth, Length& intrinsicHeight, FloatSize& intrinsicRatio)
83 {
84     // FIXME: If we want size negotiation with PDF documents as-image, this is the place to implement it (https://bugs.webkit.org/show_bug.cgi?id=12095).
85     Image::computeIntrinsicDimensions(intrinsicWidth, intrinsicHeight, intrinsicRatio);
86     intrinsicRatio = FloatSize();
87 }
88
89 bool PDFDocumentImage::dataChanged(bool allDataReceived)
90 {
91     ASSERT(!m_document);
92     if (allDataReceived && !m_document) {
93         createPDFDocument();
94
95         if (pageCount()) {
96             m_hasPage = true;
97             computeBoundsForCurrentPage();
98         }
99     }
100     return m_document; // Return true if size is available.
101 }
102
103 bool PDFDocumentImage::cacheParametersMatch(GraphicsContext& context, const FloatRect& dstRect, const FloatRect& srcRect) const
104 {
105     if (dstRect.size() != m_cachedDestinationSize)
106         return false;
107
108     if (srcRect != m_cachedSourceRect)
109         return false;
110
111     AffineTransform::DecomposedType decomposedTransform;
112     context.getCTM(GraphicsContext::DefinitelyIncludeDeviceScale).decompose(decomposedTransform);
113
114     AffineTransform::DecomposedType cachedDecomposedTransform;
115     m_cachedTransform.decompose(cachedDecomposedTransform);
116     if (decomposedTransform.scaleX != cachedDecomposedTransform.scaleX || decomposedTransform.scaleY != cachedDecomposedTransform.scaleY)
117         return false;
118
119     return true;
120 }
121
122 static void transformContextForPainting(GraphicsContext& context, const FloatRect& dstRect, const FloatRect& srcRect)
123 {
124     float hScale = dstRect.width() / srcRect.width();
125     float vScale = dstRect.height() / srcRect.height();
126
127     if (hScale != vScale) {
128         float minimumScale = std::max((dstRect.width() - 0.5) / srcRect.width(), (dstRect.height() - 0.5) / srcRect.height());
129         float maximumScale = std::min((dstRect.width() + 0.5) / srcRect.width(), (dstRect.height() + 0.5) / srcRect.height());
130
131         // If the difference between the two scales is due to integer rounding of image sizes,
132         // use the smaller of the two original scales to ensure that the image fits inside the
133         // space originally allocated for it.
134         if (minimumScale <= maximumScale) {
135             hScale = std::min(hScale, vScale);
136             vScale = hScale;
137         }
138     }
139
140     context.translate(srcRect.x() * hScale, srcRect.y() * vScale);
141     context.scale(FloatSize(hScale, -vScale));
142     context.translate(0, -srcRect.height());
143 }
144
145 void PDFDocumentImage::updateCachedImageIfNeeded(GraphicsContext& context, const FloatRect& dstRect, const FloatRect& srcRect)
146 {
147 #if PLATFORM(IOS)
148     // On iOS, if the physical memory is less than 1GB, do not allocate more than 16MB for the PDF cachedImage.
149     const size_t memoryThreshold = WTF::GB;
150     const size_t maxArea = 16 * WTF::MB / 4; // 16 MB maximum size, divided by a rough cost of 4 bytes per pixel of area.
151     
152     if (ramSize() <= memoryThreshold && ImageBuffer::compatibleBufferSize(dstRect.size(), context).area() >= maxArea) {
153         m_cachedImageBuffer = nullptr;
154         return;
155     }
156
157     // On iOS, some clients use low-quality image interpolation always, which throws off this optimization,
158     // as we never get the subsequent high-quality paint. Since live resize is rare on iOS, disable the optimization.
159     // FIXME (136593): It's also possible to do the wrong thing here if CSS specifies low-quality interpolation via the "image-rendering"
160     // property, on all platforms. We should only do this optimization if we're actually in a ImageQualityController live resize,
161     // and are guaranteed to do a high-quality paint later.
162     bool repaintIfNecessary = true;
163 #else
164     // If we have an existing image, reuse it if we're doing a low-quality paint, even if cache parameters don't match;
165     // we'll rerender when we do the subsequent high-quality paint.
166     InterpolationQuality interpolationQuality = context.imageInterpolationQuality();
167     bool repaintIfNecessary = interpolationQuality != InterpolationNone && interpolationQuality != InterpolationLow;
168 #endif
169
170     if (m_cachedImageBuffer && (!repaintIfNecessary || cacheParametersMatch(context, dstRect, srcRect)))
171         return;
172     
173     m_cachedImageBuffer = ImageBuffer::createCompatibleBuffer(FloatRect(enclosingIntRect(dstRect)).size(), context);
174     if (!m_cachedImageBuffer)
175         return;
176     auto& bufferContext = m_cachedImageBuffer->context();
177
178     transformContextForPainting(bufferContext, dstRect, srcRect);
179     drawPDFPage(bufferContext);
180
181     m_cachedTransform = context.getCTM(GraphicsContext::DefinitelyIncludeDeviceScale);
182     m_cachedDestinationSize = dstRect.size();
183     m_cachedSourceRect = srcRect;
184
185     IntSize internalSize = m_cachedImageBuffer->internalSize();
186     size_t oldCachedBytes = m_cachedBytes;
187     m_cachedBytes = safeCast<size_t>(internalSize.width()) * internalSize.height() * 4;
188
189     if (imageObserver())
190         imageObserver()->decodedSizeChanged(this, safeCast<int>(m_cachedBytes) - safeCast<int>(oldCachedBytes));
191 }
192
193 void PDFDocumentImage::draw(GraphicsContext& context, const FloatRect& dstRect, const FloatRect& srcRect, CompositeOperator op, BlendMode, ImageOrientationDescription)
194 {
195     if (!m_document || !m_hasPage)
196         return;
197
198     updateCachedImageIfNeeded(context, dstRect, srcRect);
199
200     {
201         GraphicsContextStateSaver stateSaver(context);
202         context.setCompositeOperation(op);
203
204         if (m_cachedImageBuffer)
205             context.drawImageBuffer(*m_cachedImageBuffer, dstRect);
206         else {
207             transformContextForPainting(context, dstRect, srcRect);
208             drawPDFPage(context);
209         }
210     }
211
212     if (imageObserver())
213         imageObserver()->didDraw(this);
214 }
215
216 void PDFDocumentImage::destroyDecodedData(bool)
217 {
218     m_cachedImageBuffer = nullptr;
219
220     if (imageObserver())
221         imageObserver()->decodedSizeChanged(this, -safeCast<int>(m_cachedBytes));
222
223     m_cachedBytes = 0;
224 }
225
226 #if !USE(PDFKIT_FOR_PDFDOCUMENTIMAGE)
227
228 void PDFDocumentImage::createPDFDocument()
229 {
230     RetainPtr<CGDataProviderRef> dataProvider = adoptCF(CGDataProviderCreateWithCFData(data()->createCFData().get()));
231     m_document = adoptCF(CGPDFDocumentCreateWithProvider(dataProvider.get()));
232 }
233
234 void PDFDocumentImage::computeBoundsForCurrentPage()
235 {
236     ASSERT(pageCount() > 0);
237     CGPDFPageRef cgPage = CGPDFDocumentGetPage(m_document.get(), 1);
238     CGRect mediaBox = CGPDFPageGetBoxRect(cgPage, kCGPDFMediaBox);
239
240     // Get crop box (not always there). If not, use media box.
241     CGRect r = CGPDFPageGetBoxRect(cgPage, kCGPDFCropBox);
242     if (!CGRectIsEmpty(r))
243         m_cropBox = r;
244     else
245         m_cropBox = mediaBox;
246
247     m_rotationDegrees = CGPDFPageGetRotationAngle(cgPage);
248 }
249
250 unsigned PDFDocumentImage::pageCount() const
251 {
252     return CGPDFDocumentGetNumberOfPages(m_document.get());
253 }
254
255 static void applyRotationForPainting(GraphicsContext& context, FloatSize size, int rotationDegrees)
256 {
257     if (rotationDegrees == 90)
258         context.translate(0, size.height());
259     else if (rotationDegrees == 180)
260         context.translate(size.width(), size.height());
261     else if (rotationDegrees == 270)
262         context.translate(size.width(), 0);
263
264     context.rotate(-deg2rad(static_cast<float>(rotationDegrees)));
265 }
266
267 void PDFDocumentImage::drawPDFPage(GraphicsContext& context)
268 {
269     applyRotationForPainting(context, size(), m_rotationDegrees);
270
271     context.translate(-m_cropBox.x(), -m_cropBox.y());
272
273     // CGPDF pages are indexed from 1.
274     CGContextDrawPDFPage(context.platformContext(), CGPDFDocumentGetPage(m_document.get(), 1));
275 }
276
277 #endif // !USE(PDFKIT_FOR_PDFDOCUMENTIMAGE)
278
279 void PDFDocumentImage::dump(TextStream& ts) const
280 {
281     Image::dump(ts);
282     ts.dumpProperty("page-count", pageCount());
283     ts.dumpProperty("crop-box", m_cropBox);
284     if (m_rotationDegrees)
285         ts.dumpProperty("rotation", m_rotationDegrees);
286 }
287
288 }
289
290 #endif // USE(CG)