mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:59:27 +08:00
1 parent
d3904521b9
commit
3e87048a30
8 files changed
+336
-32
No files matched your search
+22
-2
@@ -50,9 +50,29 @@
|
|||||||
<option value="600">600</option>
|
<option value="600">600</option>
|
||||||
</select>
|
</select>
|
||||||
<button id="exportPageButton" class="button" type="button" data-short="单" title="导出单页" aria-label="导出单页" disabled>单页</button>
|
<button id="exportPageButton" class="button" type="button" data-short="单" title="导出单页" aria-label="导出单页" disabled>单页</button>
|
||||||
<button id="exportButton" class="button" type="button" data-short="全" title="导出文档" aria-label="导出文档" disabled>文档</button>
|
<button id="exportButton" class="button" type="button" data-short="档" title="导出文档" aria-label="导出文档" aria-haspopup="dialog" popovertarget="exportPanel" disabled>文档</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<div id="exportPanel" class="export-panel" popover role="dialog" aria-label="导出文档">
|
||||||
|
<form id="exportForm">
|
||||||
|
<div class="export-row">
|
||||||
|
<span id="exportScopeLabel">范围</span>
|
||||||
|
<div class="export-options" role="radiogroup" aria-labelledby="exportScopeLabel">
|
||||||
|
<label><input id="exportAll" type="radio" name="scope" value="all" checked autofocus><span>全部</span></label>
|
||||||
|
<label><input id="exportSpecified" type="radio" name="scope" value="specified"><span>指定</span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="exportRangeRow" class="export-row" hidden>
|
||||||
|
<label for="exportRange">页码</label>
|
||||||
|
<input id="exportRange" type="text" aria-describedby="exportRangeStatus" autocomplete="off" spellcheck="false">
|
||||||
|
</div>
|
||||||
|
<div class="export-actions">
|
||||||
|
<div id="exportRangeStatus" class="export-range-status" role="status" aria-live="polite"></div>
|
||||||
|
<button class="button" type="button" popovertarget="exportPanel" popovertargetaction="hide">取消</button>
|
||||||
|
<button id="exportSubmit" class="button" type="submit">导出</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<main class="workspace">
|
<main class="workspace">
|
||||||
<aside class="page-list-panel">
|
<aside class="page-list-panel">
|
||||||
@@ -70,7 +90,7 @@
|
|||||||
<button type="submit" class="button">搜索</button>
|
<button type="submit" class="button">搜索</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="search-tools">
|
<div class="search-tools">
|
||||||
<span id="searchStatus" role="status" aria-live="polite">输入文字</span>
|
<span id="searchStatus" role="status" aria-live="polite"></span>
|
||||||
<span id="searchCount">0 / 0</span>
|
<span id="searchCount">0 / 0</span>
|
||||||
<button id="searchPrev" type="button" class="icon-button arrow-prev" title="上一处" aria-label="上一处" aria-keyshortcuts="Shift+F3" disabled></button>
|
<button id="searchPrev" type="button" class="icon-button arrow-prev" title="上一处" aria-label="上一处" aria-keyshortcuts="Shift+F3" disabled></button>
|
||||||
<button id="searchNext" type="button" class="icon-button arrow-next" title="下一处" aria-label="下一处" aria-keyshortcuts="F3" disabled></button>
|
<button id="searchNext" type="button" class="icon-button arrow-next" title="下一处" aria-label="下一处" aria-keyshortcuts="F3" disabled></button>
|
||||||
|
|||||||
@@ -256,6 +256,109 @@ button:not(:disabled):hover {
|
|||||||
width: 54px;
|
width: 54px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.export-panel {
|
||||||
|
top: calc(var(--bar-height) + 8px);
|
||||||
|
right: 0;
|
||||||
|
bottom: auto;
|
||||||
|
left: auto;
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
max-height: calc(100dvh - var(--bar-height) - 18px);
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--panel);
|
||||||
|
color: #1f2428;
|
||||||
|
box-shadow: var(--shadow-soft);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2em minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-options {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-options label {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-options input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-options span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: var(--control-height);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-options label + label span {
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-options input:checked + span {
|
||||||
|
background: #e4f3ef;
|
||||||
|
color: var(--accent-strong);
|
||||||
|
box-shadow: 0 0 0 1px var(--accent) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-options input:focus-visible + span {
|
||||||
|
outline: 2px solid var(--focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-row input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
height: var(--control-height);
|
||||||
|
padding: 0 6px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #1f2428;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-row input[aria-invalid="true"] {
|
||||||
|
border-color: #b14343;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-range-status {
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#exportSubmit {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--accent);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#exportSubmit:not(:disabled):hover {
|
||||||
|
background: var(--accent-strong);
|
||||||
|
}
|
||||||
|
|
||||||
.format-select:focus,
|
.format-select:focus,
|
||||||
.format-select:focus-visible {
|
.format-select:focus-visible {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
@@ -691,6 +794,7 @@ button.outline-link:hover {
|
|||||||
.empty-state[hidden],
|
.empty-state[hidden],
|
||||||
.meta-list > [hidden],
|
.meta-list > [hidden],
|
||||||
.navigation-tabs[hidden],
|
.navigation-tabs[hidden],
|
||||||
|
.export-row[hidden],
|
||||||
.page-list[hidden],
|
.page-list[hidden],
|
||||||
.page-frame[hidden],
|
.page-frame[hidden],
|
||||||
.progress-panel[hidden] {
|
.progress-panel[hidden] {
|
||||||
@@ -1326,6 +1430,11 @@ button.outline-link:hover {
|
|||||||
--sidebar-width: min(78vw, 292px);
|
--sidebar-width: min(78vw, 292px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.export-panel {
|
||||||
|
right: 10px;
|
||||||
|
width: min(300px, calc(100% - 20px));
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
--tool-gap: 6px;
|
--tool-gap: 6px;
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-6
@@ -66,6 +66,7 @@ const state = {
|
|||||||
thumbnailObserver: null,
|
thumbnailObserver: null,
|
||||||
visibleThumbnails: new Set(),
|
visibleThumbnails: new Set(),
|
||||||
exportFormats: [],
|
exportFormats: [],
|
||||||
|
exportPages: null,
|
||||||
showPages: !COMPACT_LAYOUT.matches,
|
showPages: !COMPACT_LAYOUT.matches,
|
||||||
showMeta: !COMPACT_LAYOUT.matches,
|
showMeta: !COMPACT_LAYOUT.matches,
|
||||||
};
|
};
|
||||||
@@ -97,6 +98,14 @@ const el = {
|
|||||||
exportFormat: document.querySelector("#exportFormat"),
|
exportFormat: document.querySelector("#exportFormat"),
|
||||||
exportPageButton: document.querySelector("#exportPageButton"),
|
exportPageButton: document.querySelector("#exportPageButton"),
|
||||||
exportButton: document.querySelector("#exportButton"),
|
exportButton: document.querySelector("#exportButton"),
|
||||||
|
exportPanel: document.querySelector("#exportPanel"),
|
||||||
|
exportForm: document.querySelector("#exportForm"),
|
||||||
|
exportAll: document.querySelector("#exportAll"),
|
||||||
|
exportSpecified: document.querySelector("#exportSpecified"),
|
||||||
|
exportRangeRow: document.querySelector("#exportRangeRow"),
|
||||||
|
exportRange: document.querySelector("#exportRange"),
|
||||||
|
exportRangeStatus: document.querySelector("#exportRangeStatus"),
|
||||||
|
exportSubmit: document.querySelector("#exportSubmit"),
|
||||||
emptyState: document.querySelector("#emptyState"),
|
emptyState: document.querySelector("#emptyState"),
|
||||||
progressPanel: document.querySelector("#progressPanel"),
|
progressPanel: document.querySelector("#progressPanel"),
|
||||||
progressLabel: document.querySelector("#progressLabel"),
|
progressLabel: document.querySelector("#progressLabel"),
|
||||||
@@ -201,7 +210,19 @@ document.addEventListener("selectionchange", () => {
|
|||||||
});
|
});
|
||||||
el.exportFormat.addEventListener("change", () => updateDPIControl());
|
el.exportFormat.addEventListener("change", () => updateDPIControl());
|
||||||
el.exportPageButton.addEventListener("click", () => exportFile(false));
|
el.exportPageButton.addEventListener("click", () => exportFile(false));
|
||||||
el.exportButton.addEventListener("click", () => exportFile(true));
|
el.exportPanel.addEventListener("beforetoggle", prepareExportPanel);
|
||||||
|
el.exportAll.addEventListener("change", updateExportRange);
|
||||||
|
el.exportSpecified.addEventListener("change", () => {
|
||||||
|
updateExportRange();
|
||||||
|
el.exportRange.focus();
|
||||||
|
});
|
||||||
|
el.exportRange.addEventListener("input", updateExportRange);
|
||||||
|
el.exportForm.addEventListener("submit", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!el.exportSubmit.disabled) {
|
||||||
|
return exportFile(true, state.exportPages);
|
||||||
|
}
|
||||||
|
});
|
||||||
el.refreshAppButton.addEventListener("click", refreshApplication);
|
el.refreshAppButton.addEventListener("click", refreshApplication);
|
||||||
el.pageInput.addEventListener("change", () => {
|
el.pageInput.addEventListener("change", () => {
|
||||||
const page = Number.parseInt(el.pageInput.value, 10);
|
const page = Number.parseInt(el.pageInput.value, 10);
|
||||||
@@ -254,7 +275,7 @@ updateSidebarState();
|
|||||||
boot();
|
boot();
|
||||||
|
|
||||||
function handleKeyDown(event) {
|
function handleKeyDown(event) {
|
||||||
if (event.defaultPrevented || event.isComposing || event.altKey || document.body.hasAttribute("aria-busy")) {
|
if (event.defaultPrevented || event.isComposing || event.altKey || document.body.hasAttribute("aria-busy") || el.exportPanel.matches(":popover-open")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const key = event.key;
|
const key = event.key;
|
||||||
@@ -1487,7 +1508,48 @@ async function downloadAttachment(attachment) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportFile(whole) {
|
function prepareExportPanel(event) {
|
||||||
|
if (event.newState !== "open") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state.doc || document.body.hasAttribute("aria-busy")) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.exportForm.reset();
|
||||||
|
updateExportRange();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateExportRange() {
|
||||||
|
const specified = el.exportSpecified.checked;
|
||||||
|
const value = el.exportRange.value.trim();
|
||||||
|
const openSeq = state.openSeq;
|
||||||
|
state.exportPages = null;
|
||||||
|
el.exportRangeRow.hidden = !specified;
|
||||||
|
el.exportRange.removeAttribute("aria-invalid");
|
||||||
|
el.exportSubmit.disabled = specified;
|
||||||
|
el.exportRangeStatus.textContent = specified && value ? "正在校验" : "";
|
||||||
|
if (!specified || !value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const current = () => openSeq === state.openSeq && el.exportPanel.matches(":popover-open")
|
||||||
|
&& el.exportSpecified.checked && value === el.exportRange.value.trim();
|
||||||
|
try {
|
||||||
|
const indices = await callWASM("ofdgoParsePageRange", value);
|
||||||
|
if (current()) {
|
||||||
|
state.exportPages = indices;
|
||||||
|
el.exportSubmit.disabled = false;
|
||||||
|
el.exportRangeStatus.textContent = `共 ${indices.length} 页`;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (current()) {
|
||||||
|
el.exportRange.setAttribute("aria-invalid", "true");
|
||||||
|
el.exportRangeStatus.textContent = "页码无效";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportFile(whole, indices = null) {
|
||||||
if (!state.doc || document.body.hasAttribute("aria-busy")) {
|
if (!state.doc || document.body.hasAttribute("aria-busy")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1514,7 +1576,7 @@ async function exportFile(whole) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = whole
|
const result = whole
|
||||||
? await callWASM("ofdgoExportDocument", format.value, dpi, file)
|
? await callWASM("ofdgoExportDocument", format.value, dpi, indices, file)
|
||||||
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, file);
|
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, file);
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
@@ -2124,7 +2186,7 @@ function resetSearch(clearInput = true) {
|
|||||||
if (clearInput) {
|
if (clearInput) {
|
||||||
el.searchInput.value = "";
|
el.searchInput.value = "";
|
||||||
}
|
}
|
||||||
el.searchStatus.textContent = "输入文字";
|
el.searchStatus.textContent = "";
|
||||||
el.searchResults.replaceChildren();
|
el.searchResults.replaceChildren();
|
||||||
updateSearchCount();
|
updateSearchCount();
|
||||||
clearSearchHighlights();
|
clearSearchHighlights();
|
||||||
@@ -3260,6 +3322,9 @@ function setBusy(busy, text = "", percent = 0, status = "") {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (el.exportPanel.matches(":popover-open")) {
|
||||||
|
el.exportPanel.hidePopover();
|
||||||
|
}
|
||||||
endPan();
|
endPan();
|
||||||
el.progressPanel.hidden = false;
|
el.progressPanel.hidden = false;
|
||||||
setProgress(text, percent, status);
|
setProgress(text, percent, status);
|
||||||
@@ -3317,7 +3382,7 @@ function baseFileName() {
|
|||||||
|
|
||||||
function pageFileName(extension) {
|
function pageFileName(extension) {
|
||||||
const page = String(state.pageIndex + 1).padStart(String(state.doc?.pageCount || 1).length, "0");
|
const page = String(state.pageIndex + 1).padStart(String(state.doc?.pageCount || 1).length, "0");
|
||||||
return `${baseFileName()}_p${page}.${extension}`;
|
return `${baseFileName()}_${page}.${extension}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportFormatInfo(value) {
|
function exportFormatInfo(value) {
|
||||||
|
|||||||
+26
-2
@@ -21,6 +21,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
|
|
||||||
|
"github.com/xiaoqidun/ofdgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// exportWriter 分块传递导出数据
|
// exportWriter 分块传递导出数据
|
||||||
@@ -74,6 +76,7 @@ func RunWASM() {
|
|||||||
registerCallback("ofdgoSVGFontData", svgFontData)
|
registerCallback("ofdgoSVGFontData", svgFontData)
|
||||||
registerCallback("ofdgoSearchPage", searchPage)
|
registerCallback("ofdgoSearchPage", searchPage)
|
||||||
registerCallback("ofdgoExportFormats", exportFormats)
|
registerCallback("ofdgoExportFormats", exportFormats)
|
||||||
|
registerCallback("ofdgoParsePageRange", parsePageRange)
|
||||||
registerExportCallback("ofdgoExportPage", exportPage)
|
registerExportCallback("ofdgoExportPage", exportPage)
|
||||||
registerExportCallback("ofdgoExportDocument", exportDocument)
|
registerExportCallback("ofdgoExportDocument", exportDocument)
|
||||||
registerCallback("ofdgoMatchFontFiles", matchFontFiles)
|
registerCallback("ofdgoMatchFontFiles", matchFontFiles)
|
||||||
@@ -287,6 +290,20 @@ func exportFormats(args []js.Value) (any, error) {
|
|||||||
return ExportFormats(), nil
|
return ExportFormats(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parsePageRange 解析当前文档的导出页码
|
||||||
|
// 入参: args 浏览器参数
|
||||||
|
// 返回: any 页面索引, error 错误信息
|
||||||
|
func parsePageRange(args []js.Value) (any, error) {
|
||||||
|
if currentSession == nil {
|
||||||
|
return nil, fmt.Errorf("ofd document is not opened")
|
||||||
|
}
|
||||||
|
count, err := currentSession.Reader.PageCount()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ofdgo.ParsePageRange(args[0].String(), count)
|
||||||
|
}
|
||||||
|
|
||||||
// exportPage 导出OFD单页
|
// exportPage 导出OFD单页
|
||||||
// 入参: args 浏览器参数
|
// 入参: args 浏览器参数
|
||||||
// 返回: any 导出结果, error 错误信息
|
// 返回: any 导出结果, error 错误信息
|
||||||
@@ -318,8 +335,15 @@ func exportDocument(args []js.Value) (any, error) {
|
|||||||
if currentSession == nil {
|
if currentSession == nil {
|
||||||
return nil, fmt.Errorf("ofd document is not opened")
|
return nil, fmt.Errorf("ofd document is not opened")
|
||||||
}
|
}
|
||||||
writer := bufio.NewWriterSize(exportWriter{write: args[2]}, 1<<20)
|
var indices []int
|
||||||
format, err := currentSession.ExportDocument(args[0].String(), args[1].Float(), writer)
|
if !args[2].IsNull() {
|
||||||
|
indices = make([]int, args[2].Length())
|
||||||
|
for i := range indices {
|
||||||
|
indices[i] = args[2].Index(i).Int()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer := bufio.NewWriterSize(exportWriter{write: args[3]}, 1<<20)
|
||||||
|
format, err := currentSession.ExportDocument(args[0].String(), args[1].Float(), writer, indices...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -614,15 +614,15 @@ func signatureStampInfos(positions []ofdgo.SignatureStampPosition) []SignatureSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ExportDocument 导出文档为PDF或逐页打包ZIP
|
// ExportDocument 导出文档为PDF或逐页打包ZIP
|
||||||
// 入参: value 导出格式, dpi 图片DPI, writer 输出流
|
// 入参: value 导出格式, dpi 图片DPI, writer 输出流, indices 零基页面索引,省略则全部
|
||||||
// 返回: ExportFormat 导出格式, error 错误信息
|
// 返回: ExportFormat 导出格式, error 错误信息
|
||||||
func (s *Session) ExportDocument(value string, dpi float64, writer io.Writer) (ExportFormat, error) {
|
func (s *Session) ExportDocument(value string, dpi float64, writer io.Writer, indices ...int) (ExportFormat, error) {
|
||||||
format, ok := exportFormat(value)
|
format, ok := exportFormat(value)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ExportFormat{}, fmt.Errorf("unsupported export format %s", value)
|
return ExportFormat{}, fmt.Errorf("unsupported export format %s", value)
|
||||||
}
|
}
|
||||||
if format.Value == "pdf" {
|
if format.Value == "pdf" {
|
||||||
return format, s.ExportPDF(writer)
|
return format, s.ExportPDF(writer, indices...)
|
||||||
}
|
}
|
||||||
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
|
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
|
||||||
return ExportFormat{}, fmt.Errorf("ofd document is not opened")
|
return ExportFormat{}, fmt.Errorf("ofd document is not opened")
|
||||||
@@ -631,17 +631,20 @@ func (s *Session) ExportDocument(value string, dpi float64, writer io.Writer) (E
|
|||||||
if dpi > 0 && (format.Value == "png" || format.Value == "jpg") {
|
if dpi > 0 && (format.Value == "png" || format.Value == "jpg") {
|
||||||
renderer.DPI = dpi
|
renderer.DPI = dpi
|
||||||
}
|
}
|
||||||
err := renderer.RenderToZIP(writer, format.Value)
|
err := renderer.RenderToZIP(writer, format.Value, indices...)
|
||||||
return ExportFormat{Value: "zip", Label: "ZIP", Extension: "zip", MIME: "application/zip"}, err
|
return ExportFormat{Value: "zip", Label: "ZIP", Extension: "zip", MIME: "application/zip"}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExportPDF 导出文档为PDF
|
// ExportPDF 导出文档为PDF
|
||||||
// 入参: writer 输出流
|
// 入参: writer 输出流, indices 零基页面索引,省略则全部
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (s *Session) ExportPDF(writer io.Writer) error {
|
func (s *Session) ExportPDF(writer io.Writer, indices ...int) error {
|
||||||
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
|
if s == nil || s.Reader == nil || s.Renderer == nil || s.doc == nil {
|
||||||
return fmt.Errorf("ofd document is not opened")
|
return fmt.Errorf("ofd document is not opened")
|
||||||
}
|
}
|
||||||
|
if len(indices) > 0 {
|
||||||
|
return s.Renderer.RenderToMultiPagePDF(writer, indices...)
|
||||||
|
}
|
||||||
pages := make([]*ofdgo.PageContent, len(s.doc.Pages.Page))
|
pages := make([]*ofdgo.PageContent, len(s.doc.Pages.Page))
|
||||||
for i := range pages {
|
for i := range pages {
|
||||||
page, err := s.pageContent(i)
|
page, err := s.pageContent(i)
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParsePageRange 解析一基页码及闭区间,返回升序去重的零基页面索引
|
||||||
|
// 入参: value 页码表达式,如1-6,7,8-10, count 文档总页数
|
||||||
|
// 返回: []int 页面索引, error 错误信息
|
||||||
|
func ParsePageRange(value string, count int) ([]int, error) {
|
||||||
|
selected := make(map[int]bool)
|
||||||
|
for _, part := range strings.Split(strings.ReplaceAll(value, ",", ","), ",") {
|
||||||
|
first, last, interval := strings.Cut(strings.TrimSpace(part), "-")
|
||||||
|
start, err := strconv.Atoi(strings.TrimSpace(first))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid page range %q", part)
|
||||||
|
}
|
||||||
|
end := start
|
||||||
|
if interval {
|
||||||
|
end, err = strconv.Atoi(strings.TrimSpace(last))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid page range %q", part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start < 1 || end < start || end > count {
|
||||||
|
return nil, fmt.Errorf("page range %q outside 1-%d", part, count)
|
||||||
|
}
|
||||||
|
for index := start - 1; index < end; index++ {
|
||||||
|
selected[index] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
indices := make([]int, 0, len(selected))
|
||||||
|
for index := range selected {
|
||||||
|
indices = append(indices, index)
|
||||||
|
}
|
||||||
|
slices.Sort(indices)
|
||||||
|
return indices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// exportPageIndices 校验导出索引并按原页序去重,省略索引时选取全部页面
|
||||||
|
// 入参: count 文档总页数, indices 零基页面索引
|
||||||
|
// 返回: []int 页面索引, error 错误信息
|
||||||
|
func exportPageIndices(count int, indices []int) ([]int, error) {
|
||||||
|
if count == 0 {
|
||||||
|
return nil, fmt.Errorf("no pages found")
|
||||||
|
}
|
||||||
|
if len(indices) == 0 {
|
||||||
|
indices = make([]int, count)
|
||||||
|
for index := range indices {
|
||||||
|
indices[index] = index
|
||||||
|
}
|
||||||
|
return indices, nil
|
||||||
|
}
|
||||||
|
for _, index := range indices {
|
||||||
|
if index < 0 || index >= count {
|
||||||
|
return nil, fmt.Errorf("page index %d out of range", index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
indices = slices.Clone(indices)
|
||||||
|
slices.Sort(indices)
|
||||||
|
return slices.Compact(indices), nil
|
||||||
|
}
|
||||||
+17
-15
@@ -69,9 +69,9 @@ func (r *Renderer) outputRenderer(format string) (string, func(*PageContent, io.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RenderToZIP 逐页渲染并写入ZIP,文件编号按总页数补零,出错时调用方应丢弃输出
|
// RenderToZIP 逐页渲染并写入ZIP,文件编号按总页数补零,出错时调用方应丢弃输出
|
||||||
// 入参: writer 输出流, format svg、pdf、eps、png、jpg或jpeg
|
// 入参: writer 输出流, format svg、pdf、eps、png、jpg或jpeg, indices 零基页面索引,省略则全部,按原页序去重
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (r *Renderer) RenderToZIP(writer io.Writer, format string) error {
|
func (r *Renderer) RenderToZIP(writer io.Writer, format string, indices ...int) error {
|
||||||
extension, render, err := r.outputRenderer(format)
|
extension, render, err := r.outputRenderer(format)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -80,8 +80,9 @@ func (r *Renderer) RenderToZIP(writer io.Writer, format string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if count == 0 {
|
indices, err = exportPageIndices(count, indices)
|
||||||
return fmt.Errorf("no pages found")
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
archive := zip.NewWriter(writer)
|
archive := zip.NewWriter(writer)
|
||||||
method := zip.Deflate
|
method := zip.Deflate
|
||||||
@@ -89,7 +90,7 @@ func (r *Renderer) RenderToZIP(writer io.Writer, format string) error {
|
|||||||
method = zip.Store
|
method = zip.Store
|
||||||
}
|
}
|
||||||
width := len(strconv.Itoa(count))
|
width := len(strconv.Itoa(count))
|
||||||
for index := range count {
|
for _, index := range indices {
|
||||||
page, err := r.Reader.PageContentByIndex(index)
|
page, err := r.Reader.PageContentByIndex(index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read page %d: %w", index+1, err)
|
return fmt.Errorf("failed to read page %d: %w", index+1, err)
|
||||||
@@ -269,26 +270,27 @@ func (r *Renderer) RenderToEPS(page *PageContent, writer io.Writer) error {
|
|||||||
return buffer.Flush()
|
return buffer.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderToMultiPagePDF 将整个文档导出为多页PDF
|
// RenderToMultiPagePDF 将文档页面导出为多页PDF
|
||||||
// 入参: writer 输出流
|
// 入参: writer 输出流, indices 零基页面索引,省略则全部,按原页序去重
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (r *Renderer) RenderToMultiPagePDF(writer io.Writer) error {
|
func (r *Renderer) RenderToMultiPagePDF(writer io.Writer, indices ...int) error {
|
||||||
doc, err := r.Reader.Doc()
|
doc, err := r.Reader.Doc()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(doc.Pages.Page) == 0 {
|
indices, err = exportPageIndices(len(doc.Pages.Page), indices)
|
||||||
return fmt.Errorf("no pages found")
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
pages := make([]pdfPage, len(doc.Pages.Page))
|
pages := make([]pdfPage, len(indices))
|
||||||
for i, pageRef := range doc.Pages.Page {
|
for i, index := range indices {
|
||||||
page, err := r.Reader.PageContent(pageRef)
|
page, err := r.Reader.PageContent(doc.Pages.Page[index])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read page %d: %w", i+1, err)
|
return fmt.Errorf("failed to read page %d: %w", index+1, err)
|
||||||
}
|
}
|
||||||
box, err := r.GetPageBox(page)
|
box, err := r.GetPageBox(page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read page %d area: %w", i+1, err)
|
return fmt.Errorf("failed to read page %d area: %w", index+1, err)
|
||||||
}
|
}
|
||||||
pages[i] = pdfPage{Content: page, Box: box}
|
pages[i] = pdfPage{Content: page, Box: box}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ func (n *pdfNavigation) addAction(page int, rect canvas.Rect, action Action, boo
|
|||||||
// 入参: outlines 大纲节点, level 节点层级, bookmarks 书签, pageIndex 页面索引表, pages 页面数据
|
// 入参: outlines 大纲节点, level 节点层级, bookmarks 书签, pageIndex 页面索引表, pages 页面数据
|
||||||
func (n *pdfNavigation) addOutlines(outlines []OutlineElem, level int, bookmarks map[string]Dest, pageIndex map[string]int, pages []pdfPage) {
|
func (n *pdfNavigation) addOutlines(outlines []OutlineElem, level int, bookmarks map[string]Dest, pageIndex map[string]int, pages []pdfPage) {
|
||||||
for _, outline := range outlines {
|
for _, outline := range outlines {
|
||||||
|
nextLevel := level
|
||||||
if dest := outlineDest(outline, bookmarks); dest != nil {
|
if dest := outlineDest(outline, bookmarks); dest != nil {
|
||||||
if page, ok := pageIndex[dest.PageID]; ok {
|
if page, ok := pageIndex[dest.PageID]; ok {
|
||||||
n.Outline[page] = append(n.Outline[page], pdfOutline{
|
n.Outline[page] = append(n.Outline[page], pdfOutline{
|
||||||
@@ -153,9 +154,10 @@ func (n *pdfNavigation) addOutlines(outlines []OutlineElem, level int, bookmarks
|
|||||||
Level: level,
|
Level: level,
|
||||||
Y: pdfDestY(*dest, pages[page].Box.H),
|
Y: pdfDestY(*dest, pages[page].Box.H),
|
||||||
})
|
})
|
||||||
|
nextLevel++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
n.addOutlines(outline.OutlineElem, level+1, bookmarks, pageIndex, pages)
|
n.addOutlines(outline.OutlineElem, nextLevel, bookmarks, pageIndex, pages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user