[go: nahoru, domu]

blob: e8f6907e1f32f8e68ac582128d5a3cc5d73a8a4b [file] [log] [blame]
// 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 <objidl.h>
#include <stddef.h>
#include <winspool.h>
#include <wrl/client.h>
#include <memory>
#include <utility>
#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;
}
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<char*> locked_data(hdata);
out->assign(locked_data.data(), locked_data.size());
}
return hr;
}
template <class T>
void GetDeviceCapabilityArray(const wchar_t* printer,
const wchar_t* port,
WORD id,
std::vector<T>* result) {
int count = DeviceCapabilities(printer, port, id, nullptr, nullptr);
if (count <= 0)
return;
std::vector<T> tmp;
tmp.resize(count * 2);
count = DeviceCapabilities(printer, port, id,
reinterpret_cast<LPTSTR>(tmp.data()), nullptr);
if (count <= 0)
return;
CHECK_LE(static_cast<size_t>(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<PaperName> names;
GetDeviceCapabilityArray(printer, port, DC_PAPERNAMES, &names);
std::vector<POINT> sizes;
GetDeviceCapabilityArray(printer, port, DC_PAPERSIZE, &sizes);
std::vector<WORD> 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<POINT> 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;
std::optional<gfx::Rect> GetPaperPrintableArea(
const std::string& printer_name,
const std::string& paper_vendor_id,
const gfx::Size& paper_size_um) override;
std::vector<std::string> 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<BYTE[]>(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_4*>(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)) {
logging::SystemErrorCode err = logging::GetLastSystemErrorCode();
if (err != ERROR_FILE_NOT_FOUND) {
LOG(ERROR) << ErrorMessageCheckSpooler("Error getting default printer: ",
err);
return mojom::ResultCode::kFailed;
}
// There is no default printer, which is not treated as a failure.
default_printer = std::string();
} else {
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<DEVMODE, base::FreeDeleter> 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<IStream> 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, base::FreeDeleter> devmode_out(
CreateDevMode(printer_handle.Get(), nullptr));
if (!devmode_out) {
return GetResultCodeFromSystemErrorCode(
logging::GetLastSystemErrorCode());
}
Microsoft::WRL::ComPtr<IStream> 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;
}
std::optional<gfx::Rect> 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 std::nullopt;
}
std::unique_ptr<DEVMODE, base::FreeDeleter> devmode =
CreateDevMode(printer_handle.Get(), nullptr);
if (!devmode) {
return std::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<short>(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<std::string> PrintBackendWin::GetPrinterDriverInfo(
const std::string& printer_name) {
ScopedPrinterHandle printer = GetPrinterHandle(printer_name);
return printer.IsValid() ? GetDriverInfo(printer.Get())
: std::vector<std::string>();
}
bool PrintBackendWin::IsValidPrinter(const std::string& printer_name) {
ScopedPrinterHandle printer_handle = GetPrinterHandle(printer_name);
return printer_handle.IsValid();
}
// static
scoped_refptr<PrintBackend> PrintBackend::CreateInstanceImpl(
const std::string& /*locale*/) {
return base::MakeRefCounted<PrintBackendWin>();
}
base::expected<std::string, mojom::ResultCode>
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<IStream> 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