1// Copyright (c) 2011 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "printing/printing_context_mac.h"
6
7#import <ApplicationServices/ApplicationServices.h>
8#import <AppKit/AppKit.h>
9
10#import <iomanip>
11#import <numeric>
12
13#include "base/logging.h"
14#include "base/mac/scoped_cftyperef.h"
15#include "base/mac/scoped_nsautorelease_pool.h"
16#include "base/mac/scoped_nsexception_enabler.h"
17#include "base/strings/sys_string_conversions.h"
18#include "base/strings/utf_string_conversions.h"
19#include "base/values.h"
20#include "printing/print_settings_initializer_mac.h"
21#include "printing/units.h"
22
23namespace printing {
24
25namespace {
26
27const int kMaxPaperSizeDiffereceInPoints = 2;
28
29// Return true if PPD name of paper is equal.
30bool IsPaperNameEqual(CFStringRef name1, const PMPaper& paper2) {
31  CFStringRef name2 = NULL;
32  return (name1 && PMPaperGetPPDPaperName(paper2, &name2) == noErr) &&
33         (CFStringCompare(name1, name2, kCFCompareCaseInsensitive) ==
34          kCFCompareEqualTo);
35}
36
37PMPaper MatchPaper(CFArrayRef paper_list,
38                   CFStringRef name,
39                   double width,
40                   double height) {
41  double best_match = std::numeric_limits<double>::max();
42  PMPaper best_matching_paper = NULL;
43  int num_papers = CFArrayGetCount(paper_list);
44  for (int i = 0; i < num_papers; ++i) {
45    PMPaper paper = (PMPaper)[(NSArray*)paper_list objectAtIndex : i];
46    double paper_width = 0.0;
47    double paper_height = 0.0;
48    PMPaperGetWidth(paper, &paper_width);
49    PMPaperGetHeight(paper, &paper_height);
50    double difference =
51        std::max(fabs(width - paper_width), fabs(height - paper_height));
52
53    // Ignore papers with size too different from expected.
54    if (difference > kMaxPaperSizeDiffereceInPoints)
55      continue;
56
57    if (name && IsPaperNameEqual(name, paper))
58      return paper;
59
60    if (difference < best_match) {
61      best_matching_paper = paper;
62      best_match = difference;
63    }
64  }
65  return best_matching_paper;
66}
67
68}  // namespace
69
70// static
71scoped_ptr<PrintingContext> PrintingContext::Create(Delegate* delegate) {
72  return make_scoped_ptr<PrintingContext>(new PrintingContextMac(delegate));
73}
74
75PrintingContextMac::PrintingContextMac(Delegate* delegate)
76    : PrintingContext(delegate),
77      print_info_([[NSPrintInfo sharedPrintInfo] copy]),
78      context_(NULL) {
79}
80
81PrintingContextMac::~PrintingContextMac() {
82  ReleaseContext();
83}
84
85void PrintingContextMac::AskUserForSettings(
86    int max_pages,
87    bool has_selection,
88    const PrintSettingsCallback& callback) {
89  // Third-party print drivers seem to be an area prone to raising exceptions.
90  // This will allow exceptions to be raised, but does not handle them.  The
91  // NSPrintPanel appears to have appropriate NSException handlers.
92  base::mac::ScopedNSExceptionEnabler enabler;
93
94  // Exceptions can also happen when the NSPrintPanel is being
95  // deallocated, so it must be autoreleased within this scope.
96  base::mac::ScopedNSAutoreleasePool pool;
97
98  DCHECK([NSThread isMainThread]);
99
100  // We deliberately don't feed max_pages into the dialog, because setting
101  // NSPrintLastPage makes the print dialog pre-select the option to only print
102  // a range.
103
104  // TODO(stuartmorgan): implement 'print selection only' (probably requires
105  // adding a new custom view to the panel on 10.5; 10.6 has
106  // NSPrintPanelShowsPrintSelection).
107  NSPrintPanel* panel = [NSPrintPanel printPanel];
108  NSPrintInfo* printInfo = print_info_.get();
109
110  NSPrintPanelOptions options = [panel options];
111  options |= NSPrintPanelShowsPaperSize;
112  options |= NSPrintPanelShowsOrientation;
113  options |= NSPrintPanelShowsScaling;
114  [panel setOptions:options];
115
116  // Set the print job title text.
117  gfx::NativeView parent_view = delegate_->GetParentView();
118  if (parent_view) {
119    NSString* job_title = [[parent_view window] title];
120    if (job_title) {
121      PMPrintSettings printSettings =
122          (PMPrintSettings)[printInfo PMPrintSettings];
123      PMPrintSettingsSetJobName(printSettings, (CFStringRef)job_title);
124      [printInfo updateFromPMPrintSettings];
125    }
126  }
127
128  // TODO(stuartmorgan): We really want a tab sheet here, not a modal window.
129  // Will require restructuring the PrintingContext API to use a callback.
130  NSInteger selection = [panel runModalWithPrintInfo:printInfo];
131  if (selection == NSOKButton) {
132    print_info_.reset([[panel printInfo] retain]);
133    settings_.set_ranges(GetPageRangesFromPrintInfo());
134    InitPrintSettingsFromPrintInfo();
135    callback.Run(OK);
136  } else {
137    callback.Run(CANCEL);
138  }
139}
140
141gfx::Size PrintingContextMac::GetPdfPaperSizeDeviceUnits() {
142  // NOTE: Reset |print_info_| with a copy of |sharedPrintInfo| so as to start
143  // with a clean slate.
144  print_info_.reset([[NSPrintInfo sharedPrintInfo] copy]);
145  UpdatePageFormatWithPaperInfo();
146
147  PMPageFormat page_format =
148      static_cast<PMPageFormat>([print_info_.get() PMPageFormat]);
149  PMRect paper_rect;
150  PMGetAdjustedPaperRect(page_format, &paper_rect);
151
152  // Device units are in points. Units per inch is 72.
153  gfx::Size physical_size_device_units(
154      (paper_rect.right - paper_rect.left),
155      (paper_rect.bottom - paper_rect.top));
156  DCHECK(settings_.device_units_per_inch() == kPointsPerInch);
157  return physical_size_device_units;
158}
159
160PrintingContext::Result PrintingContextMac::UseDefaultSettings() {
161  DCHECK(!in_print_job_);
162
163  print_info_.reset([[NSPrintInfo sharedPrintInfo] copy]);
164  settings_.set_ranges(GetPageRangesFromPrintInfo());
165  InitPrintSettingsFromPrintInfo();
166
167  return OK;
168}
169
170PrintingContext::Result PrintingContextMac::UpdatePrinterSettings(
171    bool external_preview,
172    bool show_system_dialog) {
173  DCHECK(!show_system_dialog);
174  DCHECK(!in_print_job_);
175
176  // NOTE: Reset |print_info_| with a copy of |sharedPrintInfo| so as to start
177  // with a clean slate.
178  print_info_.reset([[NSPrintInfo sharedPrintInfo] copy]);
179
180  if (external_preview) {
181    if (!SetPrintPreviewJob())
182      return OnError();
183  } else {
184    // Don't need this for preview.
185    if (!SetPrinter(base::UTF16ToUTF8(settings_.device_name())) ||
186        !SetCopiesInPrintSettings(settings_.copies()) ||
187        !SetCollateInPrintSettings(settings_.collate()) ||
188        !SetDuplexModeInPrintSettings(settings_.duplex_mode()) ||
189        !SetOutputColor(settings_.color())) {
190      return OnError();
191    }
192  }
193
194  if (!UpdatePageFormatWithPaperInfo() ||
195      !SetOrientationIsLandscape(settings_.landscape())) {
196    return OnError();
197  }
198
199  [print_info_.get() updateFromPMPrintSettings];
200
201  InitPrintSettingsFromPrintInfo();
202  return OK;
203}
204
205bool PrintingContextMac::SetPrintPreviewJob() {
206  PMPrintSession print_session =
207      static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
208  PMPrintSettings print_settings =
209      static_cast<PMPrintSettings>([print_info_.get() PMPrintSettings]);
210  return PMSessionSetDestination(
211      print_session, print_settings, kPMDestinationPreview,
212      NULL, NULL) == noErr;
213}
214
215void PrintingContextMac::InitPrintSettingsFromPrintInfo() {
216  PMPrintSession print_session =
217      static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
218  PMPageFormat page_format =
219      static_cast<PMPageFormat>([print_info_.get() PMPageFormat]);
220  PMPrinter printer;
221  PMSessionGetCurrentPrinter(print_session, &printer);
222  PrintSettingsInitializerMac::InitPrintSettings(
223      printer, page_format, &settings_);
224}
225
226bool PrintingContextMac::SetPrinter(const std::string& device_name) {
227  DCHECK(print_info_.get());
228  PMPrintSession print_session =
229      static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
230
231  PMPrinter current_printer;
232  if (PMSessionGetCurrentPrinter(print_session, &current_printer) != noErr)
233    return false;
234
235  CFStringRef current_printer_id = PMPrinterGetID(current_printer);
236  if (!current_printer_id)
237    return false;
238
239  base::ScopedCFTypeRef<CFStringRef> new_printer_id(
240      base::SysUTF8ToCFStringRef(device_name));
241  if (!new_printer_id.get())
242    return false;
243
244  if (CFStringCompare(new_printer_id.get(), current_printer_id, 0) ==
245          kCFCompareEqualTo) {
246    return true;
247  }
248
249  PMPrinter new_printer = PMPrinterCreateFromPrinterID(new_printer_id.get());
250  if (!new_printer)
251    return false;
252
253  OSStatus status = PMSessionSetCurrentPMPrinter(print_session, new_printer);
254  PMRelease(new_printer);
255  return status == noErr;
256}
257
258bool PrintingContextMac::UpdatePageFormatWithPaperInfo() {
259  PMPrintSession print_session =
260      static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
261
262  PMPageFormat default_page_format =
263      static_cast<PMPageFormat>([print_info_.get() PMPageFormat]);
264
265  PMPrinter current_printer = NULL;
266  if (PMSessionGetCurrentPrinter(print_session, &current_printer) != noErr)
267    return false;
268
269  double page_width = 0.0;
270  double page_height = 0.0;
271  base::ScopedCFTypeRef<CFStringRef> paper_name;
272  PMPaperMargins margins = {0};
273
274  const PrintSettings::RequestedMedia& media = settings_.requested_media();
275  if (media.IsDefault()) {
276    PMPaper default_paper;
277    if (PMGetPageFormatPaper(default_page_format, &default_paper) != noErr ||
278        PMPaperGetWidth(default_paper, &page_width) != noErr ||
279        PMPaperGetHeight(default_paper, &page_height) != noErr) {
280      return false;
281    }
282
283    // Ignore result, because we can continue without following.
284    CFStringRef tmp_paper_name = NULL;
285    PMPaperGetPPDPaperName(default_paper, &tmp_paper_name);
286    PMPaperGetMargins(default_paper, &margins);
287    paper_name.reset(tmp_paper_name, base::scoped_policy::RETAIN);
288  } else {
289    const double kMutiplier = kPointsPerInch / (10.0f * kHundrethsMMPerInch);
290    page_width = media.size_microns.width() * kMutiplier;
291    page_height = media.size_microns.height() * kMutiplier;
292    paper_name.reset(base::SysUTF8ToCFStringRef(media.vendor_id));
293  }
294
295  CFArrayRef paper_list = NULL;
296  if (PMPrinterGetPaperList(current_printer, &paper_list) != noErr)
297    return false;
298
299  PMPaper best_matching_paper =
300      MatchPaper(paper_list, paper_name, page_width, page_height);
301
302  if (best_matching_paper)
303    return UpdatePageFormatWithPaper(best_matching_paper, default_page_format);
304
305  // Do nothing if unmatched paper was default system paper.
306  if (media.IsDefault())
307    return true;
308
309  PMPaper paper = NULL;
310  if (PMPaperCreateCustom(current_printer,
311                          CFSTR("Custom paper ID"),
312                          CFSTR("Custom paper"),
313                          page_width,
314                          page_height,
315                          &margins,
316                          &paper) != noErr) {
317    return false;
318  }
319  bool result = UpdatePageFormatWithPaper(paper, default_page_format);
320  PMRelease(paper);
321  return result;
322}
323
324bool PrintingContextMac::UpdatePageFormatWithPaper(PMPaper paper,
325                                                   PMPageFormat page_format) {
326  PMPageFormat new_format = NULL;
327  if (PMCreatePageFormatWithPMPaper(&new_format, paper) != noErr)
328    return false;
329  // Copy over the original format with the new page format.
330  bool result = (PMCopyPageFormat(new_format, page_format) == noErr);
331  [print_info_.get() updateFromPMPageFormat];
332  PMRelease(new_format);
333  return result;
334}
335
336bool PrintingContextMac::SetCopiesInPrintSettings(int copies) {
337  if (copies < 1)
338    return false;
339
340  PMPrintSettings pmPrintSettings =
341      static_cast<PMPrintSettings>([print_info_.get() PMPrintSettings]);
342  return PMSetCopies(pmPrintSettings, copies, false) == noErr;
343}
344
345bool PrintingContextMac::SetCollateInPrintSettings(bool collate) {
346  PMPrintSettings pmPrintSettings =
347      static_cast<PMPrintSettings>([print_info_.get() PMPrintSettings]);
348  return PMSetCollate(pmPrintSettings, collate) == noErr;
349}
350
351bool PrintingContextMac::SetOrientationIsLandscape(bool landscape) {
352  PMPageFormat page_format =
353      static_cast<PMPageFormat>([print_info_.get() PMPageFormat]);
354
355  PMOrientation orientation = landscape ? kPMLandscape : kPMPortrait;
356
357  if (PMSetOrientation(page_format, orientation, false) != noErr)
358    return false;
359
360  PMPrintSession print_session =
361      static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
362
363  PMSessionValidatePageFormat(print_session, page_format, kPMDontWantBoolean);
364
365  [print_info_.get() updateFromPMPageFormat];
366  return true;
367}
368
369bool PrintingContextMac::SetDuplexModeInPrintSettings(DuplexMode mode) {
370  PMDuplexMode duplexSetting;
371  switch (mode) {
372    case LONG_EDGE:
373      duplexSetting = kPMDuplexNoTumble;
374      break;
375    case SHORT_EDGE:
376      duplexSetting = kPMDuplexTumble;
377      break;
378    case SIMPLEX:
379      duplexSetting = kPMDuplexNone;
380      break;
381    default:  // UNKNOWN_DUPLEX_MODE
382      return true;
383  }
384
385  PMPrintSettings pmPrintSettings =
386      static_cast<PMPrintSettings>([print_info_.get() PMPrintSettings]);
387  return PMSetDuplex(pmPrintSettings, duplexSetting) == noErr;
388}
389
390bool PrintingContextMac::SetOutputColor(int color_mode) {
391  PMPrintSettings pmPrintSettings =
392      static_cast<PMPrintSettings>([print_info_.get() PMPrintSettings]);
393  std::string color_setting_name;
394  std::string color_value;
395  GetColorModelForMode(color_mode, &color_setting_name, &color_value);
396  base::ScopedCFTypeRef<CFStringRef> color_setting(
397      base::SysUTF8ToCFStringRef(color_setting_name));
398  base::ScopedCFTypeRef<CFStringRef> output_color(
399      base::SysUTF8ToCFStringRef(color_value));
400
401  return PMPrintSettingsSetValue(pmPrintSettings,
402                                 color_setting.get(),
403                                 output_color.get(),
404                                 false) == noErr;
405}
406
407PageRanges PrintingContextMac::GetPageRangesFromPrintInfo() {
408  PageRanges page_ranges;
409  NSDictionary* print_info_dict = [print_info_.get() dictionary];
410  if (![[print_info_dict objectForKey:NSPrintAllPages] boolValue]) {
411    PageRange range;
412    range.from = [[print_info_dict objectForKey:NSPrintFirstPage] intValue] - 1;
413    range.to = [[print_info_dict objectForKey:NSPrintLastPage] intValue] - 1;
414    page_ranges.push_back(range);
415  }
416  return page_ranges;
417}
418
419PrintingContext::Result PrintingContextMac::InitWithSettings(
420    const PrintSettings& settings) {
421  DCHECK(!in_print_job_);
422
423  settings_ = settings;
424
425  NOTIMPLEMENTED();
426
427  return FAILED;
428}
429
430PrintingContext::Result PrintingContextMac::NewDocument(
431    const base::string16& document_name) {
432  DCHECK(!in_print_job_);
433
434  in_print_job_ = true;
435
436  PMPrintSession print_session =
437      static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
438  PMPrintSettings print_settings =
439      static_cast<PMPrintSettings>([print_info_.get() PMPrintSettings]);
440  PMPageFormat page_format =
441      static_cast<PMPageFormat>([print_info_.get() PMPageFormat]);
442
443  base::ScopedCFTypeRef<CFStringRef> job_title(
444      base::SysUTF16ToCFStringRef(document_name));
445  PMPrintSettingsSetJobName(print_settings, job_title.get());
446
447  OSStatus status = PMSessionBeginCGDocumentNoDialog(print_session,
448                                                     print_settings,
449                                                     page_format);
450  if (status != noErr)
451    return OnError();
452
453  return OK;
454}
455
456PrintingContext::Result PrintingContextMac::NewPage() {
457  if (abort_printing_)
458    return CANCEL;
459  DCHECK(in_print_job_);
460  DCHECK(!context_);
461
462  PMPrintSession print_session =
463      static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
464  PMPageFormat page_format =
465      static_cast<PMPageFormat>([print_info_.get() PMPageFormat]);
466  OSStatus status;
467  status = PMSessionBeginPageNoDialog(print_session, page_format, NULL);
468  if (status != noErr)
469    return OnError();
470  status = PMSessionGetCGGraphicsContext(print_session, &context_);
471  if (status != noErr)
472    return OnError();
473
474  return OK;
475}
476
477PrintingContext::Result PrintingContextMac::PageDone() {
478  if (abort_printing_)
479    return CANCEL;
480  DCHECK(in_print_job_);
481  DCHECK(context_);
482
483  PMPrintSession print_session =
484      static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
485  OSStatus status = PMSessionEndPageNoDialog(print_session);
486  if (status != noErr)
487    OnError();
488  context_ = NULL;
489
490  return OK;
491}
492
493PrintingContext::Result PrintingContextMac::DocumentDone() {
494  if (abort_printing_)
495    return CANCEL;
496  DCHECK(in_print_job_);
497
498  PMPrintSession print_session =
499      static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
500  OSStatus status = PMSessionEndDocumentNoDialog(print_session);
501  if (status != noErr)
502    OnError();
503
504  ResetSettings();
505  return OK;
506}
507
508void PrintingContextMac::Cancel() {
509  abort_printing_ = true;
510  in_print_job_ = false;
511  context_ = NULL;
512
513  PMPrintSession print_session =
514      static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
515  PMSessionEndPageNoDialog(print_session);
516}
517
518void PrintingContextMac::ReleaseContext() {
519  print_info_.reset();
520  context_ = NULL;
521}
522
523gfx::NativeDrawingContext PrintingContextMac::context() const {
524  return context_;
525}
526
527}  // namespace printing
528