mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
799041171b
commit
0ae9ad6562
22 files changed
+2220
-21
No files matched your search
@@ -18,10 +18,10 @@ import "embed"
|
||||
|
||||
// FS WebUI嵌入静态文件
|
||||
//
|
||||
//go:embed index.html ofdgo.css ofdgo.ico ofdgo.js ofdgo.json ofdgo.png ofdgo.svg ofdgo_edit.js ofdgo_font.js ofdgo_host.js ofdgo_util.js ofdgo_wasm.js ofdgo_work.js wasm_exec.js
|
||||
//go:embed index.html ofdgo.css ofdgo.ico ofdgo.js ofdgo.json ofdgo.png ofdgo.svg ofdgo_edit.js ofdgo_font.js ofdgo_host.js ofdgo_print.js ofdgo_util.js ofdgo_wasm.js ofdgo_work.js wasm_exec.js
|
||||
var FS embed.FS
|
||||
|
||||
// NativeFS WebView嵌入静态文件
|
||||
//
|
||||
//go:embed index.html ofdgo.css ofdgo.ico ofdgo.js ofdgo.json ofdgo.png ofdgo.svg ofdgo_edit.js ofdgo_font.js ofdgo_host.js ofdgo_util.js
|
||||
//go:embed index.html ofdgo.css ofdgo.ico ofdgo.js ofdgo.json ofdgo.png ofdgo.svg ofdgo_edit.js ofdgo_font.js ofdgo_host.js ofdgo_print.js ofdgo_util.js
|
||||
var NativeFS embed.FS
|
||||
@@ -31,6 +31,7 @@
|
||||
<button id="saveAsButton" class="icon-button" type="button" title="另存" disabled><span class="save-as-icon"></span></button>
|
||||
<button id="editButton" class="icon-button" type="button" title="编辑" data-pressed="false" disabled><span class="edit-icon"></span></button>
|
||||
<button id="convertButton" class="icon-button" type="button" title="转换"><span class="convert-icon"></span></button>
|
||||
<button id="printButton" class="icon-button" type="button" title="打印" disabled><span class="print-icon"></span></button>
|
||||
<button id="togglePagesButton" class="icon-button" type="button" title="页面栏" data-pressed="true"><span class="pages-icon"></span></button>
|
||||
<button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" data-pressed="true"><span class="info-icon"></span></button>
|
||||
</div>
|
||||
@@ -671,6 +672,42 @@
|
||||
<div class="form-actions"><div id="encryptionStatus" class="form-status"></div><button id="encryptionCancel" class="button" type="button">取消</button><button id="encryptionSubmit" class="button" type="submit">确定</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="printPanel" class="form-dialog print-dialog">
|
||||
<form id="printForm">
|
||||
<h2 id="printTitle" class="form-title">打印</h2>
|
||||
<div class="print-content">
|
||||
<fieldset id="printSettings" class="print-settings">
|
||||
<div class="form-row"><label for="printPrinter">打印设备</label><div class="print-field"><select id="printPrinter"><option value="browser">浏览器打印</option><option value="pdf">保存为PDF</option></select><button id="printProperties" class="small-button" type="button" hidden>设备属性</button></div></div>
|
||||
<div class="form-row" data-print-row="device" hidden><label for="printCopies">打印份数</label><div class="print-field"><input id="printCopies" type="number" min="1" max="999" value="1"><label class="print-check"><input id="printCollation" type="checkbox" checked>逐份打印</label></div></div>
|
||||
<div class="form-row"><label for="printScope">打印范围</label><select id="printScope"><option value="all">全部页面</option><option value="current">当前页面</option><option value="selected">所选页面</option><option value="range">指定页码</option></select></div>
|
||||
<div class="form-row" data-print-row="range" hidden><label for="printRange">指定页码</label><input id="printRange" type="text" placeholder="1-3,5" autocomplete="off" spellcheck="false"></div>
|
||||
<div class="form-row"><label for="printParity">奇偶筛选</label><div class="print-field"><select id="printParity"><option value="all">全部页面</option><option value="odd">奇数页面</option><option value="even">偶数页面</option></select><label class="print-check"><input id="printReverse" type="checkbox">逆序打印</label></div></div>
|
||||
<div class="form-row"><label for="printMode">页面排版</label><select id="printMode"><option value="single">单页打印</option><option value="multiple">多页合印</option><option value="booklet">小册打印</option><option value="poster">海报拼接</option></select></div>
|
||||
<div class="form-row"><label for="printPaper">纸张大小</label><select id="printPaper"><option value="210,297">A4</option><option value="297,420">A3</option><option value="148,210">A5</option><option value="176,250">B5</option><option value="215.9,279.4">Letter</option><option value="215.9,355.6">Legal</option><option value="source">原页面尺寸</option><option value="custom">自定义</option></select></div>
|
||||
<div class="form-row" data-print-row="paper" hidden><label for="printWidth">纸张尺寸</label><div class="print-field"><input id="printWidth" type="number" value="210" min="1" max="5000" step="0.1" title="纸张宽度"><span>×</span><input id="printHeight" type="number" value="297" min="1" max="5000" step="0.1" title="纸张高度"><span>mm</span></div></div>
|
||||
<div class="form-row"><label for="printOrientation">纸张方向</label><select id="printOrientation"><option value="portrait">纵向</option><option value="landscape">横向</option></select></div>
|
||||
<div class="form-row"><label for="printScaling">页面缩放</label><select id="printScaling"><option value="shrink">缩小大页</option><option value="fit">适合纸张</option><option value="actual">实际大小</option><option value="custom">自定比例</option></select></div>
|
||||
<div class="form-row" data-print-row="scale" hidden><label for="printScale">缩放比例</label><div class="print-field"><input id="printScale" type="number" min="1" max="10000" value="100" step="1"><span>%</span></div></div>
|
||||
<div class="form-row"><span>额外边距</span><div class="print-field print-margins"><input id="printMarginTop" type="number" min="0" value="0" step="0.1" title="上边距"><input id="printMarginRight" type="number" min="0" value="0" step="0.1" title="右边距"><input id="printMarginBottom" type="number" min="0" value="0" step="0.1" title="下边距"><input id="printMarginLeft" type="number" min="0" value="0" step="0.1" title="左边距"><span>mm</span></div></div>
|
||||
<div class="form-row" data-print-row="multiple" hidden><label for="printGrid">每面页数</label><select id="printGrid"><option value="1,2">2 页</option><option value="2,2">4 页</option><option value="2,3">6 页</option><option value="3,3">9 页</option><option value="4,4">16 页</option></select></div>
|
||||
<div class="form-row" data-print-row="multiple" hidden><label for="printPageOrder">排列顺序</label><select id="printPageOrder"><option value="horizontal">横向顺序</option><option value="horizontalReverse">横向逆序</option><option value="vertical">纵向顺序</option><option value="verticalReverse">纵向逆序</option></select></div>
|
||||
<div class="form-row" data-print-row="gap" hidden><label for="printGap">页面间距</label><div class="print-field"><input id="printGap" type="number" min="0" value="4" step="0.1"><span>mm</span></div></div>
|
||||
<div class="form-row" data-print-row="booklet" hidden><label for="printBinding">装订方向</label><select id="printBinding"><option value="left">左侧装订</option><option value="right">右侧装订</option></select></div>
|
||||
<div class="form-row" data-print-row="booklet" hidden><label for="printBookletSheets">每册纸数</label><input id="printBookletSheets" type="number" min="0" max="2500" placeholder="不分册" title="0表示不分册"></div>
|
||||
<div class="form-row" data-print-row="booklet" hidden><label for="printSheetFrom">纸张范围</label><div class="print-field"><input id="printSheetFrom" type="number" min="1" placeholder="起始" title="起始纸张编号"><span>至</span><input id="printSheetTo" type="number" min="1" placeholder="结束" title="结束纸张编号"></div></div>
|
||||
<div class="form-row" data-print-row="poster" hidden><label for="printOverlap">重叠宽度</label><div class="print-field"><input id="printOverlap" type="number" min="0" value="5" step="0.1"><span>mm</span></div></div>
|
||||
<div class="form-row"><label for="printDuplex">双面方式</label><select id="printDuplex"><option value="simplex">单面打印</option><option value="longEdge">自动双面(长边)</option><option value="shortEdge">自动双面(短边)</option><option value="manualLongEdge">手动双面(长边)</option><option value="manualShortEdge">手动双面(短边)</option></select></div>
|
||||
<div class="form-row" data-print-row="sides" hidden><label for="printSides">打印纸面</label><select id="printSides"><option value="all">正反两面</option><option value="front">只印正面</option><option value="back">只印背面</option></select></div>
|
||||
<div class="form-row" data-print-row="manual" hidden><span>背面处理</span><div class="print-field"><label class="print-check"><input id="printBackReverse" type="checkbox">倒序送纸</label><label class="print-check" title="背面旋转180°"><input id="printBackRotate" type="checkbox">背面旋转</label></div></div>
|
||||
<div class="form-row"><label for="printContents">打印内容</label><select id="printContents"><option value="documentAndAnnotations">文档与注解</option><option value="document">文档与表单</option><option value="annotations">仅打印注解</option><option value="forms">仅打印表单</option></select></div>
|
||||
<div class="print-options"><label><input id="printRotate" type="checkbox" checked>自动旋转</label><label><input id="printGrayscale" type="checkbox">灰度打印</label><label><input id="printBorders" type="checkbox">页面边框</label><label><input id="printCropMarks" type="checkbox">裁切标记</label><label><input id="printRaster" type="checkbox">图像打印</label><select id="printDPI" title="图像精度"><option value="150">150 DPI</option><option value="300" selected>300 DPI</option><option value="600">600 DPI</option><option value="1200">1200 DPI</option></select></div>
|
||||
<div class="print-options" data-print-row="poster" hidden><label title="仅拼接超出纸面的页面"><input id="printTileOnlyLarge" type="checkbox" checked>仅拼大页</label><label><input id="printLabels" type="checkbox" checked>拼接标签</label></div>
|
||||
</fieldset>
|
||||
<div class="print-preview"><div id="printPreview" class="print-preview-page"></div><div class="print-preview-tools"><button id="printPrevious" class="small-button" type="button">上一面</button><input id="printPreviewNumber" type="number" min="1" value="1" title="预览纸面"><span id="printPageCount">/ 0</span><button id="printNext" class="small-button" type="button">下一面</button><select id="printZoom" title="预览缩放"><option value="fit">适合</option><option value="0.5">50%</option><option value="1">100%</option><option value="2">200%</option></select></div></div>
|
||||
</div>
|
||||
<div class="form-actions"><div id="printStatus" class="form-status"></div><button id="printCancel" class="button" type="button">关闭</button><button id="printSubmit" class="button" type="submit" disabled>打印</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="exportPanel" class="form-dialog">
|
||||
<form id="exportForm">
|
||||
<h2 id="exportTitle" class="form-title">导出文档</h2>
|
||||
@@ -856,6 +893,7 @@
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div id="ofdgoPrintDocument">请先在OFDGo中打开打印预览,准备完整文档后打印。</div>
|
||||
<script type="module" src="./ofdgo.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+101
-1
@@ -26,6 +26,98 @@
|
||||
--shadow-page: 0 14px 30px rgba(31, 36, 40, 0.16);
|
||||
}
|
||||
|
||||
.form-dialog.print-dialog {
|
||||
width: 920px;
|
||||
}
|
||||
|
||||
.print-content {
|
||||
display: grid;
|
||||
grid-template-columns: 310px minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.print-settings {
|
||||
min-width: 0;
|
||||
max-height: calc(100dvh - 230px);
|
||||
margin: 0;
|
||||
padding: 0 8px 0 0;
|
||||
overflow: auto;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.print-settings .form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 4em minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.print-settings [hidden] { display: none !important; }
|
||||
.print-dialog :is(input, select, button):focus-visible { outline-offset: -2px; }
|
||||
.print-field { display: flex; min-width: 0; align-items: center; gap: 6px; }
|
||||
.print-dialog .print-settings input[type="number"] { min-width: 0; width: 100%; }
|
||||
.print-dialog .print-field > :is(input, select) { flex: 1 1 0; min-width: 0; width: 0; }
|
||||
.print-field > :is(.print-check, button) { flex: 0 0 80px; justify-content: center; }
|
||||
.print-field:has(#printBackReverse) { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
||||
.print-field:has(#printBackReverse) .print-check { justify-content: start; }
|
||||
.print-field > span { flex: none; color: var(--text-muted); white-space: nowrap; }
|
||||
.print-dialog .print-settings .print-margins input { padding: 0 3px; text-align: center; }
|
||||
.print-check { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
|
||||
.print-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: var(--control-height); align-items: center; gap: 8px; margin: 12px 0 0 calc(4em + 8px); }
|
||||
.print-options label { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
|
||||
.print-check input, .print-options input { margin: 0; accent-color: var(--accent); }
|
||||
.print-dialog .form-actions { grid-template-columns: minmax(0, 1fr) 80px 80px; margin-top: 12px; }
|
||||
.print-dialog .form-actions > .form-status { overflow-wrap: anywhere; }
|
||||
.print-dialog .form-actions > .form-status:empty { display: none; }
|
||||
.print-dialog .form-actions > button:first-of-type { grid-column: 2; }
|
||||
|
||||
.print-preview {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 300px;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.print-preview-page {
|
||||
display: grid;
|
||||
height: min(62dvh, 660px);
|
||||
min-height: 240px;
|
||||
justify-items: center;
|
||||
align-items: start;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
background: #e1e6e9;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.print-preview-page img { display: block; max-width: none; background: white; box-shadow: var(--shadow-soft); }
|
||||
.print-preview-tools { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 7px; }
|
||||
.print-dialog .print-preview-tools input { width: 52px; text-align: center; }
|
||||
.print-dialog .print-preview-tools select { width: 70px; }
|
||||
#ofdgoPrintDocument { display: none; }
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.print-content { grid-template-columns: minmax(0, 1fr); }
|
||||
.print-settings { max-height: 34dvh; }
|
||||
.print-preview-page { height: 32dvh; min-height: 160px; }
|
||||
.print-preview { min-height: 210px; }
|
||||
.print-dialog .form-actions { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.print-dialog .form-actions > .form-status { grid-column: 1 / -1; }
|
||||
.print-dialog .form-actions > button:first-of-type { grid-column: 1; }
|
||||
}
|
||||
|
||||
@media print {
|
||||
html, body { height: auto !important; min-height: 0 !important; overflow: visible !important; margin: 0 !important; padding: 0 !important; background: white !important; }
|
||||
body > :not(#ofdgoPrintDocument) { display: none !important; }
|
||||
dialog { display: none !important; }
|
||||
#ofdgoPrintDocument { display: block !important; color: black; background: white; }
|
||||
#ofdgoPrintDocument section { display: block; margin: 0; padding: 0; overflow: hidden; break-after: page; print-color-adjust: exact; -webkit-print-color-adjust: exact; }
|
||||
#ofdgoPrintDocument section:last-child { break-after: auto; }
|
||||
#ofdgoPrintDocument img { display: block; width: 100%; height: 100%; }
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -654,6 +746,7 @@ button:not(:disabled):hover {
|
||||
.save-as-icon,
|
||||
.edit-icon,
|
||||
.convert-icon,
|
||||
.print-icon,
|
||||
.pages-icon,
|
||||
.info-icon {
|
||||
width: 16px;
|
||||
@@ -663,7 +756,7 @@ button:not(:disabled):hover {
|
||||
}
|
||||
|
||||
.open-icon {
|
||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 7V4h6l3 3h9v13H3Z'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 7V3h6l3 4h9v14H3Z'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
@@ -694,6 +787,10 @@ button:not(:disabled):hover {
|
||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 7h18l-4-4M21 17H3l4 4'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
}
|
||||
|
||||
.print-icon {
|
||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9V3h12v6M6 17H3V9h18v8h-3M6 13h12v8H6Z'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
}
|
||||
|
||||
.pages-icon {
|
||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 3h18M3 12h18M3 21h18'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
}
|
||||
@@ -1395,6 +1492,9 @@ button:not(:disabled):hover {
|
||||
.info-custom-row input,
|
||||
.gradient-stop-row input[type="number"],
|
||||
.structure-attribute > input,
|
||||
.print-preview-tools input,
|
||||
.print-preview-tools select,
|
||||
.print-options select,
|
||||
.form-row input[type="text"],
|
||||
.form-row input[type="password"],
|
||||
.form-row input[type="number"],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SVG_NAMESPACE, CanvasEditor, canEditObject, editTextValue, richTextModel, fontFaceStyle, missingGlyphMessage, objectEditReason, selectedText, lineShape, selectionBounds, pagePoint } from "./ofdgo_edit.js";
|
||||
import { FontManager, FontPicker } from "./ofdgo_font.js";
|
||||
import { nativeHost, createEngineWorker, engineLimits, documentBytes, saveFilePicker, directoryPicker } from "./ofdgo_host.js";
|
||||
import { PrintDialog } from "./ofdgo_print.js";
|
||||
import { DocumentDialogs } from "./ofdgo_util.js";
|
||||
|
||||
const MM_TO_PX = 96 / 25.4;
|
||||
@@ -187,6 +188,8 @@ function createDocumentState() {
|
||||
let state = createDocumentState();
|
||||
|
||||
const el = {
|
||||
printButton: document.querySelector("#printButton"),
|
||||
printPanel: document.querySelector("#printPanel"),
|
||||
documentTitleBar: document.querySelector("#documentTitleBar"),
|
||||
documentTabs: document.querySelector("#documentTabs"),
|
||||
documentNewTab: document.querySelector("#documentNewTab"),
|
||||
@@ -650,6 +653,30 @@ const el = {
|
||||
statusText: document.querySelector("#statusText"),
|
||||
};
|
||||
|
||||
let printDialog = null;
|
||||
|
||||
function createPrintDialog() {
|
||||
return new PrintDialog({
|
||||
nativeHost,
|
||||
show: panel => documentDialogs.showApplication(panel),
|
||||
download: downloadBytes,
|
||||
cancel: tab => {
|
||||
for (const [id, request] of wasmRequests) if (request.tab === tab && ["ofdgoPreparePrint", "ofdgoLayoutPrint", "ofdgoPrintSide", "ofdgoPrintPDF"].includes(request.name)) tab.worker?.postMessage({type:"cancel", id});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function openPrintPanel() {
|
||||
const tab = documentTab();
|
||||
if (!tab.state.doc || !tab.state.ready || document.body.hasAttribute("data-busy") || formDialogOpen()) return;
|
||||
printDialog ||= createPrintDialog();
|
||||
printDialog.open({tab, call:(name, ...args) => callTabWASM(tab, name, ...args), page:tab.state.pageIndex,
|
||||
selection:[...tab.state.pageSelection].sort((a,b) => a-b), annotations:tab.state.renderAnnotations,
|
||||
name:tab.state.fileName || tab.state.doc.title || "OFDGo"});
|
||||
}
|
||||
|
||||
el.printButton.addEventListener("click", openPrintPanel);
|
||||
|
||||
const fontManager = new FontManager({
|
||||
host: nativeHost,
|
||||
onChange: scheduleFontSync,
|
||||
@@ -1857,6 +1884,11 @@ function handleKeyDown(event) {
|
||||
}
|
||||
const key = event.key;
|
||||
const target = event.target;
|
||||
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && key.toLowerCase() === "p") {
|
||||
event.preventDefault();
|
||||
openPrintPanel();
|
||||
return;
|
||||
}
|
||||
if (nativeHost && (event.ctrlKey || event.metaKey) && (key === "Tab" || key.toLowerCase() === "w")) {
|
||||
event.preventDefault();
|
||||
if (document.body.hasAttribute("data-busy") || formDialogOpen()) return;
|
||||
@@ -1976,6 +2008,7 @@ function handleKeyDown(event) {
|
||||
}
|
||||
|
||||
function formDialogOpen() {
|
||||
if (el.printPanel.open) return true;
|
||||
return Boolean(sealPreview || resourceManager.pick) || el.aboutPanel.open || el.closePanel.open || el.attachmentDialog.open || settingsElements.Panel.open || fontBindingElements.Panel.open || el.signPanel.open || el.verifyPanel.open || el.credentialsPanel.open || el.encryptionPanel.open || batchElements.Panel.open || el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open || el.documentPanel.open
|
||||
|| resourceElements.Panel.open || el.structureDialog.open || el.versionPanel.open || el.batchPagesPanel.open || el.objectStylePanel.open || el.outlinePanel.open || el.objectBoundsPanel.open || el.annotationNote.open || el.annotationCreate.open || el.objectPicker.open;
|
||||
}
|
||||
@@ -13547,6 +13580,7 @@ function updateDocumentControls() {
|
||||
const count = state.doc?.documents?.length || 0, index = managedDocumentIndex(), manager = state.documentManagement;
|
||||
const busy = document.body.hasAttribute("data-busy");
|
||||
const disabled = !state.doc || !state.ready || state.exporting || busy;
|
||||
el.printButton.disabled = disabled;
|
||||
el.documentManageButton.hidden = !state.editing;
|
||||
el.documentManageButton.disabled = disabled || state.saving;
|
||||
resourceElements.ManageButton.disabled = disabled;
|
||||
@@ -13599,6 +13633,7 @@ function updateControls() {
|
||||
el.outputCompression.disabled = !hasDoc || state.exporting;
|
||||
el.exportPageButton.disabled = !hasDoc || state.exporting || !state.exportFormats.length;
|
||||
el.exportButton.disabled = !hasDoc || state.exporting || !state.exportFormats.length;
|
||||
el.printButton.disabled = !hasDoc || !state.ready || state.exporting || document.body.hasAttribute("data-busy");
|
||||
updateDPIControl();
|
||||
}
|
||||
|
||||
|
||||
@@ -417,6 +417,7 @@ class BrowserEngine {
|
||||
"ofdgoAnnotationInfos", "ofdgoInvoices", "ofdgoInvoice", "ofdgoExportInvoice", "ofdgoValidateInvoice",
|
||||
"ofdgoActionList", "ofdgoPageSettings", "ofdgoReadAnnotation", "ofdgoPreviewSignatureSeal",
|
||||
"ofdgoExportFormats", "ofdgoOutputFormats", "ofdgoParsePageRange", "ofdgoExportPage", "ofdgoExportDocument",
|
||||
"ofdgoPreparePrint", "ofdgoLayoutPrint", "ofdgoPrintSide", "ofdgoPrintPDF", "ofdgoDiscardPrint",
|
||||
"ofdgoExportAttachment", "ofdgoExportResource", "ofdgoExportMedia", "ofdgoExportVersion"
|
||||
].includes(data.name)) this.invalidatePages();
|
||||
const preferred = data.name === "ofdgoRenderPage" ? [...this.slots].find(slot => !slot.busy && slot.pages.has(data.args[0])) : null;
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
export class PrintDialog {
|
||||
constructor(options) {
|
||||
Object.assign(this, options);
|
||||
this.el = Object.fromEntries(["Panel", "Form", "Settings", "Printer", "Properties", "Copies", "Collation", "Duplex", "BackReverse", "BackRotate", "Contents", "Scope", "Range", "Parity", "Reverse", "Mode", "Paper", "Width", "Height", "Orientation", "Scaling", "Scale", "MarginTop", "MarginRight", "MarginBottom", "MarginLeft", "Grid", "PageOrder", "Gap", "Binding", "BookletSheets", "SheetFrom", "SheetTo", "Overlap", "Sides", "Rotate", "Grayscale", "Borders", "CropMarks", "Raster", "DPI", "TileOnlyLarge", "Labels", "Preview", "Previous", "Next", "PreviewNumber", "PageCount", "Zoom", "Status", "Cancel", "Submit"].map(name => [name, document.querySelector(`#print${name}`)]));
|
||||
this.printerOptions = this.el.Printer.innerHTML;
|
||||
this.paperOptions = this.el.Paper.innerHTML;
|
||||
this.queue = Promise.resolve();
|
||||
this.sequence = 0;
|
||||
this.urls = [];
|
||||
this.root = document.querySelector("#ofdgoPrintDocument");
|
||||
this.el.Settings.addEventListener("input", event => { if (event.target === this.el.Printer) this.choosePrinter(); else this.schedule(); });
|
||||
this.el.Properties.addEventListener("click", () => this.properties());
|
||||
this.el.Form.addEventListener("submit", event => { event.preventDefault(); this.print(); });
|
||||
this.el.Cancel.addEventListener("click", () => this.el.Panel.close());
|
||||
this.el.Panel.addEventListener("close", () => this.close());
|
||||
this.el.Panel.addEventListener("cancel", event => { if (this.waiting) event.preventDefault(); });
|
||||
this.el.Previous.addEventListener("click", () => this.preview(this.side - 1));
|
||||
this.el.Next.addEventListener("click", () => this.preview(this.side + 1));
|
||||
this.el.PreviewNumber.addEventListener("change", () => this.preview(Number(this.el.PreviewNumber.value) - 1));
|
||||
this.el.Zoom.addEventListener("change", () => this.resize());
|
||||
new ResizeObserver(() => this.resize()).observe(this.el.Preview);
|
||||
window.addEventListener("afterprint", () => { if (this.waiting === "browser") this.finishPrint(); });
|
||||
window.addEventListener("beforeprint", () => {
|
||||
if (!this.root?.children.length) {
|
||||
this.root ||= this.createRoot();
|
||||
this.root.textContent = "请先在OFDGo中打开打印预览,准备完整文档后打印。";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
open(context) {
|
||||
if (this.el.Panel.open) return;
|
||||
this.context = context;
|
||||
this.prepared = false;
|
||||
this.layout = null;
|
||||
this.job = this.profile = null;
|
||||
this.side = 0;
|
||||
this.sequence++;
|
||||
this.el.Form.reset();
|
||||
this.mode = null;
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem("ofdgo.print.v2") || "null");
|
||||
for (const [name, value] of Object.entries(saved || {})) {
|
||||
const node = this.el[name];
|
||||
if (node?.type === "checkbox" && typeof value === "boolean") node.checked = value;
|
||||
else if (node && typeof value === "string") node.value = value;
|
||||
}
|
||||
} catch {}
|
||||
if (this.el.BookletSheets.value === "0") this.el.BookletSheets.value = "";
|
||||
this.el.Scope.value = context.selection.length ? "selected" : "all";
|
||||
this.el.Scope.querySelector('option[value="selected"]').disabled = !context.selection.length;
|
||||
this.el.Contents.value = context.annotations ? "documentAndAnnotations" : "document";
|
||||
this.el.Status.textContent = "";
|
||||
this.el.Preview.replaceChildren();
|
||||
this.show(this.el.Panel);
|
||||
this.loadDevices();
|
||||
}
|
||||
|
||||
isDevice() { return this.el.Printer.value.startsWith("device:"); }
|
||||
manual() { return this.el.Duplex.value.startsWith("manual"); }
|
||||
|
||||
async loadDevices() {
|
||||
const sequence = ++this.sequence;
|
||||
this.loading = true;
|
||||
this.controls();
|
||||
this.el.Printer.innerHTML = this.printerOptions;
|
||||
try {
|
||||
if (this.nativeHost) {
|
||||
this.el.Printer.querySelector('option[value="browser"]').textContent = "系统打印";
|
||||
const capabilities = await this.nativeHost.call("print.capabilities", {});
|
||||
if (!this.current(sequence)) return;
|
||||
this.capabilities = capabilities;
|
||||
const printers = capabilities.direct ? await this.nativeHost.call("print.printers", {}) : [];
|
||||
if (!this.current(sequence)) return;
|
||||
for (const printer of printers) this.el.Printer.add(new Option(printer.name, `device:${printer.name}`));
|
||||
const printer = printers.find(printer => printer.default) || printers[0];
|
||||
this.el.Printer.value = printer ? `device:${printer.name}` : "pdf";
|
||||
}
|
||||
} catch (error) { if (this.current(sequence)) this.el.Status.textContent = error.message; }
|
||||
finally {
|
||||
if (this.current(sequence)) { this.loading = false; await this.choosePrinter(); }
|
||||
}
|
||||
}
|
||||
|
||||
async choosePrinter() {
|
||||
const sequence = ++this.sequence;
|
||||
const paper = this.el.Paper.value;
|
||||
this.layout = this.job = this.profile = null;
|
||||
this.loading = true;
|
||||
this.controls();
|
||||
this.el.Paper.innerHTML = this.paperOptions;
|
||||
if (!this.isDevice()) {
|
||||
this.el.Paper.value = paper;
|
||||
if (!this.el.Paper.value) {
|
||||
const [width, height] = paper.split(",").map(Number);
|
||||
if (width > 0 && height > 0 && Number.isFinite(width) && Number.isFinite(height)) {
|
||||
this.el.Paper.value = "custom";
|
||||
this.el.Width.value = width;
|
||||
this.el.Height.value = height;
|
||||
} else this.el.Paper.value = "210,297";
|
||||
}
|
||||
}
|
||||
this.el.Properties.hidden = true;
|
||||
try {
|
||||
if (this.isDevice()) {
|
||||
const profile = await this.nativeHost.call("print.query", {printer:this.el.Printer.value.slice(7), copies:1, duplex:"simplex"});
|
||||
if (!this.current(sequence)) return;
|
||||
this.profile = profile;
|
||||
this.el.Paper.replaceChildren(...profile.papers.map(paper => new Option(paper.name, `${paper.width},${paper.height}`)));
|
||||
if (profile.customPaper) this.el.Paper.add(new Option("自定义", "custom"));
|
||||
this.el.Paper.value = `${profile.options.width},${profile.options.height}`;
|
||||
if (!this.el.Paper.value && profile.customPaper) { this.el.Paper.value = "custom"; this.el.Width.value = profile.options.width; this.el.Height.value = profile.options.height; }
|
||||
if (!this.el.Paper.value) throw new Error("设备默认纸张不在支持列表中");
|
||||
this.el.Orientation.value = profile.options.orientation;
|
||||
this.el.Copies.max = Math.min(999, profile.maxCopies || 999);
|
||||
this.el.Copies.value = 1;
|
||||
this.el.Grayscale.checked = !profile.color;
|
||||
this.el.Grayscale.disabled = !profile.color;
|
||||
this.el.Properties.hidden = !profile.properties;
|
||||
} else this.el.Grayscale.disabled = false;
|
||||
} catch (error) { if (this.current(sequence)) { this.el.Status.textContent = error.message; return; } }
|
||||
finally { if (this.current(sequence)) { this.loading = false; this.controls(); } }
|
||||
if (this.current(sequence)) this.schedule();
|
||||
}
|
||||
|
||||
async properties() {
|
||||
if (!this.isDevice() || this.running || this.loading) return;
|
||||
this.loading = true;
|
||||
this.controls();
|
||||
try { await this.nativeHost.call("print.properties", {printer:this.el.Printer.value.slice(7)}); }
|
||||
catch (error) { this.el.Status.textContent = error.message; }
|
||||
finally { this.loading = false; if (this.current()) await this.choosePrinter(); }
|
||||
}
|
||||
|
||||
deviceOptions() {
|
||||
const [width, height] = this.el.Paper.value === "custom" ? [Number(this.el.Width.value), Number(this.el.Height.value)] : this.el.Paper.value.split(",").map(Number);
|
||||
const manual = this.manual(), partial = this.el.Sides.value !== "all";
|
||||
return {printer:this.el.Printer.value.slice(7), width, height, orientation:this.el.Orientation.value,
|
||||
copies:Number(this.el.Copies.value), collation:this.el.Collation.checked ? "collated" : "uncollated",
|
||||
duplex:manual || partial ? "simplex" : this.el.Duplex.value, color:this.el.Grayscale.checked ? "grayscale" : "color",
|
||||
scale:1, pagesPerSide:1, margins:{top:0,right:0,bottom:0,left:0},
|
||||
...(this.capabilities?.backgrounds ? {backgrounds:true} : {}), ...(this.capabilities?.headersFooters ? {headersFooters:false} : {})};
|
||||
}
|
||||
|
||||
options(profile) {
|
||||
const el = this.el;
|
||||
const number = name => {
|
||||
const value = Number(el[name].value);
|
||||
if (!el[name].value.trim() || !Number.isFinite(value)) throw new Error("请输入有效数值");
|
||||
return value;
|
||||
};
|
||||
let [paperWidth, paperHeight] = el.Paper.value === "source" ? [0, 0] : el.Paper.value === "custom" ? [number("Width"), number("Height")] : el.Paper.value.split(",").map(Number);
|
||||
if (el.Orientation.value === "landscape" && paperWidth < paperHeight) [paperWidth, paperHeight] = [paperHeight, paperWidth];
|
||||
if (profile) [paperWidth, paperHeight] = [profile.width, profile.height];
|
||||
const scope = el.Scope.value;
|
||||
const [rows, columns] = el.Grid.value.split(",").map(Number);
|
||||
if (scope === "selected" && !this.context.selection.length) throw new Error("未选择页面");
|
||||
if (scope === "range" && !el.Range.value.trim()) throw new Error("请输入页码范围");
|
||||
const manual = this.manual(), duplex = el.Duplex.value.replace("manualL", "l").replace("manualS", "s");
|
||||
if (manual && this.isDevice() && number("Copies") !== 1) throw new Error("手动双面每次仅支持1份");
|
||||
return {
|
||||
...(scope === "current" ? {pages:[this.context.page]} : scope === "selected" ? {pages:this.context.selection} : scope === "range" ? {pageRange:el.Range.value.trim()} : {}),
|
||||
mode:el.Mode.value, scaling:el.Scaling.value, paperWidth, paperHeight,
|
||||
margins:{top:number("MarginTop"), right:number("MarginRight"), bottom:number("MarginBottom"), left:number("MarginLeft")},
|
||||
...(profile ? {printableArea:{x:profile.printableArea.x, y:profile.printableArea.y, w:profile.printableArea.width, h:profile.printableArea.height}} : {}),
|
||||
copies:this.isDevice() ? number("Copies") : 1, collate:el.Collation.checked, duplex,
|
||||
scale:number("Scale") / 100, rotate:el.Rotate.checked, reverse:el.Reverse.checked !== (manual && el.Sides.value === "back" && el.BackReverse.checked), parity:el.Parity.value, sides:el.Sides.value,
|
||||
backRotate:manual && el.BackRotate.checked,
|
||||
rows, columns, pageOrder:el.PageOrder.value, gap:number("Gap"), binding:el.Binding.value, bookletSheets:el.BookletSheets.value ? number("BookletSheets") : 0, overlap:number("Overlap"),
|
||||
...(el.Mode.value === "booklet" ? {sheetFrom:el.SheetFrom.value ? number("SheetFrom") : 0, sheetTo:el.SheetTo.value ? number("SheetTo") : 0} : {}),
|
||||
contents:el.Contents.value, grayscale:el.Grayscale.checked, borders:el.Borders.checked, cropMarks:el.CropMarks.checked, raster:el.Raster.checked, dpi:number("DPI"), tileOnlyLarge:el.TileOnlyLarge.checked, labels:el.Labels.checked
|
||||
};
|
||||
}
|
||||
|
||||
schedule() {
|
||||
const el = this.el, mode = el.Mode.value;
|
||||
const source = el.Paper.querySelector('option[value="source"]');
|
||||
if (source) source.disabled = mode !== "single";
|
||||
if (mode !== "single" && el.Paper.value === "source") el.Paper.value = "210,297";
|
||||
if (mode === "booklet" && this.mode !== "booklet") {
|
||||
this.orientation = el.Orientation.value;
|
||||
this.duplexBeforeBooklet = el.Duplex.value;
|
||||
this.scalingBeforeBooklet = el.Scaling.value;
|
||||
el.Scaling.value = "shrink";
|
||||
el.Duplex.value = this.isDevice() && !this.profile?.duplex ? "manualShortEdge" : "shortEdge";
|
||||
} else if (mode !== "booklet" && this.mode === "booklet") {
|
||||
el.Orientation.value = this.orientation;
|
||||
el.Duplex.value = this.duplexBeforeBooklet;
|
||||
el.Scaling.value = this.scalingBeforeBooklet;
|
||||
}
|
||||
if (mode === "booklet") el.Orientation.value = "landscape";
|
||||
this.mode = mode;
|
||||
if (this.isDevice() && !this.profile?.duplex && ["longEdge", "shortEdge"].includes(el.Duplex.value)) el.Duplex.value = el.Duplex.value === "longEdge" ? "manualLongEdge" : "manualShortEdge";
|
||||
if (mode === "booklet" && el.Duplex.value === "simplex") el.Duplex.value = this.isDevice() && !this.profile?.duplex ? "manualShortEdge" : "shortEdge";
|
||||
if (el.Duplex.value === "simplex") el.Sides.value = "all";
|
||||
const isManual = this.manual();
|
||||
el.Sides.querySelector('option[value="all"]').disabled = isManual;
|
||||
if (isManual && el.Sides.value === "all") el.Sides.value = "front";
|
||||
for (const option of el.Duplex.options) option.disabled = this.isDevice() && !this.profile?.duplex && ["longEdge", "shortEdge"].includes(option.value) || mode === "booklet" && ["simplex", "longEdge", "manualLongEdge"].includes(option.value);
|
||||
el.Orientation.disabled = mode === "booklet" || el.Paper.value === "source";
|
||||
const rows = {device:this.isDevice(), sides:el.Duplex.value !== "simplex", manual:isManual, range:el.Scope.value === "range", paper:el.Paper.value === "custom", scale:el.Scaling.value === "custom" || mode === "poster", multiple:mode === "multiple", gap:mode === "multiple" || mode === "booklet", booklet:mode === "booklet", poster:mode === "poster"};
|
||||
for (const row of el.Panel.querySelectorAll("[data-print-row]")) {
|
||||
row.hidden = !rows[row.dataset.printRow];
|
||||
}
|
||||
el.Scaling.disabled = mode === "poster" || mode === "booklet";
|
||||
el.DPI.disabled = !el.Raster.checked && !el.Grayscale.checked;
|
||||
const sequence = ++this.sequence;
|
||||
this.layout = null;
|
||||
this.job = null;
|
||||
this.controls();
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(() => {
|
||||
this.queue = this.queue.catch(() => {}).then(() => this.refresh(sequence));
|
||||
}, 160);
|
||||
}
|
||||
|
||||
async refresh(sequence) {
|
||||
if (!this.current(sequence)) return;
|
||||
const context = this.context;
|
||||
try {
|
||||
if (!this.el.Form.reportValidity()) return;
|
||||
const deviceOptions = this.isDevice() ? this.deviceOptions() : null;
|
||||
const profile = deviceOptions ? await this.nativeHost.call("print.query", deviceOptions) : null;
|
||||
if (!this.current(sequence)) return;
|
||||
this.profile = profile;
|
||||
const options = this.options(profile);
|
||||
this.el.Status.textContent = "正在准备打印";
|
||||
const layout = await context.call(this.prepared ? "ofdgoLayoutPrint" : "ofdgoPreparePrint", options);
|
||||
if (!this.current(sequence)) return;
|
||||
this.prepared = true;
|
||||
this.layout = layout;
|
||||
if (this.isDevice()) this.el.Copies.max = Math.min(999, this.profile.maxCopies || 999, layout.copiesLimit > 0 ? layout.copiesLimit : 999);
|
||||
this.job = {deviceOptions, fingerprint:profile?.fingerprint, manual:this.manual(), sides:options.sides, backContent:layout.backContent};
|
||||
this.el.Status.textContent = "";
|
||||
if (layout.sides.some(side => side.placements.some(placement => placement.clipped))) this.el.Status.textContent = "内容超出可打印区域";
|
||||
if (this.manual() && options.sides === "back" && !layout.backContent) this.el.Status.textContent = "背面为空白";
|
||||
this.remember();
|
||||
await this.preview(Math.min(this.side, layout.sides.length - 1));
|
||||
} catch (error) {
|
||||
if (this.current(sequence)) this.el.Status.textContent = error.code === "printDenied" ? "文档禁止打印" : error.message;
|
||||
} finally {
|
||||
if (this.current(sequence)) this.controls();
|
||||
}
|
||||
}
|
||||
|
||||
current(sequence = this.sequence) { return this.el.Panel.open && sequence === this.sequence && this.context && !this.context.tab.closed; }
|
||||
|
||||
async preview(index) {
|
||||
if (!this.layout || this.running || index < 0 || index >= this.layout.sides.length) return;
|
||||
const sequence = this.sequence, preview = this.previewSequence = (this.previewSequence || 0) + 1;
|
||||
this.side = index;
|
||||
this.controls();
|
||||
try {
|
||||
let {svg} = await this.context.call("ofdgoPrintSide", index);
|
||||
if (!this.current(sequence) || preview !== this.previewSequence) return;
|
||||
if (this.profile) {
|
||||
const area = this.profile.printableArea;
|
||||
svg = svg.replace("</svg>", `<rect x="${area.x}" y="${area.y}" width="${area.width}" height="${area.height}" fill="none" stroke="#88949e" stroke-width="0.2" stroke-dasharray="1 1"/></svg>`);
|
||||
}
|
||||
const url = URL.createObjectURL(new Blob([svg], {type:"image/svg+xml"}));
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
try { await img.decode(); } catch (error) { URL.revokeObjectURL(url); throw error; }
|
||||
if (!this.current(sequence) || preview !== this.previewSequence) { URL.revokeObjectURL(url); return; }
|
||||
if (this.previewURL) URL.revokeObjectURL(this.previewURL);
|
||||
this.previewURL = url;
|
||||
this.el.Preview.replaceChildren(img);
|
||||
this.resize();
|
||||
} catch (error) { if (this.current(sequence)) { this.el.Status.textContent = error.message; this.layout = null; this.controls(); } }
|
||||
}
|
||||
|
||||
resize() {
|
||||
const img = this.el.Preview.querySelector("img"), side = this.layout?.sides[this.side];
|
||||
if (!img || !side) return;
|
||||
const factor = this.el.Zoom.value === "fit" ? Math.min((this.el.Preview.clientWidth - 24) / side.width, (this.el.Preview.clientHeight - 24) / side.height) : Number(this.el.Zoom.value) * 96 / 25.4;
|
||||
img.style.width = `${Math.max(1, side.width * factor)}px`;
|
||||
img.style.height = `${Math.max(1, side.height * factor)}px`;
|
||||
}
|
||||
|
||||
controls() {
|
||||
const count = this.layout?.sides.length || 0, busy = !!this.running || !!this.loading;
|
||||
this.el.Settings.disabled = busy;
|
||||
this.el.Submit.disabled = !count || busy;
|
||||
if (this.el.Printer.value !== "pdf" && this.manual() && this.el.Sides.value === "back" && !this.layout?.backContent) this.el.Submit.disabled = true;
|
||||
this.el.Previous.disabled = busy || !count || this.side === 0;
|
||||
this.el.Next.disabled = busy || !count || this.side + 1 === count;
|
||||
this.el.PreviewNumber.disabled = busy || !count;
|
||||
this.el.PreviewNumber.max = count || 1;
|
||||
this.el.PreviewNumber.value = count ? this.side + 1 : 1;
|
||||
this.el.PageCount.textContent = `/ ${count}`;
|
||||
this.el.Cancel.textContent = busy && !this.waiting ? "取消" : "关闭";
|
||||
this.el.Cancel.disabled = !!this.waiting;
|
||||
}
|
||||
|
||||
remember() {
|
||||
const saved = {};
|
||||
for (const [name, node] of Object.entries(this.el)) if (node.closest("#printSettings") && !["Printer", "Copies", "Paper", "Scope", "Range", "Parity", "Reverse", "Sides", "Contents", "SheetFrom", "SheetTo"].includes(name) && node.matches("input, select")) saved[name] = node.type === "checkbox" ? node.checked : node.value;
|
||||
try { localStorage.setItem("ofdgo.print.v2", JSON.stringify(saved)); } catch {}
|
||||
}
|
||||
|
||||
createRoot() {
|
||||
const root = document.createElement("div");
|
||||
root.id = "ofdgoPrintDocument";
|
||||
document.body.append(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
async print() {
|
||||
if (!this.layout || !this.job || this.running) return;
|
||||
if (this.el.Printer.value === "pdf") return this.pdf();
|
||||
const sequence = this.sequence, context = this.context;
|
||||
const job = this.job;
|
||||
this.running = true;
|
||||
this.controls();
|
||||
try {
|
||||
this.root ||= this.createRoot();
|
||||
this.root.replaceChildren();
|
||||
const style = document.createElement("style");
|
||||
this.root.append(style);
|
||||
for (let i = 0; i < this.layout.sides.length; i++) {
|
||||
if (!this.current(sequence)) throw new DOMException("打印已取消", "AbortError");
|
||||
this.el.Status.textContent = `正在准备纸面 ${i + 1}/${this.layout.sides.length}`;
|
||||
const {svg} = await context.call("ofdgoPrintSide", i);
|
||||
if (!this.current(sequence)) throw new DOMException("打印已取消", "AbortError");
|
||||
const url = URL.createObjectURL(new Blob([svg], {type:"image/svg+xml"}));
|
||||
this.urls.push(url);
|
||||
const img = new Image(), section = document.createElement("section"), side = this.layout.sides[i];
|
||||
img.src = url;
|
||||
section.append(img);
|
||||
section.style.cssText = `page:ofdgoPrint${i};width:${side.width}mm;height:${side.height}mm`;
|
||||
style.textContent += `@page ofdgoPrint${i}{size:${side.width}mm ${side.height}mm;margin:0}`;
|
||||
this.root.append(section);
|
||||
await img.decode();
|
||||
}
|
||||
await document.fonts.ready;
|
||||
if (!this.current(sequence)) throw new DOMException("打印已取消", "AbortError");
|
||||
this.previousTitle = document.title;
|
||||
document.title = context.name;
|
||||
this.waiting = job.deviceOptions ? "native" : "browser";
|
||||
this.controls();
|
||||
this.el.Status.textContent = job.deviceOptions ? "正在提交打印作业" : "正在打开打印窗口";
|
||||
if (job.deviceOptions) {
|
||||
const result = await this.nativeHost.call("print", {...job.deviceOptions, fingerprint:job.fingerprint});
|
||||
if (this.current(sequence)) {
|
||||
this.finishPrint();
|
||||
if (!result?.submitted) throw new Error("打印作业未被接受");
|
||||
if (job.manual && job.sides === "front") {
|
||||
if (job.backContent) { this.el.Sides.value = "back"; this.schedule(); }
|
||||
else this.el.Status.textContent = "背面为空白";
|
||||
}
|
||||
}
|
||||
} else if (this.nativeHost) {
|
||||
const capabilities = await this.nativeHost.call("print.capabilities", {});
|
||||
if (!this.current(sequence)) throw new DOMException("打印已取消", "AbortError");
|
||||
if (!capabilities.systemDialog) throw new Error("当前运行时不支持系统打印");
|
||||
await this.nativeHost.call("print.dialog", {kind:"system"});
|
||||
if (this.current(sequence) && capabilities.dialogCompletion) this.finishPrint();
|
||||
} else if (typeof window.print === "function") window.print();
|
||||
else throw new Error("当前浏览器不支持打印");
|
||||
} catch (error) {
|
||||
if (this.current(sequence)) {
|
||||
this.finishPrint();
|
||||
this.el.Status.textContent = error.name === "AbortError" ? "打印已取消" : error.message;
|
||||
}
|
||||
} finally {
|
||||
if (this.current(sequence)) {
|
||||
if (!this.waiting) this.running = false;
|
||||
this.controls();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finishPrint() {
|
||||
if (this.previousTitle !== undefined) { document.title = this.previousTitle; delete this.previousTitle; }
|
||||
this.root?.replaceChildren();
|
||||
for (const url of this.urls) URL.revokeObjectURL(url);
|
||||
this.urls = [];
|
||||
this.el.Status.textContent = "";
|
||||
this.waiting = this.running = false;
|
||||
this.controls();
|
||||
}
|
||||
|
||||
async pdf() {
|
||||
if (!this.layout || this.running) return;
|
||||
const sequence = this.sequence, context = this.context;
|
||||
this.running = true;
|
||||
this.controls();
|
||||
this.el.Status.textContent = "正在生成打印PDF";
|
||||
try {
|
||||
const result = await context.call("ofdgoPrintPDF");
|
||||
if (this.current(sequence)) {
|
||||
await this.download(result.bytes, "application/pdf", `${context.name.replace(/\.[^.]+$/, "")}-打印.pdf`);
|
||||
if (this.current(sequence)) this.el.Status.textContent = "";
|
||||
}
|
||||
} catch (error) { if (this.current(sequence)) this.el.Status.textContent = error.message; }
|
||||
finally { if (this.current(sequence)) { this.running = false; this.controls(); } }
|
||||
}
|
||||
|
||||
close() {
|
||||
const context = this.context;
|
||||
this.sequence++;
|
||||
clearTimeout(this.timer);
|
||||
if (context) this.cancel(context.tab);
|
||||
this.finishPrint();
|
||||
if (this.previewURL) URL.revokeObjectURL(this.previewURL);
|
||||
this.previewURL = null;
|
||||
this.el.Preview.replaceChildren();
|
||||
this.context = null;
|
||||
this.queue = this.queue.catch(() => {}).then(() => context?.call("ofdgoDiscardPrint").catch(() => {}));
|
||||
}
|
||||
}
|
||||
@@ -235,11 +235,11 @@ async function handleMessage(engine, { id, name, args }) {
|
||||
let saveStamp;
|
||||
let channel;
|
||||
const signal = operations.get(id)?.signal;
|
||||
const cancellable = ["ofdgoResources", "ofdgoPrepareSave", "ofdgoSaveSnapshot", "ofdgoImportDocuments", "ofdgoImportImages", "ofdgoExportImages", "ofdgoConvertPDF", "ofdgoExportPage", "ofdgoExportDocument", "ofdgoExportAttachment", "ofdgoExportResource", "ofdgoExportMedia", "ofdgoExportVersion", "ofdgoSaveDocument", "ofdgoSaveSigned", "ofdgoImportPages"].includes(name);
|
||||
const cancellable = ["ofdgoPreparePrint", "ofdgoLayoutPrint", "ofdgoPrintSide", "ofdgoPrintPDF", "ofdgoResources", "ofdgoPrepareSave", "ofdgoSaveSnapshot", "ofdgoImportDocuments", "ofdgoImportImages", "ofdgoExportImages", "ofdgoConvertPDF", "ofdgoExportPage", "ofdgoExportDocument", "ofdgoExportAttachment", "ofdgoExportResource", "ofdgoExportMedia", "ofdgoExportVersion", "ofdgoSaveDocument", "ofdgoSaveSigned", "ofdgoImportPages"].includes(name);
|
||||
const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages" || name === "ofdgoImportDocuments";
|
||||
const converting = name === "ofdgoConvertPDF";
|
||||
const saving = name === "ofdgoSaveDocument" || name === "ofdgoSaveSnapshot" || name === "ofdgoSaveSigned";
|
||||
const resources = name === "ofdgoResources" || name === "ofdgoPrepareSave";
|
||||
const resources = ["ofdgoPreparePrint", "ofdgoLayoutPrint", "ofdgoPrintSide", "ofdgoPrintPDF", "ofdgoResources", "ofdgoPrepareSave"].includes(name);
|
||||
const key = name === "ofdgoOpen" ? args[3]?.key : name === "ofdgoSaveSigned" || name === "ofdgoLoadImport" ? args[1]?.key : null;
|
||||
const keyPassword = name === "ofdgoOpen" ? args[3]?.keyPassword : name === "ofdgoSaveSigned" || name === "ofdgoLoadImport" ? args[1]?.keyPassword : null;
|
||||
const password = converting ? args[2] : name === "ofdgoSetEncryption" ? args[0]?.password : name === "ofdgoOpen" ? args[3]?.password : name === "ofdgoLoadImport" ? args[1]?.password : null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const CACHE_PREFIX = `ofdgo:${self.registration.scope}:app:`;
|
||||
const META_CACHE = `${CACHE_PREFIX}meta`;
|
||||
const ASSETS = ["./", "ofdgo.css", "ofdgo.ico", "ofdgo.js", "ofdgo.json", "ofdgo.png", "ofdgo.svg", "ofdgo.wasm", "ofdgo_edit.js", "ofdgo_font.js", "ofdgo_host.js", "ofdgo_util.js", "ofdgo_wasm.js", "ofdgo_work.js", "wasm_exec.js"]
|
||||
const ASSETS = ["./", "ofdgo.css", "ofdgo.ico", "ofdgo.js", "ofdgo.json", "ofdgo.png", "ofdgo.svg", "ofdgo.wasm", "ofdgo_edit.js", "ofdgo_font.js", "ofdgo_host.js", "ofdgo_print.js", "ofdgo_util.js", "ofdgo_wasm.js", "ofdgo_work.js", "wasm_exec.js"]
|
||||
.map((path) => new URL(path, self.registration.scope).href);
|
||||
const CLIENT_PREFIX = new URL("__client/", self.registration.scope).href;
|
||||
|
||||
|
||||
Reference in new issue
Block a user