1 // Copyright 2011 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "printing/printing_context_mac.h"
7 #import <AppKit/AppKit.h>
8 #import <QuartzCore/QuartzCore.h>
14 #include "base/check.h"
15 #include "base/mac/scoped_cftyperef.h"
16 #include "base/strings/string_piece.h"
17 #include "base/strings/sys_string_conversions.h"
18 #include "base/strings/utf_string_conversions.h"
19 #include "base/values.h"
20 #include "build/build_config.h"
21 #include "printing/buildflags/buildflags.h"
22 #include "printing/metafile.h"
23 #include "printing/mojom/print.mojom.h"
24 #include "printing/print_settings_initializer_mac.h"
25 #include "printing/printing_features.h"
26 #include "printing/units.h"
32 const int kMaxPaperSizeDiffereceInPoints = 2;
34 // Return true if PPD name of paper is equal.
35 bool IsPaperNameEqual(CFStringRef name1, const PMPaper& paper2) {
36 CFStringRef name2 = nullptr;
37 return (name1 && PMPaperGetPPDPaperName(paper2, &name2) == noErr) &&
38 (CFStringCompare(name1, name2, kCFCompareCaseInsensitive) ==
42 PMPaper MatchPaper(CFArrayRef paper_list,
46 double best_match = std::numeric_limits<double>::max();
47 PMPaper best_matching_paper = nullptr;
48 int num_papers = CFArrayGetCount(paper_list);
49 for (int i = 0; i < num_papers; ++i) {
50 PMPaper paper = (PMPaper)((NSArray*)paper_list)[i];
51 double paper_width = 0.0;
52 double paper_height = 0.0;
53 PMPaperGetWidth(paper, &paper_width);
54 PMPaperGetHeight(paper, &paper_height);
56 std::max(fabs(width - paper_width), fabs(height - paper_height));
58 // Ignore papers with size too different from expected.
59 if (difference > kMaxPaperSizeDiffereceInPoints)
62 if (name && IsPaperNameEqual(name, paper))
65 if (difference < best_match) {
66 best_matching_paper = paper;
67 best_match = difference;
70 return best_matching_paper;
76 std::unique_ptr<PrintingContext> PrintingContext::CreateImpl(
78 bool skip_system_calls) {
79 auto context = std::make_unique<PrintingContextMac>(delegate);
80 #if BUILDFLAG(ENABLE_OOP_PRINTING)
81 if (skip_system_calls)
82 context->set_skip_system_calls();
87 PrintingContextMac::PrintingContextMac(Delegate* delegate)
88 : PrintingContext(delegate),
89 print_info_([[NSPrintInfo sharedPrintInfo] copy]),
92 PrintingContextMac::~PrintingContextMac() {
96 void PrintingContextMac::AskUserForSettings(int max_pages,
99 PrintSettingsCallback callback) {
100 // Exceptions can also happen when the NSPrintPanel is being
101 // deallocated, so it must be autoreleased within this scope.
103 DCHECK([NSThread isMainThread]);
105 // We deliberately don't feed max_pages into the dialog, because setting
106 // NSPrintLastPage makes the print dialog pre-select the option to only
109 // TODO(stuartmorgan): implement 'print selection only' (probably requires
110 // adding a new custom view to the panel on 10.5; 10.6 has
111 // NSPrintPanelShowsPrintSelection).
112 NSPrintPanel* panel = [NSPrintPanel printPanel];
113 NSPrintInfo* print_info = print_info_.get();
115 NSPrintPanelOptions options = [panel options];
116 options |= NSPrintPanelShowsPaperSize;
117 options |= NSPrintPanelShowsOrientation;
118 options |= NSPrintPanelShowsScaling;
119 [panel setOptions:options];
121 // Set the print job title text.
122 gfx::NativeView parent_view = delegate_->GetParentView();
124 NSString* job_title = [[parent_view.GetNativeNSView() window] title];
126 PMPrintSettings print_settings =
127 (PMPrintSettings)[print_info PMPrintSettings];
128 PMPrintSettingsSetJobName(print_settings, (CFStringRef)job_title);
129 [print_info updateFromPMPrintSettings];
133 // TODO(stuartmorgan): We really want a tab sheet here, not a modal window.
134 // Will require restructuring the PrintingContext API to use a callback.
136 // This function may be called in the middle of a CATransaction, where
137 // running a modal panel is forbidden. That situation isn't ideal, but from
138 // this code's POV the right answer is to defer running the panel until
139 // after the current transaction. See https://crbug.com/849538.
140 __block auto block_callback = std::move(callback);
141 [CATransaction setCompletionBlock:^{
142 NSInteger selection = [panel runModalWithPrintInfo:print_info];
143 if (selection == NSModalResponseOK) {
144 print_info_.reset([[panel printInfo] retain]);
145 settings_->set_ranges(GetPageRangesFromPrintInfo());
146 InitPrintSettingsFromPrintInfo();
147 std::move(block_callback).Run(mojom::ResultCode::kSuccess);
149 std::move(block_callback).Run(mojom::ResultCode::kCanceled);
155 gfx::Size PrintingContextMac::GetPdfPaperSizeDeviceUnits() {
156 // NOTE: Reset |print_info_| with a copy of |sharedPrintInfo| so as to start
157 // with a clean slate.
158 print_info_.reset([[NSPrintInfo sharedPrintInfo] copy]);
159 UpdatePageFormatWithPaperInfo();
161 PMPageFormat page_format =
162 static_cast<PMPageFormat>([print_info_.get() PMPageFormat]);
164 PMGetAdjustedPaperRect(page_format, &paper_rect);
166 // Device units are in points. Units per inch is 72.
167 gfx::Size physical_size_device_units((paper_rect.right - paper_rect.left),
168 (paper_rect.bottom - paper_rect.top));
169 DCHECK(settings_->device_units_per_inch() == kPointsPerInch);
170 return physical_size_device_units;
173 mojom::ResultCode PrintingContextMac::UseDefaultSettings() {
174 DCHECK(!in_print_job_);
176 print_info_.reset([[NSPrintInfo sharedPrintInfo] copy]);
177 settings_->set_ranges(GetPageRangesFromPrintInfo());
178 InitPrintSettingsFromPrintInfo();
180 return mojom::ResultCode::kSuccess;
183 mojom::ResultCode PrintingContextMac::UpdatePrinterSettings(
184 const PrinterSettings& printer_settings) {
185 DCHECK(!printer_settings.show_system_dialog);
186 DCHECK(!in_print_job_);
188 // NOTE: Reset |print_info_| with a copy of |sharedPrintInfo| so as to start
189 // with a clean slate.
190 print_info_.reset([[NSPrintInfo sharedPrintInfo] copy]);
192 if (printer_settings.external_preview) {
193 if (!SetPrintPreviewJob())
196 // Don't need this for preview.
197 if (!SetPrinter(base::UTF16ToUTF8(settings_->device_name())) ||
198 !SetCopiesInPrintSettings(settings_->copies()) ||
199 !SetCollateInPrintSettings(settings_->collate()) ||
200 !SetDuplexModeInPrintSettings(settings_->duplex_mode()) ||
201 !SetOutputColor(static_cast<int>(settings_->color())) ||
202 !SetResolution(settings_->dpi_size())) {
207 if (!UpdatePageFormatWithPaperInfo() ||
208 !SetOrientationIsLandscape(settings_->landscape())) {
212 [print_info_.get() updateFromPMPrintSettings];
214 InitPrintSettingsFromPrintInfo();
215 return mojom::ResultCode::kSuccess;
218 bool PrintingContextMac::SetPrintPreviewJob() {
219 PMPrintSession print_session =
220 static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
221 PMPrintSettings print_settings =
222 static_cast<PMPrintSettings>([print_info_.get() PMPrintSettings]);
223 return PMSessionSetDestination(print_session, print_settings,
224 kPMDestinationPreview, nullptr,
228 void PrintingContextMac::InitPrintSettingsFromPrintInfo() {
229 PMPrintSession print_session =
230 static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
231 PMPageFormat page_format =
232 static_cast<PMPageFormat>([print_info_.get() PMPageFormat]);
234 PMSessionGetCurrentPrinter(print_session, &printer);
235 PrintSettingsInitializerMac::InitPrintSettings(printer, page_format,
239 bool PrintingContextMac::SetPrinter(const std::string& device_name) {
240 DCHECK(print_info_.get());
241 PMPrintSession print_session =
242 static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
244 PMPrinter current_printer;
245 if (PMSessionGetCurrentPrinter(print_session, ¤t_printer) != noErr)
248 CFStringRef current_printer_id = PMPrinterGetID(current_printer);
249 if (!current_printer_id)
252 base::ScopedCFTypeRef<CFStringRef> new_printer_id(
253 base::SysUTF8ToCFStringRef(device_name));
254 if (!new_printer_id.get())
257 if (CFStringCompare(new_printer_id.get(), current_printer_id, 0) ==
262 PMPrinter new_printer = PMPrinterCreateFromPrinterID(new_printer_id.get());
266 OSStatus status = PMSessionSetCurrentPMPrinter(print_session, new_printer);
267 PMRelease(new_printer);
268 return status == noErr;
271 bool PrintingContextMac::UpdatePageFormatWithPaperInfo() {
272 PMPrintSession print_session =
273 static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
275 PMPageFormat default_page_format =
276 static_cast<PMPageFormat>([print_info_.get() PMPageFormat]);
278 PMPrinter current_printer = nullptr;
279 if (PMSessionGetCurrentPrinter(print_session, ¤t_printer) != noErr)
282 double page_width = 0.0;
283 double page_height = 0.0;
284 base::ScopedCFTypeRef<CFStringRef> paper_name;
285 PMPaperMargins margins = {0};
287 const PrintSettings::RequestedMedia& media = settings_->requested_media();
288 if (media.IsDefault()) {
289 PMPaper default_paper;
290 if (PMGetPageFormatPaper(default_page_format, &default_paper) != noErr ||
291 PMPaperGetWidth(default_paper, &page_width) != noErr ||
292 PMPaperGetHeight(default_paper, &page_height) != noErr) {
296 // Ignore result, because we can continue without following.
297 CFStringRef tmp_paper_name = nullptr;
298 PMPaperGetPPDPaperName(default_paper, &tmp_paper_name);
299 PMPaperGetMargins(default_paper, &margins);
300 paper_name.reset(tmp_paper_name, base::scoped_policy::RETAIN);
302 const double kMutiplier =
303 kPointsPerInch / static_cast<float>(kMicronsPerInch);
304 page_width = media.size_microns.width() * kMutiplier;
305 page_height = media.size_microns.height() * kMutiplier;
306 paper_name.reset(base::SysUTF8ToCFStringRef(media.vendor_id));
309 CFArrayRef paper_list = nullptr;
310 if (PMPrinterGetPaperList(current_printer, &paper_list) != noErr)
313 PMPaper best_matching_paper =
314 MatchPaper(paper_list, paper_name, page_width, page_height);
316 if (best_matching_paper)
317 return UpdatePageFormatWithPaper(best_matching_paper, default_page_format);
319 // Do nothing if unmatched paper was default system paper.
320 if (media.IsDefault())
323 PMPaper paper = nullptr;
324 if (PMPaperCreateCustom(current_printer, CFSTR("Custom paper ID"),
325 CFSTR("Custom paper"), page_width, page_height,
326 &margins, &paper) != noErr) {
329 bool result = UpdatePageFormatWithPaper(paper, default_page_format);
334 bool PrintingContextMac::UpdatePageFormatWithPaper(PMPaper paper,
335 PMPageFormat page_format) {
336 PMPageFormat new_format = nullptr;
337 if (PMCreatePageFormatWithPMPaper(&new_format, paper) != noErr)
339 // Copy over the original format with the new page format.
340 bool result = (PMCopyPageFormat(new_format, page_format) == noErr);
341 [print_info_.get() updateFromPMPageFormat];
342 PMRelease(new_format);
346 bool PrintingContextMac::SetCopiesInPrintSettings(int copies) {
350 PMPrintSettings print_settings =
351 static_cast<PMPrintSettings>([print_info_.get() PMPrintSettings]);
352 return PMSetCopies(print_settings, copies, false) == noErr;
355 bool PrintingContextMac::SetCollateInPrintSettings(bool collate) {
356 PMPrintSettings print_settings =
357 static_cast<PMPrintSettings>([print_info_.get() PMPrintSettings]);
358 return PMSetCollate(print_settings, collate) == noErr;
361 bool PrintingContextMac::SetOrientationIsLandscape(bool landscape) {
362 PMPageFormat page_format =
363 static_cast<PMPageFormat>([print_info_.get() PMPageFormat]);
365 PMOrientation orientation = landscape ? kPMLandscape : kPMPortrait;
367 if (PMSetOrientation(page_format, orientation, false) != noErr)
370 PMPrintSession print_session =
371 static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
373 PMSessionValidatePageFormat(print_session, page_format, kPMDontWantBoolean);
375 [print_info_.get() updateFromPMPageFormat];
379 bool PrintingContextMac::SetDuplexModeInPrintSettings(mojom::DuplexMode mode) {
380 PMDuplexMode duplexSetting;
382 case mojom::DuplexMode::kLongEdge:
383 duplexSetting = kPMDuplexNoTumble;
385 case mojom::DuplexMode::kShortEdge:
386 duplexSetting = kPMDuplexTumble;
388 case mojom::DuplexMode::kSimplex:
389 duplexSetting = kPMDuplexNone;
391 default: // kUnknownDuplexMode
395 PMPrintSettings print_settings =
396 static_cast<PMPrintSettings>([print_info_.get() PMPrintSettings]);
397 return PMSetDuplex(print_settings, duplexSetting) == noErr;
400 bool PrintingContextMac::SetOutputColor(int color_mode) {
401 const mojom::ColorModel color_model = ColorModeToColorModel(color_mode);
403 if (!base::FeatureList::IsEnabled(features::kCupsIppPrintingBackend)) {
404 std::string color_setting_name;
405 std::string color_value;
406 GetColorModelForModel(color_model, &color_setting_name, &color_value);
407 return SetKeyValue(color_setting_name, color_value);
410 // First, set the default CUPS IPP output color.
411 if (!SetKeyValue(CUPS_PRINT_COLOR_MODE,
412 GetIppColorModelForModel(color_model))) {
416 struct PpdColorSetting {
417 constexpr PpdColorSetting(base::StringPiece name,
418 base::StringPiece bw,
419 base::StringPiece color)
420 : name(name), bw(bw), color(color) {}
421 base::StringPiece name;
422 base::StringPiece bw;
423 base::StringPiece color;
426 // TODO(crbug.com/1210992): Move `kKnownPpdColorSettings` elsewhere so it can
427 // be used for general CUPS printing code (e.g., for parsing PPDs).
428 static constexpr PpdColorSetting kKnownPpdColorSettings[] = {
429 {"ARCMode", "CMBW", "CMColor"}, // Sharp
430 {"BLW", "TrueM", "FalseM"}, // Lexmark
431 {"BRMonoColor", "Mono", "FullColor"}, // Brother
432 {"BRPrintQuality", "Black", "Color"}, // Brother
433 {"CNIJGrayScale", "1", "0"}, // Canon
434 {"ColorMode", "Monochrome", "Color"}, // Samsung
435 {"ColorModel", "Gray", "Color"}, // Generic
436 {"HPColorMode", "GrayscalePrint", "ColorPrint"}, // HP
437 {"Ink", "MONO", "COLOR"}, // Epson
438 {"OKControl", "Gray", "Auto"}, // Oki
439 {"PrintoutMode", "Normal.Gray", "Normal"}, // Foomatic
440 {"SelectColor", "Grayscale", "Color"}, // Konica Minolta
441 {"XRXColor", "BW", "Automatic"}, // Xerox
442 {"XROutputColor", "PrintAsGrayscale", "PrintAsColor"}, // Xerox
445 // Even when interfacing with printer settings using CUPS IPP, the print job
446 // may still expect PPD color values if the printer was added to the system
447 // with a PPD. To avoid parsing PPDs (which is the point of using CUPS IPP),
448 // set every single known PPD color setting and hope that one of them sticks.
449 const bool is_color = IsColorModelSelected(color_model).value_or(false);
450 for (const auto& setting : kKnownPpdColorSettings) {
451 const base::StringPiece& color_setting_name = setting.name;
452 const base::StringPiece& color_value =
453 is_color ? setting.color : setting.bw;
454 if (!SetKeyValue(color_setting_name, color_value))
461 bool PrintingContextMac::SetResolution(const gfx::Size& dpi_size) {
462 if (dpi_size.IsEmpty())
465 PMPrintSession print_session =
466 static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
467 PMPrinter current_printer;
468 if (PMSessionGetCurrentPrinter(print_session, ¤t_printer) != noErr)
471 PMResolution resolution;
472 resolution.hRes = dpi_size.width();
473 resolution.vRes = dpi_size.height();
475 PMPrintSettings print_settings =
476 static_cast<PMPrintSettings>([print_info_.get() PMPrintSettings]);
477 return PMPrinterSetOutputResolution(current_printer, print_settings,
478 &resolution) == noErr;
481 bool PrintingContextMac::SetKeyValue(base::StringPiece key,
482 base::StringPiece value) {
483 PMPrintSettings print_settings =
484 static_cast<PMPrintSettings>([print_info_.get() PMPrintSettings]);
485 base::ScopedCFTypeRef<CFStringRef> cf_key(base::SysUTF8ToCFStringRef(key));
486 base::ScopedCFTypeRef<CFStringRef> cf_value(
487 base::SysUTF8ToCFStringRef(value));
489 return PMPrintSettingsSetValue(print_settings, cf_key.get(), cf_value.get(),
490 /*locked=*/false) == noErr;
493 PageRanges PrintingContextMac::GetPageRangesFromPrintInfo() {
494 PageRanges page_ranges;
495 NSDictionary* print_info_dict = [print_info_.get() dictionary];
496 if (![print_info_dict[NSPrintAllPages] boolValue]) {
498 range.from = [print_info_dict[NSPrintFirstPage] intValue] - 1;
499 range.to = [print_info_dict[NSPrintLastPage] intValue] - 1;
500 page_ranges.push_back(range);
505 mojom::ResultCode PrintingContextMac::NewDocument(
506 const std::u16string& document_name) {
507 DCHECK(!in_print_job_);
509 in_print_job_ = true;
511 if (skip_system_calls())
512 return mojom::ResultCode::kSuccess;
514 PMPrintSession print_session =
515 static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
516 PMPrintSettings print_settings =
517 static_cast<PMPrintSettings>([print_info_.get() PMPrintSettings]);
518 PMPageFormat page_format =
519 static_cast<PMPageFormat>([print_info_.get() PMPageFormat]);
521 base::ScopedCFTypeRef<CFStringRef> job_title(
522 base::SysUTF16ToCFStringRef(document_name));
523 PMPrintSettingsSetJobName(print_settings, job_title.get());
525 OSStatus status = PMSessionBeginCGDocumentNoDialog(
526 print_session, print_settings, page_format);
530 return mojom::ResultCode::kSuccess;
533 mojom::ResultCode PrintingContextMac::NewPage() {
535 return mojom::ResultCode::kCanceled;
536 DCHECK(in_print_job_);
539 PMPrintSession print_session =
540 static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
541 PMPageFormat page_format =
542 static_cast<PMPageFormat>([print_info_.get() PMPageFormat]);
544 status = PMSessionBeginPageNoDialog(print_session, page_format, nullptr);
547 status = PMSessionGetCGGraphicsContext(print_session, &context_);
551 return mojom::ResultCode::kSuccess;
554 mojom::ResultCode PrintingContextMac::PageDone() {
556 return mojom::ResultCode::kCanceled;
557 DCHECK(in_print_job_);
560 PMPrintSession print_session =
561 static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
562 OSStatus status = PMSessionEndPageNoDialog(print_session);
567 return mojom::ResultCode::kSuccess;
570 mojom::ResultCode PrintingContextMac::PrintDocument(
571 const MetafilePlayer& metafile,
572 const PrintSettings& settings,
573 uint32_t num_pages) {
574 const PageSetup& page_setup = settings.page_setup_device_units();
575 const CGRect paper_rect = gfx::Rect(page_setup.physical_size()).ToCGRect();
577 for (size_t metafile_page_number = 1; metafile_page_number <= num_pages;
578 metafile_page_number++) {
579 mojom::ResultCode result = NewPage();
580 if (result != mojom::ResultCode::kSuccess)
582 if (!metafile.RenderPage(metafile_page_number, context_, paper_rect,
583 /*autorotate=*/true, /*fit_to_page=*/false)) {
584 return mojom::ResultCode::kFailed;
587 if (result != mojom::ResultCode::kSuccess)
590 return mojom::ResultCode::kSuccess;
593 mojom::ResultCode PrintingContextMac::DocumentDone() {
595 return mojom::ResultCode::kCanceled;
596 DCHECK(in_print_job_);
598 PMPrintSession print_session =
599 static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
600 OSStatus status = PMSessionEndDocumentNoDialog(print_session);
605 return mojom::ResultCode::kSuccess;
608 void PrintingContextMac::Cancel() {
609 abort_printing_ = true;
610 in_print_job_ = false;
613 PMPrintSession print_session =
614 static_cast<PMPrintSession>([print_info_.get() PMPrintSession]);
615 PMSessionEndPageNoDialog(print_session);
618 void PrintingContextMac::ReleaseContext() {
623 printing::NativeDrawingContext PrintingContextMac::context() const {
627 } // namespace printing