/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsPrinterWin.h" #include #include #include #include "mozilla/Array.h" #include "mozilla/dom/Promise.h" #include "nsPaper.h" #include "nsPrintSettingsImpl.h" #include "nsPrintSettingsWin.h" #include "nsWindowsHelpers.h" #include "PrintBackgroundTask.h" #include "WinUtils.h" using namespace mozilla; using namespace mozilla::gfx; using namespace mozilla::widget; using mozilla::PrintSettingsInitializer; using mozilla::dom::Promise; static const double kPointsPerTenthMM = 72.0 / 254.0; static const double kPointsPerInch = 72.0; nsPrinterWin::nsPrinterWin(const CommonPaperInfoArray* aArray, const nsAString& aName) : nsPrinterBase(aArray), mName(aName), mDefaultDevmodeWStorage("nsPrinterWin::mDefaultDevmodeWStorage") {} // static already_AddRefed nsPrinterWin::Create( const CommonPaperInfoArray* aArray, const nsAString& aName) { return do_AddRef(new nsPrinterWin(aArray, aName)); } template static nsTArray GetDeviceCapabilityArray(const LPWSTR aPrinterName, WORD aCapabilityID, mozilla::Mutex& aDriverMutex, int& aCount) { MOZ_ASSERT(aCount >= 0, "Possibly passed aCount from previous error case."); nsTArray caps; // We only want to access printer drivers in the parent process. if (!XRE_IsParentProcess()) { return caps; } // Both the call to get the size and the call to actually populate the array // are relatively expensive, so as sometimes the lengths of the arrays that we // retrieve depend on each other we allow a count to be passed in to save the // first call. As we allocate double the count anyway this should allay any // safety worries. if (!aCount) { // Passing nullptr as the port here seems to work. Given that we would have // to OpenPrinter with just the name anyway to get the port that makes // sense. Also, the printer set-up seems to stop you from having two // printers with the same name. Note: this (and the call below) are blocking // calls, which could be slow. MutexAutoLock autoLock(aDriverMutex); aCount = ::DeviceCapabilitiesW(aPrinterName, nullptr, aCapabilityID, nullptr, nullptr); if (aCount <= 0) { return caps; } } // As DeviceCapabilitiesW doesn't take a size, there is a greater risk of the // buffer being overflowed, so we over-allocate for safety. caps.SetLength(aCount * 2); MutexAutoLock autoLock(aDriverMutex); int count = ::DeviceCapabilitiesW(aPrinterName, nullptr, aCapabilityID, reinterpret_cast(caps.Elements()), nullptr); if (count <= 0) { caps.Clear(); return caps; } // We know from bug 1673708 that sometimes the final array returned is smaller // than the required array count. Assert here to see if this is reproduced on // test servers. MOZ_ASSERT(count == aCount, "Different array count returned than expected."); // Note that TruncateLength will crash if count > caps.Length(). caps.TruncateLength(count); return caps; } static void DevmodeToSettingsInitializer( const nsString& aPrinterName, const DEVMODEW* aDevmode, mozilla::Mutex& aDriverMutex, PrintSettingsInitializer& aSettingsInitializer) { aSettingsInitializer.mPrinter.Assign(aPrinterName); HDC dc; { MutexAutoLock autoLock(aDriverMutex); dc = ::CreateICW(nullptr, aPrinterName.get(), nullptr, aDevmode); } nsAutoHDC printerDc(dc); MOZ_ASSERT(printerDc, "CreateICW failed"); if (!printerDc) { return; } if (aDevmode->dmFields & DM_PAPERSIZE) { aSettingsInitializer.mPaperInfo.mId.Truncate(); aSettingsInitializer.mPaperInfo.mId.AppendInt(aDevmode->dmPaperSize); // If it is not a paper size we know about, the unit will remain unchanged. nsPrintSettingsWin::PaperSizeUnitFromDmPaperSize( aDevmode->dmPaperSize, aSettingsInitializer.mPaperSizeUnit); } int pixelsPerInchY = ::GetDeviceCaps(printerDc, LOGPIXELSY); int physicalHeight = ::GetDeviceCaps(printerDc, PHYSICALHEIGHT); double heightInInches = double(physicalHeight) / pixelsPerInchY; int pixelsPerInchX = ::GetDeviceCaps(printerDc, LOGPIXELSX); int physicalWidth = ::GetDeviceCaps(printerDc, PHYSICALWIDTH); double widthInches = double(physicalWidth) / pixelsPerInchX; if (aDevmode->dmFields & DM_ORIENTATION && aDevmode->dmOrientation == DMORIENT_LANDSCAPE) { std::swap(widthInches, heightInInches); } aSettingsInitializer.mPaperInfo.mSize.SizeTo(widthInches * kPointsPerInch, heightInInches * kPointsPerInch); gfx::MarginDouble margin = WinUtils::GetUnwriteableMarginsForDeviceInInches(printerDc); aSettingsInitializer.mPaperInfo.mUnwriteableMargin = Some(MarginDouble{ margin.top * kPointsPerInch, margin.right * kPointsPerInch, margin.bottom * kPointsPerInch, margin.left * kPointsPerInch}); // Using Y to match existing code for print scaling calculations. aSettingsInitializer.mResolution = pixelsPerInchY; if (aDevmode->dmFields & DM_COLOR) { // See comment for PrintSettingsInitializer.mPrintInColor aSettingsInitializer.mPrintInColor = aDevmode->dmColor != DMCOLOR_MONOCHROME; } if (aDevmode->dmFields & DM_ORIENTATION) { aSettingsInitializer.mSheetOrientation = int32_t(aDevmode->dmOrientation == DMORIENT_PORTRAIT ? nsPrintSettings::kPortraitOrientation : nsPrintSettings::kLandscapeOrientation); } if (aDevmode->dmFields & DM_COPIES) { aSettingsInitializer.mNumCopies = aDevmode->dmCopies; } if (aDevmode->dmFields & DM_DUPLEX) { switch (aDevmode->dmDuplex) { default: MOZ_FALLTHROUGH_ASSERT("bad value for dmDuplex field"); case DMDUP_SIMPLEX: aSettingsInitializer.mDuplex = nsPrintSettings::kDuplexNone; break; case DMDUP_VERTICAL: aSettingsInitializer.mDuplex = nsPrintSettings::kDuplexFlipOnLongEdge; break; case DMDUP_HORIZONTAL: aSettingsInitializer.mDuplex = nsPrintSettings::kDuplexFlipOnShortEdge; break; } } } NS_IMETHODIMP nsPrinterWin::GetName(nsAString& aName) { aName.Assign(mName); return NS_OK; } NS_IMETHODIMP nsPrinterWin::GetSystemName(nsAString& aName) { aName.Assign(mName); return NS_OK; } namespace mozilla { template <> void ResolveOrReject(Promise& aPromise, nsPrinterWin& aPrinter, const PrintSettingsInitializer& aResult) { aPromise.MaybeResolve( RefPtr(CreatePlatformPrintSettings(aResult))); } } // namespace mozilla NS_IMETHODIMP nsPrinterWin::CopyFromWithValidation( nsIPrintSettings* aSettingsToCopyFrom, JSContext* aCx, Promise** aResultPromise) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aResultPromise); PrintSettingsInitializer settingsInitializer = aSettingsToCopyFrom->GetSettingsInitializer(); return PrintBackgroundTaskPromise( *this, aCx, aResultPromise, "CopyFromWithValidation"_ns, &nsPrinterWin::GetValidatedSettings, settingsInitializer); } bool nsPrinterWin::SupportsDuplex() const { MutexAutoLock autoLock(mDriverMutex); return ::DeviceCapabilitiesW(mName.get(), nullptr, DC_DUPLEX, nullptr, nullptr) == 1; } bool nsPrinterWin::SupportsColor() const { MutexAutoLock autoLock(mDriverMutex); return ::DeviceCapabilitiesW(mName.get(), nullptr, DC_COLORDEVICE, nullptr, nullptr) == 1; } bool nsPrinterWin::SupportsMonochrome() const { if (!SupportsColor()) { return true; } nsHPRINTER hPrinter = nullptr; if (NS_WARN_IF(!::OpenPrinterW(mName.get(), &hPrinter, nullptr))) { return false; } nsAutoPrinter autoPrinter(hPrinter); nsTArray devmodeWStorage = CopyDefaultDevmodeW(); if (devmodeWStorage.IsEmpty()) { return false; } auto* devmode = reinterpret_cast(devmodeWStorage.Elements()); devmode->dmFields |= DM_COLOR; devmode->dmColor = DMCOLOR_MONOCHROME; // Try to modify the devmode settings and see if the setting sticks. // // This has been the only reliable way to detect it that we've found. MutexAutoLock autoLock(mDriverMutex); LONG ret = ::DocumentPropertiesW(nullptr, autoPrinter.get(), mName.get(), devmode, devmode, DM_IN_BUFFER | DM_OUT_BUFFER); if (ret != IDOK) { return false; } return !(devmode->dmFields & DM_COLOR) || devmode->dmColor == DMCOLOR_MONOCHROME; } bool nsPrinterWin::SupportsCollation() const { MutexAutoLock autoLock(mDriverMutex); return ::DeviceCapabilitiesW(mName.get(), nullptr, DC_COLLATE, nullptr, nullptr) == 1; } nsPrinterBase::PrinterInfo nsPrinterWin::CreatePrinterInfo() const { return PrinterInfo{PaperList(), DefaultSettings()}; } mozilla::gfx::MarginDouble nsPrinterWin::GetMarginsForPaper( nsString aPaperId) const { gfx::MarginDouble margin; nsTArray devmodeWStorage = CopyDefaultDevmodeW(); if (devmodeWStorage.IsEmpty()) { return margin; } auto* devmode = reinterpret_cast(devmodeWStorage.Elements()); devmode->dmFields = DM_PAPERSIZE; devmode->dmPaperSize = _wtoi((const wchar_t*)aPaperId.BeginReading()); HDC dc; { MutexAutoLock autoLock(mDriverMutex); dc = ::CreateICW(nullptr, mName.get(), nullptr, devmode); } nsAutoHDC printerDc(dc); MOZ_ASSERT(printerDc, "CreateICW failed"); if (!printerDc) { return margin; } margin = WinUtils::GetUnwriteableMarginsForDeviceInInches(printerDc); margin.top *= kPointsPerInch; margin.right *= kPointsPerInch; margin.bottom *= kPointsPerInch; margin.left *= kPointsPerInch; return margin; } nsTArray nsPrinterWin::CopyDefaultDevmodeW() const { nsTArray devmodeStorageW; auto devmodeStorageWLock = mDefaultDevmodeWStorage.Lock(); if (devmodeStorageWLock->IsEmpty()) { nsHPRINTER hPrinter = nullptr; // OpenPrinter could fail if, for example, the printer has been removed // or otherwise become inaccessible since it was selected. if (NS_WARN_IF(!::OpenPrinterW(mName.get(), &hPrinter, nullptr))) { return devmodeStorageW; } nsAutoPrinter autoPrinter(hPrinter); // Allocate devmode storage of the correct size. MutexAutoLock autoLock(mDriverMutex); LONG bytesNeeded = ::DocumentPropertiesW(nullptr, autoPrinter.get(), mName.get(), nullptr, nullptr, 0); // Note that we must cast the sizeof() to a signed type so that comparison // with the signed, potentially-negative bytesNeeded will work! MOZ_ASSERT(bytesNeeded >= LONG(sizeof(DEVMODEW)), "DocumentPropertiesW failed to get valid size"); if (bytesNeeded < LONG(sizeof(DEVMODEW))) { return devmodeStorageW; } // Allocate extra space in case of bad drivers that return a too-small // result from DocumentProperties. // (See https://bugzilla.mozilla.org/show_bug.cgi?id=1664530#c5) if (!devmodeStorageWLock->SetLength(bytesNeeded * 2, fallible)) { return devmodeStorageW; } memset(devmodeStorageWLock->Elements(), 0, devmodeStorageWLock->Length()); auto* devmode = reinterpret_cast(devmodeStorageWLock->Elements()); LONG ret = ::DocumentPropertiesW(nullptr, autoPrinter.get(), mName.get(), devmode, nullptr, DM_OUT_BUFFER); MOZ_ASSERT(ret == IDOK, "DocumentPropertiesW failed"); // Make sure that the lengths in the DEVMODEW make sense. if (ret != IDOK || devmode->dmSize != sizeof(DEVMODEW) || devmode->dmSize + devmode->dmDriverExtra > devmodeStorageWLock->Length()) { // Clear mDefaultDevmodeWStorage to make sure we try again next time. devmodeStorageWLock->Clear(); return devmodeStorageW; } } devmodeStorageW.Assign(devmodeStorageWLock.ref()); return devmodeStorageW; } nsTArray nsPrinterWin::PaperList() const { // Paper IDs are returned as WORDs. int requiredArrayCount = 0; auto paperIds = GetDeviceCapabilityArray( mName.get(), DC_PAPERS, mDriverMutex, requiredArrayCount); if (!paperIds.Length()) { return {}; } // Paper names are returned in 64 long character buffers. auto paperNames = GetDeviceCapabilityArray>( mName.get(), DC_PAPERNAMES, mDriverMutex, requiredArrayCount); // Check that we have the same number of names as IDs. if (paperNames.Length() != paperIds.Length()) { return {}; } // Paper sizes are returned as POINT structs with a tenth of a millimeter as // the unit. auto paperSizes = GetDeviceCapabilityArray( mName.get(), DC_PAPERSIZE, mDriverMutex, requiredArrayCount); // Check that we have the same number of sizes as IDs. if (paperSizes.Length() != paperIds.Length()) { return {}; } nsTArray paperList; paperList.SetCapacity(paperNames.Length()); for (size_t i = 0; i < paperNames.Length(); ++i) { // Paper names are null terminated unless they are 64 characters long. auto firstNull = std::find(paperNames[i].cbegin(), paperNames[i].cend(), L'\0'); auto nameLength = firstNull - paperNames[i].cbegin(); double width = paperSizes[i].x * kPointsPerTenthMM; double height = paperSizes[i].y * kPointsPerTenthMM; // Skip if no name or invalid size. if (!nameLength || width <= 0 || height <= 0) { continue; } // Windows paper IDs are 16-bit integers; we stringify them to store in the // PaperInfo.mId field. nsString paperIdString; paperIdString.AppendInt(paperIds[i]); // We don't resolve the margins eagerly because they're really expensive (on // the order of seconds for some drivers). nsDependentSubstring name(paperNames[i].cbegin(), nameLength); paperList.AppendElement(mozilla::PaperInfo(paperIdString, nsString(name), {width, height}, Nothing())); } return paperList; } PrintSettingsInitializer nsPrinterWin::DefaultSettings() const { nsTArray devmodeWStorage = CopyDefaultDevmodeW(); if (devmodeWStorage.IsEmpty()) { return {}; } const auto* devmode = reinterpret_cast(devmodeWStorage.Elements()); PrintSettingsInitializer settingsInitializer; DevmodeToSettingsInitializer(mName, devmode, mDriverMutex, settingsInitializer); settingsInitializer.mDevmodeWStorage = std::move(devmodeWStorage); return settingsInitializer; } PrintSettingsInitializer nsPrinterWin::GetValidatedSettings( PrintSettingsInitializer aSettingsToValidate) const { // This function validates the settings by relying on the printer driver // rejecting any invalid settings and resetting them to valid values. // Create a copy of the default DEVMODE for this printer. nsTArray devmodeWStorage = CopyDefaultDevmodeW(); if (devmodeWStorage.IsEmpty()) { return aSettingsToValidate; } nsHPRINTER hPrinter = nullptr; if (NS_WARN_IF(!::OpenPrinterW(mName.get(), &hPrinter, nullptr))) { return aSettingsToValidate; } nsAutoPrinter autoPrinter(hPrinter); // Copy the settings from aSettingsToValidate into our DEVMODE. DEVMODEW* devmode = reinterpret_cast(devmodeWStorage.Elements()); if (!aSettingsToValidate.mPaperInfo.mId.IsEmpty()) { devmode->dmPaperSize = _wtoi( (const wchar_t*)aSettingsToValidate.mPaperInfo.mId.BeginReading()); devmode->dmFields |= DM_PAPERSIZE; } else { devmode->dmPaperSize = 0; devmode->dmFields &= ~DM_PAPERSIZE; } devmode->dmFields |= DM_COLOR; devmode->dmColor = aSettingsToValidate.mPrintInColor ? DMCOLOR_COLOR : DMCOLOR_MONOCHROME; // Note: small page sizes can be required here for sticker, label and slide // printers etc. see bug 1271900. if (aSettingsToValidate.mPaperInfo.mSize.height > 0) { devmode->dmPaperLength = std::round( aSettingsToValidate.mPaperInfo.mSize.height / kPointsPerTenthMM); devmode->dmFields |= DM_PAPERLENGTH; } else { devmode->dmPaperLength = 0; devmode->dmFields &= ~DM_PAPERLENGTH; } if (aSettingsToValidate.mPaperInfo.mSize.width > 0) { devmode->dmPaperWidth = std::round( aSettingsToValidate.mPaperInfo.mSize.width / kPointsPerTenthMM); devmode->dmFields |= DM_PAPERWIDTH; } else { devmode->dmPaperWidth = 0; devmode->dmFields &= ~DM_PAPERWIDTH; } // Setup Orientation devmode->dmOrientation = aSettingsToValidate.mSheetOrientation == nsPrintSettings::kPortraitOrientation ? DMORIENT_PORTRAIT : DMORIENT_LANDSCAPE; devmode->dmFields |= DM_ORIENTATION; // Setup Number of Copies devmode->dmCopies = aSettingsToValidate.mNumCopies; devmode->dmFields |= DM_COPIES; // Setup Simplex/Duplex mode devmode->dmFields |= DM_DUPLEX; switch (aSettingsToValidate.mDuplex) { case nsPrintSettings::kDuplexNone: devmode->dmDuplex = DMDUP_SIMPLEX; break; case nsPrintSettings::kDuplexFlipOnLongEdge: devmode->dmDuplex = DMDUP_VERTICAL; break; case nsPrintSettings::kDuplexFlipOnShortEdge: devmode->dmDuplex = DMDUP_HORIZONTAL; break; default: MOZ_ASSERT_UNREACHABLE("bad value for duplex option"); break; } // Apply the settings in the DEVMODE to the printer and retrieve the updated // DEVMODE back into the same structure. LONG ret; { MutexAutoLock autoLock(mDriverMutex); ret = ::DocumentPropertiesW(nullptr, autoPrinter.get(), mName.get(), devmode, devmode, DM_IN_BUFFER | DM_OUT_BUFFER); } if (ret != IDOK) { return aSettingsToValidate; } // Copy the settings back from the DEVMODE into aSettingsToValidate and // return. DevmodeToSettingsInitializer(mName, devmode, mDriverMutex, aSettingsToValidate); aSettingsToValidate.mDevmodeWStorage = std::move(devmodeWStorage); return aSettingsToValidate; }