Bug 1880704 - Crop more PDF rendering and wait differently for rendering r=mboldan
[gecko.git] / gfx / thebes / PrintTargetCG.mm
bloba88be6eafa3dc589e344872bcf415895cef31df4
1 /* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2  * This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "PrintTargetCG.h"
8 #include "cairo.h"
9 #include "cairo-quartz.h"
10 #include "mozilla/gfx/HelpersCairo.h"
11 #include "mozilla/StaticPrefs_print.h"
12 #include "nsObjCExceptions.h"
13 #include "nsString.h"
14 #include "nsIOutputStream.h"
16 namespace mozilla::gfx {
18 static size_t PutBytesNull(void* info, const void* buffer, size_t count) {
19   return count;
22 PrintTargetCG::PrintTargetCG(CGContextRef aPrintToStreamContext,
23                              PMPrintSession aPrintSession,
24                              PMPageFormat aPageFormat,
25                              PMPrintSettings aPrintSettings,
26                              const IntSize& aSize)
27     : PrintTarget(/* aCairoSurface */ nullptr, aSize),
28       mPrintToStreamContext(aPrintToStreamContext),
29       mPrintSession(aPrintSession),
30       mPageFormat(aPageFormat),
31       mPrintSettings(aPrintSettings) {
32   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
34   MOZ_ASSERT(mPrintSession && mPageFormat && mPrintSettings);
36   ::PMRetain(mPrintSession);
37   ::PMRetain(mPageFormat);
38   ::PMRetain(mPrintSettings);
40   // TODO: Add memory reporting like gfxQuartzSurface.
41   // RecordMemoryUsed(mSize.height * 4 + sizeof(gfxQuartzSurface));
43   NS_OBJC_END_TRY_IGNORE_BLOCK;
46 PrintTargetCG::~PrintTargetCG() {
47   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
49   ::PMRelease(mPrintSession);
50   ::PMRelease(mPageFormat);
51   ::PMRelease(mPrintSettings);
53   if (mPrintToStreamContext) {
54     CGContextRelease(mPrintToStreamContext);
55   }
57   NS_OBJC_END_TRY_IGNORE_BLOCK;
60 static size_t WriteStreamBytes(void* aInfo, const void* aBuffer,
61                                size_t aCount) {
62   auto* stream = static_cast<nsIOutputStream*>(aInfo);
63   auto* data = static_cast<const char*>(aBuffer);
64   size_t remaining = aCount;
65   do {
66     uint32_t wrote = 0;
67     // Handle potential narrowing from size_t to uint32_t.
68     uint32_t toWrite = uint32_t(
69         std::min(remaining, size_t(std::numeric_limits<uint32_t>::max())));
70     if (NS_WARN_IF(NS_FAILED(stream->Write(data, toWrite, &wrote)))) {
71       break;
72     }
73     data += wrote;
74     remaining -= size_t(wrote);
75   } while (remaining);
76   return aCount;
79 static void ReleaseStream(void* aInfo) {
80   auto* stream = static_cast<nsIOutputStream*>(aInfo);
81   stream->Close();
82   NS_RELEASE(stream);
85 static CGContextRef CreatePrintToStreamContext(nsIOutputStream* aOutputStream,
86                                                const IntSize& aSize) {
87   MOZ_ASSERT(aOutputStream);
89   NS_ADDREF(aOutputStream);  // Matched by the NS_RELEASE in ReleaseStream.
91   CGRect pageBox{{0.0, 0.0}, {CGFloat(aSize.width), CGFloat(aSize.height)}};
92   CGDataConsumerCallbacks callbacks = {WriteStreamBytes, ReleaseStream};
93   CGDataConsumerRef consumer = CGDataConsumerCreate(aOutputStream, &callbacks);
95   // This metadata is added by the CorePrinting APIs in the non-stream case.
96   NSString* bundleName = [NSBundle.mainBundle.localizedInfoDictionary
97       objectForKey:(NSString*)kCFBundleNameKey];
98   CFMutableDictionaryRef auxiliaryInfo = CFDictionaryCreateMutable(
99       kCFAllocatorDefault, 1, &kCFTypeDictionaryKeyCallBacks,
100       &kCFTypeDictionaryValueCallBacks);
101   CFDictionaryAddValue(auxiliaryInfo, kCGPDFContextCreator,
102                        (__bridge CFStringRef)bundleName);
104   CGContextRef pdfContext =
105       CGPDFContextCreate(consumer, &pageBox, auxiliaryInfo);
106   CGDataConsumerRelease(consumer);
107   CFRelease(auxiliaryInfo);
108   return pdfContext;
111 /* static */ already_AddRefed<PrintTargetCG> PrintTargetCG::CreateOrNull(
112     nsIOutputStream* aOutputStream, PMPrintSession aPrintSession,
113     PMPageFormat aPageFormat, PMPrintSettings aPrintSettings,
114     const IntSize& aSize) {
115   if (!Factory::CheckSurfaceSize(aSize)) {
116     return nullptr;
117   }
119   CGContextRef printToStreamContext = nullptr;
120   if (aOutputStream) {
121     printToStreamContext = CreatePrintToStreamContext(aOutputStream, aSize);
122     if (!printToStreamContext) {
123       return nullptr;
124     }
125   }
127   RefPtr<PrintTargetCG> target = new PrintTargetCG(
128       printToStreamContext, aPrintSession, aPageFormat, aPrintSettings, aSize);
130   return target.forget();
133 already_AddRefed<DrawTarget> PrintTargetCG::GetReferenceDrawTarget() {
134   if (!mRefDT) {
135     const IntSize size(1, 1);
137     CGDataConsumerCallbacks callbacks = {PutBytesNull, nullptr};
138     CGDataConsumerRef consumer = CGDataConsumerCreate(nullptr, &callbacks);
139     CGContextRef pdfContext = CGPDFContextCreate(consumer, nullptr, nullptr);
140     CGDataConsumerRelease(consumer);
142     cairo_surface_t* similar = cairo_quartz_surface_create_for_cg_context(
143         pdfContext, size.width, size.height);
145     CGContextRelease(pdfContext);
147     if (cairo_surface_status(similar)) {
148       return nullptr;
149     }
151     RefPtr<DrawTarget> dt =
152         Factory::CreateDrawTargetForCairoSurface(similar, size);
154     // The DT addrefs the surface, so we need drop our own reference to it:
155     cairo_surface_destroy(similar);
157     if (!dt || !dt->IsValid()) {
158       return nullptr;
159     }
160     mRefDT = dt.forget();
161   }
163   return do_AddRef(mRefDT);
166 nsresult PrintTargetCG::BeginPrinting(const nsAString& aTitle,
167                                       const nsAString& aPrintToFileName,
168                                       int32_t aStartPage, int32_t aEndPage) {
169   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
171   if (mPrintToStreamContext) {
172     return NS_OK;
173   }
175   // Print Core of Application Service sent print job with names exceeding
176   // 255 bytes. This is a workaround until fix it.
177   // (https://openradar.appspot.com/34428043)
178   nsAutoString adjustedTitle;
179   PrintTarget::AdjustPrintJobNameForIPP(aTitle, adjustedTitle);
181   if (!adjustedTitle.IsEmpty()) {
182     CFStringRef cfString = ::CFStringCreateWithCharacters(
183         NULL, reinterpret_cast<const UniChar*>(adjustedTitle.BeginReading()),
184         adjustedTitle.Length());
185     if (cfString) {
186       ::PMPrintSettingsSetJobName(mPrintSettings, cfString);
187       ::CFRelease(cfString);
188     }
189   }
191   OSStatus status;
192   status = ::PMSetFirstPage(mPrintSettings, aStartPage, false);
193   NS_ASSERTION(status == noErr, "PMSetFirstPage failed");
194   status = ::PMSetLastPage(mPrintSettings, aEndPage, false);
195   NS_ASSERTION(status == noErr, "PMSetLastPage failed");
197   status = ::PMSessionBeginCGDocumentNoDialog(mPrintSession, mPrintSettings,
198                                               mPageFormat);
200   return status == noErr ? NS_OK : NS_ERROR_ABORT;
202   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
205 nsresult PrintTargetCG::EndPrinting() {
206   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
208   if (mPrintToStreamContext) {
209     CGContextFlush(mPrintToStreamContext);
210     CGPDFContextClose(mPrintToStreamContext);
211     return NS_OK;
212   }
214   ::PMSessionEndDocumentNoDialog(mPrintSession);
215   return NS_OK;
217   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
220 nsresult PrintTargetCG::AbortPrinting() {
221 #ifdef DEBUG
222   mHasActivePage = false;
223 #endif
224   return EndPrinting();
227 nsresult PrintTargetCG::BeginPage(const IntSize& aSizeInPoints) {
228   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
230   unsigned int width;
231   unsigned int height;
232   if (StaticPrefs::
233           print_save_as_pdf_use_page_rule_size_as_paper_size_enabled()) {
234     width = static_cast<unsigned int>(aSizeInPoints.width);
235     height = static_cast<unsigned int>(aSizeInPoints.height);
236   } else {
237     width = static_cast<unsigned int>(mSize.width);
238     height = static_cast<unsigned int>(mSize.height);
239   }
241   CGContextRef context;
242   if (mPrintToStreamContext) {
243     CGRect bounds = CGRectMake(0, 0, width, height);
244     CGContextBeginPage(mPrintToStreamContext, &bounds);
245     context = mPrintToStreamContext;
246   } else {
247     // XXX Why are we calling this if we don't check the return value?
248     PMSessionError(mPrintSession);
250     // XXX For mixed sheet sizes that aren't simply an orientation switch, we
251     // will want to be able to pass a sheet size here, using something like:
252     //   PMRect bounds = { 0, 0, double(height), double(width) };
253     // But the docs for PMSessionBeginPageNoDialog's `pageFrame` parameter say:
254     //   "You should pass NULL, as this parameter is currentlyunsupported."
255     // https://developer.apple.com/documentation/applicationservices/1463416-pmsessionbeginpagenodialog?language=objc
256     // And indeed, it doesn't appear to do anything.
257     // (It seems weird that CGContextBeginPage (above) supports passing a rect,
258     // and that that works for setting sheet sizes in PDF output, but the Core
259     // Printing API does not.)
260     // We can always switch to PrintTargetPDF - we use that for Windows/Linux
261     // anyway. But Core Graphics output is better than Cairo's in some cases.
262     //
263     // For now, we support switching sheet orientation only:
264     if (StaticPrefs::
265             print_save_as_pdf_use_page_rule_size_as_paper_size_enabled()) {
266       ::PMOrientation pageOrientation =
267           width < height ? kPMPortrait : kPMLandscape;
268       ::PMSetOrientation(mPageFormat, pageOrientation, kPMUnlocked);
269       // We don't need to reset the orientation, since we set it for every page.
270     }
271     OSStatus status =
272         ::PMSessionBeginPageNoDialog(mPrintSession, mPageFormat, nullptr);
273     if (status != noErr) {
274       return NS_ERROR_ABORT;
275     }
277     // This call will fail if it wasn't called between the PMSessionBeginPage/
278     // PMSessionEndPage calls:
279     ::PMSessionGetCGGraphicsContext(mPrintSession, &context);
281     if (!context) {
282       return NS_ERROR_FAILURE;
283     }
284   }
286   // Initially, origin is at bottom-left corner of the paper.
287   // Here, we translate it to top-left corner of the paper.
288   CGContextTranslateCTM(context, 0, height);
289   CGContextScaleCTM(context, 1.0, -1.0);
291   cairo_surface_t* surface =
292       cairo_quartz_surface_create_for_cg_context(context, width, height);
294   if (cairo_surface_status(surface)) {
295     return NS_ERROR_FAILURE;
296   }
298   mCairoSurface = surface;
300   return PrintTarget::BeginPage(aSizeInPoints);
302   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
305 nsresult PrintTargetCG::EndPage() {
306   NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
308   cairo_surface_finish(mCairoSurface);
309   mCairoSurface = nullptr;
311   if (mPrintToStreamContext) {
312     CGContextEndPage(mPrintToStreamContext);
313   } else {
314     OSStatus status = ::PMSessionEndPageNoDialog(mPrintSession);
315     if (status != noErr) {
316       return NS_ERROR_ABORT;
317     }
318   }
320   return PrintTarget::EndPage();
322   NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
325 }  // namespace mozilla::gfx