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