mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
This commit is contained in:
1 parent
6f6fe27891
commit
61917cafce
10 files changed
+472
-161
No files matched your search
+13
-13
@@ -92,19 +92,6 @@
|
|||||||
</select>
|
</select>
|
||||||
<button id="objectStyleButton" class="editor-group-action" type="button" title="对象样式" aria-label="对象样式" aria-haspopup="dialog" aria-controls="objectStylePanel" disabled>对象</button>
|
<button id="objectStyleButton" class="editor-group-action" type="button" title="对象样式" aria-label="对象样式" aria-haspopup="dialog" aria-controls="objectStylePanel" disabled>对象</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-group editor-transform">
|
|
||||||
<select id="objectRotate" class="order-select" aria-label="对象旋转" disabled>
|
|
||||||
<option value="" hidden>旋转</option>
|
|
||||||
<option value="90">右转90°</option>
|
|
||||||
<option value="-90">左转90°</option>
|
|
||||||
</select>
|
|
||||||
<select id="objectFlip" class="order-select" aria-label="对象翻转" disabled>
|
|
||||||
<option value="" hidden>翻转</option>
|
|
||||||
<option value="horizontal">水平</option>
|
|
||||||
<option value="vertical">垂直</option>
|
|
||||||
</select>
|
|
||||||
<button id="objectBoundsButton" class="editor-group-action" type="button" title="位置尺寸" aria-label="位置尺寸" aria-haspopup="dialog" aria-controls="objectBoundsPanel" disabled>变换</button>
|
|
||||||
</div>
|
|
||||||
<div class="editor-group">
|
<div class="editor-group">
|
||||||
<button id="insertTextButton" class="button" type="button" title="文字" aria-label="文字" aria-pressed="false">文字</button>
|
<button id="insertTextButton" class="button" type="button" title="文字" aria-label="文字" aria-pressed="false">文字</button>
|
||||||
<button id="insertImageButton" class="button" type="button" title="图片" aria-label="图片">图片</button>
|
<button id="insertImageButton" class="button" type="button" title="图片" aria-label="图片">图片</button>
|
||||||
@@ -150,6 +137,19 @@
|
|||||||
<button id="resetCropButton" class="button" type="button" aria-label="还原图片" disabled>还原</button>
|
<button id="resetCropButton" class="button" type="button" aria-label="还原图片" disabled>还原</button>
|
||||||
<span>图片</span>
|
<span>图片</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="editor-group editor-transform">
|
||||||
|
<select id="objectRotate" class="order-select" aria-label="对象旋转" disabled>
|
||||||
|
<option value="" hidden>旋转</option>
|
||||||
|
<option value="90">右转90°</option>
|
||||||
|
<option value="-90">左转90°</option>
|
||||||
|
</select>
|
||||||
|
<select id="objectFlip" class="order-select" aria-label="对象翻转" disabled>
|
||||||
|
<option value="" hidden>翻转</option>
|
||||||
|
<option value="horizontal">水平</option>
|
||||||
|
<option value="vertical">垂直</option>
|
||||||
|
</select>
|
||||||
|
<button id="objectBoundsButton" class="editor-group-action" type="button" title="位置尺寸" aria-label="位置尺寸" aria-haspopup="dialog" aria-controls="objectBoundsPanel" disabled>变换</button>
|
||||||
|
</div>
|
||||||
<div class="editor-group editor-erase">
|
<div class="editor-group editor-erase">
|
||||||
<button id="eraseButton" class="button" type="button" title="擦除" aria-label="擦除" aria-pressed="false">擦除</button>
|
<button id="eraseButton" class="button" type="button" title="擦除" aria-label="擦除" aria-pressed="false">擦除</button>
|
||||||
<select id="eraseMode" aria-label="擦除方式" title="擦除方式,矩形和套索为视觉裁剪"><option value="erase-object">对象</option><option value="erase-region">矩形</option><option value="erase-path">套索</option></select>
|
<select id="eraseMode" aria-label="擦除方式" title="擦除方式,矩形和套索为视觉裁剪"><option value="erase-object">对象</option><option value="erase-region">矩形</option><option value="erase-path">套索</option></select>
|
||||||
|
|||||||
+40
-7
@@ -1257,13 +1257,13 @@ button.outline-link:hover {
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.viewer-panel:focus-visible {
|
.viewer-panel:focus {
|
||||||
outline: 2px solid var(--focus);
|
outline: none;
|
||||||
outline-offset: -2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.viewer-panel.pointer-focus:focus {
|
.viewer-panel.keyboard-focus:focus-visible {
|
||||||
outline: none;
|
outline: 2px solid var(--focus);
|
||||||
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.viewer-panel.pan-mode,
|
.viewer-panel.pan-mode,
|
||||||
@@ -1781,6 +1781,29 @@ button.outline-link:hover {
|
|||||||
background: rgba(15, 123, 104, 0.08);
|
background: rgba(15, 123, 104, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-region-link,
|
||||||
|
.page-region-link:hover,
|
||||||
|
.page-region-link:focus-visible {
|
||||||
|
pointer-events: none;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-region-link > svg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-region-link path {
|
||||||
|
pointer-events: fill;
|
||||||
|
fill: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-region-link:hover path,
|
||||||
|
.page-region-link:focus-visible path {
|
||||||
|
fill: rgba(15, 123, 104, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.annotation-note {
|
.annotation-note {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@@ -1793,7 +1816,16 @@ button.outline-link:hover {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transform: translateX(-100%);
|
transform-origin: 0 0;
|
||||||
|
transform: rotate(var(--note-rotation, 0deg)) scale(var(--note-scale, 1)) translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.annotation-note.no-zoom {
|
||||||
|
--note-scale: calc(1 / var(--surface-scale, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.annotation-note.no-rotate {
|
||||||
|
--note-rotation: calc(-1 * var(--surface-rotation, 0deg));
|
||||||
}
|
}
|
||||||
|
|
||||||
.annotation-detail {
|
.annotation-detail {
|
||||||
@@ -1804,7 +1836,8 @@ button.outline-link:hover {
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editing .annotation-note {
|
.editing .annotation-note,
|
||||||
|
.editing .page-link {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+93
-41
@@ -55,7 +55,7 @@ const state = {
|
|||||||
outlineExpanded: new Map(),
|
outlineExpanded: new Map(),
|
||||||
textFonts: [],
|
textFonts: [],
|
||||||
textFontID: null,
|
textFontID: null,
|
||||||
textDefaults: { type: "TextObject", size: 12 * 25.4 / 72, color: "#000000", wrap: true, align: "left", paragraphHeight: 0, letterSpacing: 0 },
|
textDefaults: { type: "TextObject", size: 16 * 25.4 / 72, color: "#000000", wrap: true, align: "left", paragraphHeight: 0, letterSpacing: 0 },
|
||||||
ofdBytes: null,
|
ofdBytes: null,
|
||||||
fileName: "ofdgo.ofd",
|
fileName: "ofdgo.ofd",
|
||||||
openSeq: 0,
|
openSeq: 0,
|
||||||
@@ -802,8 +802,10 @@ el.searchInput.addEventListener("input", () => resetSearch(false));
|
|||||||
el.searchPrev.addEventListener("click", () => selectSearchMatch(Math.max(0, state.searchIndex) - 1));
|
el.searchPrev.addEventListener("click", () => selectSearchMatch(Math.max(0, state.searchIndex) - 1));
|
||||||
el.searchNext.addEventListener("click", () => selectSearchMatch(state.searchIndex + 1));
|
el.searchNext.addEventListener("click", () => selectSearchMatch(state.searchIndex + 1));
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
document.addEventListener("keydown", () => el.viewerPanel.classList.remove("pointer-focus"), true);
|
document.addEventListener("keydown", event => {
|
||||||
document.addEventListener("pointerdown", () => el.viewerPanel.classList.add("pointer-focus"), true);
|
if (event.key === "Tab") el.viewerPanel.classList.add("keyboard-focus");
|
||||||
|
}, true);
|
||||||
|
document.addEventListener("pointerdown", () => el.viewerPanel.classList.remove("keyboard-focus"), true);
|
||||||
el.fontAddButton.addEventListener("click", () => openFontFile(el.fontInput));
|
el.fontAddButton.addEventListener("click", () => openFontFile(el.fontInput));
|
||||||
el.fontDirectoryButton.addEventListener("click", () => openFontFile(el.fontDirectoryInput));
|
el.fontDirectoryButton.addEventListener("click", () => openFontFile(el.fontDirectoryInput));
|
||||||
el.localFontButton.addEventListener("click", loadLocalFonts);
|
el.localFontButton.addEventListener("click", loadLocalFonts);
|
||||||
@@ -960,7 +962,7 @@ function handleKeyDown(event) {
|
|||||||
state.showPages = false;
|
state.showPages = false;
|
||||||
updateSidebarState();
|
updateSidebarState();
|
||||||
}
|
}
|
||||||
el.viewerPanel.focus({ preventScroll: true });
|
canvasEditor.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key === "Enter" && target === el.pageInput && !event.shiftKey) {
|
if (key === "Enter" && target === el.pageInput && !event.shiftKey) {
|
||||||
@@ -1424,7 +1426,7 @@ async function enterCompositeScope(index, key) {
|
|||||||
if (page && surface) mountEditorObjects(index, page, surface);
|
if (page && surface) mountEditorObjects(index, page, surface);
|
||||||
updateControls();
|
updateControls();
|
||||||
setStatus(pageStatus(index, state.doc.pageCount));
|
setStatus(pageStatus(index, state.doc.pageCount));
|
||||||
el.viewerPanel.focus({ preventScroll: true });
|
canvasEditor.focus();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (openSeq === state.openSeq) showError(err, false);
|
if (openSeq === state.openSeq) showError(err, false);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1439,7 +1441,7 @@ async function exitCompositeScope() {
|
|||||||
if (split >= 0) return enterCompositeScope(scope.index, scope.key.slice(0, split));
|
if (split >= 0) return enterCompositeScope(scope.index, scope.key.slice(0, split));
|
||||||
resetCompositeScope();
|
resetCompositeScope();
|
||||||
updateControls();
|
updateControls();
|
||||||
el.viewerPanel.focus({ preventScroll: true });
|
canvasEditor.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startImageCrop(item) {
|
async function startImageCrop(item) {
|
||||||
@@ -1524,7 +1526,7 @@ async function toggleTextTool() {
|
|||||||
state.selectObjects = true;
|
state.selectObjects = true;
|
||||||
setPan(false);
|
setPan(false);
|
||||||
canvasEditor.setTool("text");
|
canvasEditor.setTool("text");
|
||||||
el.viewerPanel.focus({ preventScroll: true });
|
canvasEditor.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function beginCanvasText(index, box, surface, page) {
|
async function beginCanvasText(index, box, surface, page) {
|
||||||
@@ -1684,7 +1686,7 @@ async function insertObject(event) {
|
|||||||
}
|
}
|
||||||
el.insertPanel.close();
|
el.insertPanel.close();
|
||||||
if (item && doc.revision === state.editorInfo?.revision) {
|
if (item && doc.revision === state.editorInfo?.revision) {
|
||||||
el.viewerPanel.focus({ preventScroll: true });
|
canvasEditor.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setEditorInfo(doc);
|
setEditorInfo(doc);
|
||||||
@@ -1695,7 +1697,7 @@ async function insertObject(event) {
|
|||||||
await refreshEditorPage(doc, index, openSeq);
|
await refreshEditorPage(doc, index, openSeq);
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
rememberEditorView(before, revision, !item && !!state.composite);
|
rememberEditorView(before, revision, !item && !!state.composite);
|
||||||
el.viewerPanel.focus({ preventScroll: true });
|
canvasEditor.focus();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
@@ -1846,7 +1848,7 @@ function toggleDrawingTool(tool) {
|
|||||||
state.selectObjects = true;
|
state.selectObjects = true;
|
||||||
setPan(false);
|
setPan(false);
|
||||||
canvasEditor.setTool(next);
|
canvasEditor.setTool(next);
|
||||||
el.viewerPanel.focus({ preventScroll: true });
|
canvasEditor.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDrawingControls() {
|
function updateDrawingControls() {
|
||||||
@@ -1972,7 +1974,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
}
|
}
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
if (!restoring) rememberEditorView(before, revision, reindex);
|
if (!restoring) rememberEditorView(before, revision, reindex);
|
||||||
if (!toolbarFocus) el.viewerPanel.focus({ preventScroll: true });
|
if (!toolbarFocus) canvasEditor.focus();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -3036,7 +3038,7 @@ async function openDocument(options = {}) {
|
|||||||
const page = options.keepPreview ? state.pageCache.get(pageIndex)
|
const page = options.keepPreview ? state.pageCache.get(pageIndex)
|
||||||
: await renderPage(pageIndex, { keepBusy: true, scroll: false, openSeq });
|
: await renderPage(pageIndex, { keepBusy: true, scroll: false, openSeq });
|
||||||
if (page && options.resetScroll) {
|
if (page && options.resetScroll) {
|
||||||
el.viewerPanel.focus({ preventScroll: true });
|
canvasEditor.focus();
|
||||||
}
|
}
|
||||||
queueNearbyPages(pageIndex, openSeq);
|
queueNearbyPages(pageIndex, openSeq);
|
||||||
loadDocumentDetails(openSeq);
|
loadDocumentDetails(openSeq);
|
||||||
@@ -3516,8 +3518,9 @@ async function processPageRenderQueue() {
|
|||||||
task.resolve(null);
|
task.resolve(null);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
page.cacheBytes = (page.svg.length + page.text.length) * 2;
|
page.cacheBytes = (page.svg.length + page.text.length + page.annotations.length) * 2;
|
||||||
page.text = JSON.parse(page.text);
|
page.text = JSON.parse(page.text);
|
||||||
|
page.annotations = JSON.parse(page.annotations);
|
||||||
state.pageCache.set(task.index, page);
|
state.pageCache.set(task.index, page);
|
||||||
}
|
}
|
||||||
task.resolve(page);
|
task.resolve(page);
|
||||||
@@ -3612,36 +3615,89 @@ function mountPageSVG(index, page, openSeq = state.openSeq) {
|
|||||||
if (state.editorInfo) {
|
if (state.editorInfo) {
|
||||||
mountEditorObjects(index, page, surface);
|
mountEditorObjects(index, page, surface);
|
||||||
}
|
}
|
||||||
mountAnnotationNotes(index, surface);
|
mountAnnotationNotes(index, surface, page.annotations);
|
||||||
for (const link of page.links) {
|
mountPageLinks(page, surface);
|
||||||
|
shell.classList.add("rendered");
|
||||||
|
renderSearchHighlights(index);
|
||||||
|
if (state.doc?.pages?.[index]) {
|
||||||
|
layoutPageShell(shell, state.doc.pages[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageLinkTarget(link) {
|
||||||
|
if (link.dest) {
|
||||||
|
const target = state.doc.pages.findIndex(page => page.id === link.dest.pageID);
|
||||||
|
return { href: target < 0 ? "#" : `#page-${target + 1}`, title: target < 0 ? "文档链接" : `第${target + 1}页`, activate: () => navigateDestination(link.dest) };
|
||||||
|
}
|
||||||
|
if (link.attachment) {
|
||||||
|
return { href: "#", title: link.fileName ? `下载附件:${link.fileName}` : "下载附件", activate: () => {
|
||||||
|
if (!link.fileName) { setStatus("附件不存在"); return; }
|
||||||
|
return downloadAttachment({ id: link.attachment, fileName: link.fileName });
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
let url;
|
||||||
|
try { url = new URL(link.uri); } catch { return null; }
|
||||||
|
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
||||||
|
return { href: url.href, title: url.href, external: true, activate: () => { window.open(url.href, "_blank", "noopener,noreferrer"); } };
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountPageLinks(page, surface) {
|
||||||
|
const groups = new Map();
|
||||||
|
const mounted = new Map();
|
||||||
|
for (const [index, link] of page.links.entries()) {
|
||||||
|
const target = pageLinkTarget(link);
|
||||||
|
if (!target) continue;
|
||||||
|
target.order = index;
|
||||||
|
const key = link.group ? JSON.stringify([link.group, link.x, link.y, link.width, link.height, link.path]) : index;
|
||||||
|
if (!groups.has(key)) groups.set(key, { link, targets: [] });
|
||||||
|
groups.get(key).targets.push(target);
|
||||||
|
}
|
||||||
|
for (const { link, targets } of groups.values()) {
|
||||||
const anchor = document.createElement("a");
|
const anchor = document.createElement("a");
|
||||||
anchor.className = "page-link";
|
anchor.className = "page-link";
|
||||||
if (link.dest) {
|
anchor.href = targets[0].href;
|
||||||
const target = state.doc.pages.findIndex(page => page.id === link.dest.pageID);
|
anchor.title = targets[0].title;
|
||||||
anchor.href = target < 0 ? "#" : `#page-${target + 1}`;
|
mounted.set(anchor, { link, targets });
|
||||||
anchor.title = target < 0 ? "文档链接" : `第${target + 1}页`;
|
if (!link.group && targets[0].external) {
|
||||||
anchor.addEventListener("click", event => { event.preventDefault(); navigateDestination(link.dest); });
|
|
||||||
} else {
|
|
||||||
let url;
|
|
||||||
try { url = new URL(link.uri); } catch { continue; }
|
|
||||||
if (url.protocol !== "http:" && url.protocol !== "https:") continue;
|
|
||||||
anchor.href = url.href;
|
|
||||||
anchor.target = "_blank";
|
anchor.target = "_blank";
|
||||||
anchor.rel = "noopener noreferrer";
|
anchor.rel = "noopener noreferrer";
|
||||||
anchor.title = url.href;
|
} else {
|
||||||
|
anchor.addEventListener("click", async event => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (document.body.hasAttribute("aria-busy")) return;
|
||||||
|
const openSeq = state.openSeq;
|
||||||
|
let actions = targets;
|
||||||
|
if (event.detail && link.group) {
|
||||||
|
const matches = new Set();
|
||||||
|
for (const node of document.elementsFromPoint(event.clientX, event.clientY)) {
|
||||||
|
const entry = mounted.get(node.closest(".page-link"));
|
||||||
|
if (entry?.link.group === link.group) entry.targets.forEach(target => matches.add(target));
|
||||||
|
}
|
||||||
|
actions = [...matches].sort((a, b) => a.order - b.order);
|
||||||
|
}
|
||||||
|
for (const target of actions) {
|
||||||
|
if (openSeq !== state.openSeq) break;
|
||||||
|
await target.activate();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
anchor.setAttribute("aria-label", anchor.title);
|
anchor.setAttribute("aria-label", anchor.title);
|
||||||
anchor.style.left = `${link.x / page.width * 100}%`;
|
anchor.style.left = `${link.x / page.width * 100}%`;
|
||||||
anchor.style.top = `${link.y / page.height * 100}%`;
|
anchor.style.top = `${link.y / page.height * 100}%`;
|
||||||
anchor.style.width = `${link.width / page.width * 100}%`;
|
anchor.style.width = `${link.width / page.width * 100}%`;
|
||||||
anchor.style.height = `${link.height / page.height * 100}%`;
|
anchor.style.height = `${link.height / page.height * 100}%`;
|
||||||
|
if (link.path) {
|
||||||
|
anchor.classList.add("page-region-link");
|
||||||
|
const region = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
region.setAttribute("viewBox", `${link.x} ${link.y} ${link.width} ${link.height}`);
|
||||||
|
region.setAttribute("aria-hidden", "true");
|
||||||
|
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||||
|
path.setAttribute("d", link.path);
|
||||||
|
region.append(path);
|
||||||
|
anchor.append(region);
|
||||||
|
}
|
||||||
surface.append(anchor);
|
surface.append(anchor);
|
||||||
}
|
}
|
||||||
shell.classList.add("rendered");
|
|
||||||
renderSearchHighlights(index);
|
|
||||||
if (state.doc?.pages?.[index]) {
|
|
||||||
layoutPageShell(shell, state.doc.pages[index]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function destinationPoint(page, x, y) {
|
function destinationPoint(page, x, y) {
|
||||||
@@ -4121,6 +4177,7 @@ function layoutPageShell(shell, page) {
|
|||||||
const y = state.rotation >= 180 ? viewHeight : 0;
|
const y = state.rotation >= 180 ? viewHeight : 0;
|
||||||
surface.style.transform = `scale(${scale}) translate(${x}px, ${y}px) rotate(${state.rotation}deg)`;
|
surface.style.transform = `scale(${scale}) translate(${x}px, ${y}px) rotate(${state.rotation}deg)`;
|
||||||
surface.style.setProperty("--surface-scale", String(scale));
|
surface.style.setProperty("--surface-scale", String(scale));
|
||||||
|
surface.style.setProperty("--surface-rotation", `${state.rotation}deg`);
|
||||||
surface.classList.toggle("sideways", state.rotation % 180 !== 0);
|
surface.classList.toggle("sideways", state.rotation % 180 !== 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4920,7 +4977,6 @@ function renderMeta(keepDetails = false) {
|
|||||||
el.metaSignatures.textContent = doc.detailsPending ? "正在检查" : doc.detailsError ? "读取失败" : String(doc.signatureCount || 0);
|
el.metaSignatures.textContent = doc.detailsPending ? "正在检查" : doc.detailsError ? "读取失败" : String(doc.signatureCount || 0);
|
||||||
renderMetaContent(el.attachmentList, [doc.attachments, doc.attachmentError], renderAttachments);
|
renderMetaContent(el.attachmentList, [doc.attachments, doc.attachmentError], renderAttachments);
|
||||||
renderMetaContent(el.signatureList, [doc.signatures, doc.signatureError], renderSignatures);
|
renderMetaContent(el.signatureList, [doc.signatures, doc.signatureError], renderSignatures);
|
||||||
renderAnnotations();
|
|
||||||
renderMetaContent(el.docFontList, doc.fonts || [], renderDocumentFonts);
|
renderMetaContent(el.docFontList, doc.fonts || [], renderDocumentFonts);
|
||||||
updateTextFonts(canvasEditor.selected, true);
|
updateTextFonts(canvasEditor.selected, true);
|
||||||
updateLocalFontButton();
|
updateLocalFontButton();
|
||||||
@@ -5189,21 +5245,17 @@ function clearRegionHighlights() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAnnotations() {
|
function mountAnnotationNotes(index, surface, annotations) {
|
||||||
for (const shell of el.svgHost.querySelectorAll(".page-shell.rendered")) {
|
|
||||||
mountAnnotationNotes(Number(shell.dataset.pageIndex), shell.querySelector(".page-surface"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mountAnnotationNotes(index, surface) {
|
|
||||||
for (const note of surface.querySelectorAll(".annotation-note")) note.remove();
|
for (const note of surface.querySelectorAll(".annotation-note")) note.remove();
|
||||||
if (!state.renderAnnotations) return;
|
if (!state.renderAnnotations) return;
|
||||||
const page = state.doc.pages[index];
|
const page = state.doc.pages[index];
|
||||||
for (const annotation of state.doc.annotations || []) {
|
for (const annotation of annotations || []) {
|
||||||
if (!annotation.visible || annotation.page !== index + 1 || !annotation.remark?.trim()) continue;
|
if (!annotation.visible || annotation.page !== index + 1 || !annotation.remark?.trim()) continue;
|
||||||
const note = document.createElement("button");
|
const note = document.createElement("button");
|
||||||
note.type = "button";
|
note.type = "button";
|
||||||
note.className = "annotation-note";
|
note.className = "annotation-note";
|
||||||
|
note.classList.toggle("no-zoom", !!annotation.noZoom);
|
||||||
|
note.classList.toggle("no-rotate", !!annotation.noRotate);
|
||||||
note.textContent = "\u24d8";
|
note.textContent = "\u24d8";
|
||||||
note.title = "查看注解";
|
note.title = "查看注解";
|
||||||
note.setAttribute("aria-label", "查看注解");
|
note.setAttribute("aria-label", "查看注解");
|
||||||
@@ -5408,7 +5460,7 @@ function startPan(event) {
|
|||||||
state.pan = { id: event.pointerId, x: event.clientX + el.viewerPanel.scrollLeft, y: event.clientY + el.viewerPanel.scrollTop };
|
state.pan = { id: event.pointerId, x: event.clientX + el.viewerPanel.scrollLeft, y: event.clientY + el.viewerPanel.scrollTop };
|
||||||
el.viewerPanel.setPointerCapture(event.pointerId);
|
el.viewerPanel.setPointerCapture(event.pointerId);
|
||||||
el.viewerPanel.classList.add("panning");
|
el.viewerPanel.classList.add("panning");
|
||||||
el.viewerPanel.focus({ preventScroll: true });
|
canvasEditor.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function movePan(event) {
|
function movePan(event) {
|
||||||
|
|||||||
@@ -242,6 +242,11 @@ export class CanvasEditor {
|
|||||||
window.addEventListener("resize", () => this.cancel());
|
window.addEventListener("resize", () => this.cancel());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this.viewer.classList.remove("keyboard-focus");
|
||||||
|
this.viewer.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
|
|
||||||
setEnabled(enabled) {
|
setEnabled(enabled) {
|
||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
this.viewer.classList.toggle("editing-select", enabled);
|
this.viewer.classList.toggle("editing-select", enabled);
|
||||||
@@ -499,7 +504,7 @@ export class CanvasEditor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.viewer.focus({ preventScroll: true });
|
this.focus();
|
||||||
const { item, box } = this.crop;
|
const { item, box } = this.crop;
|
||||||
this.drag = { crop: true, box: { ...box }, pointerID: event.pointerId, corner: resizeCorner(this.crop.node, event),
|
this.drag = { crop: true, box: { ...box }, pointerID: event.pointerId, corner: resizeCorner(this.crop.node, event),
|
||||||
page: item.page, rect: item.surface.getBoundingClientRect(), rotation: this.options.rotation(), clientX: event.clientX, clientY: event.clientY };
|
page: item.page, rect: item.surface.getBoundingClientRect(), rotation: this.options.rotation(), clientX: event.clientX, clientY: event.clientY };
|
||||||
@@ -520,7 +525,7 @@ export class CanvasEditor {
|
|||||||
const target = event.target.dataset.corner || direct && !direct.contours ? direct : this.itemsAt(event)[0];
|
const target = event.target.dataset.corner || direct && !direct.contours ? direct : this.itemsAt(event)[0];
|
||||||
if (node || target) event.preventDefault();
|
if (node || target) event.preventDefault();
|
||||||
node = target?.node;
|
node = target?.node;
|
||||||
this.viewer.focus({ preventScroll: true });
|
this.focus();
|
||||||
const additive = event.shiftKey || event.ctrlKey || event.metaKey || this.multiple;
|
const additive = event.shiftKey || event.ctrlKey || event.metaKey || this.multiple;
|
||||||
if (!target) {
|
if (!target) {
|
||||||
this.startMarquee(event, additive);
|
this.startMarquee(event, additive);
|
||||||
@@ -579,7 +584,7 @@ export class CanvasEditor {
|
|||||||
const items = this.itemsAt(event);
|
const items = this.itemsAt(event);
|
||||||
if (!items.length) return;
|
if (!items.length) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.viewer.focus({ preventScroll: true });
|
this.focus();
|
||||||
this.select(items[(items.indexOf(this.selected) + 1) % items.length]);
|
this.select(items[(items.indexOf(this.selected) + 1) % items.length]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -804,7 +809,7 @@ export class CanvasEditor {
|
|||||||
const page = this.pages.get(surface);
|
const page = this.pages.get(surface);
|
||||||
if (!page) return;
|
if (!page) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.viewer.focus({ preventScroll: true });
|
this.focus();
|
||||||
this.drag = { erase: this.tool, page, surface, items: [...surface.querySelectorAll(".edit-object")].map(node => this.nodes.get(node)),
|
this.drag = { erase: this.tool, page, surface, items: [...surface.querySelectorAll(".edit-object")].map(node => this.nodes.get(node)),
|
||||||
erased: new Set(), shape: "rectangle", pointerID: event.pointerId, clientX: event.clientX, clientY: event.clientY,
|
erased: new Set(), shape: "rectangle", pointerID: event.pointerId, clientX: event.clientX, clientY: event.clientY,
|
||||||
rect: surface.getBoundingClientRect(), rotation: this.options.rotation() };
|
rect: surface.getBoundingClientRect(), rotation: this.options.rotation() };
|
||||||
@@ -878,7 +883,7 @@ export class CanvasEditor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.viewer.focus({ preventScroll: true });
|
this.focus();
|
||||||
const shape = this.tool;
|
const shape = this.tool;
|
||||||
const style = this.options.drawStyle();
|
const style = this.options.drawStyle();
|
||||||
if (lineShape(shape)) {
|
if (lineShape(shape)) {
|
||||||
@@ -1117,7 +1122,7 @@ export class CanvasEditor {
|
|||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.closeText();
|
this.closeText();
|
||||||
this.viewer.focus({ preventScroll: true });
|
this.focus();
|
||||||
} else if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
} else if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.commitText();
|
this.commitText();
|
||||||
@@ -1224,7 +1229,7 @@ export class CanvasEditor {
|
|||||||
item.surface.classList.add("cropping");
|
item.surface.classList.add("cropping");
|
||||||
this.crop = { item, box, initialBox: { ...box }, node, mask, preview, original, pagePreview };
|
this.crop = { item, box, initialBox: { ...box }, node, mask, preview, original, pagePreview };
|
||||||
this.paintCrop();
|
this.paintCrop();
|
||||||
this.viewer.focus({ preventScroll: true });
|
this.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
paintCrop() {
|
paintCrop() {
|
||||||
|
|||||||
+29
-11
@@ -28,6 +28,7 @@ import (
|
|||||||
"image/color"
|
"image/color"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
@@ -363,11 +364,23 @@ func renderPage(args []js.Value) (any, error) {
|
|||||||
for i, link := range page.Links {
|
for i, link := range page.Links {
|
||||||
item := map[string]any{
|
item := map[string]any{
|
||||||
"uri": link.URI,
|
"uri": link.URI,
|
||||||
|
"path": link.Path,
|
||||||
|
"group": link.Group,
|
||||||
"x": link.Box.X,
|
"x": link.Box.X,
|
||||||
"y": link.Box.Y,
|
"y": link.Box.Y,
|
||||||
"width": link.Box.W,
|
"width": link.Box.W,
|
||||||
"height": link.Box.H,
|
"height": link.Box.H,
|
||||||
}
|
}
|
||||||
|
if link.Attachment != "" {
|
||||||
|
item["attachment"] = link.Attachment
|
||||||
|
attachments, _ := currentSession.Reader.Attachments()
|
||||||
|
for _, attachment := range attachments {
|
||||||
|
if attachment.ID == link.Attachment {
|
||||||
|
item["fileName"] = path.Base(currentSession.Reader.ResPath(attachment.FileLoc))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if dest := link.Dest; dest != nil {
|
if dest := link.Dest; dest != nil {
|
||||||
item["dest"] = map[string]any{"type": dest.Type, "pageID": dest.PageID, "left": dest.Left, "top": dest.Top,
|
item["dest"] = map[string]any{"type": dest.Type, "pageID": dest.PageID, "left": dest.Left, "top": dest.Top,
|
||||||
"right": dest.Right, "bottom": dest.Bottom, "zoom": dest.Zoom}
|
"right": dest.Right, "bottom": dest.Bottom, "zoom": dest.Zoom}
|
||||||
@@ -378,18 +391,23 @@ func renderPage(args []js.Value) (any, error) {
|
|||||||
for i, font := range page.Fonts {
|
for i, font := range page.Fonts {
|
||||||
fonts[i] = map[string]any{"name": font.Name, "weight": font.Weight, "style": font.Style}
|
fonts[i] = map[string]any{"name": font.Name, "weight": font.Weight, "style": font.Style}
|
||||||
}
|
}
|
||||||
|
annotations, err := json.Marshal(page.Annotations)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return successResult(map[string]any{
|
return successResult(map[string]any{
|
||||||
"index": page.Index,
|
"annotations": string(annotations),
|
||||||
"number": page.Number,
|
"index": page.Index,
|
||||||
"id": page.ID,
|
"number": page.Number,
|
||||||
"width": page.Width,
|
"id": page.ID,
|
||||||
"height": page.Height,
|
"width": page.Width,
|
||||||
"svg": page.SVG,
|
"height": page.Height,
|
||||||
"links": links,
|
"svg": page.SVG,
|
||||||
"fonts": fonts,
|
"links": links,
|
||||||
"images": svgImagesToJS(page.Images),
|
"fonts": fonts,
|
||||||
"text": string(textData),
|
"images": svgImagesToJS(page.Images),
|
||||||
"objects": objects,
|
"text": string(textData),
|
||||||
|
"objects": objects,
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+33
-30
@@ -114,25 +114,24 @@ type Session struct {
|
|||||||
|
|
||||||
// DocumentInfo 文档信息
|
// DocumentInfo 文档信息
|
||||||
type DocumentInfo struct {
|
type DocumentInfo struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
DocType string `json:"docType"`
|
DocType string `json:"docType"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Author string `json:"author"`
|
Author string `json:"author"`
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
CreationDate string `json:"creationDate"`
|
CreationDate string `json:"creationDate"`
|
||||||
ModDate string `json:"modDate"`
|
ModDate string `json:"modDate"`
|
||||||
PageCount int `json:"pageCount"`
|
PageCount int `json:"pageCount"`
|
||||||
FontCount int `json:"fontCount"`
|
FontCount int `json:"fontCount"`
|
||||||
SignatureCount int `json:"signatureCount"`
|
SignatureCount int `json:"signatureCount"`
|
||||||
SignatureError string `json:"signatureError,omitempty"`
|
SignatureError string `json:"signatureError,omitempty"`
|
||||||
AttachmentError string `json:"attachmentError,omitempty"`
|
AttachmentError string `json:"attachmentError,omitempty"`
|
||||||
Attachments []AttachmentInfo `json:"attachments,omitempty"`
|
Attachments []AttachmentInfo `json:"attachments,omitempty"`
|
||||||
Fonts []FontInfo `json:"fonts"`
|
Fonts []FontInfo `json:"fonts"`
|
||||||
Signatures []SignatureInfo `json:"signatures"`
|
Signatures []SignatureInfo `json:"signatures"`
|
||||||
Annotations []ofdgo.AnnotationInfo `json:"annotations,omitempty"`
|
Pages []PageInfo `json:"pages"`
|
||||||
Pages []PageInfo `json:"pages"`
|
Outlines []OutlineInfo `json:"outlines,omitempty"`
|
||||||
Outlines []OutlineInfo `json:"outlines,omitempty"`
|
DetailsPending bool `json:"detailsPending,omitempty"`
|
||||||
DetailsPending bool `json:"detailsPending,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OutlineInfo 目录节点信息
|
// OutlineInfo 目录节点信息
|
||||||
@@ -157,15 +156,16 @@ type PageInfo struct {
|
|||||||
|
|
||||||
// PageSVG 页面SVG结果
|
// PageSVG 页面SVG结果
|
||||||
type PageSVG struct {
|
type PageSVG struct {
|
||||||
Index int `json:"index"`
|
Index int `json:"index"`
|
||||||
Number int `json:"number"`
|
Number int `json:"number"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Width float64 `json:"width"`
|
Width float64 `json:"width"`
|
||||||
Height float64 `json:"height"`
|
Height float64 `json:"height"`
|
||||||
SVG string `json:"svg"`
|
SVG string `json:"svg"`
|
||||||
Links []ofdgo.PageLink `json:"-"`
|
Links []ofdgo.PageLink `json:"-"`
|
||||||
Fonts []ofdgo.SVGFont `json:"-"`
|
Fonts []ofdgo.SVGFont `json:"-"`
|
||||||
Images []ofdgo.SVGImage `json:"-"`
|
Images []ofdgo.SVGImage `json:"-"`
|
||||||
|
Annotations []ofdgo.AnnotationInfo `json:"annotations,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExportFormat 导出格式
|
// ExportFormat 导出格式
|
||||||
@@ -391,7 +391,6 @@ func (s *Session) Info() DocumentInfo {
|
|||||||
Pages: make([]PageInfo, 0, len(s.doc.Pages.Page)),
|
Pages: make([]PageInfo, 0, len(s.doc.Pages.Page)),
|
||||||
Outlines: s.doc.OutlineInfos(),
|
Outlines: s.doc.OutlineInfos(),
|
||||||
}
|
}
|
||||||
info.Annotations, _ = s.Reader.AnnotationInfos()
|
|
||||||
if attachments, err := s.Reader.Attachments(); err == nil {
|
if attachments, err := s.Reader.Attachments(); err == nil {
|
||||||
for _, attachment := range attachments {
|
for _, attachment := range attachments {
|
||||||
if attachment.Visible {
|
if attachment.Visible {
|
||||||
@@ -468,11 +467,15 @@ func (s *Session) RenderPageSVG(index int) (PageSVG, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return PageSVG{}, err
|
return PageSVG{}, err
|
||||||
}
|
}
|
||||||
|
annotations, err := s.Reader.AnnotationInfosByIndex(index)
|
||||||
|
if err != nil {
|
||||||
|
return PageSVG{}, err
|
||||||
|
}
|
||||||
for i, font := range resources.Fonts {
|
for i, font := range resources.Fonts {
|
||||||
s.svgFonts[font.Name] = font.Data
|
s.svgFonts[font.Name] = font.Data
|
||||||
resources.Fonts[i].Data = nil
|
resources.Fonts[i].Data = nil
|
||||||
}
|
}
|
||||||
return PageSVG{Index: index, Number: index + 1, ID: page.ID, Width: box.W, Height: box.H, SVG: buf.String(), Links: links, Fonts: resources.Fonts, Images: resources.Images}, nil
|
return PageSVG{Index: index, Number: index + 1, ID: page.ID, Width: box.W, Height: box.H, SVG: buf.String(), Links: links, Fonts: resources.Fonts, Images: resources.Images, Annotations: annotations}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SVGFontData 获取已渲染页面引用的字体数据
|
// SVGFontData 获取已渲染页面引用的字体数据
|
||||||
|
|||||||
+171
-20
@@ -18,6 +18,8 @@ import (
|
|||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/tdewolff/canvas"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Action 动作
|
// Action 动作
|
||||||
@@ -59,11 +61,16 @@ type URI struct {
|
|||||||
Base string `xml:"Base,attr"`
|
Base string `xml:"Base,attr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageLink 页面矩形链接,URI与Dest分别表示外链和文档内跳转,Box使用页面毫米坐标
|
// PageLink 页面链接,URI、Dest和Attachment分别表示外链、文档内跳转和附件
|
||||||
|
// Box使用页面毫米坐标,Path为可选的页面坐标SVG路径,存在时作为精确点击区域
|
||||||
|
// 同一来源的多个动作共享非零Group,组内按返回顺序执行
|
||||||
type PageLink struct {
|
type PageLink struct {
|
||||||
URI string
|
URI string
|
||||||
Box Box
|
Box Box
|
||||||
Dest *Dest
|
Dest *Dest
|
||||||
|
Attachment string
|
||||||
|
Path string
|
||||||
|
Group int
|
||||||
}
|
}
|
||||||
|
|
||||||
// GotoA 附件动作
|
// GotoA 附件动作
|
||||||
@@ -114,6 +121,8 @@ type RegionCommand struct {
|
|||||||
type actionSource struct {
|
type actionSource struct {
|
||||||
Box Box
|
Box Box
|
||||||
Actions []Action
|
Actions []Action
|
||||||
|
Matrix Matrix
|
||||||
|
Path string
|
||||||
}
|
}
|
||||||
|
|
||||||
// gotoDest 获取文档内跳转目标
|
// gotoDest 获取文档内跳转目标
|
||||||
@@ -229,17 +238,48 @@ func actionFloatAttr(start xml.StartElement, name string) float64 {
|
|||||||
// pageActionSources 获取页面动作来源
|
// pageActionSources 获取页面动作来源
|
||||||
// 入参: page 页面内容, box 页面区域
|
// 入参: page 页面内容, box 页面区域
|
||||||
// 返回: []actionSource 动作来源
|
// 返回: []actionSource 动作来源
|
||||||
func pageActionSources(page *PageContent, box Box) []actionSource {
|
func (r *Renderer) pageActionSources(page *PageContent, box Box) []actionSource {
|
||||||
sources := make([]actionSource, 0)
|
sources := make([]actionSource, 0)
|
||||||
if len(page.Actions) > 0 {
|
if len(page.Actions) > 0 {
|
||||||
sources = append(sources, actionSource{
|
sources = append(sources, actionSource{
|
||||||
Box: Box{W: box.W, H: box.H},
|
Box: Box{W: box.W, H: box.H},
|
||||||
Actions: page.Actions,
|
Actions: page.Actions,
|
||||||
|
Matrix: IdentityMatrix,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, layer := range page.Content.Layer {
|
for order := range 3 {
|
||||||
|
if r.Reader.doc != nil {
|
||||||
|
for _, ref := range page.Template {
|
||||||
|
kind := ref.ZOrder
|
||||||
|
if kind == "" {
|
||||||
|
kind = "Background"
|
||||||
|
}
|
||||||
|
if layerOrder(kind) != order {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if template := r.loadTemplate(ref.TemplateID); template != nil {
|
||||||
|
sources = append(sources, actionSource{Box: Box{W: box.W, H: box.H}, Actions: template.Actions, Matrix: IdentityMatrix})
|
||||||
|
for templateOrder := range 3 {
|
||||||
|
sources = r.appendLayerActionSources(sources, template.Content.Layer, templateOrder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sources = r.appendLayerActionSources(sources, page.Content.Layer, order)
|
||||||
|
}
|
||||||
|
return sources
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendLayerActionSources 按绘制顺序添加图层动作来源
|
||||||
|
// 入参: sources 动作来源, layers 页面图层, order 图层顺序
|
||||||
|
// 返回: []actionSource 动作来源
|
||||||
|
func (r *Renderer) appendLayerActionSources(sources []actionSource, layers []Layer, order int) []actionSource {
|
||||||
|
for _, layer := range layers {
|
||||||
|
if layerOrder(layer.Type) != order {
|
||||||
|
continue
|
||||||
|
}
|
||||||
for _, object := range layer.Objects {
|
for _, object := range layer.Objects {
|
||||||
sources = appendGraphicActionSources(sources, object, nil)
|
sources = r.appendGraphicActionSources(sources, object, IdentityMatrix, false, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sources
|
return sources
|
||||||
@@ -248,7 +288,7 @@ func pageActionSources(page *PageContent, box Box) []actionSource {
|
|||||||
// annotationActionSources 获取注释动作来源
|
// annotationActionSources 获取注释动作来源
|
||||||
// 入参: annotations 页面注释
|
// 入参: annotations 页面注释
|
||||||
// 返回: []actionSource 动作来源
|
// 返回: []actionSource 动作来源
|
||||||
func annotationActionSources(annotations []Annotation) []actionSource {
|
func (r *Renderer) annotationActionSources(annotations []Annotation) []actionSource {
|
||||||
sources := make([]actionSource, 0)
|
sources := make([]actionSource, 0)
|
||||||
for _, annotation := range annotations {
|
for _, annotation := range annotations {
|
||||||
if annotation.Visible != nil && !*annotation.Visible {
|
if annotation.Visible != nil && !*annotation.Visible {
|
||||||
@@ -259,51 +299,162 @@ func annotationActionSources(annotations []Annotation) []actionSource {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, object := range annotation.Appearance.Objects {
|
for _, object := range annotation.Appearance.Objects {
|
||||||
sources = appendGraphicActionSources(sources, object, &box)
|
sources = r.appendGraphicActionSources(sources, object, TranslationMatrix(box.X, box.Y), false, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sources
|
return sources
|
||||||
}
|
}
|
||||||
|
|
||||||
// appendGraphicActionSources 添加图形对象动作来源
|
// appendGraphicActionSources 添加图形对象动作来源
|
||||||
// 入参: sources 动作来源, object 图形对象, box 指定动作区域
|
// 入参: sources 动作来源, object 图形对象, parent 父级变换, boundaryInCTM 边界是否参与父级变换, seen 当前资源引用链
|
||||||
// 返回: []actionSource 动作来源
|
// 返回: []actionSource 动作来源
|
||||||
func appendGraphicActionSources(sources []actionSource, object GraphicObject, box *Box) []actionSource {
|
func (r *Renderer) appendGraphicActionSources(sources []actionSource, object GraphicObject, parent Matrix, boundaryInCTM bool, seen map[string]bool) []actionSource {
|
||||||
var boundary string
|
var boundary, ctm, resource string
|
||||||
var actions []Action
|
var actions []Action
|
||||||
var children []GraphicObject
|
var children []GraphicObject
|
||||||
|
composite := false
|
||||||
switch object.Type {
|
switch object.Type {
|
||||||
case "TextObject":
|
case "TextObject":
|
||||||
boundary = object.TextObject.Boundary
|
boundary = object.TextObject.Boundary
|
||||||
|
ctm = object.TextObject.CTM
|
||||||
actions = object.TextObject.Actions
|
actions = object.TextObject.Actions
|
||||||
case "PathObject":
|
case "PathObject":
|
||||||
boundary = object.PathObject.Boundary
|
boundary = object.PathObject.Boundary
|
||||||
|
ctm = object.PathObject.CTM
|
||||||
actions = object.PathObject.Actions
|
actions = object.PathObject.Actions
|
||||||
case "ImageObject":
|
case "ImageObject":
|
||||||
boundary = object.ImageObject.Boundary
|
boundary = object.ImageObject.Boundary
|
||||||
|
ctm = object.ImageObject.CTM
|
||||||
actions = object.ImageObject.Actions
|
actions = object.ImageObject.Actions
|
||||||
case "CompositeGraphicUnit", "CompositeObject":
|
case "CompositeGraphicUnit", "CompositeObject":
|
||||||
boundary = object.CompositeGraphicUnit.Boundary
|
boundary = object.CompositeGraphicUnit.Boundary
|
||||||
|
ctm = object.CompositeGraphicUnit.CTM
|
||||||
|
resource = object.CompositeGraphicUnit.ResourceID
|
||||||
actions = object.CompositeGraphicUnit.Actions
|
actions = object.CompositeGraphicUnit.Actions
|
||||||
children = object.CompositeGraphicUnit.Objects
|
children = object.CompositeGraphicUnit.Objects
|
||||||
|
composite = true
|
||||||
}
|
}
|
||||||
|
box, _ := ParseBox(boundary)
|
||||||
|
placement := TranslationMatrix(box.X, box.Y).Multiply(parent)
|
||||||
|
if boundaryInCTM || composite {
|
||||||
|
placement = parent.Multiply(TranslationMatrix(box.X, box.Y))
|
||||||
|
}
|
||||||
|
matrix := placement.Multiply(NewMatrix(ctm))
|
||||||
if len(actions) > 0 {
|
if len(actions) > 0 {
|
||||||
sourceBox := box
|
source := actionSource{Box: placement.TransformBox(Box{W: box.W, H: box.H}), Actions: actions, Matrix: matrix}
|
||||||
if sourceBox == nil && boundary != "" {
|
if placement.a == 1 && placement.b == 0 && placement.c == 0 && placement.d == 1 {
|
||||||
if value, err := ParseBox(boundary); err == nil {
|
source.Box.W, source.Box.H = box.W, box.H
|
||||||
sourceBox = &value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if sourceBox != nil {
|
if box.W > 0 && box.H > 0 && !axisAlignedMatrix(placement) {
|
||||||
sources = append(sources, actionSource{Box: *sourceBox, Actions: actions})
|
source.Path = canvas.Rectangle(box.W, box.H).Transform(actionCanvasMatrix(placement)).ToSVG()
|
||||||
|
}
|
||||||
|
sources = append(sources, source)
|
||||||
|
}
|
||||||
|
if resource != "" && !seen[resource] {
|
||||||
|
if unit := r.CompositeGraphicUnits[resource]; unit != nil {
|
||||||
|
if seen == nil {
|
||||||
|
seen = make(map[string]bool)
|
||||||
|
}
|
||||||
|
seen[resource] = true
|
||||||
|
sources = r.appendGraphicActionSources(sources, GraphicObject{Type: "CompositeGraphicUnit", CompositeGraphicUnit: *unit}, matrix, true, seen)
|
||||||
|
delete(seen, resource)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, child := range children {
|
for _, child := range children {
|
||||||
sources = appendGraphicActionSources(sources, child, box)
|
sources = r.appendGraphicActionSources(sources, child, matrix, boundaryInCTM, seen)
|
||||||
}
|
}
|
||||||
return sources
|
return sources
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// actionCanvasMatrix 转换页面坐标矩阵,不翻转Y轴
|
||||||
|
// 入参: matrix 页面变换
|
||||||
|
// 返回: canvas.Matrix 路径变换
|
||||||
|
func actionCanvasMatrix(matrix Matrix) canvas.Matrix {
|
||||||
|
return canvas.Matrix{{matrix.a, matrix.c, matrix.e}, {matrix.b, matrix.d, matrix.f}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// actionRegionPath 解析复杂点击区域并转换为页面坐标
|
||||||
|
// 入参: region 动作区域, matrix 坐标变换
|
||||||
|
// 返回: *canvas.Path 点击路径
|
||||||
|
func actionRegionPath(region *Region, matrix Matrix) *canvas.Path {
|
||||||
|
path := &canvas.Path{}
|
||||||
|
for _, area := range region.Area {
|
||||||
|
start := parseFloats(area.Start)
|
||||||
|
if len(start) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
part := &canvas.Path{}
|
||||||
|
part.MoveTo(start[0], start[1])
|
||||||
|
for _, command := range area.Command {
|
||||||
|
p1, p2, p3 := parseFloats(command.Point1), parseFloats(command.Point2), parseFloats(command.Point3)
|
||||||
|
switch command.Type {
|
||||||
|
case "Move":
|
||||||
|
if len(p1) == 2 {
|
||||||
|
part.Close()
|
||||||
|
part.MoveTo(p1[0], p1[1])
|
||||||
|
}
|
||||||
|
case "Line":
|
||||||
|
if len(p1) == 2 {
|
||||||
|
part.LineTo(p1[0], p1[1])
|
||||||
|
}
|
||||||
|
case "QuadraticBezier":
|
||||||
|
if len(p1) == 2 && len(p2) == 2 {
|
||||||
|
part.QuadTo(p1[0], p1[1], p2[0], p2[1])
|
||||||
|
}
|
||||||
|
case "CubicBezier":
|
||||||
|
if len(p3) == 2 {
|
||||||
|
first, second := part.Pos(), canvas.Point{X: p3[0], Y: p3[1]}
|
||||||
|
if len(p1) == 2 {
|
||||||
|
first = canvas.Point{X: p1[0], Y: p1[1]}
|
||||||
|
}
|
||||||
|
if len(p2) == 2 {
|
||||||
|
second = canvas.Point{X: p2[0], Y: p2[1]}
|
||||||
|
}
|
||||||
|
part.CubeTo(first.X, first.Y, second.X, second.Y, p3[0], p3[1])
|
||||||
|
}
|
||||||
|
case "Arc":
|
||||||
|
size, end := parseFloats(command.EllipseSize), parseFloats(command.EndPoint)
|
||||||
|
angle, err := strconv.ParseFloat(command.RotationAngle, 64)
|
||||||
|
large, largeErr := strconv.ParseBool(command.LargeArc)
|
||||||
|
sweep, sweepErr := strconv.ParseBool(command.SweepDirection)
|
||||||
|
if len(end) == 2 && err == nil && largeErr == nil && sweepErr == nil {
|
||||||
|
rx, ry := 0.0, 0.0
|
||||||
|
if len(size) > 0 {
|
||||||
|
rx, ry = size[0], size[0]
|
||||||
|
}
|
||||||
|
if len(size) > 1 {
|
||||||
|
ry = size[1]
|
||||||
|
}
|
||||||
|
part.ArcTo(rx, ry, angle, large, sweep, end[0], end[1])
|
||||||
|
}
|
||||||
|
case "Close":
|
||||||
|
part.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
part.Close()
|
||||||
|
path = path.Append(part)
|
||||||
|
}
|
||||||
|
if canvas.Equal(matrix.a*matrix.d-matrix.b*matrix.c, 0) {
|
||||||
|
path = path.ReplaceArcs()
|
||||||
|
}
|
||||||
|
return path.Transform(actionCanvasMatrix(matrix))
|
||||||
|
}
|
||||||
|
|
||||||
|
// actionLinkRegion 获取动作点击区域
|
||||||
|
// 入参: source 动作来源, action 动作
|
||||||
|
// 返回: Box 外接矩形, string 精确路径
|
||||||
|
func actionLinkRegion(source actionSource, action Action) (Box, string) {
|
||||||
|
if action.Region == nil {
|
||||||
|
return source.Box, source.Path
|
||||||
|
}
|
||||||
|
path := actionRegionPath(action.Region, source.Matrix)
|
||||||
|
if path.Empty() {
|
||||||
|
return Box{}, ""
|
||||||
|
}
|
||||||
|
box := path.Bounds()
|
||||||
|
return Box{X: box.X0, Y: box.Y0, W: box.W(), H: box.H()}, path.ToSVG()
|
||||||
|
}
|
||||||
|
|
||||||
// resolveActionURI 解析URI动作地址
|
// resolveActionURI 解析URI动作地址
|
||||||
// 入参: action URI动作
|
// 入参: action URI动作
|
||||||
// 返回: string URI地址
|
// 返回: string URI地址
|
||||||
|
|||||||
+51
-19
@@ -14,7 +14,10 @@
|
|||||||
|
|
||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import "encoding/xml"
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
// Annotations 注释集合
|
// Annotations 注释集合
|
||||||
type Annotations struct {
|
type Annotations struct {
|
||||||
@@ -42,6 +45,8 @@ type Annotation struct {
|
|||||||
Creator string `xml:"Creator,attr"`
|
Creator string `xml:"Creator,attr"`
|
||||||
LastModDate string `xml:"LastModDate,attr"`
|
LastModDate string `xml:"LastModDate,attr"`
|
||||||
Visible *bool `xml:"Visible,attr"`
|
Visible *bool `xml:"Visible,attr"`
|
||||||
|
NoZoom bool `xml:"NoZoom,attr"`
|
||||||
|
NoRotate bool `xml:"NoRotate,attr"`
|
||||||
Remark string `xml:"Remark"`
|
Remark string `xml:"Remark"`
|
||||||
Appearance Appearance
|
Appearance Appearance
|
||||||
}
|
}
|
||||||
@@ -57,6 +62,8 @@ type AnnotationInfo struct {
|
|||||||
LastModDate string `json:"lastModDate,omitempty"`
|
LastModDate string `json:"lastModDate,omitempty"`
|
||||||
Remark string `json:"remark,omitempty"`
|
Remark string `json:"remark,omitempty"`
|
||||||
Visible bool `json:"visible"`
|
Visible bool `json:"visible"`
|
||||||
|
NoZoom bool `json:"noZoom,omitempty"`
|
||||||
|
NoRotate bool `json:"noRotate,omitempty"`
|
||||||
X float64 `json:"x"`
|
X float64 `json:"x"`
|
||||||
Y float64 `json:"y"`
|
Y float64 `json:"y"`
|
||||||
Width float64 `json:"width"`
|
Width float64 `json:"width"`
|
||||||
@@ -82,28 +89,53 @@ func (r *Reader) AnnotationInfos() ([]AnnotationInfo, error) {
|
|||||||
}
|
}
|
||||||
var infos []AnnotationInfo
|
var infos []AnnotationInfo
|
||||||
for index, page := range doc.Pages.Page {
|
for index, page := range doc.Pages.Page {
|
||||||
for _, annotation := range r.Annots[page.ID] {
|
infos = append(infos, r.annotationInfos(index, page.ID)...)
|
||||||
box, _ := ParseBox(annotation.Appearance.Boundary)
|
|
||||||
infos = append(infos, AnnotationInfo{
|
|
||||||
ID: annotation.ID,
|
|
||||||
Page: index + 1,
|
|
||||||
PageID: page.ID,
|
|
||||||
Type: annotation.Type,
|
|
||||||
Subtype: annotation.Subtype,
|
|
||||||
Creator: annotation.Creator,
|
|
||||||
LastModDate: annotation.LastModDate,
|
|
||||||
Remark: annotation.Remark,
|
|
||||||
Visible: annotation.Visible == nil || *annotation.Visible,
|
|
||||||
X: box.X,
|
|
||||||
Y: box.Y,
|
|
||||||
Width: box.W,
|
|
||||||
Height: box.H,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return infos, nil
|
return infos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AnnotationInfosByIndex 获取指定页面的注释信息,包含不可见注释,不解析页面图元
|
||||||
|
// 入参: index 页面索引,从0开始
|
||||||
|
// 返回: []AnnotationInfo 注释信息, error 错误信息
|
||||||
|
func (r *Reader) AnnotationInfosByIndex(index int) ([]AnnotationInfo, error) {
|
||||||
|
doc, err := r.Doc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if index < 0 || index >= len(doc.Pages.Page) {
|
||||||
|
return nil, fmt.Errorf("page index out of range: %d", index)
|
||||||
|
}
|
||||||
|
return r.annotationInfos(index, doc.Pages.Page[index].ID), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// annotationInfos 提取指定页面的注释信息
|
||||||
|
// 入参: index 页面索引, pageID 页面标识
|
||||||
|
// 返回: []AnnotationInfo 注释信息
|
||||||
|
func (r *Reader) annotationInfos(index int, pageID string) []AnnotationInfo {
|
||||||
|
var infos []AnnotationInfo
|
||||||
|
for _, annotation := range r.Annots[pageID] {
|
||||||
|
box, _ := ParseBox(annotation.Appearance.Boundary)
|
||||||
|
infos = append(infos, AnnotationInfo{
|
||||||
|
ID: annotation.ID,
|
||||||
|
Page: index + 1,
|
||||||
|
PageID: pageID,
|
||||||
|
Type: annotation.Type,
|
||||||
|
Subtype: annotation.Subtype,
|
||||||
|
Creator: annotation.Creator,
|
||||||
|
LastModDate: annotation.LastModDate,
|
||||||
|
Remark: annotation.Remark,
|
||||||
|
Visible: annotation.Visible == nil || *annotation.Visible,
|
||||||
|
NoZoom: annotation.NoZoom,
|
||||||
|
NoRotate: annotation.NoRotate,
|
||||||
|
X: box.X,
|
||||||
|
Y: box.Y,
|
||||||
|
Width: box.W,
|
||||||
|
Height: box.H,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return infos
|
||||||
|
}
|
||||||
|
|
||||||
// parseAnnotations 解析注释文件
|
// parseAnnotations 解析注释文件
|
||||||
// 入参: doc 文档结构
|
// 入参: doc 文档结构
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
|
|||||||
+23
-10
@@ -180,7 +180,7 @@ func (r *Renderer) renderPageToContext(ctx *canvas.Context, page *PageContent, d
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageLinks 获取页面及可见注释的矩形链接,解析书签目标,不包含自定义Region
|
// PageLinks 获取页面、模板和可见注释的点击链接,包含复杂区域、组合图元和附件动作
|
||||||
// 入参: page 页面内容
|
// 入参: page 页面内容
|
||||||
// 返回: []PageLink 页面链接, error 错误信息
|
// 返回: []PageLink 页面链接, error 错误信息
|
||||||
func (r *Renderer) PageLinks(page *PageContent) ([]PageLink, error) {
|
func (r *Renderer) PageLinks(page *PageContent) ([]PageLink, error) {
|
||||||
@@ -188,20 +188,23 @@ func (r *Renderer) PageLinks(page *PageContent) ([]PageLink, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
sources := pageActionSources(page, box)
|
sources := r.pageActionSources(page, box)
|
||||||
if r.RenderAnnotations {
|
if r.RenderAnnotations {
|
||||||
sources = append(sources, annotationActionSources(r.Reader.Annots[page.ID])...)
|
sources = append(sources, r.annotationActionSources(r.Reader.Annots[page.ID])...)
|
||||||
}
|
}
|
||||||
var links []PageLink
|
var links []PageLink
|
||||||
var bookmarks map[string]Dest
|
var bookmarks map[string]Dest
|
||||||
for _, source := range sources {
|
for sourceIndex, source := range sources {
|
||||||
if source.Box.W <= 0 || source.Box.H <= 0 {
|
start := len(links)
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, action := range source.Actions {
|
for _, action := range source.Actions {
|
||||||
if action.Event != "CLICK" || action.Region != nil {
|
if action.Event != "CLICK" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
box, path := actionLinkRegion(source, action)
|
||||||
|
if box.W <= 0 || box.H <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
link := PageLink{Box: box, Path: path}
|
||||||
if action.Goto != nil {
|
if action.Goto != nil {
|
||||||
if bookmarks == nil {
|
if bookmarks == nil {
|
||||||
doc, err := r.Reader.Doc()
|
doc, err := r.Reader.Doc()
|
||||||
@@ -215,10 +218,20 @@ func (r *Renderer) PageLinks(page *PageContent) ([]PageLink, error) {
|
|||||||
}
|
}
|
||||||
if dest := gotoDest(action.Goto, bookmarks); dest != nil {
|
if dest := gotoDest(action.Goto, bookmarks); dest != nil {
|
||||||
value := *dest
|
value := *dest
|
||||||
links = append(links, PageLink{Dest: &value, Box: source.Box})
|
link.Dest = &value
|
||||||
|
links = append(links, link)
|
||||||
}
|
}
|
||||||
} else if action.URI != nil && action.URI.URI != "" {
|
} else if action.URI != nil && action.URI.URI != "" {
|
||||||
links = append(links, PageLink{URI: resolveActionURI(*action.URI), Box: source.Box})
|
link.URI = resolveActionURI(*action.URI)
|
||||||
|
links = append(links, link)
|
||||||
|
} else if action.GotoA != nil {
|
||||||
|
link.Attachment = action.GotoA.AttachID
|
||||||
|
links = append(links, link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(links)-start > 1 {
|
||||||
|
for i := start; i < len(links); i++ {
|
||||||
|
links[i].Group = sourceIndex + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,16 +95,20 @@ func newPDFNavigation(renderer *Renderer, doc *Document, pages []pdfPage) *pdfNa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, page := range pages {
|
for i, page := range pages {
|
||||||
sources := pageActionSources(page.Content, page.Box)
|
sources := renderer.pageActionSources(page.Content, page.Box)
|
||||||
if renderer.RenderAnnotations {
|
if renderer.RenderAnnotations {
|
||||||
sources = append(sources, annotationActionSources(renderer.Reader.Annots[page.Content.ID])...)
|
sources = append(sources, renderer.annotationActionSources(renderer.Reader.Annots[page.Content.ID])...)
|
||||||
}
|
}
|
||||||
for _, source := range sources {
|
for _, source := range sources {
|
||||||
rect := pdfSourceRect(source.Box, page.Box.H)
|
|
||||||
for _, action := range source.Actions {
|
for _, action := range source.Actions {
|
||||||
if action.Event != "CLICK" {
|
if action.Event != "CLICK" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
box, _ := actionLinkRegion(source, action)
|
||||||
|
if box.W <= 0 || box.H <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rect := pdfSourceRect(box, page.Box.H)
|
||||||
navigation.addAction(i, rect, action, bookmarks, pageIndex, pages)
|
navigation.addAction(i, rect, action, bookmarks, pageIndex, pages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user