tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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