mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
This commit is contained in:
1 parent
b30a44b7ce
commit
0bc3f69df0
9 files changed
+258
-32
No files matched your search
@@ -18,6 +18,7 @@
|
|||||||
<div class="tool-group">
|
<div class="tool-group">
|
||||||
<button id="ofdButton" class="icon-button" type="button" title="打开" aria-label="打开" aria-keyshortcuts="Control+O Meta+O"><span class="open-icon" aria-hidden="true"></span></button>
|
<button id="ofdButton" class="icon-button" type="button" title="打开" aria-label="打开" aria-keyshortcuts="Control+O Meta+O"><span class="open-icon" aria-hidden="true"></span></button>
|
||||||
<button id="newButton" class="icon-button" type="button" title="新建" aria-label="新建"><span class="new-icon" aria-hidden="true"></span></button>
|
<button id="newButton" class="icon-button" type="button" title="新建" aria-label="新建"><span class="new-icon" aria-hidden="true"></span></button>
|
||||||
|
<button id="saveButton" class="icon-button" type="button" title="保存" aria-label="保存" aria-keyshortcuts="Control+S Meta+S" disabled><span class="save-icon" aria-hidden="true"></span></button>
|
||||||
<button id="editButton" class="icon-button" type="button" title="编辑" aria-label="编辑" aria-pressed="false" disabled><span class="edit-icon" aria-hidden="true"></span></button>
|
<button id="editButton" class="icon-button" type="button" title="编辑" aria-label="编辑" aria-pressed="false" disabled><span class="edit-icon" aria-hidden="true"></span></button>
|
||||||
<button id="togglePagesButton" class="icon-button" type="button" title="页面栏" aria-label="页面栏" aria-pressed="true">☰</button>
|
<button id="togglePagesButton" class="icon-button" type="button" title="页面栏" aria-label="页面栏" aria-pressed="true">☰</button>
|
||||||
<button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" aria-label="信息栏" aria-pressed="true">ⓘ</button>
|
<button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" aria-label="信息栏" aria-pressed="true">ⓘ</button>
|
||||||
@@ -60,11 +61,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div id="editorTools" class="editor-toolbar" role="toolbar" aria-label="编辑工具" hidden>
|
<div id="editorTools" class="editor-toolbar" role="toolbar" aria-label="编辑工具" hidden>
|
||||||
<div class="editor-group editor-file">
|
<div class="editor-group editor-actions">
|
||||||
<button id="saveButton" class="button" type="button" title="另存为OFD" aria-label="另存为OFD" aria-keyshortcuts="Control+S Meta+S">保存</button>
|
|
||||||
<button id="undoButton" class="button" type="button" disabled>撤销</button>
|
<button id="undoButton" class="button" type="button" disabled>撤销</button>
|
||||||
<button id="redoButton" class="button" type="button" disabled>重做</button>
|
<button id="redoButton" class="button" type="button" disabled>重做</button>
|
||||||
<span>文件</span>
|
<span>操作</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-group editor-objects">
|
<div class="editor-group editor-objects">
|
||||||
<button id="compositeBackButton" class="button" type="button" title="返回上层" hidden>返回</button>
|
<button id="compositeBackButton" class="button" type="button" title="返回上层" hidden>返回</button>
|
||||||
|
|||||||
+8
-10
@@ -111,11 +111,11 @@ a:focus-visible {
|
|||||||
border-right: 1px solid var(--line);
|
border-right: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-file,
|
|
||||||
.editor-image {
|
.editor-image {
|
||||||
grid-template-columns: repeat(2, max-content);
|
grid-template-columns: repeat(2, max-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-actions,
|
||||||
.editor-transform,
|
.editor-transform,
|
||||||
.editor-erase {
|
.editor-erase {
|
||||||
grid-template-columns: max-content;
|
grid-template-columns: max-content;
|
||||||
@@ -215,15 +215,6 @@ a:focus-visible {
|
|||||||
grid-template-columns: repeat(2, 84px);
|
grid-template-columns: repeat(2, 84px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-file #saveButton {
|
|
||||||
grid-row: 1 / 3;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-file #redoButton {
|
|
||||||
grid-column: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-picker {
|
.font-picker {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -517,6 +508,13 @@ button:not(:disabled):hover {
|
|||||||
transform: translate(-50%, -50%) rotate(90deg);
|
transform: translate(-50%, -50%) rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.save-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: currentColor;
|
||||||
|
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='M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2Z'/%3E%3Cpath d='M17 21v-8H7v8M7 3v5h8'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
.edit-icon {
|
.edit-icon {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 15px;
|
width: 15px;
|
||||||
|
|||||||
+32
-8
@@ -1333,7 +1333,8 @@ function handleKeyDown(event) {
|
|||||||
}
|
}
|
||||||
const key = event.key;
|
const key = event.key;
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && key.toLowerCase() === "s" && state.editorInfo) {
|
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && key.toLowerCase() === "s" && state.doc
|
||||||
|
&& (state.editorInfo || state.ofdBytes)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!document.body.hasAttribute("aria-busy") && !formDialogOpen()) exportFile(true, null, "ofd");
|
if (!document.body.hasAttribute("aria-busy") && !formDialogOpen()) exportFile(true, null, "ofd");
|
||||||
return;
|
return;
|
||||||
@@ -1938,7 +1939,7 @@ async function toggleEditor() {
|
|||||||
canvasEditor.clear();
|
canvasEditor.clear();
|
||||||
setPan(false);
|
setPan(false);
|
||||||
await openDocument({ doc, openSeq, skipAutoFonts: true, pageIndex, fitMode, scale,
|
await openDocument({ doc, openSeq, skipAutoFonts: true, pageIndex, fitMode, scale,
|
||||||
keepPreview: true, previewAnchor: anchor });
|
keepPreview: true, preserveThumbnails: true, previewAnchor: anchor });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (openSeq === state.openSeq) showError(err, false);
|
if (openSeq === state.openSeq) showError(err, false);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -3888,7 +3889,13 @@ async function openDocument(options = {}) {
|
|||||||
}
|
}
|
||||||
const openSeq = options.openSeq || (state.openSeq += 1);
|
const openSeq = options.openSeq || (state.openSeq += 1);
|
||||||
const previewPosition = options.previewScroll && state.pageWindow ? pageWindowAnchor(state.pageWindow) : null;
|
const previewPosition = options.previewScroll && state.pageWindow ? pageWindowAnchor(state.pageWindow) : null;
|
||||||
const previewIDs = options.keepPreview ? [...new Set([...state.visiblePages, ...state.visibleThumbnails])].map((index) => state.doc.pages[index].id) : [];
|
const viewport = options.keepPreview ? el.viewerPanel.getBoundingClientRect() : null;
|
||||||
|
const previewIDs = viewport ? [...state.visiblePages].filter((index) => {
|
||||||
|
const bounds = pageShell(index)?.getBoundingClientRect();
|
||||||
|
return bounds && bounds.bottom > viewport.top && bounds.top < viewport.bottom;
|
||||||
|
}).map((index) => state.doc.pages[index].id) : [];
|
||||||
|
const thumbnailPreviews = options.preserveThumbnails ? new Map([...state.pageCache.values()]
|
||||||
|
.filter((page) => page.id !== state.doc.pages[state.pageIndex].id).map((page) => [page.id, page])) : null;
|
||||||
if (!state.ready || state.wasmExited) {
|
if (!state.ready || state.wasmExited) {
|
||||||
await ensureWASM();
|
await ensureWASM();
|
||||||
}
|
}
|
||||||
@@ -3958,6 +3965,12 @@ async function openDocument(options = {}) {
|
|||||||
if (options.keepPreview) {
|
if (options.keepPreview) {
|
||||||
resetPageLoading();
|
resetPageLoading();
|
||||||
state.pageCache.clear();
|
state.pageCache.clear();
|
||||||
|
if (thumbnailPreviews) {
|
||||||
|
for (const page of doc.pages) {
|
||||||
|
const preview = thumbnailPreviews.get(page.id);
|
||||||
|
if (preview) state.pageCache.set(page.index, { ...preview, previewOnly: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
const indices = new Set([pageIndex, ...doc.pages.filter((page) => previewIDs.includes(page.id)).map((page) => page.index)]);
|
const indices = new Set([pageIndex, ...doc.pages.filter((page) => previewIDs.includes(page.id)).map((page) => page.index)]);
|
||||||
for (const index of indices) {
|
for (const index of indices) {
|
||||||
await loadPageData(index, { openSeq, priority: 0, refresh: true });
|
await loadPageData(index, { openSeq, priority: 0, refresh: true });
|
||||||
@@ -3977,7 +3990,7 @@ async function openDocument(options = {}) {
|
|||||||
renderPageFlow();
|
renderPageFlow();
|
||||||
if (options.keepPreview) {
|
if (options.keepPreview) {
|
||||||
for (const [index, page] of state.pageCache) {
|
for (const [index, page] of state.pageCache) {
|
||||||
mountPageSVG(index, page, openSeq);
|
if (!page.previewOnly) mountPageSVG(index, page, openSeq);
|
||||||
}
|
}
|
||||||
releasePageResources();
|
releasePageResources();
|
||||||
}
|
}
|
||||||
@@ -4347,7 +4360,7 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const saving = value === "ofd";
|
const saving = value === "ofd";
|
||||||
if (saving && !state.editorInfo && !encryption) {
|
if (saving && !state.editorInfo && !state.ofdBytes && !encryption) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!saving && state.doc.encryption?.encrypted && !window.confirm("此格式将输出明文,是否继续?")) return;
|
if (!saving && state.doc.encryption?.encrypted && !window.confirm("此格式将输出明文,是否继续?")) return;
|
||||||
@@ -4388,12 +4401,21 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value,
|
|||||||
delete encryption.recipientFiles;
|
delete encryption.recipientFiles;
|
||||||
if (openSeq !== state.openSeq) return;
|
if (openSeq !== state.openSeq) return;
|
||||||
}
|
}
|
||||||
const result = encryption ? await callWASM("ofdgoSaveEncrypted", indices, encryption, file) : saving ? await callWASM("ofdgoSaveDocument", indices, file) : whole
|
const readingSave = saving && !state.editorInfo && !encryption && indices === null;
|
||||||
|
const result = readingSave
|
||||||
|
? { label: "OFD", size: state.ofdBytes.byteLength, mime, blob: state.ofdBytes }
|
||||||
|
: encryption ? await callWASM("ofdgoSaveEncrypted", indices, encryption, file) : saving ? await callWASM("ofdgoSaveDocument", indices, file) : whole
|
||||||
? await callWASM("ofdgoExportDocument", format.value, dpi, indices, state.renderBackend, file)
|
? await callWASM("ofdgoExportDocument", format.value, dpi, indices, state.renderBackend, file)
|
||||||
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, state.renderBackend, file);
|
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, state.renderBackend, file);
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (readingSave && file) {
|
||||||
|
const writer = await file.createWritable();
|
||||||
|
await writer.write(result.blob);
|
||||||
|
await writer.close();
|
||||||
|
delete result.blob;
|
||||||
|
}
|
||||||
if (result.blob) {
|
if (result.blob) {
|
||||||
downloadBytes(result.blob, result.mime, fileName);
|
downloadBytes(result.blob, result.mime, fileName);
|
||||||
}
|
}
|
||||||
@@ -4655,7 +4677,8 @@ async function renderFlowPage(index, options = {}) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const page = await loadPageData(index, { openSeq, priority: options.priority ?? 2 });
|
const page = await loadPageData(index, { openSeq, priority: options.priority ?? 2,
|
||||||
|
refresh: state.editing && state.pageCache.get(index)?.previewOnly });
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -7147,7 +7170,8 @@ function updateEditorTools() {
|
|||||||
el.editorTools.hidden = !state.editing;
|
el.editorTools.hidden = !state.editing;
|
||||||
const pagesDisabled = !state.editing || !state.ready || state.exporting;
|
const pagesDisabled = !state.editing || !state.ready || state.exporting;
|
||||||
el.insertTextButton.disabled = el.insertImageButton.disabled = pagesDisabled || !pageCan("insert");
|
el.insertTextButton.disabled = el.insertImageButton.disabled = pagesDisabled || !pageCan("insert");
|
||||||
el.saveButton.disabled = el.addPageButton.disabled = pagesDisabled;
|
el.saveButton.disabled = !state.doc || !state.ready || state.exporting || (!state.editorInfo && !state.ofdBytes);
|
||||||
|
el.addPageButton.disabled = pagesDisabled;
|
||||||
const selectedPages = state.editing ? selectedPageIndexes() : [state.pageIndex];
|
const selectedPages = state.editing ? selectedPageIndexes() : [state.pageIndex];
|
||||||
const selectionDisabled = pagesDisabled || !selectedPages.length;
|
const selectionDisabled = pagesDisabled || !selectedPages.length;
|
||||||
el.batchPagesButton.disabled = selectionDisabled;
|
el.batchPagesButton.disabled = selectionDisabled;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ require (
|
|||||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||||
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1
|
github.com/xiaoqidun/jbig2 v1.1.1
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260924110344-e481bdea25e9
|
github.com/xiaoqidun/pdfgo v0.0.0-20260924121050-2d9bc5cc700d
|
||||||
golang.org/x/crypto v0.57.0
|
golang.org/x/crypto v0.57.0
|
||||||
golang.org/x/image v0.46.0
|
golang.org/x/image v0.46.0
|
||||||
golang.org/x/text v0.42.0
|
golang.org/x/text v0.42.0
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
|||||||
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
|
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
|
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260924110344-e481bdea25e9 h1:MjDKg9lMG/5AmEr+SV7C2y4dzfhpfXDSYScfu2mZixQ=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260924121050-2d9bc5cc700d h1:ko1gZlKK5MvOjjTxr1tYyysVlWVIDvgVi+bogBi6pUw=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260924110344-e481bdea25e9/go.mod h1:/D0XHgfZ7xNhix95swpNaThpuqoJjVR5wIadogtR5pI=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260924121050-2d9bc5cc700d/go.mod h1:/D0XHgfZ7xNhix95swpNaThpuqoJjVR5wIadogtR5pI=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||||
|
|||||||
+18
-3
@@ -113,13 +113,12 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
if err := importer.flushPath(); err != nil {
|
if err := importer.flushPath(); err != nil {
|
||||||
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
||||||
}
|
}
|
||||||
if err := importer.links(page); err != nil {
|
if err := importer.annotations(ctx, page); err != nil {
|
||||||
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
||||||
}
|
}
|
||||||
if _, err := editor.CopyObjects(importer.page, importer.objects, 0, 0); err != nil {
|
if err := importer.commitObjects(); err != nil {
|
||||||
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
||||||
}
|
}
|
||||||
importer.objects = nil
|
|
||||||
importer.report.Pages++
|
importer.report.Pages++
|
||||||
if options.Progress != nil {
|
if options.Progress != nil {
|
||||||
return options.Progress(index + 1)
|
return options.Progress(index + 1)
|
||||||
@@ -135,6 +134,19 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
return editor, importer.report, nil
|
return editor, importer.report, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// commitObjects 将当前页面待转换对象写入编辑文档
|
||||||
|
// 返回: error 对象写入错误
|
||||||
|
func (p *pdfImporter) commitObjects() error {
|
||||||
|
if len(p.objects) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := p.editor.CopyObjects(p.page, p.objects, 0, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.objects = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ConvertPDF 将PDF转换并写入OFD,转换阶段失败时不写入目标
|
// ConvertPDF 将PDF转换并写入OFD,转换阶段失败时不写入目标
|
||||||
// 入参: ctx 取消上下文, source PDF数据, size 字节数, output OFD输出, options 转换选项
|
// 入参: ctx 取消上下文, source PDF数据, size 字节数, output OFD输出, options 转换选项
|
||||||
// 返回: PDFImportReport 转换统计, error 错误信息
|
// 返回: PDFImportReport 转换统计, error 错误信息
|
||||||
@@ -265,6 +277,9 @@ func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) *Clips {
|
|||||||
// 入参: mark PDF路径绘制信息
|
// 入参: mark PDF路径绘制信息
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (p *pdfImporter) path(mark pdfgo.PathMark) error {
|
func (p *pdfImporter) path(mark pdfgo.PathMark) error {
|
||||||
|
if mark.Style.BlendMode == "Multiply" {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "multiply path"}
|
||||||
|
}
|
||||||
if !mark.Stroke || mark.Fill || mark.Style.Stroke.Alpha != 1 {
|
if !mark.Stroke || mark.Fill || mark.Style.Stroke.Alpha != 1 {
|
||||||
if err := p.flushPath(); err != nil {
|
if err := p.flushPath(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
// 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 (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/png"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/xiaoqidun/pdfgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// signatureWidget 转换可见签名外观,不将PDF签名误写成OFD签名
|
||||||
|
// 入参: ctx 取消上下文, page PDF页面, annotation 签名控件
|
||||||
|
// 返回: error 外观或转换错误
|
||||||
|
func (p *pdfImporter) signatureWidget(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation) error {
|
||||||
|
if annotation.Dictionary["FT"] != pdfgo.Name("Sig") || annotation.Dictionary["V"] == nil {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "non-signature widget"}
|
||||||
|
}
|
||||||
|
flags := int64(0)
|
||||||
|
if value := annotation.Dictionary["F"]; value != nil {
|
||||||
|
resolved, err := p.reader.Resolve(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
number, ok := resolved.(pdfgo.Integer)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid PDF annotation flags")
|
||||||
|
}
|
||||||
|
flags = int64(number)
|
||||||
|
}
|
||||||
|
const invisibleFlags = 1 | 2 | 32
|
||||||
|
if flags&invisibleFlags != 0 {
|
||||||
|
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "hidden PDF signature not transferred"})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var images []pdfgo.ImageMark
|
||||||
|
visitor := pdfgo.Visitor{
|
||||||
|
Path: func(pdfgo.PathMark) error { return &pdfgo.UnsupportedError{Feature: "signature appearance path"} },
|
||||||
|
Text: func(pdfgo.TextMark) error { return &pdfgo.UnsupportedError{Feature: "signature appearance text"} },
|
||||||
|
Image: func(mark pdfgo.ImageMark) error {
|
||||||
|
images = append(images, mark)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := p.reader.WalkAnnotationAppearance(ctx, page, annotation, visitor); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(images) != 1 {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "signature appearance composition"}
|
||||||
|
}
|
||||||
|
mark := images[0]
|
||||||
|
message := "PDF signature appearance imported; digital signature not transferred"
|
||||||
|
if mark.Style.BlendMode == "Multiply" {
|
||||||
|
if err := p.multiplySignatureAppearance(page, annotation, mark); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
message = "PDF signature appearance flattened over page content; digital signature not transferred"
|
||||||
|
} else if mark.Style.BlendMode == "" || mark.Style.BlendMode == "Normal" || mark.Style.BlendMode == "Compatible" {
|
||||||
|
if err := p.image(mark); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "signature appearance blend mode"}
|
||||||
|
}
|
||||||
|
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: message})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// multiplySignatureAppearance 合成签名覆盖区域,正文及其他页面对象保持独立
|
||||||
|
// 入参: page PDF页面, annotation 签名控件, mark 印章图像
|
||||||
|
// 返回: error 图像或渲染错误
|
||||||
|
func (p *pdfImporter) multiplySignatureAppearance(page *pdfgo.Page, annotation pdfgo.Annotation, mark pdfgo.ImageMark) error {
|
||||||
|
if err := p.commitObjects(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
base, err := renderImportedPage(p.editor, p.page)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, width, height := pdfPageMatrix(page)
|
||||||
|
appearance := NewEditor()
|
||||||
|
appearance.SetRenderBackends(p.editor.backends)
|
||||||
|
if _, err := appearance.AddPage(width, height); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stamp := pdfImporter{reader: p.reader, editor: appearance, matrix: p.matrix, pageBox: page.CropBox}
|
||||||
|
mark.Style.BlendMode = "Normal"
|
||||||
|
if err := stamp.image(mark); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := appearance.CopyObjects(0, stamp.objects, 0, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
overlay, err := renderImportedPage(appearance, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if base.Bounds() != overlay.Bounds() {
|
||||||
|
return fmt.Errorf("signature appearance page size mismatch")
|
||||||
|
}
|
||||||
|
annotationBox := annotation.Rect
|
||||||
|
box := pdfBounds([]pdfgo.Point{
|
||||||
|
p.matrix.Apply(pdfgo.Point{X: annotationBox.XMin, Y: annotationBox.YMin}),
|
||||||
|
p.matrix.Apply(pdfgo.Point{X: annotationBox.XMax, Y: annotationBox.YMin}),
|
||||||
|
p.matrix.Apply(pdfgo.Point{X: annotationBox.XMax, Y: annotationBox.YMax}),
|
||||||
|
p.matrix.Apply(pdfgo.Point{X: annotationBox.XMin, Y: annotationBox.YMax}),
|
||||||
|
})
|
||||||
|
const dpi = 300.0
|
||||||
|
pixelsPerMM := dpi / 25.4
|
||||||
|
region := image.Rect(int(math.Floor(box.X*pixelsPerMM)), int(math.Floor(box.Y*pixelsPerMM)), int(math.Ceil((box.X+box.W)*pixelsPerMM)), int(math.Ceil((box.Y+box.H)*pixelsPerMM))).Intersect(base.Bounds())
|
||||||
|
if region.Empty() {
|
||||||
|
return fmt.Errorf("signature appearance outside page")
|
||||||
|
}
|
||||||
|
composite := image.NewRGBA(image.Rect(0, 0, region.Dx(), region.Dy()))
|
||||||
|
for y := region.Min.Y; y < region.Max.Y; y++ {
|
||||||
|
for x := region.Min.X; x < region.Max.X; x++ {
|
||||||
|
background := color.RGBAModel.Convert(base.At(x, y)).(color.RGBA)
|
||||||
|
foreground := color.RGBAModel.Convert(overlay.At(x, y)).(color.RGBA)
|
||||||
|
composite.SetRGBA(x-region.Min.X, y-region.Min.Y, color.RGBA{
|
||||||
|
R: uint8((int(background.R)*int(foreground.R) + 127) / 255),
|
||||||
|
G: uint8((int(background.G)*int(foreground.G) + 127) / 255),
|
||||||
|
B: uint8((int(background.B)*int(foreground.B) + 127) / 255),
|
||||||
|
A: 255,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var encoded bytes.Buffer
|
||||||
|
if err := png.Encode(&encoded, composite); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
id, err := p.editor.AddImage(encoded.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
regionBox := Box{X: float64(region.Min.X) / pixelsPerMM, Y: float64(region.Min.Y) / pixelsPerMM, W: float64(region.Dx()) / pixelsPerMM, H: float64(region.Dy()) / pixelsPerMM}
|
||||||
|
p.objects = append(p.objects, GraphicObject{Type: "ImageObject", ImageObject: ImageObject{Boundary: pdfBoundary(regionBox), ResourceID: id}})
|
||||||
|
p.report.ImageObjects++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderImportedPage 在固定印刷分辨率绘制待合成页面
|
||||||
|
// 入参: editor 编辑文档, page 页序号
|
||||||
|
// 返回: image.Image 页面像素, error 渲染错误
|
||||||
|
func renderImportedPage(editor *Editor, page int) (image.Image, error) {
|
||||||
|
reader, err := editor.Reader()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
content, err := reader.PageContentByIndex(page)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewRenderer(reader, WithDPI(300), WithRenderBackends(editor.backends)).RenderToImage(content)
|
||||||
|
}
|
||||||
+10
-3
@@ -15,21 +15,28 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// links 将无外观链接转换为OFD点击区域,其他注解或外观返回不支持错误
|
// annotations 将链接和签名外观按页面顺序转换为OFD对象
|
||||||
// 入参: page PDF页面
|
// 入参: ctx 取消上下文, page PDF页面
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (p *pdfImporter) links(page *pdfgo.Page) error {
|
func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page) error {
|
||||||
annotations, err := page.Annotations()
|
annotations, err := page.Annotations()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, annotation := range annotations {
|
for _, annotation := range annotations {
|
||||||
dict := annotation.Dictionary
|
dict := annotation.Dictionary
|
||||||
|
if annotation.Subtype == "Widget" {
|
||||||
|
if err := p.signatureWidget(ctx, page, annotation); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
if annotation.Subtype != "Link" {
|
if annotation.Subtype != "Link" {
|
||||||
return &pdfgo.UnsupportedError{Feature: "annotation " + string(annotation.Subtype)}
|
return &pdfgo.UnsupportedError{Feature: "annotation " + string(annotation.Subtype)}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-2
@@ -33,6 +33,9 @@ import (
|
|||||||
// 入参: mark PDF图像绘制信息
|
// 入参: mark PDF图像绘制信息
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
||||||
|
if mark.Style.BlendMode == "Multiply" {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "multiply image outside annotation appearance"}
|
||||||
|
}
|
||||||
if err := p.flushPath(); err != nil {
|
if err := p.flushPath(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -113,6 +116,9 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
|||||||
// 入参: mark PDF文字绘制信息
|
// 入参: mark PDF文字绘制信息
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
||||||
|
if mark.Style.BlendMode == "Multiply" {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "multiply text"}
|
||||||
|
}
|
||||||
if err := p.flushPath(); err != nil {
|
if err := p.flushPath(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -209,11 +215,15 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
|||||||
box := pdfBounds(points)
|
box := pdfBounds(points)
|
||||||
fill, stroke, visible := mark.Mode == 0 || mark.Mode == 2, mark.Mode == 1 || mark.Mode == 2, mark.Mode != 3
|
fill, stroke, visible := mark.Mode == 0 || mark.Mode == 2, mark.Mode == 1 || mark.Mode == 2, mark.Mode != 3
|
||||||
if stroke {
|
if stroke {
|
||||||
margin := mark.Style.LineWidth * unit * math.Hypot(m[0], m[1]) / 2 * math.Max(1, mark.Style.MiterLimit)
|
scale := math.Sqrt(math.Abs(m[0]*m[3] - m[1]*m[2]))
|
||||||
|
if scale == 0 || math.IsNaN(scale) || math.IsInf(scale, 0) {
|
||||||
|
return &pdfgo.UnsupportedError{Feature: "degenerate text stroke matrix"}
|
||||||
|
}
|
||||||
|
margin := mark.Style.LineWidth * unit / 2 * math.Max(1, mark.Style.MiterLimit)
|
||||||
box = Box{box.X - margin, box.Y - margin, box.W + 2*margin, box.H + 2*margin}
|
box = Box{box.X - margin, box.Y - margin, box.W + 2*margin, box.H + 2*margin}
|
||||||
color := StrokeColor(*p.color(mark.Style.Stroke))
|
color := StrokeColor(*p.color(mark.Style.Stroke))
|
||||||
object.StrokeColor = &color
|
object.StrokeColor = &color
|
||||||
object.LineWidth = mark.Style.LineWidth * unit
|
object.LineWidth = mark.Style.LineWidth * unit / scale
|
||||||
object.LineWidthSet = object.LineWidth == 0
|
object.LineWidthSet = object.LineWidth == 0
|
||||||
object.Join = []string{"Miter", "Round", "Bevel"}[mark.Style.Join]
|
object.Join = []string{"Miter", "Round", "Bevel"}[mark.Style.Join]
|
||||||
object.MiterLimit = mark.Style.MiterLimit
|
object.MiterLimit = mark.Style.MiterLimit
|
||||||
|
|||||||
Reference in new issue
Block a user