1/**************************************************************************** 2** 3** Copyright (C) 2014 John Layt <jlayt@kde.org> 4** Contact: https://www.qt.io/licensing/ 5** 6** This file is part of the plugins of the Qt Toolkit. 7** 8** $QT_BEGIN_LICENSE:LGPL$ 9** Commercial License Usage 10** Licensees holding valid commercial Qt licenses may use this file in 11** accordance with the commercial license agreement provided with the 12** Software or, alternatively, in accordance with the terms contained in 13** a written agreement between you and The Qt Company. For licensing terms 14** and conditions see https://www.qt.io/terms-conditions. For further 15** information use the contact form at https://www.qt.io/contact-us. 16** 17** GNU Lesser General Public License Usage 18** Alternatively, this file may be used under the terms of the GNU Lesser 19** General Public License version 3 as published by the Free Software 20** Foundation and appearing in the file LICENSE.LGPL3 included in the 21** packaging of this file. Please review the following information to 22** ensure the GNU Lesser General Public License version 3 requirements 23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. 24** 25** GNU General Public License Usage 26** Alternatively, this file may be used under the terms of the GNU 27** General Public License version 2.0 or (at your option) the GNU General 28** Public license version 3 or any later version approved by the KDE Free 29** Qt Foundation. The licenses are as published by the Free Software 30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 31** included in the packaging of this file. Please review the following 32** information to ensure the GNU General Public License requirements will 33** be met: https://www.gnu.org/licenses/gpl-2.0.html and 34** https://www.gnu.org/licenses/gpl-3.0.html. 35** 36** $QT_END_LICENSE$ 37** 38****************************************************************************/ 39 40#include <ApplicationServices/ApplicationServices.h> 41 42#include "qcocoaprintdevice.h" 43 44#if QT_CONFIG(mimetype) 45#include <QtCore/qmimedatabase.h> 46#endif 47#include <qdebug.h> 48 49#include <QtCore/private/qcore_mac_p.h> 50 51QT_BEGIN_NAMESPACE 52 53#ifndef QT_NO_PRINTER 54 55// The CUPS PPD APIs were deprecated in CUPS 1.6/macOS 10.8, but 56// the replacement APIs are unfortunately not sufficient. See: 57// https://bugreports.qt.io/browse/QTBUG-56545 58#pragma clang diagnostic push 59#pragma clang diagnostic ignored "-Wdeprecated-declarations" 60 61static QPrint::DuplexMode macToDuplexMode(const PMDuplexMode &mode) 62{ 63 if (mode == kPMDuplexTumble) 64 return QPrint::DuplexShortSide; 65 else if (mode == kPMDuplexNoTumble) 66 return QPrint::DuplexLongSide; 67 else // kPMDuplexNone or kPMSimplexTumble 68 return QPrint::DuplexNone; 69} 70 71QCocoaPrintDevice::QCocoaPrintDevice() 72 : QPlatformPrintDevice(), 73 m_printer(nullptr), 74 m_session(nullptr), 75 m_ppd(nullptr) 76{ 77} 78 79QCocoaPrintDevice::QCocoaPrintDevice(const QString &id) 80 : QPlatformPrintDevice(id), 81 m_printer(nullptr), 82 m_session(nullptr), 83 m_ppd(nullptr) 84{ 85 if (!id.isEmpty()) { 86 m_printer = PMPrinterCreateFromPrinterID(id.toCFString()); 87 if (m_printer) { 88 m_name = QString::fromCFString(PMPrinterGetName(m_printer)); 89 m_location = QString::fromCFString(PMPrinterGetLocation(m_printer)); 90 CFStringRef cfMakeAndModel; 91 if (PMPrinterGetMakeAndModelName(m_printer, &cfMakeAndModel) == noErr) 92 m_makeAndModel = QString::fromCFString(cfMakeAndModel); 93 Boolean isRemote; 94 if (PMPrinterIsRemote(m_printer, &isRemote) == noErr) 95 m_isRemote = isRemote; 96 if (PMCreateSession(&m_session) == noErr) 97 PMSessionSetCurrentPMPrinter(m_session, m_printer); 98 99 // No native api to query these options, need to use PPD directly, note is deprecated from 1.6 onwards 100 if (openPpdFile()) { 101 // Note this is if the hardware does multiple copies, not if Cups can 102 m_supportsMultipleCopies = !m_ppd->manual_copies; 103 // Note this is if the hardware does collation, not if Cups can 104 ppd_option_t *collate = ppdFindOption(m_ppd, "Collate"); 105 if (collate) 106 m_supportsCollateCopies = true; 107 m_supportsCustomPageSizes = m_ppd->custom_max[0] > 0 && m_ppd->custom_max[1] > 0; 108 m_minimumPhysicalPageSize = QSize(m_ppd->custom_min[0], m_ppd->custom_min[1]); 109 m_maximumPhysicalPageSize = QSize(m_ppd->custom_max[0], m_ppd->custom_max[1]); 110 m_customMargins = QMarginsF(m_ppd->custom_margins[0], m_ppd->custom_margins[3], 111 m_ppd->custom_margins[2], m_ppd->custom_margins[1]); 112 } 113 } 114 } 115} 116 117QCocoaPrintDevice::~QCocoaPrintDevice() 118{ 119 if (m_ppd) 120 ppdClose(m_ppd); 121 for (PMPaper paper : m_macPapers) 122 PMRelease(paper); 123 // Releasing the session appears to also release the printer 124 if (m_session) 125 PMRelease(m_session); 126 else if (m_printer) 127 PMRelease(m_printer); 128} 129 130bool QCocoaPrintDevice::isValid() const 131{ 132 return m_printer ? true : false; 133} 134 135bool QCocoaPrintDevice::isDefault() const 136{ 137 return PMPrinterIsDefault(m_printer); 138} 139 140QPrint::DeviceState QCocoaPrintDevice::state() const 141{ 142 PMPrinterState state; 143 if (PMPrinterGetState(m_printer, &state) == noErr) { 144 if (state == kPMPrinterIdle) 145 return QPrint::Idle; 146 else if (state == kPMPrinterProcessing) 147 return QPrint::Active; 148 else if (state == kPMPrinterStopped) 149 return QPrint::Error; 150 } 151 return QPrint::Error; 152} 153 154QPageSize QCocoaPrintDevice::createPageSize(const PMPaper &paper) const 155{ 156 CFStringRef key; 157 double width; 158 double height; 159 CFStringRef localizedName; 160 if (PMPaperGetPPDPaperName(paper, &key) == noErr 161 && PMPaperGetWidth(paper, &width) == noErr 162 && PMPaperGetHeight(paper, &height) == noErr 163 && PMPaperCreateLocalizedName(paper, m_printer, &localizedName) == noErr) { 164 QPageSize pageSize = QPlatformPrintDevice::createPageSize(QString::fromCFString(key),QSize(width, height), 165 QString::fromCFString(localizedName)); 166 CFRelease(localizedName); 167 return pageSize; 168 } 169 return QPageSize(); 170} 171 172void QCocoaPrintDevice::loadPageSizes() const 173{ 174 m_pageSizes.clear(); 175 for (PMPaper paper : m_macPapers) 176 PMRelease(paper); 177 m_macPapers.clear(); 178 m_printableMargins.clear(); 179 CFArrayRef paperSizes; 180 if (PMPrinterGetPaperList(m_printer, &paperSizes) == noErr) { 181 int count = CFArrayGetCount(paperSizes); 182 for (int i = 0; i < count; ++i) { 183 PMPaper paper = static_cast<PMPaper>(const_cast<void *>(CFArrayGetValueAtIndex(paperSizes, i))); 184 QPageSize pageSize = createPageSize(paper); 185 if (pageSize.isValid()) { 186 m_pageSizes.append(pageSize); 187 PMRetain(paper); 188 m_macPapers.insert(pageSize.key(), paper); 189 PMPaperMargins printMargins; 190 PMPaperGetMargins(paper, &printMargins); 191 m_printableMargins.insert(pageSize.key(), QMarginsF(printMargins.left, printMargins.top, 192 printMargins.right, printMargins.bottom)); 193 } 194 } 195 } 196 m_havePageSizes = true; 197} 198 199QPageSize QCocoaPrintDevice::defaultPageSize() const 200{ 201 QPageSize pageSize; 202 PMPageFormat pageFormat; 203 PMPaper paper; 204 if (PMCreatePageFormat(&pageFormat) == noErr) { 205 if (PMSessionDefaultPageFormat(m_session, pageFormat) == noErr 206 && PMGetPageFormatPaper(pageFormat, &paper) == noErr) { 207 pageSize = createPageSize(paper); 208 } 209 PMRelease(pageFormat); 210 } 211 return pageSize; 212} 213 214QMarginsF QCocoaPrintDevice::printableMargins(const QPageSize &pageSize, 215 QPageLayout::Orientation orientation, 216 int resolution) const 217{ 218 Q_UNUSED(orientation) 219 Q_UNUSED(resolution) 220 if (!m_havePageSizes) 221 loadPageSizes(); 222 if (m_printableMargins.contains(pageSize.key())) 223 return m_printableMargins.value(pageSize.key()); 224 return m_customMargins; 225} 226 227void QCocoaPrintDevice::loadResolutions() const 228{ 229 m_resolutions.clear(); 230 UInt32 count; 231 if (PMPrinterGetPrinterResolutionCount(m_printer, &count) == noErr) { 232 // 1-based index 233 for (UInt32 i = 1; i <= count; ++i) { 234 PMResolution resolution; 235 if (PMPrinterGetIndexedPrinterResolution(m_printer, i, &resolution) == noErr) 236 m_resolutions.append(int(resolution.hRes)); 237 } 238 } 239 m_haveResolutions = true; 240} 241 242int QCocoaPrintDevice::defaultResolution() const 243{ 244 int defaultResolution = 72; 245 PMPrintSettings settings; 246 if (PMCreatePrintSettings(&settings) == noErr) { 247 PMResolution resolution; 248 if (PMSessionDefaultPrintSettings(m_session, settings) == noErr 249 && PMPrinterGetOutputResolution(m_printer, settings, &resolution) == noErr) { 250 // PMPrinterGetOutputResolution usually fails with -9589 kPMKeyNotFound as not set in PPD 251 defaultResolution = int(resolution.hRes); 252 } 253 PMRelease(settings); 254 } 255 // If no value returned (usually means not set in PPD) then use supported resolutions which 256 // OSX will have populated with at least one default value (but why not returned by call?) 257 if (defaultResolution <= 0) { 258 if (!m_haveResolutions) 259 loadResolutions(); 260 if (m_resolutions.count() > 0) 261 return m_resolutions.at(0); // First value or highest? Only likely to be one anyway. 262 return 72; // TDOD More sensible default value??? 263 } 264 return defaultResolution; 265} 266 267void QCocoaPrintDevice::loadInputSlots() const 268{ 269 // NOTE: Implemented in both CUPS and Mac plugins, please keep in sync 270 // TODO Deal with concatenated names like Tray1Manual or Tray1_Man, 271 // will currently show as CustomInputSlot 272 // TODO Deal with separate ManualFeed key 273 // Try load standard PPD options first 274 m_inputSlots.clear(); 275 if (m_ppd) { 276 ppd_option_t *inputSlots = ppdFindOption(m_ppd, "InputSlot"); 277 if (inputSlots) { 278 for (int i = 0; i < inputSlots->num_choices; ++i) 279 m_inputSlots.append(QPrintUtils::ppdChoiceToInputSlot(inputSlots->choices[i])); 280 } 281 // If no result, try just the default 282 if (m_inputSlots.size() == 0) { 283 inputSlots = ppdFindOption(m_ppd, "DefaultInputSlot"); 284 if (inputSlots) 285 m_inputSlots.append(QPrintUtils::ppdChoiceToInputSlot(inputSlots->choices[0])); 286 } 287 } 288 // If still no result, just use Auto 289 if (m_inputSlots.size() == 0) 290 m_inputSlots.append(QPlatformPrintDevice::defaultInputSlot()); 291 m_haveInputSlots = true; 292} 293 294QPrint::InputSlot QCocoaPrintDevice::defaultInputSlot() const 295{ 296 // No native api to query, use PPD directly 297 // NOTE: Implemented in both CUPS and Mac plugins, please keep in sync 298 // Try load standard PPD option first 299 if (m_ppd) { 300 ppd_option_t *inputSlot = ppdFindOption(m_ppd, "DefaultInputSlot"); 301 if (inputSlot) 302 return QPrintUtils::ppdChoiceToInputSlot(inputSlot->choices[0]); 303 // If no result, then try a marked option 304 ppd_choice_t *defaultChoice = ppdFindMarkedChoice(m_ppd, "InputSlot"); 305 if (defaultChoice) 306 return QPrintUtils::ppdChoiceToInputSlot(*defaultChoice); 307 } 308 // Otherwise return Auto 309 return QPlatformPrintDevice::defaultInputSlot(); 310} 311 312void QCocoaPrintDevice::loadOutputBins() const 313{ 314 // No native api to query, use PPD directly 315 // NOTE: Implemented in both CUPS and Mac plugins, please keep in sync 316 m_outputBins.clear(); 317 if (m_ppd) { 318 ppd_option_t *outputBins = ppdFindOption(m_ppd, "OutputBin"); 319 if (outputBins) { 320 for (int i = 0; i < outputBins->num_choices; ++i) 321 m_outputBins.append(QPrintUtils::ppdChoiceToOutputBin(outputBins->choices[i])); 322 } 323 // If no result, try just the default 324 if (m_outputBins.size() == 0) { 325 outputBins = ppdFindOption(m_ppd, "DefaultOutputBin"); 326 if (outputBins) 327 m_outputBins.append(QPrintUtils::ppdChoiceToOutputBin(outputBins->choices[0])); 328 } 329 } 330 // If still no result, just use Auto 331 if (m_outputBins.size() == 0) 332 m_outputBins.append(QPlatformPrintDevice::defaultOutputBin()); 333 m_haveOutputBins = true; 334} 335 336QPrint::OutputBin QCocoaPrintDevice::defaultOutputBin() const 337{ 338 // No native api to query, use PPD directly 339 // NOTE: Implemented in both CUPS and Mac plugins, please keep in sync 340 // Try load standard PPD option first 341 if (m_ppd) { 342 ppd_option_t *outputBin = ppdFindOption(m_ppd, "DefaultOutputBin"); 343 if (outputBin) 344 return QPrintUtils::ppdChoiceToOutputBin(outputBin->choices[0]); 345 // If no result, then try a marked option 346 ppd_choice_t *defaultChoice = ppdFindMarkedChoice(m_ppd, "OutputBin"); 347 if (defaultChoice) 348 return QPrintUtils::ppdChoiceToOutputBin(*defaultChoice); 349 } 350 // Otherwise return AutoBin 351 return QPlatformPrintDevice::defaultOutputBin(); 352} 353 354void QCocoaPrintDevice::loadDuplexModes() const 355{ 356 // No native api to query, use PPD directly 357 // NOTE: Implemented in both CUPS and Mac plugins, please keep in sync 358 // Try load standard PPD options first 359 m_duplexModes.clear(); 360 if (m_ppd) { 361 ppd_option_t *duplexModes = ppdFindOption(m_ppd, "Duplex"); 362 if (duplexModes) { 363 for (int i = 0; i < duplexModes->num_choices; ++i) 364 m_duplexModes.append(QPrintUtils::ppdChoiceToDuplexMode(duplexModes->choices[i].choice)); 365 } 366 // If no result, try just the default 367 if (m_duplexModes.size() == 0) { 368 duplexModes = ppdFindOption(m_ppd, "DefaultDuplex"); 369 if (duplexModes) 370 m_duplexModes.append(QPrintUtils::ppdChoiceToDuplexMode(duplexModes->choices[0].choice)); 371 } 372 } 373 // If still no result, or not added in PPD, then add None 374 if (m_duplexModes.size() == 0 || !m_duplexModes.contains(QPrint::DuplexNone)) 375 m_duplexModes.append(QPrint::DuplexNone); 376 // If have both modes, then can support DuplexAuto 377 if (m_duplexModes.contains(QPrint::DuplexLongSide) && m_duplexModes.contains(QPrint::DuplexShortSide)) 378 m_duplexModes.append(QPrint::DuplexAuto); 379 m_haveDuplexModes = true; 380} 381 382QPrint::DuplexMode QCocoaPrintDevice::defaultDuplexMode() const 383{ 384 QPrint::DuplexMode defaultMode = QPrint::DuplexNone; 385 PMPrintSettings settings; 386 if (PMCreatePrintSettings(&settings) == noErr) { 387 PMDuplexMode duplexMode; 388 if (PMSessionDefaultPrintSettings(m_session, settings) == noErr 389 && PMGetDuplex(settings, &duplexMode) == noErr) { 390 defaultMode = macToDuplexMode(duplexMode); 391 } 392 PMRelease(settings); 393 } 394 return defaultMode; 395} 396 397void QCocoaPrintDevice::loadColorModes() const 398{ 399 // No native api to query, use PPD directly 400 m_colorModes.clear(); 401 m_colorModes.append(QPrint::GrayScale); 402 if (!m_ppd || (m_ppd && m_ppd->color_device)) 403 m_colorModes.append(QPrint::Color); 404 m_haveColorModes = true; 405} 406 407QPrint::ColorMode QCocoaPrintDevice::defaultColorMode() const 408{ 409 // No native api to query, use PPD directly 410 // NOTE: Implemented in both CUPS and Mac plugins, please keep in sync 411 // Not a proper option, usually only know if supports color or not, but some 412 // users known to abuse ColorModel to always force GrayScale. 413 if (m_ppd && supportedColorModes().contains(QPrint::Color)) { 414 ppd_option_t *colorModel = ppdFindOption(m_ppd, "DefaultColorModel"); 415 if (!colorModel) 416 colorModel = ppdFindOption(m_ppd, "ColorModel"); 417 if (!colorModel || qstrcmp(colorModel->defchoice, "Gray") != 0) 418 return QPrint::Color; 419 } 420 return QPrint::GrayScale; 421} 422 423#if QT_CONFIG(mimetype) 424void QCocoaPrintDevice::loadMimeTypes() const 425{ 426 // TODO Check how settings affect returned list 427 m_mimeTypes.clear(); 428 QMimeDatabase db; 429 PMPrintSettings settings; 430 if (PMCreatePrintSettings(&settings) == noErr) { 431 CFArrayRef mimeTypes; 432 if (PMPrinterGetMimeTypes(m_printer, settings, &mimeTypes) == noErr) { 433 int count = CFArrayGetCount(mimeTypes); 434 for (int i = 0; i < count; ++i) { 435 CFStringRef mimeName = static_cast<CFStringRef>(const_cast<void *>(CFArrayGetValueAtIndex(mimeTypes, i))); 436 QMimeType mimeType = db.mimeTypeForName(QString::fromCFString(mimeName)); 437 if (mimeType.isValid()) 438 m_mimeTypes.append(mimeType); 439 } 440 } 441 PMRelease(settings); 442 } 443 m_haveMimeTypes = true; 444} 445#endif // mimetype 446 447bool QCocoaPrintDevice::openPpdFile() 448{ 449 if (m_ppd) 450 ppdClose(m_ppd); 451 m_ppd = nullptr; 452 CFURLRef ppdURL = nullptr; 453 char ppdPath[MAXPATHLEN]; 454 if (PMPrinterCopyDescriptionURL(m_printer, kPMPPDDescriptionType, &ppdURL) == noErr 455 && ppdURL) { 456 if (CFURLGetFileSystemRepresentation(ppdURL, true, (UInt8*)ppdPath, sizeof(ppdPath))) 457 m_ppd = ppdOpenFile(ppdPath); 458 CFRelease(ppdURL); 459 } 460 return m_ppd ? true : false; 461} 462 463PMPrinter QCocoaPrintDevice::macPrinter() const 464{ 465 return m_printer; 466} 467 468// Returns a cached printer PMPaper, or creates and caches a new custom PMPaper 469// Caller should never release a cached PMPaper! 470PMPaper QCocoaPrintDevice::macPaper(const QPageSize &pageSize) const 471{ 472 if (!m_havePageSizes) 473 loadPageSizes(); 474 // If keys match, then is a supported size or an existing custom size 475 if (m_macPapers.contains(pageSize.key())) 476 return m_macPapers.value(pageSize.key()); 477 // For any other page size, whether custom or just unsupported, needs to be a custom PMPaper 478 PMPaper paper = nullptr; 479 PMPaperMargins paperMargins; 480 paperMargins.left = m_customMargins.left(); 481 paperMargins.right = m_customMargins.right(); 482 paperMargins.top = m_customMargins.top(); 483 paperMargins.bottom = m_customMargins.bottom(); 484 PMPaperCreateCustom(m_printer, QCFString(pageSize.key()), QCFString(pageSize.name()), 485 pageSize.sizePoints().width(), pageSize.sizePoints().height(), 486 &paperMargins, &paper); 487 m_macPapers.insert(pageSize.key(), paper); 488 return paper; 489} 490 491#pragma clang diagnostic pop 492 493#endif // QT_NO_PRINTER 494 495QT_END_NAMESPACE 496