/* 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/. */
let { ConsoleAPI } = ChromeUtils.importESModule( "resource://gre/modules/Console.sys.mjs"
); // Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
let _logger = new ConsoleAPI({
prefix: "printUI",
maxLogLevel: getMaxLogLevel(),
});
function onPrefChange() { if (_logger) {
_logger.maxLogLevel = getMaxLogLevel();
}
} // Watch for pref changes and the maxLogLevel for the logger
Services.prefs.addObserver("print.debug", onPrefChange);
window.addEventListener("unload", () => {
Services.prefs.removeObserver("print.debug", onPrefChange);
}); return _logger;
})();
function serializeSettings(settings) {
let re = /^(k[A-Z]|resolution)/; // accessing settings.resolution throws an exception?
let types = new Set(["string", "boolean", "number", "undefined"]);
let nameValues = {}; for (let key in settings) { try { if (!re.test(key) && types.has(typeof settings[key])) {
nameValues[key] = settings[key];
}
} catch (e) {
logger.warn("Exception accessing setting: ", key, e);
}
} return JSON.stringify(nameValues, null, 2);
}
let printPending = false;
let deferredTasks = []; function createDeferredTask(fn, timeout) {
let task = new DeferredTask(fn, timeout);
deferredTasks.push(task); return task;
}
function cancelDeferredTasks() { for (let task of deferredTasks) {
task.disarm();
}
PrintEventHandler._updatePrintPreviewTask?.disarm();
deferredTasks = [];
}
// These settings do not have an associated pref value or flag, but // changing them requires us to update the print preview.
_nonFlaggedUpdatePreviewSettings: new Set([ "pageRanges", "numPagesPerSheet", "sourceVersion",
]),
_noPreviewUpdateSettings: new Set(["numCopies", "printDuplex"]),
// Do not keep a reference to source browser, it may mutate after printing // is initiated and the print preview clone must be a snapshot from the // time that the print was started.
let sourceBrowsingContext = this.printPreviewEl.getSourceBrowsingContext();
document.addEventListener("print", async () => {
let cancelButton = document.getElementById("cancel-button");
document.l10n.setAttributes(
cancelButton,
cancelButton.dataset.closeL10nId
);
let didPrint = await this.print(); if (!didPrint) { // Re-enable elements of the form if the user cancels saving or // if a deferred task rendered the page invalid. this.printForm.enable();
} // Reset the cancel button regardless of the outcome.
document.l10n.setAttributes(
cancelButton,
cancelButton.dataset.cancelL10nId
);
}); this._createDelayedSettingsChangeTask();
document.addEventListener("update-print-settings", e => { this.handleSettingsChange(e.detail);
});
document.addEventListener("cancel-print-settings", e => { this._delayedSettingsChangeTask.disarm(); for (let setting of Object.keys(e.detail)) { deletethis._delayedChanges[setting];
}
});
document.addEventListener("cancel-print", () => this.cancelPrint());
document.addEventListener("open-system-dialog", async () => { // This file in only used if pref print.always_print_silent is false, so // no need to check that here.
// Hide the dialog box before opening system dialog // We cannot close the window yet because the browsing context for the // print preview browser is needed to print the page.
let sourceBrowser = this.printPreviewEl.getSourceBrowsingContext().top.embedderElement;
let dialogBoxManager =
PrintUtils.getTabDialogBox(sourceBrowser).getTabDialogManager();
dialogBoxManager.hideDialog(sourceBrowser);
// Use our settings to prepopulate the system dialog. // The system print dialog won't recognize our internal save-to-pdf // pseudo-printer. We need to pass it a settings object from any // system recognized printer.
let settings = this.settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER
? PrintUtils.getPrintSettings(this.viewSettings.defaultSystemPrinter)
: this.settings.clone(); // We set the title so that if the user chooses save-to-PDF from the // system dialog the title will be used to generate the prepopulated // filename in the file picker.
settings.title = this.activeTitle;
Glean.printing.dialogOpenedViaPreviewTm.add(1); const doPrint = await this._showPrintDialog(
window, this.hasSelection,
settings
); if (!doPrint) {
Glean.printing.dialogViaPreviewCancelledTm.add(1);
window.close(); return;
}
await this.print(settings);
});
let originalError; const printersByPriority = [
selectedPrinter.value,
...Object.getOwnPropertyNames(printersByName).filter(
name => name != selectedPrinter.value
),
];
// Try to update settings, falling back to any available printer for (const printerName of printersByPriority) { try {
let settingsToChange = await this.refreshSettings(printerName);
await this.updateSettings(settingsToChange, true);
originalError = null; break;
} catch (e) { if (!originalError) {
originalError = e; // Report on how often fetching the last used printer settings fails. this.reportPrintingError("PRINTER_SETTINGS_LAST_USED");
}
}
}
// Only throw original error if no fallback was possible if (originalError) { this.reportPrintingError("PRINTER_SETTINGS"); throw originalError;
}
let initialPreviewDone = this._updatePrintPreview();
// Use a DeferredTask for updating the preview. This will ensure that we // only have one update running at a time. this._createUpdatePrintPreviewTask(initialPreviewDone);
document.dispatchEvent( new CustomEvent("available-destinations", {
detail: destinations,
})
);
document.dispatchEvent( new CustomEvent("print-settings", {
detail: this.viewSettings,
})
);
document.body.removeAttribute("loading");
await new Promise(resolve => window.requestAnimationFrame(resolve));
// Now that we're showing the form, select the destination select.
document.getElementById("printer-picker").focus({ focusVisible: true });
await initialPreviewDone;
},
async print(systemDialogSettings) { // Disable the form when a print is in progress this.printForm.disable();
if (Object.keys(this._delayedChanges).length) { // Make sure any pending changes get saved.
let task = this._delayedSettingsChangeTask; this._createDelayedSettingsChangeTask();
await task.finalize();
}
if (this.settings.pageRanges.length) { // Finish any running previews to verify the range is still valid.
let task = this._updatePrintPreviewTask; this._createUpdatePrintPreviewTask();
await task.finalize();
}
if (!this.printForm.checkValidity() || this.previewIsEmpty) { returnfalse;
}
let settings = systemDialogSettings || this.settings;
// This seems like it should be handled automatically but it isn't.
PSSVC.maybeSaveLastUsedPrinterNameToPrefs(settings.printerName);
try { // We'll provide our own progress indicator.
let l10nId =
settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER
? "printui-print-progress-indicator-saving"
: "printui-print-progress-indicator";
document.l10n.setAttributes(this.printProgressIndicator, l10nId); this.printProgressIndicator.hidden = false;
let bc = this.printPreviewEl.currentBrowsingContext;
await this._doPrint(bc, settings);
} catch (e) {
console.error(e);
}
if (settings.printerName == PrintUtils.SAVE_TO_PDF_PRINTER) { // Clear the file name from the preference value since it may potentially // contain sensitive information from the page title (Bug 1675965)
let prefName = "print.printer_" +
settings.printerName.replace(/ /g, "_") + ".print_to_filename";
Services.prefs.clearUserPref(prefName);
}
window.close(); returntrue;
},
/** * Prints the window. This method has been abstracted into a helper for * testing purposes.
*/
_doPrint(aBrowsingContext, aSettings) { return aBrowsingContext.print(aSettings);
},
async refreshSettings(printerName) { this.currentPrinterName = printerName;
let currentPrinter; try {
currentPrinter =
await PrintSettingsViewProxy.resolvePropertiesForPrinter(printerName);
} catch (e) { this.reportPrintingError("PRINTER_PROPERTIES"); throw e;
} if (this.currentPrinterName != printerName) { // Refresh settings could take a while, if the destination has changed // then we don't want to update the settings after all. return {};
}
// Some settings are only used by the UI // assigning new values should update the underlying settings this.viewSettings = new Proxy(this.settings, PrintSettingsViewProxy); returnthis.getSettingsToUpdate();
},
getSettingsToUpdate() { // Get the previously-changed settings we want to try to use on this printer
let settingsToUpdate = Object.assign({}, this._userChangedSettings);
// Ensure the color option is correct, if either of the supportsX flags are // false then the user cannot change the value through the UI. if (!this.viewSettings.supportsColor) {
settingsToUpdate.printInColor = false;
} elseif (!this.viewSettings.supportsMonochrome) {
settingsToUpdate.printInColor = true;
}
if (settingsToUpdate.sourceVersion == "simplified") { if (this.viewSettings.printBackgrounds) { // Remember that this was true before so it gets restored if the // format is changed to something else. this._userChangedSettings.printBackgrounds = true;
} // Backgrounds are removed in simplified mode and this setting changes // the output subtly to be less legible.
settingsToUpdate.printBackgrounds = false;
}
if (
settingsToUpdate.printInColor != this._userChangedSettings.printInColor
) { deletethis._userChangedSettings.printInColor;
}
// See if the paperId needs to change.
let paperId = settingsToUpdate.paperId || this.viewSettings.paperId;
logger.debug("Using paperId: ", paperId);
logger.debug( "Available paper sizes: ",
PrintSettingsViewProxy.availablePaperSizes
);
let matchedPaper =
paperId && PrintSettingsViewProxy.availablePaperSizes[paperId]; if (!matchedPaper) {
let paperWidth, paperHeight, paperSizeUnit; if (settingsToUpdate.paperId) { // The user changed paperId in this instance and session, // We should have details on the paper size from the previous printer
paperId = settingsToUpdate.paperId;
let cachedPaperWrapper = this.allPaperSizes[paperId]; // for the purposes of finding a best-size match, we'll use mm
paperWidth = cachedPaperWrapper.paper.width * MM_PER_POINT;
paperHeight = cachedPaperWrapper.paper.height * MM_PER_POINT;
paperSizeUnit = PrintEventHandler.settings.kPaperSizeMillimeters;
} else {
paperId = this.viewSettings.paperId;
logger.debug( "No paperId or matchedPaper, get a new default from viewSettings:",
paperId
);
paperWidth = this.viewSettings.paperWidth;
paperHeight = this.viewSettings.paperHeight;
paperSizeUnit = this.viewSettings.paperSizeUnit;
}
matchedPaper = PrintSettingsViewProxy.getBestPaperMatch(
paperWidth,
paperHeight,
paperSizeUnit
);
} if (!matchedPaper) { // We didn't find a good match. Take the first paper size
matchedPaper = Object.values(
PrintSettingsViewProxy.availablePaperSizes
)[0]; deletethis._userChangedSettings.paperId;
} if (matchedPaper.id !== paperId) { // The exact paper id doesn't exist for this printer
logger.log(
`Requested paperId: "${paperId}" missing on this printer, using: ${matchedPaper.id} instead`
); deletethis._userChangedSettings.paperId;
} // Always write paper details back to settings
settingsToUpdate.paperId = matchedPaper.id;
handleSettingsChange(changedSettings = {}) {
let delayedChanges = {};
let instantChanges = {}; for (let [setting, value] of Object.entries(changedSettings)) { switch (setting) { case"pageRanges": case"scaling":
delayedChanges[setting] = value; break; case"customMargins": deletethis._delayedChanges.margins;
changedSettings.margins == "custom"
? (delayedChanges[setting] = value)
: (instantChanges[setting] = value); break; default:
instantChanges[setting] = value; break;
}
} if (Object.keys(delayedChanges).length) { this._scheduleDelayedSettingsChange(delayedChanges);
} if (Object.keys(instantChanges).length) { this.onUserSettingsChange(instantChanges);
}
},
async onUserSettingsChange(changedSettings = {}) {
let previewableChange = false; for (let [setting, value] of Object.entries(changedSettings)) {
Glean.printing.settingsChanged[setting].add(1); // Update the list of user-changed settings, which we attempt to maintain // across printer changes. this._userChangedSettings[setting] = value; if (!this._noPreviewUpdateSettings.has(setting)) {
previewableChange = true;
}
} if (changedSettings.printerName) {
logger.debug( "onUserSettingsChange, changing to printerName:",
changedSettings.printerName
); this.printForm.printerChanging = true; this.printForm.disable(el => el.id != "printer-picker");
let { printerName } = changedSettings; // Treat a printerName change separately, because it involves a settings // object switch and we don't want to set the new name on the old settings.
changedSettings = await this.refreshSettings(printerName); if (printerName != this.currentPrinterName) { // Don't continue this update if the printer changed again. return;
} this.printForm.printerChanging = false; this.printForm.enable();
} else {
changedSettings = this.getSettingsToUpdate();
}
let shouldPreviewUpdate =
(await this.updateSettings(
changedSettings,
!!changedSettings.printerName
)) && previewableChange;
if (shouldPreviewUpdate && !printPending) { // We do not need to arm the preview task if the user has already printed // and finalized any deferred tasks. this.updatePrintPreview();
}
document.dispatchEvent( new CustomEvent("print-settings", {
detail: this.viewSettings,
})
);
},
async updateSettings(changedSettings = {}, printerChanged = false) {
let updatePreviewWithoutFlag = false;
let flags = 0;
logger.debug("updateSettings ", changedSettings, printerChanged);
if (printerChanged || changedSettings.paperId) { // The paper's margin properties are async, // so resolve those now before we update the settings try {
let paperWrapper = await PrintSettingsViewProxy.fetchPaperMargins(
changedSettings.paperId || this.viewSettings.paperId
);
// See if we also need to change the custom margin values
let paperHeightInInches = paperWrapper.paper.height * INCHES_PER_POINT;
let paperWidthInInches = paperWrapper.paper.width * INCHES_PER_POINT;
let height =
(changedSettings.orientation || this.viewSettings.orientation) == 0
? paperHeightInInches
: paperWidthInInches;
let width =
(changedSettings.orientation || this.viewSettings.orientation) == 0
? paperWidthInInches
: paperHeightInInches;
for (let [setting, value] of Object.entries(changedSettings)) { // Always write paper changes back to settings as pref-derived values could be bad if ( this.viewSettings[setting] != value ||
(printerChanged && setting == "paperId")
) { if (setting == "pageRanges") { // The page range is kept as an array. If the user switches between all // and custom with no specified range input (which is represented as an // empty array), we do not want to send an update. if (!this.viewSettings[setting].length && !value.length) { continue;
}
} this.viewSettings[setting] = value;
if (
setting in this.settingFlags &&
setting in this._userChangedSettings
) {
flags |= this.settingFlags[setting];
}
updatePreviewWithoutFlag |= this._nonFlaggedUpdatePreviewSettings.has(setting);
}
}
/** * Queue a task to update the print preview. It will start immediately or when * the in progress update completes.
*/
async updatePrintPreview() { // Make sure the rendering state is set so we don't visibly update the // sheet count with incomplete data. this._updatePrintPreviewTask.arm();
},
/** * Creates a print preview or refreshes the preview with new settings when omitted. * * @return {Promise} Resolves when the preview has been updated.
*/
async _updatePrintPreview() {
let { settings } = this;
let totalPageCount, sheetCount, isEmpty, orientation, pageWidth, pageHeight; try { // This resolves with a PrintPreviewSuccessInfo dictionary.
let { sourceVersion } = this.viewSettings;
let sourceURI = this.activeURI; // The printing backend can't generate a title for the selection document // since it is only a fragment of the page, give it the active title.
settings.title = this.viewSettings.sourceVersion == "selection" ? this.activeTitle : ""; this._lastPrintPreviewSettings = settings;
({
totalPageCount,
sheetCount,
isEmpty,
orientation,
pageWidth,
pageHeight,
} = await this.printPreviewEl.printPreview(settings, {
sourceVersion,
sourceURI,
}));
} catch (e) { this.reportPrintingError("PRINT_PREVIEW");
console.error(e); throw e;
}
// If there is a set orientation, update the settings to use it. In this // case, the document will already have used this orientation to create // the print preview. if (orientation != "unspecified") { const kIPrintSettings = Ci.nsIPrintSettings;
settings.orientation =
orientation == "landscape"
? kIPrintSettings.kLandscapeOrientation
: kIPrintSettings.kPortraitOrientation;
document.dispatchEvent(new CustomEvent("hide-orientation"));
}
// If the page size is set, check whether we should use it as our paper size.
let isUsingPageRuleSizeAsPaperSize =
settings.usePageRuleSizeAsPaperSize &&
pageWidth !== null &&
pageHeight !== null; if (isUsingPageRuleSizeAsPaperSize) { // We canonically represent paper sizes using the width/height of a portrait-oriented sheet, // with landscape-orientation applied as a supplemental rotation. // If the page-size is landscape oriented, we flip the pageWidth / pageHeight here // in order to pass a canonical representation into the paper-size settings. if (orientation == "landscape") {
[pageHeight, pageWidth] = [pageWidth, pageHeight];
}
let matchedPaper = PrintSettingsViewProxy.getBestPaperMatch(
pageWidth,
pageHeight,
settings.kPaperSizeInches
); if (matchedPaper) {
settings.paperId = matchedPaper.id;
}
this.previewIsEmpty = isEmpty; // If the preview is empty, we know our range is greater than the number of pages. // We have to send a pageRange update to display a non-empty page. if (this.previewIsEmpty) { this.viewSettings.pageRanges = []; this.updatePrintPreview();
}
/** * Shows the system dialog. This method has been abstracted into a helper for * testing purposes. The showPrintDialog() call blocks until the dialog is * closed, so we mark it as async to allow us to reject from the test.
*/
async _showPrintDialog(aWindow, aHaveSelection, aSettings) { return PrintUtils.handleSystemPrintDialog(
aWindow,
aHaveSelection,
aSettings
);
},
};
var PrintSettingsViewProxy = {
get defaultHeadersAndFooterValues() { const defaultBranch = Services.prefs.getDefaultBranch("");
let settingValues = {}; for (let [name, pref] of Object.entries(this.headerFooterSettingsPrefs)) {
settingValues[name] = defaultBranch.getStringPref(pref);
} // We only need to retrieve these defaults once and they will not change
Object.defineProperty(this, "defaultHeadersAndFooterValues", {
value: settingValues,
}); return settingValues;
},
// Custom margins are not saved by a pref, so we need to keep track of them // in order to save the value.
_lastCustomMarginValues: {
marginTop: null,
marginBottom: null,
marginLeft: null,
marginRight: null,
},
// This list was taken from nsDeviceContextSpecWin.cpp which records telemetry on print target type
knownSaveToFilePrinters: new Set([ "Microsoft Print to PDF", "Adobe PDF", "Bullzip PDF Printer", "CutePDF Writer", "doPDF", "Foxit Reader PDF Printer", "Nitro PDF Creator", "novaPDF", "PDF-XChange", "PDF24 PDF", "PDFCreator", "PrimoPDF", "Soda PDF", "Solid PDF Creator", "Universal Document Converter", "Microsoft XPS Document Writer",
]),
getBestPaperMatch(paperWidth, paperHeight, paperSizeUnit) {
let paperSizes = Object.values(this.availablePaperSizes); if (!(paperWidth && paperHeight)) { returnnull;
} // first try to match on the paper dimensions using the current units
let unitsPerPoint;
let altUnitsPerPoint; if (paperSizeUnit == PrintEventHandler.settings.kPaperSizeMillimeters) {
unitsPerPoint = MM_PER_POINT;
altUnitsPerPoint = INCHES_PER_POINT;
} else {
unitsPerPoint = INCHES_PER_POINT;
altUnitsPerPoint = MM_PER_POINT;
} // equality to 1pt. const equal = (a, b) => Math.abs(a - b) < 1; const findMatch = (widthPts, heightPts) =>
paperSizes.find(paperWrapper => { // the dimensions on the nsIPaper object are in points
let result =
equal(widthPts, paperWrapper.paper.width) &&
equal(heightPts, paperWrapper.paper.height); return result;
}); // Look for a paper with matching dimensions, using the current printer's // paper size unit, then the alternate unit
let matchedPaper =
findMatch(paperWidth / unitsPerPoint, paperHeight / unitsPerPoint) ||
findMatch(paperWidth / altUnitsPerPoint, paperHeight / altUnitsPerPoint);
if (matchedPaper) { return matchedPaper;
} returnnull;
},
async fetchPaperMargins(paperId) { // resolve any async and computed properties we need on the paper
let paperWrapper = this.availablePaperSizes[paperId]; if (!paperWrapper) { thrownew Error("Can't fetchPaperMargins: " + paperId);
} if (paperWrapper._resolved) { // We've already resolved and calculated these values return paperWrapper;
}
let margins; try {
margins = await paperWrapper.paper.unwriteableMargin;
} catch (e) { this.reportPrintingError("UNWRITEABLE_MARGIN"); throw e;
}
margins.QueryInterface(Ci.nsIPaperMargin);
// margin dimensions are given on the paper in points, setting values need to be in inches
paperWrapper.unwriteableMarginTop = margins.top * INCHES_PER_POINT;
paperWrapper.unwriteableMarginRight = margins.right * INCHES_PER_POINT;
paperWrapper.unwriteableMarginBottom = margins.bottom * INCHES_PER_POINT;
paperWrapper.unwriteableMarginLeft = margins.left * INCHES_PER_POINT; // No need to re-resolve static properties
paperWrapper._resolved = true; return paperWrapper;
},
async resolvePropertiesForPrinter(printerName) { // resolve any async properties we need on the printer
let printerInfo = this.availablePrinters[printerName]; if (printerInfo._resolved) { // Store a convenience reference this.availablePaperSizes = printerInfo.availablePaperSizes; return printerInfo;
}
printerInfo.paperList = basePrinterInfo.paperList;
printerInfo.defaultSettings = basePrinterInfo.defaultSettings;
} elseif (printerName == PrintUtils.SAVE_TO_PDF_PRINTER) { // The Mozilla PDF pseudo-printer has no actual nsIPrinter implementation
printerInfo.defaultSettings = PSSVC.createNewPrintSettings();
printerInfo.defaultSettings.printerName = printerName;
printerInfo.defaultSettings.toFileName = "";
printerInfo.defaultSettings.outputFormat =
Ci.nsIPrintSettings.kOutputFormatPDF;
printerInfo.defaultSettings.outputDestination =
Ci.nsIPrintSettings.kOutputDestinationFile;
printerInfo.defaultSettings.usePageRuleSizeAsPaperSize =
Services.prefs.getBoolPref( "print.save_as_pdf.use_page_rule_size_as_paper_size.enabled", false
);
printerInfo.paperList = this.fallbackPaperList;
}
printerInfo.settings = printerInfo.defaultSettings.clone(); // Apply any previously persisted user values // Don't apply kInitSavePrintToFile though, that should only be true for // the PDF printer.
printerInfo.settings.outputDestination =
printerName == PrintUtils.SAVE_TO_PDF_PRINTER
? Ci.nsIPrintSettings.kOutputDestinationFile
: Ci.nsIPrintSettings.kOutputDestinationPrinter;
let flags =
printerInfo.settings.kInitSaveAll ^
printerInfo.settings.kInitSavePrintToFile;
PSSVC.initPrintSettingsFromPrefs(printerInfo.settings, true, flags); // We set `isInitializedFromPrinter` to make sure that that's set on the // SAVE_TO_PDF_PRINTER settings. The naming is poor, but that tells the // platform code that the settings object is complete.
printerInfo.settings.isInitializedFromPrinter = true;
printerInfo.settings.toFileName = "";
// prepare the available paper sizes for this printer if (!printerInfo.paperList?.length) {
logger.warn( "Printer has empty paperList: ",
printerInfo.printer.id, "using fallbackPaperList"
);
printerInfo.paperList = this.fallbackPaperList;
} // don't trust the settings to provide valid paperSizeUnit values
let sizeUnit =
printerInfo.settings.paperSizeUnit ==
printerInfo.settings.kPaperSizeMillimeters
? printerInfo.settings.kPaperSizeMillimeters
: printerInfo.settings.kPaperSizeInches;
let papersById = (printerInfo.availablePaperSizes = {}); // Store a convenience reference this.availablePaperSizes = papersById;
for (let paper of printerInfo.paperList) {
paper.QueryInterface(Ci.nsIPaper); // Bug 1662239: I'm seeing multiple duplicate entries for each paper size // so ensure we have one entry per name if (!papersById[paper.id]) {
papersById[paper.id] = {
paper,
id: paper.id,
name: paper.name, // XXXsfoster: Eventually we want to get the unit from the nsIPaper object
sizeUnit,
};
}
} // Update our cache of all the paper sizes by name
Object.assign(PrintEventHandler.allPaperSizes, papersById);
// The printer properties don't change, mark this as resolved for next time
printerInfo._resolved = true;
logger.debug("Resolved printerInfo:", printerInfo); return printerInfo;
},
case"marginOptions": {
let allMarginPresets = this.get(target, "marginPresets");
let uniqueMargins = new Set();
let marginsEnabled = {}; for (let name of ["none", "default", "minimum", "custom"]) {
let { marginTop, marginLeft, marginBottom, marginRight } =
allMarginPresets[name];
let key = [marginTop, marginLeft, marginBottom, marginRight].join( ","
); // Custom margins are initialized to default margins
marginsEnabled[name] = !uniqueMargins.has(key) || name == "custom";
uniqueMargins.add(key);
} return marginsEnabled;
}
case"margins":
let marginSettings = {
marginTop: target.marginTop,
marginLeft: target.marginLeft,
marginBottom: target.marginBottom,
marginRight: target.marginRight,
}; // see if they match the none, minimum, or default margin values
let allMarginPresets = this.get(target, "marginPresets"); const marginsMatch = function (lhs, rhs) { return Object.keys(marginSettings).every(
name => lhs[name].toFixed(2) == rhs[name].toFixed(2)
);
}; const potentialPresets = (function () {
let presets = []; const minimumIsNone = marginsMatch(
allMarginPresets.none,
allMarginPresets.minimum
); // We only attempt to match the serialized values against the "none" // preset if the unwriteable margins are being ignored or are zero. if (target.ignoreUnwriteableMargins || minimumIsNone) {
presets.push("none");
} if (!minimumIsNone) {
presets.push("minimum");
}
presets.push("default"); return presets;
})(); for (let presetName of potentialPresets) {
let marginPresets = allMarginPresets[presetName]; if (marginsMatch(marginSettings, marginPresets)) { return presetName;
}
}
// Fall back to custom for other values return"custom";
case"printDuplex": switch (target.duplex) { case Ci.nsIPrintSettings.kDuplexNone: break; case Ci.nsIPrintSettings.kDuplexFlipOnLongEdge: return"long-edge"; case Ci.nsIPrintSettings.kDuplexFlipOnShortEdge: return"short-edge"; default:
logger.warn("Unexpected duplex value: ", target.duplex);
} return"off"; case"printBackgrounds": return target.printBGImages || target.printBGColors;
case"printFootersHeaders": // if any of the footer and headers settings have a non-empty string value // we consider that "enabled" return Object.keys(this.headerFooterSettingsPrefs).some(
name => !!target[name]
);
set(target, name, value) { switch (name) { case"margins": if (!["default", "minimum", "none", "custom"].includes(value)) {
logger.warn("Unexpected margin preset name: ", value);
value = "default";
}
let paperWrapper = this.get(target, "currentPaper");
let marginPresets = PrintEventHandler.getMarginPresets(
value,
paperWrapper
); for (let [settingName, presetValue] of Object.entries(marginPresets)) {
target[settingName] = presetValue;
}
target.honorPageRuleMargins = value == "default";
target.ignoreUnwriteableMargins = value == "none"; break;
case"paperId": {
let paperId = value;
let paperWrapper = this.availablePaperSizes[paperId]; // Dimensions on the paper object are in pts. // We convert to the printer's specified unit when updating settings
let unitsPerPoint =
paperWrapper.sizeUnit == target.kPaperSizeMillimeters
? MM_PER_POINT
: INCHES_PER_POINT; // paperWidth and paperHeight are calculated values that we always treat as suspect and // re-calculate whenever the paperId changes
target.paperSizeUnit = paperWrapper.sizeUnit;
target.paperWidth = paperWrapper.paper.width * unitsPerPoint;
target.paperHeight = paperWrapper.paper.height * unitsPerPoint; // Unwriteable margins were pre-calculated from their async values when the paper size // was selected. They are always in inches
target.unwriteableMarginTop = paperWrapper.unwriteableMarginTop;
target.unwriteableMarginRight = paperWrapper.unwriteableMarginRight;
target.unwriteableMarginBottom = paperWrapper.unwriteableMarginBottom;
target.unwriteableMarginLeft = paperWrapper.unwriteableMarginLeft;
target.paperId = paperWrapper.paper.id; // pull new margin values for the new paper size this.set(target, "margins", this.get(target, "margins")); break;
}
case"printerName": // Can't set printerName, settings objects belong to a specific printer. break;
case"printFootersHeaders": // To disable header & footers, set them all to empty. // To enable, restore default values for each of the header & footer settings. for (let [settingName, defaultValue] of Object.entries( this.defaultHeadersAndFooterValues
)) {
target[settingName] = value ? defaultValue : "";
} break;
case"customMargins": if (value != null) { for (let [settingName, newVal] of Object.entries(value)) {
target[settingName] = newVal; this._lastCustomMarginValues[settingName] = newVal;
}
} break;
update(settings) { // If there are no default system printers available and we are not on mac, // we should hide the system dialog because it won't be populated with // the correct settings. Mac and Gtk support save to pdf functionality // in the native dialog, so it can be shown regardless. this.querySelector("#system-print").hidden =
AppConstants.platform === "win" && !settings.defaultSystemPrinter;
enable() {
let isValid = this.checkValidity();
document.body.toggleAttribute("invalid", !isValid); if (isValid) { for (let element of this.elements) { if (!element.hasAttribute("disallowed")) {
element.disabled = false;
}
} // aria-describedby will usually cause the first value to be reported. // Unfortunately, screen readers don't pick up description changes from // dialogs, so we must use a live region. To avoid double reporting of // the first value, we don't set aria-live initially. We only set it for // subsequent updates. // aria-live is set on the parent because sheetCount itself might be // hidden and then shown, and updates are only reported for live // regions that were already visible.
document
.querySelector("#sheet-count")
.parentNode.setAttribute("aria-live", "polite");
} else { // Find the invalid element
let invalidElement; for (let element of this.elements) { if (!element.checkValidity()) {
invalidElement = element; break;
}
}
let section = invalidElement.closest(".section-block");
document.body.toggleAttribute("invalid", !isValid); // We're hiding the sheet count and aria-describedby includes the // content of hidden elements, so remove aria-describedby.
document.body.removeAttribute("aria-describedby"); for (let element of this.elements) { // If we're valid, enable all inputs. // Otherwise, disable the valid inputs other than the cancel button and the elements // in the invalid section.
element.disabled =
element.hasAttribute("disallowed") ||
(!isValid &&
element.validity.valid &&
element.name != "cancel" &&
element.closest(".section-block") != this._printerDestination &&
element.closest(".section-block") != section);
}
}
}
disable(filterFn) { for (let element of this.elements) { if (filterFn && !filterFn(element)) { continue;
}
element.disabled = element.name != "cancel";
}
}
setOptions(optionValues = []) { this.textContent = ""; for (let optionData of optionValues) {
let opt = new Option(
optionData.name, "value" in optionData ? optionData.value : optionData.name
); if (optionData.nameId) {
document.l10n.setAttributes(opt, optionData.nameId);
} // option selectedness is set via update() and assignment to this.value this.options.add(opt);
}
}
update(settings) { if (this.settingName) { this.value = settings[this.settingName];
}
}
// Unhide the paper-size picker, if we've stopped using the page size as paper-size. if (this._section.hidden && !settings.usePageRuleSizeAsPaperSize) { this._section.hidden = false;
}
}
// If the user had an invalid input and switches back to "fit to page", // we repopulate the scale field with the stored, valid scaling value.
let isValid = this._percentScale.checkValidity(); if (
!this._percentScale.value ||
(this._shrinkToFitChoice.checked && !isValid) ||
(this.printerName != printerName && !isValid)
) { // Only allow whole numbers. 0.14 * 100 would have decimal places, etc. this._percentScale.value = parseInt(scaling * 100, 10); this.printerName = printerName; if (!isValid) { this.dispatchEvent(new Event("revalidate", { bubbles: true })); this._scaleError.hidden = true;
}
}
}
if (this._rangePicker.value == "odd") { for (let i = 1; i <= this._numPages; i += 2) { this._pagesSet.add(i);
}
} elseif (this._rangePicker.value == "even") { for (let i = 2; i <= this._numPages; i += 2) { this._pagesSet.add(i);
}
}
if (!this._rangeInput.checkValidity()) { this._rangeInput.setCustomValidity(""); this._rangeInput.value = "";
}
}
// If it's valid, update the page range and hide the error messages. // Otherwise, set the appropriate error message if (this._rangeInput.validity.valid || !isCustom) {
window.clearTimeout(this.showErrorTimeoutId); this._startRangeOverflowError.hidden = this._rangeError.hidden = true;
} else { this._rangeInput.focus();
}
}
handleKeypress(e) {
let char = String.fromCharCode(e.charCode);
let acceptedChar = char.match(/^[0-9,-]$/); if (!acceptedChar && !char.match("\x00") && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
}
}
handlePaste(e) {
let paste = (e.clipboardData || window.clipboardData)
.getData("text")
.trim(); if (!paste.match(/^[0-9,-]*$/)) {
e.preventDefault();
}
}
// This method has been abstracted into a helper for testing purposes
_validateRangeInput(value, numPages) { this._pagesSet.clear(); var ranges = value.split(",");
for (let range of ranges) {
let rangeParts = range.split("-"); if (rangeParts.length > 2) { this._rangeInput.setCustomValidity("invalid"); this._rangeInput.title = ""; this._pagesSet.clear(); return;
}
let startRange = parseInt(rangeParts[0], 10);
let endRange = parseInt(
rangeParts.length == 2 ? rangeParts[1] : rangeParts[0],
10
);
if (isNaN(startRange) && isNaN(endRange)) { continue;
}
// If the startRange was not specified, then we infer this // to be 1. if (isNaN(startRange) && rangeParts[0] == "") {
startRange = 1;
} // If the end range was not specified, then we infer this // to be the total number of pages. if (isNaN(endRange) && rangeParts[1] == "") {
endRange = numPages;
}
// Check the range for errors if (endRange < startRange) { this._rangeInput.setCustomValidity("startRangeOverflow"); this._pagesSet.clear(); return;
} elseif (
startRange > numPages ||
endRange > numPages ||
startRange == 0
) { this._rangeInput.setCustomValidity("invalid"); this._rangeInput.title = ""; this._pagesSet.clear(); return;
}
for (let i = startRange; i <= endRange; i++) { this._pagesSet.add(i);
}
}
this._rangeInput.setCustomValidity("");
}
validateRangeInput() {
let value = ["custom", "current"].includes(this._rangePicker.value)
? this._rangeInput.value
: ""; this._validateRangeInput(value, this._numPages);
}
if (e.type == "page-count") {
let { totalPages } = e.detail; // This means we have already handled the page count event // and do not need to dispatch another event. if (this._numPages == totalPages) { return;
}
let prevPages = Array.from(this._pagesSet); this.updatePageRange(); if (
prevPages.length != this._pagesSet.size ||
!prevPages.every(page => this._pagesSet.has(page))
) { // If the calculated set of pages has changed then we need to dispatch // a new pageRanges setting :( // Ideally this would be resolved in the settings code since it should // only happen for the "N-" case where pages N through the end of the // document are in the range. this.dispatchPageRange(false);
}
update(settings) { // Re-evaluate which margin options should be enabled whenever the printer or paper changes this._toInchesMultiplier =
settings.paperSizeUnit == settings.kPaperSizeMillimeters
? INCHES_PER_MM
: 1; if (
settings.paperId !== this._paperId ||
settings.printerName !== this._printerName ||
settings.orientation !== this._orientation
) {
let enabledMargins = settings.marginOptions; for (let option of this._marginPicker.options) {
option.hidden = !enabledMargins[option.value];
} this._paperId = settings.paperId; this._printerName = settings.printerName; this._orientation = settings.orientation;
// Paper dimensions are in the paperSizeUnit. As the margin values are in inches // we'll normalize to that when storing max dimensions
let height = this._orientation == 0 ? settings.paperHeight : settings.paperWidth;
let width = this._orientation == 0 ? settings.paperWidth : settings.paperHeight;
let heightInches =
Math.round(this._toInchesMultiplier * height * 100) / 100;
let widthInches =
Math.round(this._toInchesMultiplier * width * 100) / 100;
// The values in custom fields should be initialized to custom margin values // and must be overriden if they are no longer valid. this.setAllMarginValues(settings); this.updateMaxValues(); this.dispatchEvent(new Event("revalidate", { bubbles: true })); this._marginError.hidden = true;
}
if (settings.paperSizeUnit !== this._sizeUnit) { this._sizeUnit = settings.paperSizeUnit;
let unitStr = this._sizeUnit == settings.kPaperSizeMillimeters ? "mm" : "inches"; for (let elem of this.querySelectorAll("[data-unit-prefix-l10n-id]")) {
let l10nId = elem.getAttribute("data-unit-prefix-l10n-id") + unitStr;
document.l10n.setAttributes(elem, l10nId);
}
}
// We need to ensure we don't override the value if the value should be custom. if (this._marginPicker.value != "custom") { // Reset the custom margin values if they are not valid and revalidate the form if (
!this._customTopMargin.checkValidity() ||
!this._customBottomMargin.checkValidity() ||
!this._customLeftMargin.checkValidity() ||
!this._customRightMargin.checkValidity()
) {
window.clearTimeout(this.showErrorTimeoutId); this.setAllMarginValues(settings); this.updateMaxValues(); this.dispatchEvent(new Event("revalidate", { bubbles: true })); this._marginError.hidden = true;
} if (settings.margins == "custom") { // Ensure that we display the custom margin boxes this.querySelector(".margin-group").hidden = false;
} this._marginPicker.value = settings.margins;
}
}
handleEvent(e) { if (e.target == this._marginPicker) {
let customMargin = e.target.value == "custom"; this.querySelector(".margin-group").hidden = !customMargin; if (customMargin) { // Update the custom margin values to ensure consistency this.updateCustomMargins(); return;
}
if (
e.target == this._customTopMargin ||
e.target == this._customBottomMargin ||
e.target == this._customLeftMargin ||
e.target == this._customRightMargin
) { if (e.target.checkValidity()) { this.updateMaxValues();
} if ( this._customTopMargin.validity.valid && this._customBottomMargin.validity.valid && this._customLeftMargin.validity.valid && this._customRightMargin.validity.valid
) { this.formatMargin(e.target); this.updateCustomMargins();
} elseif (e.target.validity.stepMismatch) { // If this is the third digit after the decimal point, we should // truncate the string. this.formatMargin(e.target);
}
}
render() { if (!this.numCopies || !this.sheetCount) { return;
}
let sheetCount = this.sheetCount;
// When printing to a printer (not to a file) update // the sheet count to account for duplex printing. if ( this.outputDestination == Ci.nsIPrintSettings.kOutputDestinationPrinter && this.duplex != Ci.nsIPrintSettings.kDuplexNone
) {
sheetCount = Math.ceil(sheetCount / 2);
}
async function pickFileName(contentTitle, currentURI) {
let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
let [title] = await document.l10n.formatMessages([
{ id: "printui-save-to-pdf-title" },
]);
title = title.value;
let filename; if (contentTitle != "") {
filename = contentTitle;
} else {
let url = new URL(currentURI);
let path = decodeURIComponent(url.pathname);
path = path.replace(/\/$/, "");
filename = path.split("/").pop(); if (filename == "") {
filename = url.hostname;
}
} if (!filename.endsWith(".pdf")) { // macOS and linux don't set the extension based on the default extension. // Windows won't add the extension a second time, fortunately. // If it already ends with .pdf though, adding it again isn't needed.
filename += ".pdf";
}
filename = DownloadPaths.sanitize(filename);
let retval = await new Promise(resolve => picker.open(resolve));
if (retval == 1) { thrownew Error({ reason: "cancelled" });
} else { // OK clicked (retval == 0) or replace confirmed (retval == 2)
// Workaround: When trying to replace an existing file that is open in another application (i.e. a locked file), // the print progress listener is never called. This workaround ensures that a correct status is always returned. try {
let fstream = Cc[ "@mozilla.org/network/file-output-stream;1"
].createInstance(Ci.nsIFileOutputStream);
fstream.init(picker.file, 0x2a, 0o666, 0); // ioflags = write|create|truncate, file permissions = rw-rw-rw-
fstream.close();
// Remove the file to reduce the likelihood of the user opening an empty or damaged fle when the // preview is loading
await IOUtils.remove(picker.file.path);
} catch (e) { thrownew Error({ reason: retval == 0 ? "not_saved" : "not_replaced" });
}
}
return picker.file.path;
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.67 Sekunden
(vorverarbeitet am 2026-04-26)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.