PrintTargetCG.mm (10634B)
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/. */ 5 6 #include "PrintTargetCG.h" 7 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" 15 16 namespace mozilla::gfx { 17 18 static size_t PutBytesNull(void* info, const void* buffer, size_t count) { 19 return count; 20 } 21 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; 33 34 MOZ_ASSERT(mPrintSession && mPageFormat && mPrintSettings); 35 36 ::PMRetain(mPrintSession); 37 ::PMRetain(mPageFormat); 38 ::PMRetain(mPrintSettings); 39 40 // TODO: Add memory reporting like gfxQuartzSurface. 41 // RecordMemoryUsed(mSize.height * 4 + sizeof(gfxQuartzSurface)); 42 43 NS_OBJC_END_TRY_IGNORE_BLOCK; 44 } 45 46 PrintTargetCG::~PrintTargetCG() { 47 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; 48 49 ::PMRelease(mPrintSession); 50 ::PMRelease(mPageFormat); 51 ::PMRelease(mPrintSettings); 52 53 if (mPrintToStreamContext) { 54 CGContextRelease(mPrintToStreamContext); 55 } 56 57 NS_OBJC_END_TRY_IGNORE_BLOCK; 58 } 59 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; 77 } 78 79 static void ReleaseStream(void* aInfo) { 80 auto* stream = static_cast<nsIOutputStream*>(aInfo); 81 stream->Close(); 82 NS_RELEASE(stream); 83 } 84 85 static CGContextRef CreatePrintToStreamContext(nsIOutputStream* aOutputStream, 86 const IntSize& aSize) { 87 MOZ_ASSERT(aOutputStream); 88 89 NS_ADDREF(aOutputStream); // Matched by the NS_RELEASE in ReleaseStream. 90 91 CGRect pageBox{{0.0, 0.0}, {CGFloat(aSize.width), CGFloat(aSize.height)}}; 92 CGDataConsumerCallbacks callbacks = {WriteStreamBytes, ReleaseStream}; 93 CGDataConsumerRef consumer = CGDataConsumerCreate(aOutputStream, &callbacks); 94 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); 103 104 CGContextRef pdfContext = 105 CGPDFContextCreate(consumer, &pageBox, auxiliaryInfo); 106 CGDataConsumerRelease(consumer); 107 CFRelease(auxiliaryInfo); 108 return pdfContext; 109 } 110 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 } 118 119 CGContextRef printToStreamContext = nullptr; 120 if (aOutputStream) { 121 printToStreamContext = CreatePrintToStreamContext(aOutputStream, aSize); 122 if (!printToStreamContext) { 123 return nullptr; 124 } 125 } 126 127 RefPtr<PrintTargetCG> target = new PrintTargetCG( 128 printToStreamContext, aPrintSession, aPageFormat, aPrintSettings, aSize); 129 130 return target.forget(); 131 } 132 133 already_AddRefed<DrawTarget> PrintTargetCG::GetReferenceDrawTarget() { 134 if (!mRefDT) { 135 const IntSize size(1, 1); 136 137 CGDataConsumerCallbacks callbacks = {PutBytesNull, nullptr}; 138 CGDataConsumerRef consumer = CGDataConsumerCreate(nullptr, &callbacks); 139 CGContextRef pdfContext = CGPDFContextCreate(consumer, nullptr, nullptr); 140 CGDataConsumerRelease(consumer); 141 142 cairo_surface_t* similar = cairo_quartz_surface_create_for_cg_context( 143 pdfContext, size.width, size.height); 144 145 CGContextRelease(pdfContext); 146 147 if (cairo_surface_status(similar)) { 148 return nullptr; 149 } 150 151 RefPtr<DrawTarget> dt = 152 Factory::CreateDrawTargetForCairoSurface(similar, size); 153 154 // The DT addrefs the surface, so we need drop our own reference to it: 155 cairo_surface_destroy(similar); 156 157 if (!dt || !dt->IsValid()) { 158 return nullptr; 159 } 160 mRefDT = dt.forget(); 161 } 162 163 return do_AddRef(mRefDT); 164 } 165 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; 170 171 if (mPrintToStreamContext) { 172 return NS_OK; 173 } 174 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); 180 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 } 190 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"); 196 197 status = ::PMSessionBeginCGDocumentNoDialog(mPrintSession, mPrintSettings, 198 mPageFormat); 199 200 return status == noErr ? NS_OK : NS_ERROR_ABORT; 201 202 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); 203 } 204 205 nsresult PrintTargetCG::EndPrinting() { 206 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 207 208 if (mPrintToStreamContext) { 209 CGContextFlush(mPrintToStreamContext); 210 CGPDFContextClose(mPrintToStreamContext); 211 return NS_OK; 212 } 213 214 ::PMSessionEndDocumentNoDialog(mPrintSession); 215 return NS_OK; 216 217 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); 218 } 219 220 nsresult PrintTargetCG::AbortPrinting() { 221 #ifdef DEBUG 222 mHasActivePage = false; 223 #endif 224 return EndPrinting(); 225 } 226 227 nsresult PrintTargetCG::BeginPage(const IntSize& aSizeInPoints) { 228 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 229 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 } 240 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); 249 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 } 276 277 // This call will fail if it wasn't called between the PMSessionBeginPage/ 278 // PMSessionEndPage calls: 279 ::PMSessionGetCGGraphicsContext(mPrintSession, &context); 280 281 if (!context) { 282 return NS_ERROR_FAILURE; 283 } 284 } 285 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); 290 291 cairo_surface_t* surface = 292 cairo_quartz_surface_create_for_cg_context(context, width, height); 293 294 if (cairo_surface_status(surface)) { 295 return NS_ERROR_FAILURE; 296 } 297 298 mCairoSurface = surface; 299 300 return PrintTarget::BeginPage(aSizeInPoints); 301 302 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); 303 } 304 305 nsresult PrintTargetCG::EndPage() { 306 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 307 308 cairo_surface_finish(mCairoSurface); 309 mCairoSurface = nullptr; 310 311 if (mPrintToStreamContext) { 312 CGContextEndPage(mPrintToStreamContext); 313 } else { 314 OSStatus status = ::PMSessionEndPageNoDialog(mPrintSession); 315 if (status != noErr) { 316 return NS_ERROR_ABORT; 317 } 318 } 319 320 return PrintTarget::EndPage(); 321 322 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); 323 } 324 325 } // namespace mozilla::gfx