mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
This commit is contained in:
1 parent
2ddde73b65
commit
fa8fec9830
9 files changed
+737
-71
No files matched your search
+67
-17
@@ -37,6 +37,7 @@ const state = {
|
|||||||
selectObjects: true,
|
selectObjects: true,
|
||||||
dirty: false,
|
dirty: false,
|
||||||
editorInfo: null,
|
editorInfo: null,
|
||||||
|
editorViews: new Map(),
|
||||||
savedRevision: null,
|
savedRevision: null,
|
||||||
insertObject: null,
|
insertObject: null,
|
||||||
importPageCount: 0,
|
importPageCount: 0,
|
||||||
@@ -1196,6 +1197,20 @@ function setEditorInfo(doc) {
|
|||||||
setDirty(doc.revision !== state.savedRevision);
|
setDirty(doc.revision !== state.savedRevision);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function editorLocation() {
|
||||||
|
const pending = canvasEditor.pendingSelection;
|
||||||
|
const index = pending?.index ?? canvasEditor.selected?.index ?? state.pageIndex;
|
||||||
|
return { page: state.doc.pages[index].id, scope: state.composite?.key || "", ids: pending?.ids || canvasEditor.items().map(item => item.id) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function rememberEditorView(before, revision, reindex = false) {
|
||||||
|
for (const key of state.editorViews.keys()) {
|
||||||
|
if (key > revision) state.editorViews.delete(key);
|
||||||
|
}
|
||||||
|
state.editorViews.set(state.editorInfo.revision, { before, after: editorLocation(), reindex });
|
||||||
|
if (state.editorViews.size > 100) state.editorViews.delete(state.editorViews.keys().next().value);
|
||||||
|
}
|
||||||
|
|
||||||
function pageCan(capability, index = state.pageIndex) {
|
function pageCan(capability, index = state.pageIndex) {
|
||||||
if (state.composite && capability === "insert") return false;
|
if (state.composite && capability === "insert") return false;
|
||||||
const bit = { insert: 1, copy: 2, delete: 4, move: 8, resize: 16 }[capability];
|
const bit = { insert: 1, copy: 2, delete: 4, move: 8, resize: 16 }[capability];
|
||||||
@@ -1228,6 +1243,7 @@ async function toggleEditor() {
|
|||||||
const doc = await callWASM("ofdgoEditDocument");
|
const doc = await callWASM("ofdgoEditDocument");
|
||||||
if (openSeq !== state.openSeq) return;
|
if (openSeq !== state.openSeq) return;
|
||||||
state.savedRevision = doc.revision;
|
state.savedRevision = doc.revision;
|
||||||
|
state.editorViews.clear();
|
||||||
setEditorInfo(doc);
|
setEditorInfo(doc);
|
||||||
state.editing = true;
|
state.editing = true;
|
||||||
state.composite = null;
|
state.composite = null;
|
||||||
@@ -1283,6 +1299,7 @@ async function createDocument(event) {
|
|||||||
state.editing = true;
|
state.editing = true;
|
||||||
state.composite = null;
|
state.composite = null;
|
||||||
state.objectClipboard = null;
|
state.objectClipboard = null;
|
||||||
|
state.editorViews.clear();
|
||||||
state.styleClipboard = null;
|
state.styleClipboard = null;
|
||||||
state.fontRenderPending = false;
|
state.fontRenderPending = false;
|
||||||
canvasEditor.clear();
|
canvasEditor.clear();
|
||||||
@@ -1387,6 +1404,10 @@ async function exitCompositeScope() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function startImageCrop(item) {
|
async function startImageCrop(item) {
|
||||||
|
if (item.scoped) {
|
||||||
|
canvasEditor.startCrop(item, { svg: item.surface.querySelector(".ofd-svg").cloneNode(true), urls: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
const urls = [];
|
const urls = [];
|
||||||
let mounted = false;
|
let mounted = false;
|
||||||
@@ -1603,6 +1624,7 @@ async function insertObject(event) {
|
|||||||
}
|
}
|
||||||
let openSeq = state.openSeq;
|
let openSeq = state.openSeq;
|
||||||
const item = state.insertObject;
|
const item = state.insertObject;
|
||||||
|
const before = editorLocation(), revision = state.editorInfo.revision;
|
||||||
const index = item?.index ?? state.pageIndex;
|
const index = item?.index ?? state.pageIndex;
|
||||||
const page = state.doc.pages[index];
|
const page = state.doc.pages[index];
|
||||||
const x = Math.min(20, page.width / 10);
|
const x = Math.min(20, page.width / 10);
|
||||||
@@ -1633,6 +1655,7 @@ async function insertObject(event) {
|
|||||||
openSeq = ++state.openSeq;
|
openSeq = ++state.openSeq;
|
||||||
await refreshEditorPage(doc, index, openSeq);
|
await refreshEditorPage(doc, index, openSeq);
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
|
rememberEditorView(before, revision);
|
||||||
el.viewerPanel.focus({ preventScroll: true });
|
el.viewerPanel.focus({ preventScroll: true });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1705,7 +1728,7 @@ async function changeTextStyle(color) {
|
|||||||
updateObjectControls(canvasEditor.selected, true);
|
updateObjectControls(canvasEditor.selected, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.composite) {
|
if (state.composite && color) {
|
||||||
if (color && fill !== null && canEditObject(item, "paint")) {
|
if (color && fill !== null && canEditObject(item, "paint")) {
|
||||||
await changeDocument("ofdgoCompositeTextColor", item, fill);
|
await changeDocument("ofdgoCompositeTextColor", item, fill);
|
||||||
}
|
}
|
||||||
@@ -1821,6 +1844,8 @@ async function changeDocument(name, item, ...args) {
|
|||||||
const active = document.activeElement;
|
const active = document.activeElement;
|
||||||
const toolbarFocus = el.editorTools.contains(active) ? active : null;
|
const toolbarFocus = el.editorTools.contains(active) ? active : null;
|
||||||
const previous = currentPageInfo();
|
const previous = currentPageInfo();
|
||||||
|
const before = editorLocation(), revision = state.editorInfo.revision;
|
||||||
|
const restoring = name === "ofdgoUndo" || name === "ofdgoRedo";
|
||||||
const { scrollLeft, scrollTop } = el.viewerPanel;
|
const { scrollLeft, scrollTop } = el.viewerPanel;
|
||||||
const importing = name === "ofdgoImportPages";
|
const importing = name === "ofdgoImportPages";
|
||||||
if (importing) state.importing = true;
|
if (importing) state.importing = true;
|
||||||
@@ -1833,10 +1858,10 @@ async function changeDocument(name, item, ...args) {
|
|||||||
const scope = state.composite;
|
const scope = state.composite;
|
||||||
const operation = { ofdgoTransformObject: "transform", ofdgoTransformObjects: "transform", ofdgoRotateObjects: "rotate", ofdgoFlipObjects: "flip", ofdgoResizeObjects: "resize",
|
const operation = { ofdgoTransformObject: "transform", ofdgoTransformObjects: "transform", ofdgoRotateObjects: "rotate", ofdgoFlipObjects: "flip", ofdgoResizeObjects: "resize",
|
||||||
ofdgoAlignObject: "align", ofdgoAlignObjects: "align", ofdgoDistributeObjects: "distribute", ofdgoStyleObjects: "style", ofdgoUpdatePathStyle: "paint", ofdgoCompositeTextColor: "textColor",
|
ofdgoAlignObject: "align", ofdgoAlignObjects: "align", ofdgoDistributeObjects: "distribute", ofdgoStyleObjects: "style", ofdgoUpdatePathStyle: "paint", ofdgoCompositeTextColor: "textColor",
|
||||||
|
ofdgoUpdateText: "text", ofdgoStyleText: "textStyle", ofdgoCropImage: "crop",
|
||||||
ofdgoDeleteObject: "delete", ofdgoDeleteObjects: "delete", ofdgoCopyObjects: "copy", ofdgoOrderObjects: "order" }[name];
|
ofdgoDeleteObject: "delete", ofdgoDeleteObjects: "delete", ofdgoCopyObjects: "copy", ofdgoOrderObjects: "order" }[name];
|
||||||
const members = item?.items || (item ? [item] : []);
|
const members = item?.items || (item ? [item] : []);
|
||||||
const scoped = scope && members.length && members.every(member => member.scoped);
|
const scoped = scope && members.length && members.every(member => member.scoped);
|
||||||
if (!item) resetCompositeScope();
|
|
||||||
if (scoped && !operation) throw new Error("内部对象暂不支持此操作");
|
if (scoped && !operation) throw new Error("内部对象暂不支持此操作");
|
||||||
const doc = scoped
|
const doc = scoped
|
||||||
? await callWASM("ofdgoChangeCompositeObjects", item.index, scope.key, Array.isArray(item.id) ? item.id : members.map(member => member.id), operation, ...args)
|
? await callWASM("ofdgoChangeCompositeObjects", item.index, scope.key, Array.isArray(item.id) ? item.id : members.map(member => member.id), operation, ...args)
|
||||||
@@ -1847,7 +1872,20 @@ async function changeDocument(name, item, ...args) {
|
|||||||
if (doc.revision === state.editorInfo?.revision) {
|
if (doc.revision === state.editorInfo?.revision) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if ((name === "ofdgoUndo" || name === "ofdgoRedo") && JSON.stringify(doc.outlines) !== JSON.stringify(state.doc.outlines)) {
|
const view = name === "ofdgoUndo" ? state.editorViews.get(revision) : name === "ofdgoRedo" ? state.editorViews.get(doc.revision) : null;
|
||||||
|
const reindex = !!scope && (scoped && ["ofdgoDeleteObject", "ofdgoDeleteObjects", "ofdgoCopyObjects", "ofdgoOrderObjects"].includes(name)
|
||||||
|
|| name === "ofdgoPasteObjects" && !!args[4]);
|
||||||
|
const reindexed = restoring ? view?.reindex && view.before : reindex && before;
|
||||||
|
const clipboard = state.objectClipboard;
|
||||||
|
if (reindexed && clipboard?.scope?.startsWith(reindexed.scope + "/") && clipboard.page === reindexed.page) state.objectClipboard = null;
|
||||||
|
const location = name === "ofdgoUndo" ? view?.before : view?.after;
|
||||||
|
const restoredIndex = location ? doc.pages.findIndex(page => page.id === location.page) : -1;
|
||||||
|
if (restoredIndex >= 0) {
|
||||||
|
state.composite = location.scope ? { index: restoredIndex, key: location.scope, objects: [] } : null;
|
||||||
|
} else if (!item && !(name === "ofdgoPasteObjects" && args[4])) {
|
||||||
|
resetCompositeScope();
|
||||||
|
}
|
||||||
|
if (restoring && JSON.stringify(doc.outlines) !== JSON.stringify(state.doc.outlines)) {
|
||||||
state.outlineSelection = null;
|
state.outlineSelection = null;
|
||||||
state.outlineExpanded.clear();
|
state.outlineExpanded.clear();
|
||||||
}
|
}
|
||||||
@@ -1856,6 +1894,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
Object.assign(state.doc, { title: doc.title, author: doc.author, subject: doc.subject });
|
Object.assign(state.doc, { title: doc.title, author: doc.author, subject: doc.subject });
|
||||||
renderMeta();
|
renderMeta();
|
||||||
updateControls();
|
updateControls();
|
||||||
|
rememberEditorView(before, revision);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (name === "ofdgoChangeOutline" || name === "ofdgoMoveOutline") {
|
if (name === "ofdgoChangeOutline" || name === "ofdgoMoveOutline") {
|
||||||
@@ -1865,6 +1904,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
renderOutlines(false);
|
renderOutlines(false);
|
||||||
showNavigation(el.outlinesTab);
|
showNavigation(el.outlinesTab);
|
||||||
updateControls();
|
updateControls();
|
||||||
|
rememberEditorView(before, revision);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const clearSelection = !item || ["ofdgoDeleteObject", "ofdgoDeleteObjects", "ofdgoEraseObjects", "ofdgoEraseObjectsPath"].includes(name);
|
const clearSelection = !item || ["ofdgoDeleteObject", "ofdgoDeleteObjects", "ofdgoEraseObjects", "ofdgoEraseObjectsPath"].includes(name);
|
||||||
@@ -1880,14 +1920,16 @@ async function changeDocument(name, item, ...args) {
|
|||||||
await refreshEditorPage(doc, item ? item.index : args[0], openSeq, clearSelection);
|
await refreshEditorPage(doc, item ? item.index : args[0], openSeq, clearSelection);
|
||||||
} else {
|
} else {
|
||||||
const samePage = doc.pages.findIndex((page) => page.id === previous.id);
|
const samePage = doc.pages.findIndex((page) => page.id === previous.id);
|
||||||
const pageIndex = doc.pageIndex ?? (samePage < 0 ? Math.min(state.pageIndex, doc.pageCount - 1) : samePage);
|
const pageIndex = restoredIndex >= 0 ? restoredIndex : doc.pageIndex ?? (samePage < 0 ? Math.min(state.pageIndex, doc.pageCount - 1) : samePage);
|
||||||
const page = doc.pages[pageIndex];
|
const page = doc.pages[pageIndex];
|
||||||
const keepScroll = doc.pageIndex === undefined && page.id === previous.id && pageIndex === state.pageIndex
|
const keepScroll = doc.pageIndex === undefined && page.id === previous.id && pageIndex === state.pageIndex
|
||||||
&& page.width === previous.width && page.height === previous.height;
|
&& page.width === previous.width && page.height === previous.height;
|
||||||
await openDocument({ doc, openSeq, skipAutoFonts: true, pageIndex, fitMode: state.fitMode, scale: state.scale,
|
await openDocument({ doc, openSeq, skipAutoFonts: true, pageIndex, fitMode: state.fitMode, scale: state.scale,
|
||||||
keepPreview: true, clearSelection, previewScroll: keepScroll ? { scrollLeft, scrollTop } : null });
|
keepPreview: true, clearSelection, selection: restoredIndex >= 0 ? { index: restoredIndex, ids: location.ids } : null,
|
||||||
|
previewScroll: keepScroll ? { scrollLeft, scrollTop } : null });
|
||||||
}
|
}
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
|
if (!restoring) rememberEditorView(before, revision, reindex);
|
||||||
if (!toolbarFocus) el.viewerPanel.focus({ preventScroll: true });
|
if (!toolbarFocus) el.viewerPanel.focus({ preventScroll: true });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2439,6 +2481,7 @@ async function openOFD(file) {
|
|||||||
state.objectClipboard = null;
|
state.objectClipboard = null;
|
||||||
state.styleClipboard = null;
|
state.styleClipboard = null;
|
||||||
state.editorInfo = null;
|
state.editorInfo = null;
|
||||||
|
state.editorViews.clear();
|
||||||
state.fontRenderPending = false;
|
state.fontRenderPending = false;
|
||||||
state.savedRevision = null;
|
state.savedRevision = null;
|
||||||
canvasEditor.clear();
|
canvasEditor.clear();
|
||||||
@@ -2910,6 +2953,7 @@ async function openDocument(options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (options.clearSelection) canvasEditor.clear();
|
if (options.clearSelection) canvasEditor.clear();
|
||||||
|
if (options.selection) canvasEditor.pendingSelection = options.selection;
|
||||||
resetPageFlow(options.keepPreview);
|
resetPageFlow(options.keepPreview);
|
||||||
renderPageList();
|
renderPageList();
|
||||||
if (options.keepPreview || options.resetScroll || el.outlineList.childElementCount === 0) {
|
if (options.keepPreview || options.resetScroll || el.outlineList.childElementCount === 0) {
|
||||||
@@ -3697,9 +3741,9 @@ function copyEditorSelection(event) {
|
|||||||
|| canvasEditor.input || canvasEditor.crop || canvasEditor.drag || document.body.hasAttribute("aria-busy")) return false;
|
|| canvasEditor.input || canvasEditor.crop || canvasEditor.drag || document.body.hasAttribute("aria-busy")) return false;
|
||||||
const items = canvasEditor.items().slice().sort((a, b) => a.order - b.order);
|
const items = canvasEditor.items().slice().sort((a, b) => a.order - b.order);
|
||||||
const cut = event.type === "cut";
|
const cut = event.type === "cut";
|
||||||
if (state.composite) {
|
if (state.composite && items.some(item => item.container !== items[0].container)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setStatus("内部对象请用复制按钮");
|
setStatus("请选择同一组内的对象");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!canEditObject(canvasEditor.selected, "copy") || cut && !canEditObject(canvasEditor.selected, "delete")) {
|
if (!canEditObject(canvasEditor.selected, "copy") || cut && !canEditObject(canvasEditor.selected, "delete")) {
|
||||||
@@ -3708,7 +3752,7 @@ function copyEditorSelection(event) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const index = items[0].index, ids = items.map(item => item.id);
|
const index = items[0].index, ids = items.map(item => item.id);
|
||||||
const clipboard = { token: crypto.randomUUID(), page: state.doc.pages[index].id, bounds: selectionBounds(items), x: 0, y: 0, cut };
|
const clipboard = { token: crypto.randomUUID(), page: state.doc.pages[index].id, scope: state.composite?.key || "", bounds: selectionBounds(items), x: 0, y: 0, cut };
|
||||||
if (canvasEditor.nudge) {
|
if (canvasEditor.nudge) {
|
||||||
clipboard.bounds.x += canvasEditor.nudge.x;
|
clipboard.bounds.x += canvasEditor.nudge.x;
|
||||||
clipboard.bounds.y += canvasEditor.nudge.y;
|
clipboard.bounds.y += canvasEditor.nudge.y;
|
||||||
@@ -3719,10 +3763,10 @@ function copyEditorSelection(event) {
|
|||||||
state.objectClipboard = clipboard;
|
state.objectClipboard = clipboard;
|
||||||
const capture = () => {
|
const capture = () => {
|
||||||
const openSeq = state.openSeq, revision = state.editorInfo.revision;
|
const openSeq = state.openSeq, revision = state.editorInfo.revision;
|
||||||
return callWASM("ofdgoCaptureObjects", index, ids, clipboard.token).then(async () => {
|
return callWASM("ofdgoCaptureObjects", index, ids, clipboard.token, clipboard.scope).then(async () => {
|
||||||
clipboard.cut = false;
|
clipboard.cut = false;
|
||||||
if (cut && openSeq === state.openSeq && revision === state.editorInfo?.revision && index === state.pageIndex && state.objectClipboard === clipboard) {
|
if (cut && openSeq === state.openSeq && revision === state.editorInfo?.revision && index === state.pageIndex && state.objectClipboard === clipboard && clipboard.scope === (state.composite?.key || "")) {
|
||||||
clipboard.cut = Boolean(await changeDocument("ofdgoDeleteObjects", { index, id: ids }));
|
clipboard.cut = Boolean(await changeDocument("ofdgoDeleteObjects", { index, id: ids, items }));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}, err => {
|
}, err => {
|
||||||
@@ -3753,7 +3797,7 @@ async function pasteEditorContent(event) {
|
|||||||
const files = Array.from(event.clipboardData.files);
|
const files = Array.from(event.clipboardData.files);
|
||||||
const value = event.clipboardData.getData("text/plain").replace(/\t/g, " ");
|
const value = event.clipboardData.getData("text/plain").replace(/\t/g, " ");
|
||||||
if ((canvasEditor.nudge || canvasEditor.nudgeCommit) && !await canvasEditor.commitNudge()) return;
|
if ((canvasEditor.nudge || canvasEditor.nudgeCommit) && !await canvasEditor.commitNudge()) return;
|
||||||
if (document.body.hasAttribute("aria-busy") || canvasEditor.input || canvasEditor.crop || canvasEditor.drag || !pageCan("insert")) return;
|
if (document.body.hasAttribute("aria-busy") || canvasEditor.input || canvasEditor.crop || canvasEditor.drag) return;
|
||||||
const index = state.pageIndex, page = state.doc.pages[index];
|
const index = state.pageIndex, page = state.doc.pages[index];
|
||||||
if (token) {
|
if (token) {
|
||||||
const clipboard = state.objectClipboard;
|
const clipboard = state.objectClipboard;
|
||||||
@@ -3763,6 +3807,11 @@ async function pasteEditorContent(event) {
|
|||||||
}
|
}
|
||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
if (!await clipboard.ready || openSeq !== state.openSeq || index !== state.pageIndex || state.objectClipboard !== clipboard) return;
|
if (!await clipboard.ready || openSeq !== state.openSeq || index !== state.pageIndex || state.objectClipboard !== clipboard) return;
|
||||||
|
if ((clipboard.scope || "") !== (state.composite?.key || "") || clipboard.scope && clipboard.page !== page.id) {
|
||||||
|
setStatus("请回到原编辑范围粘贴");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!clipboard.scope && !pageCan("insert")) return;
|
||||||
const visible = visiblePageBounds(index), bounds = clipboard.bounds;
|
const visible = visiblePageBounds(index), bounds = clipboard.bounds;
|
||||||
const offset = clipboard.page === page.id ? { x: clipboard.x + (clipboard.cut ? 0 : 3), y: clipboard.y + (clipboard.cut ? 0 : 3) } : { x: 0, y: 0 };
|
const offset = clipboard.page === page.id ? { x: clipboard.x + (clipboard.cut ? 0 : 3), y: clipboard.y + (clipboard.cut ? 0 : 3) } : { x: 0, y: 0 };
|
||||||
for (const [axis, size] of [["x", "width"], ["y", "height"]]) {
|
for (const [axis, size] of [["x", "width"], ["y", "height"]]) {
|
||||||
@@ -3772,7 +3821,7 @@ async function pasteEditorContent(event) {
|
|||||||
offset[axis] = Math.max(visible[axis] + margin, Math.min(position, visible[axis] + visible[size] - margin - bounds[size])) - bounds[axis];
|
offset[axis] = Math.max(visible[axis] + margin, Math.min(position, visible[axis] + visible[size] - margin - bounds[size])) - bounds[axis];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (await changeDocument("ofdgoPasteObjects", null, index, token, offset.x, offset.y)) {
|
if (await changeDocument("ofdgoPasteObjects", null, index, token, offset.x, offset.y, clipboard.scope || "")) {
|
||||||
clipboard.page = page.id;
|
clipboard.page = page.id;
|
||||||
clipboard.x = offset.x;
|
clipboard.x = offset.x;
|
||||||
clipboard.y = offset.y;
|
clipboard.y = offset.y;
|
||||||
@@ -3780,6 +3829,7 @@ async function pasteEditorContent(event) {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!pageCan("insert")) return;
|
||||||
const visible = visiblePageBounds(index), margin = Math.min(20, visible.width / 10);
|
const visible = visiblePageBounds(index), margin = Math.min(20, visible.width / 10);
|
||||||
const x = visible.x + margin, y = visible.y + Math.min(20, visible.height / 10), width = visible.width - margin * 2;
|
const x = visible.x + margin, y = visible.y + Math.min(20, visible.height / 10), width = visible.width - margin * 2;
|
||||||
if (files.length) {
|
if (files.length) {
|
||||||
@@ -5554,14 +5604,14 @@ function updateObjectControls(item, reset = false) {
|
|||||||
el.objectBoundsButton.disabled = disabled || cropping || !canEditObject(item, "transform");
|
el.objectBoundsButton.disabled = disabled || cropping || !canEditObject(item, "transform");
|
||||||
el.objectAlign.disabled = disabled || cropping || !canEditObject(item, "arrange");
|
el.objectAlign.disabled = disabled || cropping || !canEditObject(item, "arrange");
|
||||||
el.objectRotate.disabled = el.objectFlip.disabled = disabled || cropping || !canEditObject(item, state.composite ? "transform" : "arrange");
|
el.objectRotate.disabled = el.objectFlip.disabled = disabled || cropping || !canEditObject(item, state.composite ? "transform" : "arrange");
|
||||||
el.cropImageButton.disabled = disabled || item.type !== "ImageObject" || !canEditObject(item, "update");
|
el.cropImageButton.disabled = disabled || item.type !== "ImageObject" || !canEditObject(item, "cropImage");
|
||||||
el.imageFit.disabled = el.cropImageButton.disabled || cropping;
|
el.imageFit.disabled = el.cropImageButton.disabled || cropping || Boolean(item.scoped);
|
||||||
el.cropImageButton.textContent = cropping ? "完成" : "裁剪";
|
el.cropImageButton.textContent = cropping ? "完成" : "裁剪";
|
||||||
el.cropImageButton.setAttribute("aria-label", cropping ? "完成裁剪" : "裁剪图片");
|
el.cropImageButton.setAttribute("aria-label", cropping ? "完成裁剪" : "裁剪图片");
|
||||||
el.cropImageButton.setAttribute("aria-pressed", String(cropping));
|
el.cropImageButton.setAttribute("aria-pressed", String(cropping));
|
||||||
el.resetCropButton.textContent = cropping ? "取消" : "还原";
|
el.resetCropButton.textContent = cropping ? "取消" : "还原";
|
||||||
el.resetCropButton.setAttribute("aria-label", cropping ? "取消裁剪" : "还原图片");
|
el.resetCropButton.setAttribute("aria-label", cropping ? "取消裁剪" : "还原图片");
|
||||||
el.resetCropButton.disabled = el.cropImageButton.disabled || !cropping && (!item.imageBounds || ["x", "y", "width", "height"].every(key => Math.abs(item[key] - item.imageBounds[key]) < 1e-9));
|
el.resetCropButton.disabled = el.cropImageButton.disabled || !cropping && (item.scoped || !item.imageBounds || ["x", "y", "width", "height"].every(key => Math.abs(item[key] - item.imageBounds[key]) < 1e-9));
|
||||||
el.objectDistribute.disabled = el.objectAlign.disabled || !item.items || item.items.length < 3;
|
el.objectDistribute.disabled = el.objectAlign.disabled || !item.items || item.items.length < 3;
|
||||||
el.editObjectButton.disabled = disabled || Boolean(item.items) || !canEditObject(item, "enter") && (item.type === "PathObject"
|
el.editObjectButton.disabled = disabled || Boolean(item.items) || !canEditObject(item, "enter") && (item.type === "PathObject"
|
||||||
|| !canEditObject(item, item.type === "TextObject" ? "reflow" : item.type === "ImageObject" ? "replaceImage" : "update"));
|
|| !canEditObject(item, item.type === "TextObject" ? "reflow" : item.type === "ImageObject" ? "replaceImage" : "update"));
|
||||||
@@ -5573,7 +5623,7 @@ function updateObjectControls(item, reset = false) {
|
|||||||
el.textSize.disabled = textDisabled || text !== state.textDefaults && !item.draft && (!canEditObject(item, "reflow") || Boolean(item?.items) && !canEditObject(item, "layoutKnown"));
|
el.textSize.disabled = textDisabled || text !== state.textDefaults && !item.draft && (!canEditObject(item, "reflow") || Boolean(item?.items) && !canEditObject(item, "layoutKnown"));
|
||||||
el.textColor.disabled = textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "paint");
|
el.textColor.disabled = textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "paint");
|
||||||
el.textFontAdd.disabled = !state.editing || !state.ready || state.exporting;
|
el.textFontAdd.disabled = !state.editing || !state.ready || state.exporting;
|
||||||
el.textAlign.disabled = el.textWrap.disabled = el.textLineHeight.disabled = el.textSpacing.disabled = el.textSize.disabled || Boolean(item?.items || item?.draft) || Boolean(canvasEditor.input);
|
el.textAlign.disabled = el.textWrap.disabled = el.textLineHeight.disabled = el.textSpacing.disabled = el.textSize.disabled || Boolean(item?.items || item?.draft) || Boolean(canvasEditor.input || state.composite);
|
||||||
el.paragraphButton.disabled = el.textAlign.disabled;
|
el.paragraphButton.disabled = el.textAlign.disabled;
|
||||||
const layoutKnown = text === state.textDefaults || Boolean(item?.draft) || canEditObject(item, "layoutKnown");
|
const layoutKnown = text === state.textDefaults || Boolean(item?.draft) || canEditObject(item, "layoutKnown");
|
||||||
el.textAlign.value = layoutKnown ? text.align || "left" : "";
|
el.textAlign.value = layoutKnown ? text.align || "left" : "";
|
||||||
|
|||||||
+141
-18
@@ -613,8 +613,9 @@ var currentEditor *ofdgo.Editor
|
|||||||
|
|
||||||
// editorClipboard 当前编辑文档中的对象快照与剪贴板标识
|
// editorClipboard 当前编辑文档中的对象快照与剪贴板标识
|
||||||
type editorClipboard struct {
|
type editorClipboard struct {
|
||||||
token string
|
token string
|
||||||
objects []ofdgo.GraphicObject
|
objects []ofdgo.GraphicObject
|
||||||
|
composite *ofdgo.CompositeSelection
|
||||||
}
|
}
|
||||||
|
|
||||||
// copiedObjects 当前对象剪贴板,不保存字体或图片的重复数据
|
// copiedObjects 当前对象剪贴板,不保存字体或图片的重复数据
|
||||||
@@ -832,7 +833,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
|||||||
// 返回: map[string]any 前端能力
|
// 返回: map[string]any 前端能力
|
||||||
func editorCapabilities(capability ofdgo.ObjectCapabilities, kind string) map[string]any {
|
func editorCapabilities(capability ofdgo.ObjectCapabilities, kind string) map[string]any {
|
||||||
result := map[string]any{"update": capability.Update, "paint": capability.Paint, "replaceFont": capability.ReplaceFont, "reflow": capability.Reflow, "layoutKnown": capability.LayoutKnown, "transform": capability.Transform, "arrange": capability.Arrange, "copy": capability.Copy, "delete": capability.Delete, "order": capability.Order, "reason": capability.Reason, "reasonCode": string(capability.ReasonCode),
|
result := map[string]any{"update": capability.Update, "paint": capability.Paint, "replaceFont": capability.ReplaceFont, "reflow": capability.Reflow, "layoutKnown": capability.LayoutKnown, "transform": capability.Transform, "arrange": capability.Arrange, "copy": capability.Copy, "delete": capability.Delete, "order": capability.Order, "reason": capability.Reason, "reasonCode": string(capability.ReasonCode),
|
||||||
"replaceImage": capability.ReplaceImage, "enter": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
|
"replaceImage": capability.ReplaceImage, "cropImage": capability.CropImage, "enter": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
|
||||||
if missing := capability.MissingGlyphs; missing != nil {
|
if missing := capability.MissingGlyphs; missing != nil {
|
||||||
result["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters}
|
result["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters}
|
||||||
}
|
}
|
||||||
@@ -926,6 +927,15 @@ func compositeObjects(args []js.Value) (any, error) {
|
|||||||
object := member.Object.TextObject
|
object := member.Object.TextObject
|
||||||
item["text"], _ = object.TextLayout()
|
item["text"], _ = object.TextLayout()
|
||||||
item["font"], item["fontName"], item["size"] = object.Font, fontNames[object.Font], object.Size
|
item["font"], item["fontName"], item["size"] = object.Font, fontNames[object.Font], object.Size
|
||||||
|
item["bounds"] = editorBox(box)
|
||||||
|
if member.Capabilities.Reflow {
|
||||||
|
inverse, _ := member.Matrix.Invert()
|
||||||
|
local := inverse.TransformBox(box)
|
||||||
|
item["textFrame"] = map[string]any{"width": math.Max(object.Size, local.X+local.W), "height": math.Max(object.Size, local.Y+local.H), "matrix": editorMatrix(member.Matrix)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if member.Capabilities.CropImage {
|
||||||
|
item["imageBounds"] = editorBox(box)
|
||||||
}
|
}
|
||||||
objects = append(objects, item)
|
objects = append(objects, item)
|
||||||
}
|
}
|
||||||
@@ -943,17 +953,9 @@ func changeCompositeObjects(args []js.Value) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var indexes []int
|
indexes, err := compositeIndexes(key, stringsFromJS(args[2]))
|
||||||
for _, id := range stringsFromJS(args[2]) {
|
if err != nil {
|
||||||
part, found := strings.CutPrefix(id, key+"/")
|
return err
|
||||||
if !found {
|
|
||||||
return fmt.Errorf("selection is outside composite")
|
|
||||||
}
|
|
||||||
i, err := strconv.Atoi(part)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
indexes = append(indexes, i)
|
|
||||||
}
|
}
|
||||||
page := args[0].Int()
|
page := args[0].Int()
|
||||||
operation := args[3].String()
|
operation := args[3].String()
|
||||||
@@ -966,6 +968,13 @@ func changeCompositeObjects(args []js.Value) (any, error) {
|
|||||||
case "order":
|
case "order":
|
||||||
selected, err = currentEditor.OrderCompositeObjects(page, path, indexes, args[4].String())
|
selected, err = currentEditor.OrderCompositeObjects(page, path, indexes, args[4].String())
|
||||||
return err
|
return err
|
||||||
|
case "text", "textStyle":
|
||||||
|
return changeCompositeText(page, path, indexes, operation, args[4:])
|
||||||
|
case "crop":
|
||||||
|
if len(indexes) != 1 {
|
||||||
|
return fmt.Errorf("image crop requires one member")
|
||||||
|
}
|
||||||
|
return currentEditor.CropCompositeImage(page, path, indexes[0], ofdgo.Box{X: args[4].Float(), Y: args[5].Float(), W: args[6].Float(), H: args[7].Float()})
|
||||||
case "image":
|
case "image":
|
||||||
if len(indexes) != 1 {
|
if len(indexes) != 1 {
|
||||||
return fmt.Errorf("image replacement requires one member")
|
return fmt.Errorf("image replacement requires one member")
|
||||||
@@ -1038,6 +1047,82 @@ func changeCompositeObjects(args []js.Value) (any, error) {
|
|||||||
return doc, nil
|
return doc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compositeIndexes 将画布成员路径转换为同一范围的序号
|
||||||
|
// 入参: key 父路径, ids 成员路径
|
||||||
|
// 返回: []int 成员序号, error 错误信息
|
||||||
|
func compositeIndexes(key string, ids []string) ([]int, error) {
|
||||||
|
indexes := make([]int, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
part, found := strings.CutPrefix(id, key+"/")
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("selection is outside composite")
|
||||||
|
}
|
||||||
|
i, err := strconv.Atoi(part)
|
||||||
|
if err != nil || i < 0 {
|
||||||
|
return nil, fmt.Errorf("invalid composite member")
|
||||||
|
}
|
||||||
|
indexes = append(indexes, i)
|
||||||
|
}
|
||||||
|
return indexes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// changeCompositeText 修改内部文字,嵌入字体前校验选区用字
|
||||||
|
// 入参: page 页面索引, path 父路径, indexes 成员序号, operation 操作, args 内容及样式
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func changeCompositeText(page int, path ofdgo.ObjectPath, indexes []int, operation string, args []js.Value) error {
|
||||||
|
var value *string
|
||||||
|
if operation == "text" {
|
||||||
|
if len(indexes) != 1 {
|
||||||
|
return fmt.Errorf("text editing requires one member")
|
||||||
|
}
|
||||||
|
if !args[0].IsNull() {
|
||||||
|
text := args[0].String()
|
||||||
|
value = &text
|
||||||
|
}
|
||||||
|
args = args[1:]
|
||||||
|
}
|
||||||
|
style := ofdgo.TextStyle{Size: args[1].Float()}
|
||||||
|
if !args[0].IsNull() {
|
||||||
|
data, err := bytesFromJS(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
members, err := currentEditor.CompositeObjects(page, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var text strings.Builder
|
||||||
|
for _, index := range indexes {
|
||||||
|
if index >= len(members) || !members[index].Capabilities.ReplaceFont {
|
||||||
|
return fmt.Errorf("composite font replacement is not supported")
|
||||||
|
}
|
||||||
|
if value != nil {
|
||||||
|
text.WriteString(*value)
|
||||||
|
} else {
|
||||||
|
text.WriteString(members[index].Object.TextObject.Text())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := checkFontGlyphs(data, text.String()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
style.Font, err = currentEditor.AddFont(FontFile{Data: data}, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !args[2].IsNull() {
|
||||||
|
var fill ofdgo.FillColor
|
||||||
|
if err := setEditorColor(&fill, args[2].String()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
style.Color = fill.Value
|
||||||
|
}
|
||||||
|
if value != nil {
|
||||||
|
return currentEditor.UpdateCompositeText(page, path, indexes[0], *value, style)
|
||||||
|
}
|
||||||
|
return currentEditor.StyleCompositeText(page, path, indexes, style)
|
||||||
|
}
|
||||||
|
|
||||||
// editorFont 读取对象实际使用的内嵌字体,供画布输入使用
|
// editorFont 读取对象实际使用的内嵌字体,供画布输入使用
|
||||||
// 入参: args 字体资源标识
|
// 入参: args 字体资源标识
|
||||||
// 返回: any 字体数据, error 错误信息
|
// 返回: any 字体数据, error 错误信息
|
||||||
@@ -1152,27 +1237,65 @@ func copyObjects(args []js.Value) (any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// captureObjects 保存独立选区快照,不修改文档或历史
|
// captureObjects 保存独立选区快照,不修改文档或历史
|
||||||
// 入参: args 页面索引、对象标识数组和剪贴板标识
|
// 入参: args 页面索引、对象标识数组、剪贴板标识和可选父路径
|
||||||
// 返回: any 空结果, error 错误信息
|
// 返回: any 空结果, error 错误信息
|
||||||
func captureObjects(args []js.Value) (any, error) {
|
func captureObjects(args []js.Value) (any, error) {
|
||||||
if currentEditor == nil {
|
if currentEditor == nil {
|
||||||
return nil, fmt.Errorf("no document is being edited")
|
return nil, fmt.Errorf("no document is being edited")
|
||||||
}
|
}
|
||||||
objects, err := currentEditor.Objects(args[0].Int(), stringsFromJS(args[1]))
|
clipboard := &editorClipboard{token: args[2].String()}
|
||||||
|
var err error
|
||||||
|
if len(args) > 3 && args[3].String() != "" {
|
||||||
|
key := args[3].String()
|
||||||
|
path, pathErr := compositePath(key)
|
||||||
|
if pathErr != nil {
|
||||||
|
return nil, pathErr
|
||||||
|
}
|
||||||
|
indexes, indexErr := compositeIndexes(key, stringsFromJS(args[1]))
|
||||||
|
if indexErr != nil {
|
||||||
|
return nil, indexErr
|
||||||
|
}
|
||||||
|
clipboard.composite, err = currentEditor.CaptureCompositeObjects(args[0].Int(), path, indexes)
|
||||||
|
} else {
|
||||||
|
clipboard.objects, err = currentEditor.Objects(args[0].Int(), stringsFromJS(args[1]))
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
copiedObjects = &editorClipboard{args[2].String(), objects}
|
copiedObjects = clipboard
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pasteObjects 将当前文档的对象快照粘贴到目标页
|
// pasteObjects 将当前文档的对象快照粘贴到目标页
|
||||||
// 入参: args 目标页、剪贴板标识和横纵位移
|
// 入参: args 目标页、剪贴板标识、横纵位移和可选父路径
|
||||||
// 返回: any 文档信息及新选区, error 错误信息
|
// 返回: any 文档信息及新选区, error 错误信息
|
||||||
func pasteObjects(args []js.Value) (any, error) {
|
func pasteObjects(args []js.Value) (any, error) {
|
||||||
if currentEditor == nil || copiedObjects == nil || copiedObjects.token != args[1].String() {
|
if currentEditor == nil || copiedObjects == nil || copiedObjects.token != args[1].String() {
|
||||||
return nil, fmt.Errorf("object clipboard is no longer available")
|
return nil, fmt.Errorf("object clipboard is no longer available")
|
||||||
}
|
}
|
||||||
|
if copiedObjects.composite != nil {
|
||||||
|
if len(args) < 5 || args[4].String() == "" {
|
||||||
|
return nil, fmt.Errorf("paste requires the original composite scope")
|
||||||
|
}
|
||||||
|
key := args[4].String()
|
||||||
|
path, err := compositePath(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
indexes, err := currentEditor.PasteCompositeObjects(args[0].Int(), path, copiedObjects.composite, args[2].Float(), args[3].Float())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
info, err := previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
||||||
|
result := editorSelectionInfo{editorInfo: info}
|
||||||
|
for _, index := range indexes {
|
||||||
|
result.SelectedIDs = append(result.SelectedIDs, fmt.Sprintf("%s/%d", key, index))
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
if len(args) > 4 && args[4].String() != "" {
|
||||||
|
return nil, fmt.Errorf("top-level objects cannot be pasted into a composite")
|
||||||
|
}
|
||||||
return copyEditorObjects(args[0].Int(), copiedObjects.objects, args[2].Float(), args[3].Float())
|
return copyEditorObjects(args[0].Int(), copiedObjects.objects, args[2].Float(), args[3].Float())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+43
-18
@@ -17,6 +17,7 @@ package ofdgo
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"math"
|
"math"
|
||||||
@@ -33,13 +34,14 @@ type ObjectPath struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CompositeMember 复合对象的直接成员快照,Object包含有效样式,不用于整体替换原对象
|
// CompositeMember 复合对象的直接成员快照,Object包含有效样式,不用于整体替换原对象
|
||||||
// Bounds与Contours使用页面毫米坐标,StrokeScale为对象描边到页面的倍率
|
// Bounds与Contours使用页面毫米坐标,Matrix将对象局部坐标映射至页面,StrokeScale为描边倍率
|
||||||
// Position表示当前范围内的直接容器位置,结构操作后需重新枚举
|
// Position表示当前范围内的直接容器位置,结构操作后需重新枚举
|
||||||
type CompositeMember struct {
|
type CompositeMember struct {
|
||||||
Object GraphicObject
|
Object GraphicObject
|
||||||
Bounds Box
|
Bounds Box
|
||||||
Contours []ObjectContour
|
Contours []ObjectContour
|
||||||
StrokeScale float64
|
StrokeScale float64
|
||||||
|
Matrix Matrix
|
||||||
Capabilities ObjectCapabilities
|
Capabilities ObjectCapabilities
|
||||||
Position ObjectPosition
|
Position ObjectPosition
|
||||||
Transform bool
|
Transform bool
|
||||||
@@ -380,10 +382,24 @@ func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorComp
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
style = cloneEditorData(node.object)
|
style = cloneEditorData(node.object)
|
||||||
}
|
}
|
||||||
|
if transform && err == nil && node.object.Type == "TextObject" && editorXMLSupported(node.node) {
|
||||||
|
capability.ReplaceFont = true
|
||||||
|
text := cloneEditorData(style.TextObject)
|
||||||
|
if err := e.prepareText(&text); err != nil {
|
||||||
|
capability.ReasonCode, capability.Reason = editReason(err), err.Error()
|
||||||
|
errors.As(err, &capability.MissingGlyphs)
|
||||||
|
} else if _, err := text.TextFrame(); err == nil {
|
||||||
|
capability.Reflow = text.ReadDirection == 0 && text.CharDirection == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if transform && node.object.Type == "ImageObject" && bounds.box.W > 0 && bounds.box.H > 0 {
|
||||||
|
clips := node.object.ImageObject.Clips
|
||||||
|
_, capability.CropImage = node.matrix(clips == nil || clips.TransFlag == nil || *clips.TransFlag).Invert()
|
||||||
|
}
|
||||||
_, ctm := editorGeometry(node.object)
|
_, ctm := editorGeometry(node.object)
|
||||||
matrix := node.parent.Multiply(NewMatrix(ctm))
|
matrix := node.parent.Multiply(NewMatrix(ctm))
|
||||||
position := positions[node.span.parent]
|
position := positions[node.span.parent]
|
||||||
result[i] = CompositeMember{Object: style, Bounds: bounds.box, Contours: bounds.contours, StrokeScale: math.Sqrt(math.Abs(matrix.a*matrix.d - matrix.b*matrix.c)), Capabilities: capability, Position: position, Transform: transform}
|
result[i] = CompositeMember{Object: style, Bounds: bounds.box, Contours: bounds.contours, Matrix: node.matrix(true), StrokeScale: math.Sqrt(math.Abs(matrix.a*matrix.d - matrix.b*matrix.c)), Capabilities: capability, Position: position, Transform: transform}
|
||||||
position.Index++
|
position.Index++
|
||||||
positions[node.span.parent] = position
|
positions[node.span.parent] = position
|
||||||
}
|
}
|
||||||
@@ -537,26 +553,35 @@ func (e *Editor) changeCompositeObjects(page int, path ObjectPath, indexes []int
|
|||||||
// 入参: page 页面索引, path 父路径, indexes 成员序号, edit 修改回调
|
// 入参: page 页面索引, path 父路径, indexes 成员序号, edit 修改回调
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) editCompositeObjects(page int, path ObjectPath, indexes []int, edit func(*Renderer, []*editorCompositeNode, []CompositeMember) error) (err error) {
|
func (e *Editor) editCompositeObjects(page int, path ObjectPath, indexes []int, edit func(*Renderer, []*editorCompositeNode, []CompositeMember) error) (err error) {
|
||||||
|
return e.editCompositeScope(page, path, func(renderer *Renderer, _ *editorCompositeNode, nodes []*editorCompositeNode) error {
|
||||||
|
members := e.measureCompositeMembers(renderer, nodes)
|
||||||
|
selected := make(map[int]bool)
|
||||||
|
var selectedNodes []*editorCompositeNode
|
||||||
|
var selectedMembers []CompositeMember
|
||||||
|
for _, i := range indexes {
|
||||||
|
if i < 0 || i >= len(nodes) || selected[i] {
|
||||||
|
return fmt.Errorf("invalid composite selection")
|
||||||
|
}
|
||||||
|
selected[i] = true
|
||||||
|
selectedNodes = append(selectedNodes, nodes[i])
|
||||||
|
selectedMembers = append(selectedMembers, members[i])
|
||||||
|
}
|
||||||
|
if len(selected) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return edit(renderer, selectedNodes, selectedMembers)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// editCompositeScope 原子提交内部范围修改,支持向空容器粘贴并在失败时回收新增资源
|
||||||
|
// 入参: page 页面索引, path 父路径, edit 修改回调
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) editCompositeScope(page int, path ObjectPath, edit func(*Renderer, *editorCompositeNode, []*editorCompositeNode) error) (err error) {
|
||||||
reader, renderer, root, nodes, err := e.compositeScope(page, path)
|
reader, renderer, root, nodes, err := e.compositeScope(page, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
members := e.measureCompositeMembers(renderer, nodes)
|
|
||||||
selected := make(map[int]bool)
|
|
||||||
var selectedNodes []*editorCompositeNode
|
|
||||||
var selectedMembers []CompositeMember
|
|
||||||
for _, i := range indexes {
|
|
||||||
if i < 0 || i >= len(nodes) || selected[i] {
|
|
||||||
return fmt.Errorf("invalid composite selection")
|
|
||||||
}
|
|
||||||
selected[i] = true
|
|
||||||
selectedNodes = append(selectedNodes, nodes[i])
|
|
||||||
selectedMembers = append(selectedMembers, members[i])
|
|
||||||
}
|
|
||||||
if len(selected) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
count, maximum := len(e.resources), e.maxID
|
count, maximum := len(e.resources), e.maxID
|
||||||
ready := e.source.idsReady
|
ready := e.source.idsReady
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -577,7 +602,7 @@ func (e *Editor) editCompositeObjects(page int, path ObjectPath, indexes []int,
|
|||||||
if err = e.prepareSourceIDs(); err != nil {
|
if err = e.prepareSourceIDs(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = edit(renderer, selectedNodes, selectedMembers); err != nil {
|
if err = edit(renderer, root, nodes); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
data, changed, err := e.writeCompositeNode(root)
|
data, changed, err := e.writeCompositeNode(root)
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CompositeSelection 同一内部容器的独立选区快照,共享不可变字体和图片资源
|
||||||
|
// 仅可粘贴回当前Editor的原范围,源对象修改或删除不改变快照,不用于跨文档交换
|
||||||
|
// 父路径的祖先成员增删或排序后需重新捕获,避免使用已失效的序号路径
|
||||||
|
type CompositeSelection struct {
|
||||||
|
editor *Editor
|
||||||
|
page string
|
||||||
|
path ObjectPath
|
||||||
|
references int
|
||||||
|
containers []int
|
||||||
|
nodes []*editorCompositeNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureCompositeObjects 捕获内部选区,不修改文档或分配资源
|
||||||
|
// 入参: page 页面索引, path 父复合路径, indexes 同一容器的成员序号
|
||||||
|
// 返回: *CompositeSelection 独立快照, error 错误信息
|
||||||
|
func (e *Editor) CaptureCompositeObjects(page int, path ObjectPath, indexes []int) (*CompositeSelection, error) {
|
||||||
|
reader, renderer, root, nodes, err := e.compositeScope(page, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
if len(indexes) == 0 {
|
||||||
|
return nil, fmt.Errorf("composite selection is empty")
|
||||||
|
}
|
||||||
|
indexes = slices.Clone(indexes)
|
||||||
|
slices.Sort(indexes)
|
||||||
|
members := e.measureCompositeMembers(renderer, nodes)
|
||||||
|
var owner *editorCompositeNode
|
||||||
|
var container *editorXML
|
||||||
|
selection := &CompositeSelection{editor: e, page: e.pages[page].ID, path: ObjectPath{ID: path.ID, Children: slices.Clone(path.Children)}}
|
||||||
|
for j, i := range indexes {
|
||||||
|
if i < 0 || i >= len(nodes) || j > 0 && indexes[j-1] == i || !members[i].Capabilities.Copy {
|
||||||
|
return nil, fmt.Errorf("invalid composite copy selection")
|
||||||
|
}
|
||||||
|
node := nodes[i]
|
||||||
|
if j == 0 {
|
||||||
|
owner, container = node.owner, node.span.parent
|
||||||
|
} else if node.span.parent != container {
|
||||||
|
return nil, fmt.Errorf("copy requires members in the same container")
|
||||||
|
}
|
||||||
|
copy := *node
|
||||||
|
copy.owner, copy.span = nil, nil
|
||||||
|
selection.nodes = append(selection.nodes, ©)
|
||||||
|
}
|
||||||
|
scope := root.loadedScope(path.Children)
|
||||||
|
for scope != owner {
|
||||||
|
scope = scope.ref
|
||||||
|
selection.references++
|
||||||
|
}
|
||||||
|
for node := container; node != owner.node; node = node.parent {
|
||||||
|
containers := compositeContainers(node.parent)
|
||||||
|
selection.containers = append(selection.containers, slices.Index(containers, node))
|
||||||
|
}
|
||||||
|
slices.Reverse(selection.containers)
|
||||||
|
return selection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PasteCompositeObjects 将快照粘贴至原内部容器末尾,不依赖源成员当前的序号或存在性
|
||||||
|
// 位移使用当前父变换下的页面毫米,父容器和继承样式沿用当前范围,提交一次撤销记录
|
||||||
|
// 入参: page 页面索引, path 父复合路径, selection 捕获的快照, dx、dy 页面位移
|
||||||
|
// 返回: []int 新成员序号, error 错误信息
|
||||||
|
func (e *Editor) PasteCompositeObjects(page int, path ObjectPath, selection *CompositeSelection, dx, dy float64) ([]int, error) {
|
||||||
|
if selection == nil || selection.editor != e || path.ID != selection.path.ID || !slices.Equal(path.Children, selection.path.Children) {
|
||||||
|
return nil, fmt.Errorf("paste requires the original composite scope")
|
||||||
|
}
|
||||||
|
if !finite(dx) || !finite(dy) {
|
||||||
|
return nil, fmt.Errorf("paste requires finite offsets")
|
||||||
|
}
|
||||||
|
var result []int
|
||||||
|
err := e.editCompositeScope(page, path, func(renderer *Renderer, root *editorCompositeNode, _ []*editorCompositeNode) error {
|
||||||
|
if e.pages[page].ID != selection.page {
|
||||||
|
return fmt.Errorf("paste requires the original page")
|
||||||
|
}
|
||||||
|
scope := root.loadedScope(path.Children)
|
||||||
|
owner := scope
|
||||||
|
for i := 0; i < selection.references; i++ {
|
||||||
|
owner = owner.ref
|
||||||
|
if owner == nil {
|
||||||
|
return fmt.Errorf("composite resource container no longer exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
container := owner.node
|
||||||
|
for _, index := range selection.containers {
|
||||||
|
containers := compositeContainers(container)
|
||||||
|
if index >= len(containers) {
|
||||||
|
return fmt.Errorf("composite page block no longer exists")
|
||||||
|
}
|
||||||
|
container = containers[index]
|
||||||
|
}
|
||||||
|
box, _ := ParseBox(owner.object.CompositeGraphicUnit.Boundary)
|
||||||
|
parent := owner.parent.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(NewMatrix(owner.object.CompositeGraphicUnit.CTM))
|
||||||
|
nodes := make([]*editorCompositeNode, len(selection.nodes))
|
||||||
|
for i, node := range selection.nodes {
|
||||||
|
copy := *node
|
||||||
|
copy.parent, copy.boundaryInCTM = parent, owner.boundaryInCTM
|
||||||
|
nodes[i] = ©
|
||||||
|
}
|
||||||
|
data, err := e.copyCompositeNodes(renderer, nodes, dx, dy)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
next, reached := 0, false
|
||||||
|
var count func(*editorCompositeNode)
|
||||||
|
count = func(node *editorCompositeNode) {
|
||||||
|
if node.ref != nil {
|
||||||
|
count(node.ref)
|
||||||
|
}
|
||||||
|
if reached {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, child := range node.children {
|
||||||
|
if node != owner || child.span.start < container.close {
|
||||||
|
next++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reached = node == owner
|
||||||
|
}
|
||||||
|
count(scope)
|
||||||
|
patch := editorXMLPatch{container.close, container.close, data}
|
||||||
|
if container.open == container.end {
|
||||||
|
patch = editorXMLContent(owner.data, container, data)
|
||||||
|
}
|
||||||
|
owner.patches = append(owner.patches, patch)
|
||||||
|
for i := range nodes {
|
||||||
|
result = append(result, next+i)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadedScope 获取已由compositeScope展开并校验的范围
|
||||||
|
// 入参: path 逐层成员序号
|
||||||
|
// 返回: *editorCompositeNode 范围节点
|
||||||
|
func (n *editorCompositeNode) loadedScope(path []int) *editorCompositeNode {
|
||||||
|
for _, index := range path {
|
||||||
|
var nodes []*editorCompositeNode
|
||||||
|
var collect func(*editorCompositeNode)
|
||||||
|
collect = func(node *editorCompositeNode) {
|
||||||
|
if node.ref != nil {
|
||||||
|
collect(node.ref)
|
||||||
|
}
|
||||||
|
nodes = append(nodes, node.children...)
|
||||||
|
}
|
||||||
|
collect(n)
|
||||||
|
n = nodes[index]
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// compositeContainers 获取直接内容容器,序号不随绘制成员增删变化
|
||||||
|
// 入参: node 父节点
|
||||||
|
// 返回: []*editorXML 内容容器与页块
|
||||||
|
func compositeContainers(node *editorXML) []*editorXML {
|
||||||
|
var result []*editorXML
|
||||||
|
for _, child := range node.children {
|
||||||
|
if child.name.Local == "Content" || child.name.Local == "PageBlock" {
|
||||||
|
result = append(result, child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
// 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"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// matrix 获取对象局部坐标到页面的变换,兼容旧式内联边界
|
||||||
|
// 入参: transform 是否包含对象CTM
|
||||||
|
// 返回: Matrix 坐标变换
|
||||||
|
func (n *editorCompositeNode) matrix(transform bool) Matrix {
|
||||||
|
boundary, ctm := editorGeometry(n.object)
|
||||||
|
box, _ := ParseBox(boundary)
|
||||||
|
m := IdentityMatrix
|
||||||
|
if transform {
|
||||||
|
m = NewMatrix(ctm)
|
||||||
|
if n.object.Type == "ImageObject" && ctm == "" {
|
||||||
|
m = Matrix{a: box.W, d: box.H}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if n.boundaryInCTM {
|
||||||
|
return n.parent.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(m)
|
||||||
|
}
|
||||||
|
if !transform {
|
||||||
|
return TranslationMatrix(box.X, box.Y)
|
||||||
|
}
|
||||||
|
return TranslationMatrix(box.X, box.Y).Multiply(n.parent).Multiply(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CropCompositeImage 将内部图片与页面矩形取交集,保留原裁剪、蒙版、变换和边框
|
||||||
|
// 只收窄可见区域,撤销恢复上次裁剪,不重采样图片
|
||||||
|
// 入参: page 页面索引, path 父复合路径, index 图片序号, box 页面毫米范围
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) CropCompositeImage(page int, path ObjectPath, index int, box Box) error {
|
||||||
|
if _, err := creationBox(editorBoxString(box)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
|
node, member := nodes[0], members[0]
|
||||||
|
if !member.Capabilities.CropImage {
|
||||||
|
return fmt.Errorf("composite image cannot be cropped")
|
||||||
|
}
|
||||||
|
before := member.Bounds
|
||||||
|
if box.X <= before.X && box.Y <= before.Y && box.X+box.W >= before.X+before.W && box.Y+box.H >= before.Y+before.H {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if box.X+box.W <= before.X || box.Y+box.H <= before.Y || box.X >= before.X+before.W || box.Y >= before.Y+before.H {
|
||||||
|
return fmt.Errorf("crop does not intersect the image")
|
||||||
|
}
|
||||||
|
clips := node.object.ImageObject.Clips
|
||||||
|
inverse, _ := node.matrix(clips == nil || clips.TransFlag == nil || *clips.TransFlag).Invert()
|
||||||
|
shape, err := NewShape(ShapeRectangle, box)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fill, stroke := true, false
|
||||||
|
shape.Fill, shape.Stroke = &fill, &stroke
|
||||||
|
data, err := encodeOFDXML(func(x *ofdXML) {
|
||||||
|
x.clips(&Clips{Clip: []Clip{{Area: []ClipArea{{CTM: inverse.String(), Path: []PathObject{shape}}}}}})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data = bytes.TrimPrefix(data, []byte(xml.Header))
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var patch editorXMLPatch
|
||||||
|
if original := node.node.child("Clips"); original != nil {
|
||||||
|
clip, err := editorXMLStandalone(data[root.children[0].start:root.children[0].end], root.children[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
content := append(bytes.Clone(node.data[original.open:original.close]), clip...)
|
||||||
|
patch = editorXMLContent(node.data, original, content)
|
||||||
|
} else {
|
||||||
|
data, err = editorXMLStandalone(data, root)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
content := append(data, node.data[node.node.open:node.node.close]...)
|
||||||
|
patch = editorXMLContent(node.data, node.node, content)
|
||||||
|
}
|
||||||
|
updated, err := newEditorCompositeNode(editorPatchXML(node.data, []editorXMLPatch{patch}))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
node.data, node.node, node.object, node.changed = updated.data, updated.node, updated.object, true
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -60,23 +60,9 @@ func (e *Editor) CopyCompositeObjects(page int, path ObjectPath, indexes []int,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
slices.SortFunc(nodes, func(a, b *editorCompositeNode) int { return a.index - b.index })
|
slices.SortFunc(nodes, func(a, b *editorCompositeNode) int { return a.index - b.index })
|
||||||
ids := make(map[string]string)
|
data, err := e.copyCompositeNodes(renderer, nodes, dx, dy)
|
||||||
for _, node := range nodes {
|
if err != nil {
|
||||||
if err := e.collectCompositeIDs(node.node, ids); err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var data []byte
|
|
||||||
for _, node := range nodes {
|
|
||||||
copy := *node
|
|
||||||
if err := e.transformCompositeMember(renderer, ©, TranslationMatrix(dx, dy)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fragment, err := editorXMLRemapIDs(copy.data, copy.node, ids)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
data = append(data, fragment...)
|
|
||||||
}
|
}
|
||||||
owner := nodes[0].owner
|
owner := nodes[0].owner
|
||||||
owner.patches = append(owner.patches, editorXMLPatch{container.close, container.close, data})
|
owner.patches = append(owner.patches, editorXMLPatch{container.close, container.close, data})
|
||||||
@@ -97,6 +83,31 @@ func (e *Editor) CopyCompositeObjects(page int, path ObjectPath, indexes []int,
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copyCompositeNodes 复制成员原文并统一重映射标识,保持副本之间的引用
|
||||||
|
// 入参: renderer 渲染器, nodes 快照, dx、dy 页面位移
|
||||||
|
// 返回: []byte 副本XML, error 错误信息
|
||||||
|
func (e *Editor) copyCompositeNodes(renderer *Renderer, nodes []*editorCompositeNode, dx, dy float64) ([]byte, error) {
|
||||||
|
ids := make(map[string]string)
|
||||||
|
for _, node := range nodes {
|
||||||
|
if err := e.collectCompositeIDs(node.node, ids); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var data []byte
|
||||||
|
for _, node := range nodes {
|
||||||
|
copy := *node
|
||||||
|
if err := e.transformCompositeMember(renderer, ©, TranslationMatrix(dx, dy)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fragment, err := editorXMLRemapIDs(copy.data, copy.node, ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data = append(data, fragment...)
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
// OrderCompositeObjects 调整同一直接容器内的绘制顺序,不改变页块和其他实例
|
// OrderCompositeObjects 调整同一直接容器内的绘制顺序,不改变页块和其他实例
|
||||||
// 入参: page 页面索引, path 父复合路径, indexes 成员序号, order 为up、down、top或bottom
|
// 入参: page 页面索引, path 父复合路径, indexes 成员序号, order 为up、down、top或bottom
|
||||||
// 返回: []int 调整后的选区序号, error 错误信息
|
// 返回: []int 调整后的选区序号, error 错误信息
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
// 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"
|
||||||
|
"image/color"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateCompositeText 修改内部普通横向文字,保留首行起点、变换、裁剪及未修改属性
|
||||||
|
// 复杂字形定位不自动重排,空样式保留原字体、字号和颜色
|
||||||
|
// 入参: page 页面索引, path 父复合路径, index 成员序号, value 文字, style 文字样式增量
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) UpdateCompositeText(page int, path ObjectPath, index int, value string, style TextStyle) error {
|
||||||
|
return e.changeCompositeText(page, path, []int{index}, &value, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleCompositeText 批量修改内部文字字体、字号或颜色,换字体保留原文字定位
|
||||||
|
// 字体缺字或复杂字形映射使整批操作失败,不使用回退字体代替原文
|
||||||
|
// 入参: page 页面索引, path 父复合路径, indexes 成员序号, style 文字样式增量
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) StyleCompositeText(page int, path ObjectPath, indexes []int, style TextStyle) error {
|
||||||
|
return e.changeCompositeText(page, path, indexes, nil, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
// changeCompositeText 生成独立文字副本,全部校验通过后合并原文
|
||||||
|
// 入参: page 页面索引, path 父复合路径, indexes 成员序号, value 新内容,nil保留定位, style 样式增量
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) changeCompositeText(page int, path ObjectPath, indexes []int, value *string, style TextStyle) error {
|
||||||
|
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
|
for i, node := range nodes {
|
||||||
|
member := members[i]
|
||||||
|
if node.object.Type != "TextObject" || !member.Capabilities.ReplaceFont {
|
||||||
|
return fmt.Errorf("composite text style is not supported")
|
||||||
|
}
|
||||||
|
object := cloneEditorData(node.object)
|
||||||
|
text := cloneEditorData(member.Object.TextObject)
|
||||||
|
if style.Font != "" {
|
||||||
|
text.Font = style.Font
|
||||||
|
}
|
||||||
|
content := text.Text()
|
||||||
|
if value != nil {
|
||||||
|
content = *value
|
||||||
|
}
|
||||||
|
if content != text.Text() || style.Size != 0 && style.Size != text.Size {
|
||||||
|
if style.Size != 0 {
|
||||||
|
text.Size = style.Size
|
||||||
|
}
|
||||||
|
if err := e.layoutCompositeText(&text, content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
object.TextObject.Size, object.TextObject.TextCode = text.Size, text.TextCode
|
||||||
|
}
|
||||||
|
if style.Font != "" && style.Font != object.TextObject.Font {
|
||||||
|
if err := e.prepareText(&text); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
object.TextObject.Font = style.Font
|
||||||
|
}
|
||||||
|
if style.Color != "" {
|
||||||
|
if err := creationColor(&FillColor{Value: style.Color}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var r, g, b float64
|
||||||
|
fmt.Sscan(style.Color, &r, &g, &b)
|
||||||
|
fill, err := e.RGBColor(color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 255})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
object.TextObject.FillColor = editorStyleColor(fill, member.Object.TextObject.FillColor)
|
||||||
|
}
|
||||||
|
if err := node.update(object); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// layoutCompositeText 重排普通文字并保持原首行基线起点
|
||||||
|
// 入参: text 有效文字样式, value 新内容
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) layoutCompositeText(text *TextObject, value string) error {
|
||||||
|
if len(text.TextCode) == 0 {
|
||||||
|
return fmt.Errorf("text codes are empty")
|
||||||
|
}
|
||||||
|
origin, err := creationNumbers(text.TextCode[0].X+" "+text.TextCode[0].Y, 2)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := e.LayoutText(text, value, TextLayout{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
baseline, _ := strconv.ParseFloat(text.TextCode[0].Y, 64)
|
||||||
|
for i := range text.TextCode {
|
||||||
|
code := &text.TextCode[i]
|
||||||
|
cx, _ := strconv.ParseFloat(code.X, 64)
|
||||||
|
cy, _ := strconv.ParseFloat(code.Y, 64)
|
||||||
|
code.X, code.Y = ofdNumber(cx+origin[0]), ofdNumber(cy+origin[1]-baseline)
|
||||||
|
}
|
||||||
|
return e.prepareText(text)
|
||||||
|
}
|
||||||
@@ -196,6 +196,11 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
used[resource.font.ID] = make(map[uint16]bool)
|
used[resource.font.ID] = make(map[uint16]bool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(used) == 0 {
|
||||||
|
return nil, progress.report("fonts", 0, 0)
|
||||||
|
}
|
||||||
|
refs := editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool)}
|
||||||
|
composite := false
|
||||||
total := len(e.pages) + len(e.resources)
|
total := len(e.pages) + len(e.resources)
|
||||||
for i, page := range e.pages {
|
for i, page := range e.pages {
|
||||||
if err := progress.report("fonts", i, total); err != nil {
|
if err := progress.report("fonts", i, total); err != nil {
|
||||||
@@ -203,6 +208,23 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
}
|
}
|
||||||
for _, layer := range page.Content.Layer {
|
for _, layer := range page.Content.Layer {
|
||||||
for _, object := range layer.Objects {
|
for _, object := range layer.Objects {
|
||||||
|
if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
|
||||||
|
composite = true
|
||||||
|
origin := e.objectOrigin(editorObjectID(object))
|
||||||
|
if origin == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
data, err := editorXMLObject(origin.data, origin.node, origin.object, object)
|
||||||
|
if err == nil {
|
||||||
|
data, err = editorXMLStandalone(data, origin.node)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, safe := refs.scan(bytes.NewReader(data), "Page.xml"); !safe {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
if object.Type != "TextObject" {
|
if object.Type != "TextObject" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -219,6 +241,30 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if composite {
|
||||||
|
_, _, _, files := e.usedResources()
|
||||||
|
for i, resource := range e.resources {
|
||||||
|
if resource.composite == "" || !slices.Contains(files, resource.name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := progress.report("fonts", len(e.pages)+i, total); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, safe := refs.scan(bytes.NewReader(resource.data), resource.name); !safe {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id, usage := range refs.fonts {
|
||||||
|
glyphs := used[id]
|
||||||
|
if usage.unsafe || len(usage.glyphs) != 0 {
|
||||||
|
delete(used, id)
|
||||||
|
} else if glyphs != nil {
|
||||||
|
for char := range usage.chars {
|
||||||
|
glyphs[e.fonts[id].GlyphIndex(char)] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
result := make(map[string][]byte)
|
result := make(map[string][]byte)
|
||||||
for i := range e.resources {
|
for i := range e.resources {
|
||||||
if err := progress.report("fonts", len(e.pages)+i, total); err != nil {
|
if err := progress.report("fonts", len(e.pages)+i, total); err != nil {
|
||||||
|
|||||||
@@ -62,13 +62,14 @@ type editorSourcePage struct {
|
|||||||
// Update表示完整替换内容,ReplaceFont表示可显式替换文字字体,Transform表示保留文字定位的几何变换,Order仅限同一图层或末级页块
|
// Update表示完整替换内容,ReplaceFont表示可显式替换文字字体,Transform表示保留文字定位的几何变换,Order仅限同一图层或末级页块
|
||||||
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复
|
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复
|
||||||
// Paint表示可独立修改纯色填充和描边,不要求重新排版文字
|
// Paint表示可独立修改纯色填充和描边,不要求重新排版文字
|
||||||
// ReplaceImage表示可替换图片数据,内部成员保留原布局,不开放裁剪或重新适应
|
// ReplaceImage表示可替换图片数据,CropImage表示可裁剪图片;内部裁剪与原裁剪取交集
|
||||||
// MissingGlyphs提供缺字导致操作受限时的结构化诊断
|
// MissingGlyphs提供缺字导致操作受限时的结构化诊断
|
||||||
type ObjectCapabilities struct {
|
type ObjectCapabilities struct {
|
||||||
Update bool
|
Update bool
|
||||||
Paint bool
|
Paint bool
|
||||||
ReplaceFont bool
|
ReplaceFont bool
|
||||||
ReplaceImage bool
|
ReplaceImage bool
|
||||||
|
CropImage bool
|
||||||
Reflow bool
|
Reflow bool
|
||||||
LayoutKnown bool
|
LayoutKnown bool
|
||||||
Transform bool
|
Transform bool
|
||||||
@@ -358,6 +359,7 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
|
|||||||
origin := e.objectOrigin(id)
|
origin := e.objectOrigin(id)
|
||||||
if origin == nil {
|
if origin == nil {
|
||||||
all.ReplaceImage = object.Type == "ImageObject"
|
all.ReplaceImage = object.Type == "ImageObject"
|
||||||
|
all.CropImage = all.ReplaceImage
|
||||||
return all, nil
|
return all, nil
|
||||||
}
|
}
|
||||||
node := origin.node
|
node := origin.node
|
||||||
@@ -388,6 +390,7 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
|
|||||||
}
|
}
|
||||||
all.Paint = all.Paint && e.editorPaintable(object)
|
all.Paint = all.Paint && e.editorPaintable(object)
|
||||||
all.ReplaceImage = all.Update && object.Type == "ImageObject"
|
all.ReplaceImage = all.Update && object.Type == "ImageObject"
|
||||||
|
all.CropImage = all.ReplaceImage
|
||||||
if object.Type == "ImageObject" && object.ImageObject.Border != nil && len(object.ImageObject.Actions) != 0 {
|
if object.Type == "ImageObject" && object.ImageObject.Border != nil && len(object.ImageObject.Actions) != 0 {
|
||||||
all.Transform, all.Arrange = false, false
|
all.Transform, all.Arrange = false, false
|
||||||
all.Reason, all.ReasonCode = "image actions cannot be transformed together with the border", EditUnsupportedObject
|
all.Reason, all.ReasonCode = "image actions cannot be transformed together with the border", EditUnsupportedObject
|
||||||
|
|||||||
Reference in new issue
Block a user