// Copyright 2012 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "printing/backend/print_backend.h" #include #include #include #include #include #include #include "base/logging.h" #include "base/memory/free_deleter.h" #include "base/numerics/safe_conversions.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_piece.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "base/threading/scoped_blocking_call.h" #include "base/types/expected.h" #include "base/win/scoped_bstr.h" #include "base/win/scoped_hdc.h" #include "base/win/scoped_hglobal.h" #include "base/win/windows_types.h" #include "printing/backend/print_backend_consts.h" #include "printing/backend/printing_info_win.h" #include "printing/backend/spooler_win.h" #include "printing/backend/win_helper.h" #include "printing/mojom/print.mojom.h" #include "printing/printing_utils.h" #include "printing/units.h" #include "ui/gfx/geometry/rect.h" #include "ui/gfx/geometry/size.h" namespace printing { namespace { // Wrapper class to close provider automatically. class ScopedProvider { public: explicit ScopedProvider(HPTPROVIDER provider) : provider_(provider) {} // Once the object is destroyed, it automatically closes the provider by // calling the XPSModule API. ~ScopedProvider() { if (provider_) XPSModule::CloseProvider(provider_); } private: HPTPROVIDER provider_; }; std::string ErrorMessageCheckSpooler(const std::string& base_message, logging::SystemErrorCode err) { std::string message = base_message; if (err != ERROR_SUCCESS) { message += logging::SystemErrorCodeToString(err); } if (internal::IsSpoolerRunning() != internal::SpoolerServiceStatus::kRunning) { message += " Windows print spooler is not running"; } else if (err == ERROR_SUCCESS) { message += " unknown internal printing error"; } return message; } // `GetResultCodeFromSystemErrorCode()` is only ever invoked when something has // gone wrong while interacting with the OS printing system. If the cause of // the failure was not of the type to register and be and available from // `GetLastError()` then we should just use the general error result. mojom::ResultCode GetResultCodeFromSystemErrorCode( logging::SystemErrorCode system_code) { if (system_code == ERROR_ACCESS_DENIED) return mojom::ResultCode::kAccessDenied; return mojom::ResultCode::kFailed; } ScopedPrinterHandle GetPrinterHandle(const std::string& printer_name) { ScopedPrinterHandle handle; handle.OpenPrinterWithName(base::UTF8ToWide(printer_name).c_str()); return handle; } HRESULT StreamOnHGlobalToString(IStream* stream, std::string* out) { DCHECK(stream); DCHECK(out); HGLOBAL hdata = nullptr; HRESULT hr = GetHGlobalFromStream(stream, &hdata); if (SUCCEEDED(hr)) { DCHECK(hdata); base::win::ScopedHGlobal locked_data(hdata); out->assign(locked_data.release(), locked_data.Size()); } return hr; } template void GetDeviceCapabilityArray(const wchar_t* printer, const wchar_t* port, WORD id, std::vector* result) { int count = DeviceCapabilities(printer, port, id, nullptr, nullptr); if (count <= 0) return; std::vector tmp; tmp.resize(count * 2); count = DeviceCapabilities(printer, port, id, reinterpret_cast(tmp.data()), nullptr); if (count <= 0) return; CHECK_LE(static_cast(count), tmp.size()); tmp.resize(count); result->swap(tmp); } gfx::Size GetDefaultDpi(HDC hdc) { int dpi_x = GetDeviceCaps(hdc, LOGPIXELSX); int dpi_y = GetDeviceCaps(hdc, LOGPIXELSY); return gfx::Size(dpi_x, dpi_y); } gfx::Rect LoadPaperPrintableAreaUm(const wchar_t* printer, DEVMODE* devmode) { base::win::ScopedCreateDC hdc( CreateDC(L"WINSPOOL", printer, nullptr, devmode)); gfx::Size default_dpi = GetDefaultDpi(hdc.get()); gfx::Rect printable_area_device_units = GetPrintableAreaDeviceUnits(hdc.get()); // Device units can be non-square, so scale for non-square pixels and convert // to microns. gfx::Rect printable_area_um = gfx::Rect(ConvertUnit(printable_area_device_units.x(), default_dpi.width(), kMicronsPerInch), ConvertUnit(printable_area_device_units.y(), default_dpi.height(), kMicronsPerInch), ConvertUnit(printable_area_device_units.width(), default_dpi.width(), kMicronsPerInch), ConvertUnit(printable_area_device_units.height(), default_dpi.height(), kMicronsPerInch)); return printable_area_um; } void LoadPaper(const wchar_t* printer, const wchar_t* port, DEVMODE* devmode, PrinterSemanticCapsAndDefaults* caps) { static const size_t kToUm = 100; // Windows uses 0.1mm. static const size_t kMaxPaperName = 64; struct PaperName { wchar_t chars[kMaxPaperName]; }; DCHECK_EQ(sizeof(PaperName), sizeof(wchar_t) * kMaxPaperName); // Paper std::vector names; GetDeviceCapabilityArray(printer, port, DC_PAPERNAMES, &names); std::vector sizes; GetDeviceCapabilityArray(printer, port, DC_PAPERSIZE, &sizes); std::vector ids; GetDeviceCapabilityArray(printer, port, DC_PAPERS, &ids); DCHECK_EQ(ids.size(), sizes.size()); DCHECK_EQ(names.size(), sizes.size()); if (ids.size() != sizes.size()) ids.clear(); if (names.size() != sizes.size()) names.clear(); for (size_t i = 0; i < sizes.size(); ++i) { const gfx::Size size_um(sizes[i].x * kToUm, sizes[i].y * kToUm); // Skip papers with empty paper sizes. if (size_um.IsEmpty()) { continue; } std::string display_name; if (!names.empty()) { const wchar_t* name_start = names[i].chars; std::wstring tmp_name(name_start, kMaxPaperName); // Trim trailing zeros. tmp_name = tmp_name.c_str(); display_name = base::WideToUTF8(tmp_name); } std::string vendor_id; gfx::Rect printable_area_um; if (!ids.empty()) { vendor_id = base::NumberToString(ids[i]); // `LoadPaperPrintableAreaUm()` has to create a new device context, which // is very expensive for some printer drivers. Since this is in an // inner loop for paper sizes, this can have a significant impact on // Print Preview behavior, locking it up with no response for an order // of tens of seconds. This size of impact of this is driver dependent, // and in many cases is not of notice for most users. // // Due to the high cost of some printer drivers, only retrieve the // printable area for the default paper size. Callers can make use of // `GetPaperPrintableArea()` to get the printable area for other paper // sizes as needed. // // TODO(crbug.com/1424368): Remove this limitation compared to other // platforms if an alternate way of getting the printable area for all // paper sizes can be done without a huge performance penalty. For // now this workaround is only made for in-browser queries. if (devmode && (devmode->dmPaperSize == ids[i])) { printable_area_um = LoadPaperPrintableAreaUm(printer, devmode); } } // Default to the paper size if printable area is missing. // We've seen some drivers have a printable area that goes out of bounds of // the paper size. In those cases, set the printable area to be the size. // (See crbug.com/1412305.) const gfx::Rect size_um_rect(size_um); if (printable_area_um.IsEmpty() || !size_um_rect.Contains(printable_area_um)) { printable_area_um = size_um_rect; } caps->papers.push_back(PrinterSemanticCapsAndDefaults::Paper( display_name, vendor_id, size_um, printable_area_um)); } if (!devmode) return; // Copy paper with the same ID as default paper. if (devmode->dmFields & DM_PAPERSIZE) { std::string default_vendor_id = base::NumberToString(devmode->dmPaperSize); for (const PrinterSemanticCapsAndDefaults::Paper& paper : caps->papers) { if (paper.vendor_id() == default_vendor_id) { caps->default_paper = paper; break; } } } gfx::Size default_size; if (devmode->dmFields & DM_PAPERWIDTH) default_size.set_width(devmode->dmPaperWidth * kToUm); if (devmode->dmFields & DM_PAPERLENGTH) default_size.set_height(devmode->dmPaperLength * kToUm); // Reset default paper if `dmPaperWidth` or `dmPaperLength` does not match // default paper set by `dmPaperSize`. if (!default_size.IsEmpty() && default_size != caps->default_paper.size_um()) { caps->default_paper = PrinterSemanticCapsAndDefaults::Paper( /*display_name=*/"", /*vendor_id=*/"", default_size); } } void LoadDpi(const wchar_t* printer, const wchar_t* port, const DEVMODE* devmode, PrinterSemanticCapsAndDefaults* caps) { std::vector dpis; GetDeviceCapabilityArray(printer, port, DC_ENUMRESOLUTIONS, &dpis); for (size_t i = 0; i < dpis.size(); ++i) caps->dpis.push_back(gfx::Size(dpis[i].x, dpis[i].y)); if (devmode && (devmode->dmFields & DM_PRINTQUALITY) && devmode->dmPrintQuality > 0) { caps->default_dpi.SetSize(devmode->dmPrintQuality, devmode->dmPrintQuality); if (devmode->dmFields & DM_YRESOLUTION) { caps->default_dpi.set_height(devmode->dmYResolution); } } // If there's no DPI in the list, add the default DPI to the list. if (dpis.empty()) { if (caps->default_dpi.IsEmpty()) { base::win::ScopedCreateDC hdc( CreateDC(L"WINSPOOL", printer, nullptr, devmode)); caps->default_dpi = GetDefaultDpi(hdc.get()); } caps->dpis.push_back(caps->default_dpi); } } } // namespace class PrintBackendWin : public PrintBackend { public: PrintBackendWin() = default; // PrintBackend implementation. mojom::ResultCode EnumeratePrinters(PrinterList& printer_list) override; mojom::ResultCode GetDefaultPrinterName( std::string& default_printer) override; mojom::ResultCode GetPrinterBasicInfo( const std::string& printer_name, PrinterBasicInfo* printer_info) override; mojom::ResultCode GetPrinterSemanticCapsAndDefaults( const std::string& printer_name, PrinterSemanticCapsAndDefaults* printer_info) override; mojom::ResultCode GetPrinterCapsAndDefaults( const std::string& printer_name, PrinterCapsAndDefaults* printer_info) override; absl::optional GetPaperPrintableArea( const std::string& printer_name, const std::string& paper_vendor_id, const gfx::Size& paper_size_um) override; std::vector GetPrinterDriverInfo( const std::string& printer_name) override; bool IsValidPrinter(const std::string& printer_name) override; protected: ~PrintBackendWin() override = default; }; mojom::ResultCode PrintBackendWin::EnumeratePrinters( PrinterList& printer_list) { DCHECK(printer_list.empty()); DWORD bytes_needed = 0; DWORD count_returned = 0; constexpr DWORD kFlags = PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS; const DWORD kLevel = 4; EnumPrinters(kFlags, nullptr, kLevel, nullptr, 0, &bytes_needed, &count_returned); logging::SystemErrorCode code = logging::GetLastSystemErrorCode(); if (code == ERROR_SUCCESS) { // If EnumPrinters() succeeded, that means there are no printer drivers // installed because 0 bytes was sufficient. DCHECK_EQ(bytes_needed, 0u); VLOG(1) << "Found no printers"; return mojom::ResultCode::kSuccess; } if (code != ERROR_INSUFFICIENT_BUFFER) { LOG(ERROR) << "Error enumerating printers: " << logging::SystemErrorCodeToString(code); return GetResultCodeFromSystemErrorCode(code); } auto printer_info_buffer = std::make_unique(bytes_needed); if (!EnumPrinters(kFlags, nullptr, kLevel, printer_info_buffer.get(), bytes_needed, &bytes_needed, &count_returned)) { NOTREACHED(); return GetResultCodeFromSystemErrorCode(logging::GetLastSystemErrorCode()); } // No need to worry about a query failure for `GetDefaultPrinterName()` here, // that would mean we can just treat it as there being no default printer. std::string default_printer; GetDefaultPrinterName(default_printer); PRINTER_INFO_4* printer_info = reinterpret_cast(printer_info_buffer.get()); for (DWORD index = 0; index < count_returned; index++) { ScopedPrinterHandle printer; PrinterBasicInfo info; if (printer.OpenPrinterWithName(printer_info[index].pPrinterName) && InitBasicPrinterInfo(printer.Get(), &info)) { info.is_default = (info.printer_name == default_printer); printer_list.push_back(info); } } VLOG(1) << "Found " << count_returned << " printers"; return mojom::ResultCode::kSuccess; } mojom::ResultCode PrintBackendWin::GetDefaultPrinterName( std::string& default_printer) { DWORD size = MAX_PATH; TCHAR default_printer_name[MAX_PATH]; base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, base::BlockingType::MAY_BLOCK); if (!::GetDefaultPrinter(default_printer_name, &size)) { LOG(ERROR) << ErrorMessageCheckSpooler("Error getting default printer: ", logging::GetLastSystemErrorCode()); return mojom::ResultCode::kFailed; } default_printer = base::WideToUTF8(default_printer_name); return mojom::ResultCode::kSuccess; } mojom::ResultCode PrintBackendWin::GetPrinterBasicInfo( const std::string& printer_name, PrinterBasicInfo* printer_info) { ScopedPrinterHandle printer_handle = GetPrinterHandle(printer_name); if (!printer_handle.IsValid()) return GetResultCodeFromSystemErrorCode(logging::GetLastSystemErrorCode()); if (!InitBasicPrinterInfo(printer_handle.Get(), printer_info)) { // InitBasicPrinterInfo() doesn't set a system error code, so just treat as // general failure. return mojom::ResultCode::kFailed; } std::string default_printer; mojom::ResultCode result = GetDefaultPrinterName(default_printer); if (result != mojom::ResultCode::kSuccess) { // Query failure means we can treat this printer as not the default. printer_info->is_default = false; } else { printer_info->is_default = (printer_info->printer_name == default_printer); } return mojom::ResultCode::kSuccess; } mojom::ResultCode PrintBackendWin::GetPrinterSemanticCapsAndDefaults( const std::string& printer_name, PrinterSemanticCapsAndDefaults* printer_info) { ScopedPrinterHandle printer_handle = GetPrinterHandle(printer_name); if (!printer_handle.IsValid()) { logging::SystemErrorCode err = logging::GetLastSystemErrorCode(); LOG(WARNING) << "Failed to open printer, error = " << logging::SystemErrorCodeToString(err); return GetResultCodeFromSystemErrorCode(err); } PrinterInfo5 info_5; if (!info_5.Init(printer_handle.Get())) return GetResultCodeFromSystemErrorCode(logging::GetLastSystemErrorCode()); const wchar_t* name = info_5.get()->pPrinterName; const wchar_t* port = info_5.get()->pPortName; DCHECK_EQ(name, base::UTF8ToWide(printer_name)); PrinterSemanticCapsAndDefaults caps; std::unique_ptr user_settings = CreateDevMode(printer_handle.Get(), nullptr); if (user_settings) { caps.color_default = IsDevModeWithColor(user_settings.get()); if (user_settings->dmFields & DM_DUPLEX) { switch (user_settings->dmDuplex) { case DMDUP_SIMPLEX: caps.duplex_default = mojom::DuplexMode::kSimplex; break; case DMDUP_VERTICAL: caps.duplex_default = mojom::DuplexMode::kLongEdge; break; case DMDUP_HORIZONTAL: caps.duplex_default = mojom::DuplexMode::kShortEdge; break; default: NOTREACHED(); } } if (user_settings->dmFields & DM_COLLATE) caps.collate_default = (user_settings->dmCollate == DMCOLLATE_TRUE); } else { LOG(WARNING) << "Fallback to color/simplex mode."; caps.color_default = caps.color_changeable; caps.duplex_default = mojom::DuplexMode::kSimplex; } // Get printer capabilities. For more info see here: // http://msdn.microsoft.com/en-us/library/windows/desktop/dd183552(v=vs.85).aspx caps.color_changeable = (DeviceCapabilities(name, port, DC_COLORDEVICE, nullptr, nullptr) == 1); caps.color_model = mojom::ColorModel::kColor; caps.bw_model = mojom::ColorModel::kGray; caps.duplex_modes.push_back(mojom::DuplexMode::kSimplex); if (DeviceCapabilities(name, port, DC_DUPLEX, nullptr, nullptr) == 1) { caps.duplex_modes.push_back(mojom::DuplexMode::kLongEdge); caps.duplex_modes.push_back(mojom::DuplexMode::kShortEdge); } caps.collate_capable = (DeviceCapabilities(name, port, DC_COLLATE, nullptr, nullptr) == 1); caps.copies_max = std::max(1, DeviceCapabilities(name, port, DC_COPIES, nullptr, nullptr)); LoadPaper(name, port, user_settings.get(), &caps); LoadDpi(name, port, user_settings.get(), &caps); *printer_info = caps; return mojom::ResultCode::kSuccess; } mojom::ResultCode PrintBackendWin::GetPrinterCapsAndDefaults( const std::string& printer_name, PrinterCapsAndDefaults* printer_info) { DCHECK(printer_info); ScopedXPSInitializer xps_initializer; CHECK(xps_initializer.initialized()); if (!IsValidPrinter(printer_name)) return GetResultCodeFromSystemErrorCode(logging::GetLastSystemErrorCode()); HPTPROVIDER provider = nullptr; std::wstring wide_printer_name = base::UTF8ToWide(printer_name); HRESULT hr = XPSModule::OpenProvider(wide_printer_name, 1, &provider); if (!provider) return mojom::ResultCode::kSuccess; { Microsoft::WRL::ComPtr print_capabilities_stream; hr = CreateStreamOnHGlobal(nullptr, TRUE, &print_capabilities_stream); DCHECK(SUCCEEDED(hr)); if (print_capabilities_stream.Get()) { base::win::ScopedBstr error; hr = XPSModule::GetPrintCapabilities( provider, nullptr, print_capabilities_stream.Get(), error.Receive()); DCHECK(SUCCEEDED(hr)); if (FAILED(hr)) { // Failures from getting print capabilities don't give a system error, // so just indicate general failure. return mojom::ResultCode::kFailed; } hr = StreamOnHGlobalToString(print_capabilities_stream.Get(), &printer_info->printer_capabilities); DCHECK(SUCCEEDED(hr)); printer_info->caps_mime_type = "text/xml"; } ScopedPrinterHandle printer_handle; if (printer_handle.OpenPrinterWithName(wide_printer_name.c_str())) { std::unique_ptr devmode_out( CreateDevMode(printer_handle.Get(), nullptr)); if (!devmode_out) { return GetResultCodeFromSystemErrorCode( logging::GetLastSystemErrorCode()); } Microsoft::WRL::ComPtr printer_defaults_stream; hr = CreateStreamOnHGlobal(nullptr, TRUE, &printer_defaults_stream); DCHECK(SUCCEEDED(hr)); if (printer_defaults_stream.Get()) { DWORD dm_size = devmode_out->dmSize + devmode_out->dmDriverExtra; hr = XPSModule::ConvertDevModeToPrintTicket( provider, dm_size, devmode_out.get(), kPTJobScope, printer_defaults_stream.Get()); DCHECK(SUCCEEDED(hr)); if (SUCCEEDED(hr)) { hr = StreamOnHGlobalToString(printer_defaults_stream.Get(), &printer_info->printer_defaults); DCHECK(SUCCEEDED(hr)); printer_info->defaults_mime_type = "text/xml"; } } } XPSModule::CloseProvider(provider); } return mojom::ResultCode::kSuccess; } absl::optional PrintBackendWin::GetPaperPrintableArea( const std::string& printer_name, const std::string& paper_vendor_id, const gfx::Size& paper_size_um) { ScopedPrinterHandle printer_handle = GetPrinterHandle(printer_name); if (!printer_handle.IsValid()) { return absl::nullopt; } std::unique_ptr devmode = CreateDevMode(printer_handle.Get(), nullptr); if (!devmode) { return absl::nullopt; } unsigned id = 0; // If the paper size is a custom user size, setting by ID may not work. if (base::StringToUint(paper_vendor_id, &id) && id && id < DMPAPER_USER) { devmode->dmFields |= DM_PAPERSIZE; devmode->dmPaperSize = static_cast(id); } else if (!paper_size_um.IsEmpty()) { static constexpr int kTenthsOfMillimetersPerInch = 254; devmode->dmFields |= DM_PAPERWIDTH | DM_PAPERLENGTH; devmode->dmPaperWidth = ConvertUnit(paper_size_um.width(), kMicronsPerInch, kTenthsOfMillimetersPerInch); devmode->dmPaperLength = ConvertUnit( paper_size_um.height(), kMicronsPerInch, kTenthsOfMillimetersPerInch); } return LoadPaperPrintableAreaUm(base::UTF8ToWide(printer_name).c_str(), devmode.get()); } // Gets the information about driver for a specific printer. std::vector PrintBackendWin::GetPrinterDriverInfo( const std::string& printer_name) { ScopedPrinterHandle printer = GetPrinterHandle(printer_name); return printer.IsValid() ? GetDriverInfo(printer.Get()) : std::vector(); } bool PrintBackendWin::IsValidPrinter(const std::string& printer_name) { ScopedPrinterHandle printer_handle = GetPrinterHandle(printer_name); return printer_handle.IsValid(); } // static scoped_refptr PrintBackend::CreateInstanceImpl( const std::string& /*locale*/) { return base::MakeRefCounted(); } base::expected PrintBackend::GetXmlPrinterCapabilitiesForXpsDriver( const std::string& printer_name) { ScopedXPSInitializer xps_initializer; CHECK(xps_initializer.initialized()); if (!IsValidPrinter(printer_name)) { return base::unexpected( GetResultCodeFromSystemErrorCode(logging::GetLastSystemErrorCode())); } HPTPROVIDER provider = nullptr; std::wstring wide_printer_name = base::UTF8ToWide(printer_name); HRESULT hr = XPSModule::OpenProvider(wide_printer_name, /*version=*/1, &provider); ScopedProvider scoped_provider(provider); if (FAILED(hr) || !provider) { LOG(ERROR) << "Failed to open provider"; return base::unexpected(mojom::ResultCode::kFailed); } Microsoft::WRL::ComPtr print_capabilities_stream; hr = CreateStreamOnHGlobal(/*hGlobal=*/nullptr, /*fDeleteOnRelease=*/TRUE, &print_capabilities_stream); if (FAILED(hr) || !print_capabilities_stream.Get()) { LOG(ERROR) << "Failed to create stream"; return base::unexpected(mojom::ResultCode::kFailed); } base::win::ScopedBstr error; hr = XPSModule::GetPrintCapabilities(provider, /*print_ticket=*/nullptr, print_capabilities_stream.Get(), error.Receive()); if (FAILED(hr)) { LOG(ERROR) << "Failed to get print capabilities"; // Failures from getting print capabilities don't give a system error, // so just indicate general failure. return base::unexpected(mojom::ResultCode::kFailed); } std::string capabilities_xml; hr = StreamOnHGlobalToString(print_capabilities_stream.Get(), &capabilities_xml); if (FAILED(hr)) { LOG(ERROR) << "Failed to convert stream to string"; return base::unexpected(mojom::ResultCode::kFailed); } DVLOG(2) << "Printer capabilities info: Name = " << printer_name << ", capabilities = " << capabilities_xml; return capabilities_xml; } } // namespace printing