mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
feat(编辑能力): 完善跨文档复制、样式与选取
This commit is contained in:
1 parent
20374f52f1
commit
88adea348b
6 files changed
+605
-98
No files matched your search
+36
-27
@@ -42,7 +42,7 @@ let credentialRequest = null;
|
|||||||
let documentDialogs = null;
|
let documentDialogs = null;
|
||||||
let cancelPageTouch = null;
|
let cancelPageTouch = null;
|
||||||
|
|
||||||
const workspace = { tabs: [], active: null, sequence: 0, nextID: 0, switching: false, closing: false };
|
const workspace = { tabs: [], active: null, sequence: 0, nextID: 0, switching: false, closing: false, objectClipboard: null };
|
||||||
|
|
||||||
function createResourceManager() {
|
function createResourceManager() {
|
||||||
return { kind: "Font", scope: "", views: new Map(), items: [], filteredItems: [], start: -1, end: -1, selected: null, references: [], referenceOffset: 0, targets: [], pick: null, busy: false, sequence: 0, url: null, fileAction: null };
|
return { kind: "Font", scope: "", views: new Map(), items: [], filteredItems: [], start: -1, end: -1, selected: null, references: [], referenceOffset: 0, targets: [], pick: null, busy: false, sequence: 0, url: null, fileAction: null };
|
||||||
@@ -108,7 +108,7 @@ function createDocumentState() {
|
|||||||
openActionFrame: 0,
|
openActionFrame: 0,
|
||||||
outlineAction: "add",
|
outlineAction: "add",
|
||||||
styleOriginal: null,
|
styleOriginal: null,
|
||||||
objectClipboard: null,
|
objectClipboardToken: null,
|
||||||
styleClipboard: null,
|
styleClipboard: null,
|
||||||
outlineExpanded: new Map(),
|
outlineExpanded: new Map(),
|
||||||
textFonts: [],
|
textFonts: [],
|
||||||
@@ -3247,7 +3247,7 @@ async function switchReadingVersion(id) {
|
|||||||
const doc = await callTabWASM(tab, "ofdgoSelectVersion", id);
|
const doc = await callTabWASM(tab, "ofdgoSelectVersion", id);
|
||||||
if (tab.closed || tab.state !== state || state.doc !== originalDocument || openSeq !== state.openSeq && view === tab.view) return;
|
if (tab.closed || tab.state !== state || state.doc !== originalDocument || openSeq !== state.openSeq && view === tab.view) return;
|
||||||
openSeq = state.openSeq = workspace.sequence = Math.max(workspace.sequence, state.openSeq) + 1;
|
openSeq = state.openSeq = workspace.sequence = Math.max(workspace.sequence, state.openSeq) + 1;
|
||||||
state.objectClipboard = state.styleClipboard = null;
|
state.objectClipboardToken = state.styleClipboard = null;
|
||||||
delete state.textDefaults.fontChoice;
|
delete state.textDefaults.fontChoice;
|
||||||
if (state.editorInfo) {
|
if (state.editorInfo) {
|
||||||
if (!dirty) state.savedRevision = doc.revision;
|
if (!dirty) state.savedRevision = doc.revision;
|
||||||
@@ -3473,7 +3473,7 @@ async function switchDocument(index) {
|
|||||||
state.packageDirty ||= state.dirty;
|
state.packageDirty ||= state.dirty;
|
||||||
state.savedRevision = doc.revision;
|
state.savedRevision = doc.revision;
|
||||||
state.editorViews.clear();
|
state.editorViews.clear();
|
||||||
state.objectClipboard = state.styleClipboard = null;
|
state.objectClipboardToken = state.styleClipboard = null;
|
||||||
delete state.textDefaults.fontChoice;
|
delete state.textDefaults.fontChoice;
|
||||||
state.ofdBytes = null;
|
state.ofdBytes = null;
|
||||||
state.editorInfo = {revision: doc.revision, canUndo: doc.canUndo, canRedo: doc.canRedo,
|
state.editorInfo = {revision: doc.revision, canUndo: doc.canUndo, canRedo: doc.canRedo,
|
||||||
@@ -3590,7 +3590,7 @@ async function toggleEditor() {
|
|||||||
state.ofdBytes = null;
|
state.ofdBytes = null;
|
||||||
state.selectObjects = true;
|
state.selectObjects = true;
|
||||||
state.conversionWarnings = [];
|
state.conversionWarnings = [];
|
||||||
state.objectClipboard = state.styleClipboard = null;
|
state.objectClipboardToken = state.styleClipboard = null;
|
||||||
state.pageCache.clear();
|
state.pageCache.clear();
|
||||||
tab.refreshPending = true;
|
tab.refreshPending = true;
|
||||||
return;
|
return;
|
||||||
@@ -3602,7 +3602,7 @@ async function toggleEditor() {
|
|||||||
state.selectObjects = true;
|
state.selectObjects = true;
|
||||||
state.ofdBytes = null;
|
state.ofdBytes = null;
|
||||||
state.conversionWarnings = [];
|
state.conversionWarnings = [];
|
||||||
state.objectClipboard = null;
|
state.objectClipboardToken = null;
|
||||||
state.styleClipboard = null;
|
state.styleClipboard = null;
|
||||||
canvasEditor.richClipboard = null;
|
canvasEditor.richClipboard = null;
|
||||||
canvasEditor.clear();
|
canvasEditor.clear();
|
||||||
@@ -4127,7 +4127,7 @@ async function ensureRichInput() {
|
|||||||
let editing = canvasEditor.input;
|
let editing = canvasEditor.input;
|
||||||
if (!editing) {
|
if (!editing) {
|
||||||
const item = canvasEditor.selected;
|
const item = canvasEditor.selected;
|
||||||
if (!item || item.items || item.scoped || item.type !== "TextObject") return false;
|
if (!item || item.items || item.type !== "TextObject") return false;
|
||||||
await editCanvasObject(item);
|
await editCanvasObject(item);
|
||||||
editing = canvasEditor.input;
|
editing = canvasEditor.input;
|
||||||
}
|
}
|
||||||
@@ -4157,6 +4157,13 @@ async function changeRangeFormat(key, value) {
|
|||||||
state.textDefaults[key] = value ?? (key === "background" ? state.textDefaults.background ? "" : textBackground.value : !state.textDefaults[key]);
|
state.textDefaults[key] = value ?? (key === "background" ? state.textDefaults.background ? "" : textBackground.value : !state.textDefaults[key]);
|
||||||
updateObjectControls(null); return;
|
updateObjectControls(null); return;
|
||||||
}
|
}
|
||||||
|
const item = canvasEditor.selected;
|
||||||
|
if (!canvasEditor.input && (item?.items || item?.scoped)) {
|
||||||
|
const current = currentTextStyle()[key];
|
||||||
|
const patch = { [key]: value ?? (key === "background" ? current ? "" : textBackground.value : current !== true) };
|
||||||
|
await changeDocument("ofdgoFormatText", { ...item, id: (item.items || [item]).map(member => member.id) }, JSON.stringify(patch));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!await ensureRichInput()) return;
|
if (!await ensureRichInput()) return;
|
||||||
const rich = canvasEditor.input.rich, current = rich.state()[key];
|
const rich = canvasEditor.input.rich, current = rich.state()[key];
|
||||||
rich.apply({ [key]: value ?? (key === "background" ? current ? "" : textBackground.value : current !== true) });
|
rich.apply({ [key]: value ?? (key === "background" ? current ? "" : textBackground.value : current !== true) });
|
||||||
@@ -4425,7 +4432,6 @@ async function changeTextFont() {
|
|||||||
setBusy(false, "", 0, "", tab);
|
setBusy(false, "", 0, "", tab);
|
||||||
}
|
}
|
||||||
} else if (item?.type === "TextObject") {
|
} else if (item?.type === "TextObject") {
|
||||||
if (item.items?.some(member => member.richText)) return;
|
|
||||||
if (canEditObject(item, "replaceFont") && (!font.embedded || item.font !== font.id.slice(9))) {
|
if (canEditObject(item, "replaceFont") && (!font.embedded || item.font !== font.id.slice(9))) {
|
||||||
const changed = await changeDocument("ofdgoStyleText", { ...item, id: (item.items || [item]).map(member => member.id) }, readTextFont(font, tab), 0, null);
|
const changed = await changeDocument("ofdgoStyleText", { ...item, id: (item.items || [item]).map(member => member.id) }, readTextFont(font, tab), 0, null);
|
||||||
if (isForegroundDocument(tab) && (changed || openSeq === state.openSeq)) updateTextFonts(canvasEditor.selected, true);
|
if (isForegroundDocument(tab) && (changed || openSeq === state.openSeq)) updateTextFonts(canvasEditor.selected, true);
|
||||||
@@ -4573,7 +4579,6 @@ async function changeTextStyle(color) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (item.items) {
|
if (item.items) {
|
||||||
if (item.items.some(member => member.richText)) return;
|
|
||||||
if (color ? !canEditObject(item, "paint") : !canEditObject(item, "layoutKnown") || !canEditObject(item, "reflow")) return;
|
if (color ? !canEditObject(item, "paint") : !canEditObject(item, "layoutKnown") || !canEditObject(item, "reflow")) return;
|
||||||
if (!color && !el.textSize.value) return;
|
if (!color && !el.textSize.value) return;
|
||||||
if (size !== item.size || fill !== null) {
|
if (size !== item.size || fill !== null) {
|
||||||
@@ -4713,7 +4718,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
const scope = objectParent(item);
|
const scope = objectParent(item);
|
||||||
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", ofdgoLayoutText: "layout", ofdgoFitImage: "fit", ofdgoResetCompositeCrop: "resetCrop",
|
ofdgoUpdateText: "text", ofdgoStyleText: "textStyle", ofdgoFormatText: "formatText", ofdgoCropImage: "crop", ofdgoLayoutText: "layout", ofdgoFitImage: "fit", ofdgoResetCompositeCrop: "resetCrop",
|
||||||
ofdgoReshapeObject: "reshape", ofdgoReshapeLine: "line", ofdgoEraseObjects: "erase", ofdgoEraseObjectsPath: "erasePath", ofdgoPasteStyle: "pasteStyle",
|
ofdgoReshapeObject: "reshape", ofdgoReshapeLine: "line", ofdgoEraseObjects: "erase", ofdgoEraseObjectsPath: "erasePath", ofdgoPasteStyle: "pasteStyle",
|
||||||
ofdgoDeleteObject: "delete", ofdgoDeleteObjects: "delete", ofdgoCopyObjects: "copy", ofdgoOrderObjects: "order", ofdgoGroupObjects: "group" }[name];
|
ofdgoDeleteObject: "delete", ofdgoDeleteObjects: "delete", ofdgoCopyObjects: "copy", ofdgoOrderObjects: "order", ofdgoGroupObjects: "group" }[name];
|
||||||
const members = item?.items ? [...item.items].sort((a, b) => a.order - b.order) : item ? [item] : [];
|
const members = item?.items ? [...item.items].sort((a, b) => a.order - b.order) : item ? [item] : [];
|
||||||
@@ -4726,7 +4731,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
if (scopedRichText) args.push(scope);
|
if (scopedRichText) args.push(scope);
|
||||||
const textSelection = name === "ofdgoUpdateTextSelection";
|
const textSelection = name === "ofdgoUpdateTextSelection";
|
||||||
if (scoped && !operation && !textSelection && !scopedRichText) throw new Error("内部对象暂不支持此操作");
|
if (scoped && !operation && !textSelection && !scopedRichText) throw new Error("内部对象暂不支持此操作");
|
||||||
const doc = name === "ofdgoStyleText"
|
const doc = ["ofdgoStyleText", "ofdgoFormatText"].includes(name)
|
||||||
? await callWASM(name, item.index, ids, ...args)
|
? await callWASM(name, item.index, ids, ...args)
|
||||||
: textSelection
|
: textSelection
|
||||||
? await callWASM(name, item.index, item.selectionIDs, ...args)
|
? await callWASM(name, item.index, item.selectionIDs, ...args)
|
||||||
@@ -4777,7 +4782,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
if (state.documentManagement && (name === "ofdgoImportDocuments" || name === "ofdgoChangeDocument" && args[0] === "add")) {
|
if (state.documentManagement && (name === "ofdgoImportDocuments" || name === "ofdgoChangeDocument" && args[0] === "add")) {
|
||||||
state.documentManagement.key = documentEntryKey(nextDocument);
|
state.documentManagement.key = documentEntryKey(nextDocument);
|
||||||
}
|
}
|
||||||
state.objectClipboard = state.styleClipboard = null;
|
state.objectClipboardToken = state.styleClipboard = null;
|
||||||
canvasEditor.richClipboard = null;
|
canvasEditor.richClipboard = null;
|
||||||
state.pageSelection.clear();
|
state.pageSelection.clear();
|
||||||
state.pageSelectionAnchor = null;
|
state.pageSelectionAnchor = null;
|
||||||
@@ -4828,7 +4833,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
}
|
}
|
||||||
const clearSelection = !item && !editingAnnotation || name === "ofdgoChangeAnnotations" && args[0] === "delete"
|
const clearSelection = !item && !editingAnnotation || name === "ofdgoChangeAnnotations" && args[0] === "delete"
|
||||||
|| ["ofdgoDeleteObject", "ofdgoDeleteObjects", "ofdgoEraseObjects", "ofdgoEraseObjectsPath"].includes(name);
|
|| ["ofdgoDeleteObject", "ofdgoDeleteObjects", "ofdgoEraseObjects", "ofdgoEraseObjectsPath"].includes(name);
|
||||||
if (name === "ofdgoCopyObjects" || name === "ofdgoPasteObjects" || name === "ofdgoGroupObjects" || inserting || name === "ofdgoWriteAnnotation" && !editingAnnotation) {
|
if (name === "ofdgoCopyObjects" || ["ofdgoPasteObjects", "ofdgoImportObjects"].includes(name) || name === "ofdgoGroupObjects" || inserting || name === "ofdgoWriteAnnotation" && !editingAnnotation) {
|
||||||
state.selectObjects = true;
|
state.selectObjects = true;
|
||||||
if (name !== "ofdgoInsertInk") canvasEditor.setTool("");
|
if (name !== "ofdgoInsertInk") canvasEditor.setTool("");
|
||||||
pageView.setPan(false);
|
pageView.setPan(false);
|
||||||
@@ -4842,7 +4847,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
} else if (restoring && !doc.changes?.all && doc.changes?.pages?.length === 1 && doc.changes.pages[0] === state.pageIndex && (restoredIndex < 0 || restoredIndex === state.pageIndex)) {
|
} else if (restoring && !doc.changes?.all && doc.changes?.pages?.length === 1 && doc.changes.pages[0] === state.pageIndex && (restoredIndex < 0 || restoredIndex === state.pageIndex)) {
|
||||||
if (location) canvasEditor.pendingSelection = {index:state.pageIndex, ids:location.ids};
|
if (location) canvasEditor.pendingSelection = {index:state.pageIndex, ids:location.ids};
|
||||||
await refreshEditorPage(doc, state.pageIndex, openSeq, clearSelection);
|
await refreshEditorPage(doc, state.pageIndex, openSeq, clearSelection);
|
||||||
} else if (item || name === "ofdgoPasteObjects" || inserting || editingAnnotation) {
|
} else if (item || ["ofdgoPasteObjects", "ofdgoImportObjects"].includes(name) || inserting || editingAnnotation) {
|
||||||
await refreshEditorPage(doc, item ? item.index : args[0], openSeq, clearSelection);
|
await refreshEditorPage(doc, item ? item.index : args[0], openSeq, clearSelection);
|
||||||
} else {
|
} else {
|
||||||
const samePage = pageIndexByID(doc.pages, previous.id);
|
const samePage = pageIndexByID(doc.pages, previous.id);
|
||||||
@@ -10485,7 +10490,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(), source: `${tab.id}:${state.wasmSeq}:${state.doc.documentIndex || 0}:${state.doc.documentVersion || ""}`, page: state.doc.pages[index].id, 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;
|
||||||
@@ -10493,17 +10498,19 @@ function copyEditorSelection(event) {
|
|||||||
event.clipboardData.setData(OBJECT_CLIPBOARD_TYPE, clipboard.token);
|
event.clipboardData.setData(OBJECT_CLIPBOARD_TYPE, clipboard.token);
|
||||||
event.clipboardData.setData("text/plain", items.map(item => item.text || "").filter(Boolean).join("\n"));
|
event.clipboardData.setData("text/plain", items.map(item => item.text || "").filter(Boolean).join("\n"));
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
state.objectClipboard = clipboard;
|
state.objectClipboardToken = clipboard.token;
|
||||||
|
workspace.objectClipboard = clipboard;
|
||||||
const capture = () => {
|
const capture = () => {
|
||||||
const openSeq = state.openSeq, revision = state.editorInfo.revision;
|
const openSeq = state.openSeq, revision = state.editorInfo.revision;
|
||||||
return callTabWASM(tab, "ofdgoCaptureObjects", index, ids, clipboard.token).then(async () => {
|
return callTabWASM(tab, "ofdgoCaptureObjects", index, ids, clipboard.token).then(async result => {
|
||||||
|
clipboard.data = result.bytes;
|
||||||
clipboard.cut = false;
|
clipboard.cut = false;
|
||||||
if (cut && isForegroundDocument(tab) && openSeq === state.openSeq && revision === state.editorInfo?.revision && index === state.pageIndex && state.objectClipboard === clipboard) {
|
if (cut && isForegroundDocument(tab) && openSeq === state.openSeq && revision === state.editorInfo?.revision && index === state.pageIndex && state.objectClipboardToken === clipboard.token && workspace.objectClipboard === clipboard) {
|
||||||
clipboard.cut = Boolean(await changeDocument("ofdgoDeleteObjects", { index, id: ids, items }));
|
clipboard.cut = Boolean(await changeDocument("ofdgoDeleteObjects", { index, id: ids, items }));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}, err => {
|
}, err => {
|
||||||
if (isForegroundDocument(tab) && state.objectClipboard === clipboard) showError(err, false);
|
if (isForegroundDocument(tab) && state.objectClipboardToken === clipboard.token) showError(err, false);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -10534,16 +10541,17 @@ async function pasteEditorContent(event) {
|
|||||||
if (!isForegroundDocument(tab) || document.body.hasAttribute("data-busy") || canvasEditor.input || canvasEditor.crop || canvasEditor.drag) return;
|
if (!isForegroundDocument(tab) || document.body.hasAttribute("data-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 = workspace.objectClipboard;
|
||||||
if (!clipboard || token !== clipboard.token) {
|
if (!clipboard || token !== clipboard.token) {
|
||||||
setStatus("复制内容已失效,请重新复制");
|
setStatus("复制内容已失效,请重新复制");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
if (!await clipboard.ready || !isForegroundDocument(tab) || openSeq !== state.openSeq || index !== state.pageIndex || state.objectClipboard !== clipboard) return;
|
if (!await clipboard.ready || !isForegroundDocument(tab) || openSeq !== state.openSeq || index !== state.pageIndex || workspace.objectClipboard !== clipboard) return;
|
||||||
if (!pageCan("insert")) return;
|
if (!pageCan("insert")) return;
|
||||||
const visible = visiblePageBounds(index), bounds = clipboard.bounds;
|
const visible = visiblePageBounds(index), bounds = clipboard.bounds;
|
||||||
const offset = (clipboard.targetPage || clipboard.page) === page.id ? { x: clipboard.x + (clipboard.cut ? 0 : 3), y: clipboard.y + (clipboard.cut ? 0 : 3) } : { x: 0, y: 0 };
|
const target = `${tab.id}:${state.wasmSeq}:${state.doc.documentIndex || 0}:${state.doc.documentVersion || ""}`;
|
||||||
|
const offset = (clipboard.target || clipboard.source) === target && (clipboard.targetPage || 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"]]) {
|
||||||
const position = bounds[axis] + offset[axis];
|
const position = bounds[axis] + offset[axis];
|
||||||
if (position < visible[axis] || position + bounds[size] > visible[axis] + visible[size]) {
|
if (position < visible[axis] || position + bounds[size] > visible[axis] + visible[size]) {
|
||||||
@@ -10551,7 +10559,8 @@ 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(state.objectClipboardToken === clipboard.token ? "ofdgoPasteObjects" : "ofdgoImportObjects", null, index, state.objectClipboardToken === clipboard.token ? token : clipboard.data, offset.x, offset.y)) {
|
||||||
|
clipboard.target = target;
|
||||||
clipboard.targetPage = page.id;
|
clipboard.targetPage = page.id;
|
||||||
clipboard.x = offset.x;
|
clipboard.x = offset.x;
|
||||||
clipboard.y = offset.y;
|
clipboard.y = offset.y;
|
||||||
@@ -12327,19 +12336,19 @@ function updateObjectControls(item, reset = false) {
|
|||||||
el.multiSelectButton.disabled = !state.editing || !canvasEditor.enabled || !state.ready || state.exporting;
|
el.multiSelectButton.disabled = !state.editing || !canvasEditor.enabled || !state.ready || state.exporting;
|
||||||
const selection = selectedText(item);
|
const selection = selectedText(item);
|
||||||
const text = selection ? currentTextStyle() : state.textDefaults;
|
const text = selection ? currentTextStyle() : state.textDefaults;
|
||||||
const textDisabled = !state.editing || !state.ready || state.exporting || Boolean(item?.items && (!selection || item.items.some(member => member.richText))) || Boolean(canvasEditor.input?.composing || canvasEditor.input?.saving);
|
const textDisabled = !state.editing || !state.ready || state.exporting || Boolean(item?.items && !selection) || Boolean(canvasEditor.input?.composing || canvasEditor.input?.saving);
|
||||||
fontPicker.setDisabled(textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "replaceFont"));
|
fontPicker.setDisabled(textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "replaceFont"));
|
||||||
el.textSize.disabled = textDisabled || text !== state.textDefaults && !item.draft && (!canEditObject(item, "reflow") || Boolean(item?.items && !item.selectionIDs) && !canEditObject(item, "layoutKnown"));
|
el.textSize.disabled = textDisabled || text !== state.textDefaults && !item.draft && (!canEditObject(item, "reflow") || Boolean(item?.items && !item.selectionIDs) && !canEditObject(item, "layoutKnown"));
|
||||||
el.textColor.disabled = textDisabled || text !== state.textDefaults && !item.draft && !item.richText && !canEditObject(item, "paint");
|
el.textColor.disabled = textDisabled || text !== state.textDefaults && !item.draft && !item.richText && !canEditObject(item, "paint");
|
||||||
el.textFontAdd.disabled = !state.editing || !state.ready || state.exporting;
|
el.textFontAdd.disabled = !state.editing || !state.ready || state.exporting;
|
||||||
const formatsDisabled = textDisabled || Boolean(item?.items || item?.scoped) || Boolean(item && !item.draft && (!selection || !canEditObject(item, "textContent")));
|
const formatsDisabled = textDisabled || Boolean(item && !item.draft && (!selection || !canEditObject(item, "textContent")));
|
||||||
for (const button of textFormatButtons) {
|
for (const button of textFormatButtons) {
|
||||||
button.disabled = formatsDisabled;
|
button.disabled = formatsDisabled;
|
||||||
const key = button.dataset.textFormat;
|
const key = button.dataset.textFormat;
|
||||||
button.setAttribute("data-pressed", text[key] === undefined && (canvasEditor.input?.rich || item?.richText) ? "mixed" : String(Boolean(text[key])));
|
button.setAttribute("data-pressed", text[key] === undefined && (canvasEditor.input?.rich || item?.richText || item?.items) ? "mixed" : String(Boolean(text[key])));
|
||||||
}
|
}
|
||||||
textBackground.disabled = formatsDisabled;
|
textBackground.disabled = formatsDisabled;
|
||||||
textBackground.classList.toggle("mixed-color", Boolean((canvasEditor.input?.rich || item?.richText) && text.background === undefined));
|
textBackground.classList.toggle("mixed-color", Boolean((canvasEditor.input?.rich || item?.richText || item?.items) && text.background === undefined));
|
||||||
if (text.background) textBackground.value = text.background;
|
if (text.background) textBackground.value = text.background;
|
||||||
document.querySelector("#textHighlight").style.setProperty("--highlight-color", textBackground.value);
|
document.querySelector("#textHighlight").style.setProperty("--highlight-color", textBackground.value);
|
||||||
const richHistory = canvasEditor.input?.rich || canvasEditor.input?.history;
|
const richHistory = canvasEditor.input?.rich || canvasEditor.input?.history;
|
||||||
|
|||||||
@@ -372,9 +372,12 @@ function arrowPath(shape, box) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function selectionBounds(items) {
|
export function selectionBounds(items) {
|
||||||
const x = Math.min(...items.map(item => item.x)), y = Math.min(...items.map(item => item.y));
|
let x = Infinity, y = Infinity, right = -Infinity, bottom = -Infinity;
|
||||||
return { x, y, width: Math.max(...items.map(item => item.x + item.width)) - x,
|
for (const item of items) {
|
||||||
height: Math.max(...items.map(item => item.y + item.height)) - y };
|
x = Math.min(x, item.x); y = Math.min(y, item.y);
|
||||||
|
right = Math.max(right, item.x + item.width); bottom = Math.max(bottom, item.y + item.height);
|
||||||
|
}
|
||||||
|
return { x, y, width: right - x, height: bottom - y };
|
||||||
}
|
}
|
||||||
|
|
||||||
function resizeCorner(node, event) {
|
function resizeCorner(node, event) {
|
||||||
@@ -523,13 +526,42 @@ export function selectedText(item) {
|
|||||||
if (!item?.items) return item?.type === "TextObject" ? item : null;
|
if (!item?.items) return item?.type === "TextObject" ? item : null;
|
||||||
if (!item.items.every(member => member.type === "TextObject")) return null;
|
if (!item.items.every(member => member.type === "TextObject")) return null;
|
||||||
const text = { ...item, type: "TextObject" };
|
const text = { ...item, type: "TextObject" };
|
||||||
for (const key of ["font", "fontName", "size", "color"]) {
|
const formats = item.items.flatMap(member => member.richText ? member.richText.runs.map(run => run.format) : [{ ...member, ...richTextModel(member).runs[0].format }]);
|
||||||
const value = item.items[0][key];
|
for (const key of ["font", "fontName", "size", "color", "bold", "italic", "underline", "strikeout", "shadow", "background"]) {
|
||||||
text[key] = item.items.every(member => member[key] === value) ? value : undefined;
|
const value = formats[0][key];
|
||||||
|
text[key] = formats.every(format => format[key] === value) ? value : undefined;
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ObjectIndex {
|
||||||
|
constructor(items) {
|
||||||
|
const build = items => {
|
||||||
|
if (!items.length) return null;
|
||||||
|
const bounds = selectionBounds(items);
|
||||||
|
if (items.length <= 16) return { ...bounds, items };
|
||||||
|
const axis = bounds.width >= bounds.height ? "x" : "y";
|
||||||
|
items.sort((a, b) => a[axis] - b[axis]);
|
||||||
|
const middle = items.length >> 1;
|
||||||
|
return { ...bounds, left: build(items.slice(0, middle)), right: build(items.slice(middle)) };
|
||||||
|
};
|
||||||
|
this.root = build([...items]);
|
||||||
|
}
|
||||||
|
|
||||||
|
query(point, tolerance = 0) {
|
||||||
|
const result = [];
|
||||||
|
const contains = box => point.x >= box.x - tolerance && point.x <= box.x + box.width + tolerance
|
||||||
|
&& point.y >= box.y - tolerance && point.y <= box.y + box.height + tolerance;
|
||||||
|
const visit = node => {
|
||||||
|
if (!node || !contains(node)) return;
|
||||||
|
if (node.items) result.push(...node.items.filter(contains));
|
||||||
|
else { visit(node.left); visit(node.right); }
|
||||||
|
};
|
||||||
|
visit(this.root);
|
||||||
|
return result.sort((a, b) => b.order - a.order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class CanvasEditor {
|
export class CanvasEditor {
|
||||||
constructor(viewer, options) {
|
constructor(viewer, options) {
|
||||||
this.viewer = viewer;
|
this.viewer = viewer;
|
||||||
@@ -602,8 +634,7 @@ export class CanvasEditor {
|
|||||||
const pending = this.pendingSelection?.index === index;
|
const pending = this.pendingSelection?.index === index;
|
||||||
const selected = new Set(pending ? this.pendingSelection.ids || page.objects.slice(-1).map(item => item.id)
|
const selected = new Set(pending ? this.pendingSelection.ids || page.objects.slice(-1).map(item => item.id)
|
||||||
: this.items().filter(item => item.index === index).map(item => item.id));
|
: this.items().filter(item => item.index === index).map(item => item.id));
|
||||||
const selection = [];
|
const selection = [], items = [];
|
||||||
this.pages.set(surface, { index, width: page.width, height: page.height });
|
|
||||||
const layer = document.createElement("div");
|
const layer = document.createElement("div");
|
||||||
layer.className = "edit-layer";
|
layer.className = "edit-layer";
|
||||||
const artwork = new Map([...surface.querySelectorAll("[data-ofd-object], [data-ofd-child]")].map((node, order) => [node.getAttribute("data-ofd-object") || node.getAttribute("data-ofd-child"), { node, order }]));
|
const artwork = new Map([...surface.querySelectorAll("[data-ofd-object], [data-ofd-child]")].map((node, order) => [node.getAttribute("data-ofd-object") || node.getAttribute("data-ofd-child"), { node, order }]));
|
||||||
@@ -645,6 +676,7 @@ export class CanvasEditor {
|
|||||||
item.contour = { svg, path: contour };
|
item.contour = { svg, path: contour };
|
||||||
}
|
}
|
||||||
this.nodes.set(node, item);
|
this.nodes.set(node, item);
|
||||||
|
items.push(item);
|
||||||
node.addEventListener("focus", () => {
|
node.addEventListener("focus", () => {
|
||||||
if (this.input) this.selectAfterText(item);
|
if (this.input) this.selectAfterText(item);
|
||||||
else this.select(item);
|
else this.select(item);
|
||||||
@@ -655,6 +687,7 @@ export class CanvasEditor {
|
|||||||
selection.push(item);
|
selection.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.pages.set(surface, { index, width: page.width, height: page.height, hitIndex: new ObjectIndex(items) });
|
||||||
surface.append(layer);
|
surface.append(layer);
|
||||||
if (pending || selected.size) this.setSelection(selection);
|
if (pending || selected.size) this.setSelection(selection);
|
||||||
if (pending) this.pendingSelection = null;
|
if (pending) this.pendingSelection = null;
|
||||||
@@ -956,7 +989,7 @@ export class CanvasEditor {
|
|||||||
const rect = surface.getBoundingClientRect(), rotation = this.options.rotation();
|
const rect = surface.getBoundingClientRect(), rotation = this.options.rotation();
|
||||||
const point = pagePoint(event.clientX, event.clientY, rect, page, rotation);
|
const point = pagePoint(event.clientX, event.clientY, rect, page, rotation);
|
||||||
const tolerance = 3 * page.width / (rotation % 180 ? rect.height : rect.width);
|
const tolerance = 3 * page.width / (rotation % 180 ? rect.height : rect.width);
|
||||||
return [...surface.querySelectorAll(".edit-object")].map(node => this.nodes.get(node)).reverse().filter(item => !item.parent || this.items().some(selected => selected.id.startsWith(item.parent + "/"))).filter(item => item.type === "TextObject"
|
return page.hitIndex.query(point, tolerance).filter(item => !item.parent || this.items().some(selected => selected.id.startsWith(item.parent + "/"))).filter(item => item.type === "TextObject"
|
||||||
? point.x >= item.x - tolerance && point.x <= item.x + item.width + tolerance
|
? point.x >= item.x - tolerance && point.x <= item.x + item.width + tolerance
|
||||||
&& point.y >= item.y - tolerance && point.y <= item.y + item.height + tolerance
|
&& point.y >= item.y - tolerance && point.y <= item.y + item.height + tolerance
|
||||||
: this.contains(item, point, tolerance));
|
: this.contains(item, point, tolerance));
|
||||||
|
|||||||
+89
-6
@@ -3862,6 +3862,7 @@ func (e *businessEngine) editorAppearance(item map[string]any, object ofdgo.Grap
|
|||||||
var fill, stroke *ofdgo.FillColor
|
var fill, stroke *ofdgo.FillColor
|
||||||
switch object.Type {
|
switch object.Type {
|
||||||
case "TextObject":
|
case "TextObject":
|
||||||
|
item["bold"], item["italic"] = object.TextObject.Weight >= 700, object.TextObject.Italic
|
||||||
fill = object.TextObject.FillColor
|
fill = object.TextObject.FillColor
|
||||||
stroke = (*ofdgo.FillColor)(object.TextObject.StrokeColor)
|
stroke = (*ofdgo.FillColor)(object.TextObject.StrokeColor)
|
||||||
alpha = object.TextObject.Alpha
|
alpha = object.TextObject.Alpha
|
||||||
@@ -4889,10 +4890,19 @@ func (e *businessEngine) styleText(args []platformValue) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if object.Type != "TextObject" {
|
if object.Type == "TextObject" {
|
||||||
return fmt.Errorf("object %q is not text", id)
|
|
||||||
}
|
|
||||||
value.WriteString(object.TextObject.Text())
|
value.WriteString(object.TextObject.Text())
|
||||||
|
} else {
|
||||||
|
path, err := e.compositePath(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
text, err := e.currentEditor.TargetRichText(page, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
value.WriteString(text.Text())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := e.checkFontGlyphs(data, value.String()); err != nil {
|
if err := e.checkFontGlyphs(data, value.String()); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -4917,6 +4927,33 @@ func (e *businessEngine) styleText(args []platformValue) (any, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formatText 批量修改选区格式,未指定的属性保持各自原值
|
||||||
|
// 入参: args 页面索引、完整路径数组和格式JSON
|
||||||
|
// 返回: any 文档信息, error 校验或提交错误
|
||||||
|
func (e *businessEngine) formatText(args []platformValue) (any, error) {
|
||||||
|
return e.changeObjects(func() error {
|
||||||
|
paths, err := e.editorTargetPaths(e.stringsFromJS(args[1]))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var patch ofdgo.TextFormatPatch
|
||||||
|
if err := json.Unmarshal([]byte(args[2].String()), &patch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, value := range []*string{patch.Color, patch.Background} {
|
||||||
|
if value == nil || *value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var color ofdgo.FillColor
|
||||||
|
if err := e.setEditorColor(&color, *value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*value = color.Value
|
||||||
|
}
|
||||||
|
return e.currentEditor.FormatTargetsText(args[0].Int(), paths, patch)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// editorRichText 将富文本复合图元作为一个文本框提供给编辑器
|
// editorRichText 将富文本复合图元作为一个文本框提供给编辑器
|
||||||
// 入参: item 浏览器对象, object 标准复合图元, value 富文本, fonts 字体名称, local 局部编辑框, matrix 页面变换
|
// 入参: item 浏览器对象, object 标准复合图元, value 富文本, fonts 字体名称, local 局部编辑框, matrix 页面变换
|
||||||
func (e *businessEngine) editorRichText(item map[string]any, object ofdgo.GraphicObject, value ofdgo.RichText, fonts map[string]string, local ofdgo.Box, matrix ofdgo.Matrix) {
|
func (e *businessEngine) editorRichText(item map[string]any, object ofdgo.GraphicObject, value ofdgo.RichText, fonts map[string]string, local ofdgo.Box, matrix ofdgo.Matrix) {
|
||||||
@@ -4966,7 +5003,11 @@ func (e *businessEngine) getRichText(args []platformValue) (any, error) {
|
|||||||
if e.currentEditor == nil {
|
if e.currentEditor == nil {
|
||||||
return nil, fmt.Errorf("no document is being edited")
|
return nil, fmt.Errorf("no document is being edited")
|
||||||
}
|
}
|
||||||
value, err := e.currentEditor.RichText(args[0].Int(), args[1].String())
|
path, err := e.compositePath(args[1].String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
value, err := e.currentEditor.TargetRichText(args[0].Int(), path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -5394,7 +5435,7 @@ func (e *businessEngine) copyObjects(args []platformValue) (any, error) {
|
|||||||
|
|
||||||
// captureObjects 保存独立选区快照,不修改文档或历史
|
// captureObjects 保存独立选区快照,不修改文档或历史
|
||||||
// 入参: args 页面索引、对象路径数组和剪贴板标识
|
// 入参: args 页面索引、对象路径数组和剪贴板标识
|
||||||
// 返回: any 空结果, error 错误信息
|
// 返回: any 可跨文档导入的选区包, error 错误信息
|
||||||
func (e *businessEngine) captureObjects(args []platformValue) (any, error) {
|
func (e *businessEngine) captureObjects(args []platformValue) (any, error) {
|
||||||
if e.currentEditor == nil {
|
if e.currentEditor == nil {
|
||||||
return nil, fmt.Errorf("no document is being edited")
|
return nil, fmt.Errorf("no document is being edited")
|
||||||
@@ -5404,8 +5445,16 @@ func (e *businessEngine) captureObjects(args []platformValue) (any, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
clipboard.token = args[2].String()
|
clipboard.token = args[2].String()
|
||||||
|
paths, err := e.editorTargetPaths(e.stringsFromJS(args[1]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var output bytes.Buffer
|
||||||
|
if _, err := e.currentEditor.WriteTargetsTo(context.Background(), &output, args[0].Int(), paths, true); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
e.copiedObjects = clipboard
|
e.copiedObjects = clipboard
|
||||||
return nil, nil
|
return successResult(map[string]any{"bytes": bytesToJS(output.Bytes())}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pasteObjects 将当前文档的对象快照粘贴到目标页
|
// pasteObjects 将当前文档的对象快照粘贴到目标页
|
||||||
@@ -5423,6 +5472,38 @@ func (e *businessEngine) pasteObjects(args []platformValue) (any, error) {
|
|||||||
return editorSelectionInfo{info, ids}, err
|
return editorSelectionInfo{info, ids}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// importObjects 将独立选区包导入当前文档,来源标签无需保持打开
|
||||||
|
// 入参: args 目标页、选区包和横纵位移
|
||||||
|
// 返回: any 文档信息及新选区, error 读取或导入错误
|
||||||
|
func (e *businessEngine) importObjects(args []platformValue) (any, error) {
|
||||||
|
if e.currentEditor == nil {
|
||||||
|
return nil, fmt.Errorf("no document is being edited")
|
||||||
|
}
|
||||||
|
data, err := e.bytesFromJS(args[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
input, err := ofdgo.NewInput(bytes.NewReader(data), int64(len(data)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer input.Close()
|
||||||
|
reader, err := ofdgo.NewReader(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
objects, annotations, err := e.currentEditor.ImportSelection(context.Background(), reader, args[0].Int(), ofdgo.ObjectImportOptions{DX: args[2].Float(), DY: args[3].Float(), AllowDecrypted: true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, id := range annotations {
|
||||||
|
objects = append(objects, "annotation:"+id)
|
||||||
|
}
|
||||||
|
info, err := e.previewEditor(e.currentEditor, e.currentSession.Renderer.RenderAnnotations)
|
||||||
|
return editorSelectionInfo{info, objects}, err
|
||||||
|
}
|
||||||
|
|
||||||
// copyEditorObjects 复制快照并更新预览,返回新对象标识
|
// copyEditorObjects 复制快照并更新预览,返回新对象标识
|
||||||
// 入参: page 目标页, objects 对象快照, dx 横向位移,单位为毫米, dy 纵向位移,单位为毫米
|
// 入参: page 目标页, objects 对象快照, dx 横向位移,单位为毫米, dy 纵向位移,单位为毫米
|
||||||
// 返回: any 文档信息及新选区, error 错误信息
|
// 返回: any 文档信息及新选区, error 错误信息
|
||||||
@@ -7872,6 +7953,7 @@ func (e *businessEngine) businessCommands() map[string]func([]platformValue) (an
|
|||||||
"ofdgoPreviewTextSelection": e.previewTextSelection,
|
"ofdgoPreviewTextSelection": e.previewTextSelection,
|
||||||
"ofdgoUpdateTextSelection": e.updateTextSelection,
|
"ofdgoUpdateTextSelection": e.updateTextSelection,
|
||||||
"ofdgoStyleText": e.styleText,
|
"ofdgoStyleText": e.styleText,
|
||||||
|
"ofdgoFormatText": e.formatText,
|
||||||
"ofdgoGetRichText": e.getRichText,
|
"ofdgoGetRichText": e.getRichText,
|
||||||
"ofdgoSaveRichText": e.saveRichText,
|
"ofdgoSaveRichText": e.saveRichText,
|
||||||
"ofdgoPreviewRichText": e.previewRichText,
|
"ofdgoPreviewRichText": e.previewRichText,
|
||||||
@@ -7897,6 +7979,7 @@ func (e *businessEngine) businessCommands() map[string]func([]platformValue) (an
|
|||||||
"ofdgoCopyObjects": e.copyObjects,
|
"ofdgoCopyObjects": e.copyObjects,
|
||||||
"ofdgoCaptureObjects": e.captureObjects,
|
"ofdgoCaptureObjects": e.captureObjects,
|
||||||
"ofdgoPasteObjects": e.pasteObjects,
|
"ofdgoPasteObjects": e.pasteObjects,
|
||||||
|
"ofdgoImportObjects": e.importObjects,
|
||||||
"ofdgoOrderObjects": e.orderObjects,
|
"ofdgoOrderObjects": e.orderObjects,
|
||||||
"ofdgoDistributeObjects": e.distributeObjects,
|
"ofdgoDistributeObjects": e.distributeObjects,
|
||||||
"ofdgoRotateObjects": e.rotateObjects,
|
"ofdgoRotateObjects": e.rotateObjects,
|
||||||
|
|||||||
+94
-29
@@ -112,63 +112,107 @@ func (e *Editor) ImportObjects(source *Editor, sourcePage int, ids []string, pag
|
|||||||
// 入参: ctx 取消上下文, source 来源编辑器, sourcePage 来源页, ids 对象标识, page 目标页, options 迁移选项
|
// 入参: ctx 取消上下文, source 来源编辑器, sourcePage 来源页, ids 对象标识, page 目标页, options 迁移选项
|
||||||
// 返回: []string 新对象标识, error 校验、迁移或取消错误
|
// 返回: []string 新对象标识, error 校验、迁移或取消错误
|
||||||
func (e *Editor) ImportObjectsContext(ctx context.Context, source *Editor, sourcePage int, ids []string, page int, options ObjectImportOptions) ([]string, error) {
|
func (e *Editor) ImportObjectsContext(ctx context.Context, source *Editor, sourcePage int, ids []string, page int, options ObjectImportOptions) ([]string, error) {
|
||||||
|
objects, _, err := e.importSelection(ctx, source, sourcePage, ids, nil, page, options, true)
|
||||||
|
return objects, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportSelection 导入独立单页选区包,重映射对象和资源标识,一次撤销恢复全部
|
||||||
|
// 入参: ctx 取消上下文, source 单页选区阅读器, page 目标页, options 位移及导入配置
|
||||||
|
// 返回: []string 正文标识, []string 注解标识, error 校验或迁移错误
|
||||||
|
func (e *Editor) ImportSelection(ctx context.Context, source *Reader, page int, options ObjectImportOptions) ([]string, []string, error) {
|
||||||
|
doc, err := source.DocContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if len(doc.Pages.Page) != 1 {
|
||||||
|
return nil, nil, fmt.Errorf("selection requires exactly one page")
|
||||||
|
}
|
||||||
|
origin, err := source.EditorContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
content, err := origin.page(0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
var objects, annotations []string
|
||||||
|
for _, layer := range content.Content.Layer {
|
||||||
|
for _, object := range layer.Objects {
|
||||||
|
objects = append(objects, editorObjectID(object))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notes, err := origin.Annotations(0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
for _, note := range notes {
|
||||||
|
annotations = append(annotations, note.ID)
|
||||||
|
}
|
||||||
|
return e.importSelection(ctx, origin, 0, objects, annotations, page, options, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// importSelection 迁移正文与注解,共用资源映射并保持来源绘制顺序
|
||||||
|
// 入参: ctx 取消上下文, source 来源编辑器, sourcePage 来源页, ids 正文标识, annotations 注解标识, page 目标页, options 导入配置, strictLinks 拒绝跨页链接
|
||||||
|
// 返回: []string 正文标识, []string 注解标识, error 迁移或提交错误
|
||||||
|
func (e *Editor) importSelection(ctx context.Context, source *Editor, sourcePage int, ids, annotations []string, page int, options ObjectImportOptions, strictLinks bool) ([]string, []string, error) {
|
||||||
progress := editorProgress(options.OnProgress).withContext(ctx)
|
progress := editorProgress(options.OnProgress).withContext(ctx)
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if !finite(options.DX) || !finite(options.DY) {
|
if !finite(options.DX) || !finite(options.DY) {
|
||||||
return nil, fmt.Errorf("import requires finite offsets")
|
return nil, nil, fmt.Errorf("import requires finite offsets")
|
||||||
}
|
}
|
||||||
if source.encryption != nil && e.encryption == nil && !options.AllowDecrypted {
|
if source.encryption != nil && e.encryption == nil && !options.AllowDecrypted {
|
||||||
return nil, ErrEncryptionPolicyRequired
|
return nil, nil, ErrEncryptionPolicyRequired
|
||||||
}
|
}
|
||||||
objects, err := source.Objects(sourcePage, ids)
|
objects, err := source.Objects(sourcePage, ids)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
target, err := e.page(page)
|
target, err := e.page(page)
|
||||||
if err != nil || len(objects) == 0 {
|
if err != nil || len(objects) == 0 && len(annotations) == 0 {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if source == e {
|
if source == e {
|
||||||
if err := progress.report("commit", 0, 0); err != nil {
|
if err := progress.report("commit", 0, 0); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
return e.CopyObjectsContext(ctx, page, objects, options.DX, options.DY)
|
copied, err := e.CopyObjectsContext(ctx, page, objects, options.DX, options.DY)
|
||||||
|
return copied, nil, err
|
||||||
}
|
}
|
||||||
reader, err := source.readerContext(ctx, progress)
|
reader, err := source.readerContext(ctx, progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
doc, err := reader.Doc()
|
doc, err := reader.Doc()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
ref := doc.Pages.Page[sourcePage]
|
ref := doc.Pages.Page[sourcePage]
|
||||||
base, migration, err := e.prepareImport(ctx, reader, doc, map[string]bool{ref.ID: true}, false, progress)
|
base, migration, err := e.prepareImport(ctx, reader, doc, map[string]bool{ref.ID: true}, false, progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
migration.ids[editorImportKey(ref.ID)], migration.strictLinks = target.ID, true
|
migration.ids[editorImportKey(ref.ID)], migration.strictLinks = target.ID, strictLinks
|
||||||
pageName := reader.ResPath(ref.BaseLoc)
|
pageName := reader.ResPath(ref.BaseLoc)
|
||||||
content, err := reader.PageContentByIndex(sourcePage)
|
content, err := reader.PageContentByIndex(sourcePage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
for _, value := range content.PageRes {
|
for _, value := range content.PageRes {
|
||||||
name, err := migration.resolve(pageName, "", value)
|
name, err := migration.resolve(pageName, "", value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if err := migration.resourceIndex(name); err != nil {
|
if err := migration.resourceIndex(name); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
layers := make([][]byte, len(objects))
|
layers := make([][]byte, len(objects))
|
||||||
for i, object := range objects {
|
for i, object := range objects {
|
||||||
if err := progress.report("objects", i, len(objects)); err != nil {
|
if err := progress.report("objects", i, len(objects)); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
var data []byte
|
var data []byte
|
||||||
var attrs ofdAttrs
|
var attrs ofdAttrs
|
||||||
@@ -187,32 +231,48 @@ func (e *Editor) ImportObjectsContext(ctx context.Context, source *Editor, sourc
|
|||||||
data, err = editorObjectXML(object)
|
data, err = editorObjectXML(object)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
data, err = editorXMLContainer("Layer", attrs, data)
|
data, err = editorXMLContainer("Layer", attrs, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXMLContext(ctx, data)
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
layers[i], err = migration.encode(editorImportEntry{name: pageName, data: data}, root)
|
layers[i], err = migration.encode(editorImportEntry{name: pageName, data: data}, root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
notes := &AnnotationSelection{editor: e, document: e.documentIdentity()}
|
||||||
|
for _, id := range annotations {
|
||||||
|
data, err := source.annotationXML(sourcePage, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
data, err = migration.encode(editorImportEntry{name: pageName, data: data}, root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
notes.data = append(notes.data, data)
|
||||||
|
}
|
||||||
if err := migration.encodeResources(); err != nil {
|
if err := migration.encodeResources(); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
for _, resource := range e.resources {
|
for _, resource := range e.resources {
|
||||||
delete(migration.files, resource.name)
|
delete(migration.files, resource.name)
|
||||||
}
|
}
|
||||||
next, err := migration.merge(base, nil, nil, nil)
|
next, err := migration.merge(base, nil, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
var copied []string
|
var copied, copiedNotes []string
|
||||||
err = e.Transaction(func(editor *Editor) error {
|
err = e.Transaction(func(editor *Editor) error {
|
||||||
if err := progress.report("commit", 0, 0); err != nil {
|
if err := progress.report("commit", 0, 0); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -236,13 +296,13 @@ func (e *Editor) ImportObjectsContext(ctx context.Context, source *Editor, sourc
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
object := layer.Objects[0]
|
object := layer.Objects[0]
|
||||||
|
object.TextObject.layout = objects[i].TextObject.layout
|
||||||
|
object.state = objects[i].state
|
||||||
|
object.CompositeGraphicUnit.states = remapCompositeStates(objects[i].CompositeGraphicUnit.states, migration.ids)
|
||||||
imported[i], err = editor.copiedObjectStyle(object, layer.DrawParam)
|
imported[i], err = editor.copiedObjectStyle(object, layer.DrawParam)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
imported[i].TextObject.layout = objects[i].TextObject.layout
|
|
||||||
imported[i].state = objects[i].state
|
|
||||||
imported[i].CompositeGraphicUnit.states = remapCompositeStates(objects[i].CompositeGraphicUnit.states, migration.ids)
|
|
||||||
imported[i].origin = &editorObjectOrigin{editor: editor, document: editor.documentIdentity(), data: data, node: root.child("Layer").children[0], object: object}
|
imported[i].origin = &editorObjectOrigin{editor: editor, document: editor.documentIdentity(), data: data, node: root.child("Layer").children[0], object: object}
|
||||||
}
|
}
|
||||||
copied, err = editor.CopyObjectsContext(ctx, page, imported, options.DX, options.DY)
|
copied, err = editor.CopyObjectsContext(ctx, page, imported, options.DX, options.DY)
|
||||||
@@ -253,12 +313,17 @@ func (e *Editor) ImportObjectsContext(ctx context.Context, source *Editor, sourc
|
|||||||
for i, object := range objects {
|
for i, object := range objects {
|
||||||
orderedIDs[i] = editorObjectID(object)
|
orderedIDs[i] = editorObjectID(object)
|
||||||
}
|
}
|
||||||
return editor.copyRichTextMetadata(source, sourcePage, orderedIDs, page, copied)
|
notes.document = editor.documentIdentity()
|
||||||
|
copiedNotes, err = editor.PasteAnnotations(page, notes, options.DX, options.DY)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return editor.copyRichTextMetadata(ctx, source, sourcePage, orderedIDs, annotations, page, copied, copiedNotes)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
return copied, err
|
return copied, copiedNotes, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImportPages 将来源当前文档中的指定页面插入目标位置,保持输入顺序并作为一次撤销操作
|
// ImportPages 将来源当前文档中的指定页面插入目标位置,保持输入顺序并作为一次撤销操作
|
||||||
|
|||||||
+195
-24
@@ -25,6 +25,7 @@ import (
|
|||||||
"image/color"
|
"image/color"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -338,10 +339,17 @@ func (e *Editor) UpdateCompositeRichText(page int, path ObjectPath, index int, v
|
|||||||
return fmt.Errorf("invalid rich text frame")
|
return fmt.Errorf("invalid rich text frame")
|
||||||
}
|
}
|
||||||
return e.Transaction(func(e *Editor) error {
|
return e.Transaction(func(e *Editor) error {
|
||||||
|
member, err := e.CompositeObject(page, path, index)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if member.Object.Type == "TextObject" {
|
||||||
|
return e.updateCompositePlainRichText(page, path, index, value, offset, width)
|
||||||
|
}
|
||||||
var metadata richTextMetadata
|
var metadata richTextMetadata
|
||||||
entryIndex := -1
|
entryIndex := -1
|
||||||
changed := false
|
changed := false
|
||||||
err := e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
err = e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
node := nodes[0]
|
node := nodes[0]
|
||||||
current, frame, _, err := e.CompositeRichText(page, members[0])
|
current, frame, _, err := e.CompositeRichText(page, members[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -388,7 +396,7 @@ func (e *Editor) UpdateCompositeRichText(page int, path ObjectPath, index int, v
|
|||||||
if err != nil || !changed {
|
if err != nil || !changed {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
member, err := e.CompositeObject(page, path, index)
|
member, err = e.CompositeObject(page, path, index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -659,18 +667,24 @@ func (e *Editor) putRichTextMetadata(page int, id string, meta richTextMetadata,
|
|||||||
// 入参: page 页面索引, object 普通文字快照
|
// 入参: page 页面索引, object 普通文字快照
|
||||||
// 返回: RichText 单片段, error 不安全提升错误
|
// 返回: RichText 单片段, error 不安全提升错误
|
||||||
func (e *Editor) promoteRichText(page int, object GraphicObject) (RichText, error) {
|
func (e *Editor) promoteRichText(page int, object GraphicObject) (RichText, error) {
|
||||||
t := object.TextObject
|
layer, _, err := e.findObject(page, object.TextObject.ID)
|
||||||
unsupported := func() (RichText, error) {
|
|
||||||
return RichText{}, &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("text cannot be safely promoted to rich text")}
|
|
||||||
}
|
|
||||||
layer, _, err := e.findObject(page, t.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return RichText{}, err
|
return RichText{}, err
|
||||||
}
|
}
|
||||||
if layer.DrawParam != "" || t.DrawParam != "" || t.Clips != nil || len(t.Actions) != 0 || t.Alpha != nil && *t.Alpha != 255 || t.Visible != nil && !*t.Visible || t.Stroke != nil && *t.Stroke || t.Fill != nil && !*t.Fill || t.Decoration != "" || t.VScale != 0 || t.HScale != 0 && t.HScale != 1 || t.ReadDirection != 0 || t.CharDirection != 0 || t.Weight != 0 && t.Weight != 400 && t.Weight != 700 {
|
if origin := e.objectOrigin(object.TextObject.ID); layer.DrawParam != "" || origin != nil && !editorXMLSupported(origin.node) {
|
||||||
return unsupported()
|
return RichText{}, &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("text cannot be safely promoted to rich text")}
|
||||||
}
|
}
|
||||||
if origin := e.objectOrigin(t.ID); origin != nil && !editorXMLSupported(origin.node) {
|
return e.promoteTextValue(object.TextObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
// promoteTextValue 校验普通文字是否可无损提升,保留明确段落或可验证单行布局
|
||||||
|
// 入参: t 有效文字样式与局部定位
|
||||||
|
// 返回: RichText 单片段, error 布局或格式错误
|
||||||
|
func (e *Editor) promoteTextValue(t TextObject) (RichText, error) {
|
||||||
|
unsupported := func() (RichText, error) {
|
||||||
|
return RichText{}, &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("text cannot be safely promoted to rich text")}
|
||||||
|
}
|
||||||
|
if t.DrawParam != "" || t.Clips != nil || len(t.Actions) != 0 || t.Alpha != nil && *t.Alpha != 255 || t.Visible != nil && !*t.Visible || t.Stroke != nil && *t.Stroke || t.Fill != nil && !*t.Fill || t.Decoration != "" || t.VScale != 0 || t.HScale != 0 && t.HScale != 1 || t.ReadDirection != 0 || t.CharDirection != 0 || t.Weight != 0 && t.Weight != 400 && t.Weight != 700 {
|
||||||
return unsupported()
|
return unsupported()
|
||||||
}
|
}
|
||||||
frame, err := t.TextFrame()
|
frame, err := t.TextFrame()
|
||||||
@@ -754,29 +768,87 @@ func (e *Editor) richPlainOffset(text TextObject) (float64, float64, error) {
|
|||||||
return x, y - baseline, nil
|
return x, y - baseline, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// copyRichTextMetadata 复制已识别组合的语义,使用目标标准子对象的字体引用
|
// copyRichTextMetadata 迁移选区及其组合、注解内部的编辑标引,复用标准内容中的字体引用
|
||||||
// 入参: source 来源编辑器, sourcePage 来源页, ids 来源标识, page 目标页, copied 目标标识
|
// 入参: ctx 取消上下文, source 来源编辑器, sourcePage 来源页, ids 正文标识, annotations 注解标识, page 目标页, copied 新正文标识, copiedNotes 新注解标识
|
||||||
// 返回: error 元数据损坏或提交错误
|
// 返回: error 元数据损坏、结构不一致或提交错误
|
||||||
func (e *Editor) copyRichTextMetadata(source *Editor, sourcePage int, ids []string, page int, copied []string) error {
|
func (e *Editor) copyRichTextMetadata(ctx context.Context, source *Editor, sourcePage int, ids, annotations []string, page int, copied, copiedNotes []string) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
metadata := source.richTextMetadataIndex()
|
||||||
|
if metadata.err != nil || len(metadata.entries) == 0 {
|
||||||
|
return metadata.err
|
||||||
|
}
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
var copyObject func(GraphicObject, GraphicObject) error
|
||||||
|
copyObject = func(before, after GraphicObject) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if before.Type != "CompositeObject" && before.Type != "CompositeGraphicUnit" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
id := editorObjectID(after)
|
||||||
|
if id != "" {
|
||||||
|
if seen[id] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
seen[id] = true
|
||||||
|
}
|
||||||
|
entry, err := source.richTextEntry(sourcePage, editorObjectID(before))
|
||||||
|
if err == nil {
|
||||||
|
if _, err := source.richTextObject(sourcePage, before); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.putCompositeRichTextMetadata(page, after, entry.metadata, -1)
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrNotRichText) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
units := []CompositeGraphicUnit{before.CompositeGraphicUnit, after.CompositeGraphicUnit}
|
||||||
|
for i, owner := range []*Editor{source, e} {
|
||||||
|
if units[i].ResourceID != "" {
|
||||||
|
definition, err := owner.compositeDefinition(units[i].ResourceID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
units[i] = definition.object.CompositeGraphicUnit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
original, target := slices.Collect(units[0].objects()), slices.Collect(units[1].objects())
|
||||||
|
if len(original) != len(target) {
|
||||||
|
return fmt.Errorf("imported composite structure differs")
|
||||||
|
}
|
||||||
|
for i := range original {
|
||||||
|
if err := copyObject(original[i], target[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
for i, id := range ids {
|
for i, id := range ids {
|
||||||
object, err := source.Object(sourcePage, id)
|
before, err := source.Object(sourcePage, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if object.Type != "CompositeObject" {
|
after, err := e.Object(page, copied[i])
|
||||||
continue
|
|
||||||
}
|
|
||||||
entry, err := source.richTextEntry(sourcePage, id)
|
|
||||||
if errors.Is(err, ErrNotRichText) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := source.RichText(sourcePage, id); err != nil {
|
if err := copyObject(before, after); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := e.putRichTextMetadata(page, copied[i], entry.metadata, -1); err != nil {
|
}
|
||||||
|
for i, id := range annotations {
|
||||||
|
before, err := source.Annotation(sourcePage, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
after, err := e.Annotation(page, copiedNotes[i])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := copyObject(GraphicObject{Type: "CompositeGraphicUnit", CompositeGraphicUnit: CompositeGraphicUnit{Objects: before.Appearance.Objects}}, GraphicObject{Type: "CompositeGraphicUnit", CompositeGraphicUnit: CompositeGraphicUnit{Objects: after.Appearance.Objects}}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -945,3 +1017,102 @@ func richTextMetadataFontSafe(value string) bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formatCompositeText 修改内部富文本格式,普通文字安全提升后保留原实例变换
|
||||||
|
// 入参: page 页面索引, path 父路径, index 成员序号, patch 格式增量
|
||||||
|
// 返回: error 识别、排版或提交错误
|
||||||
|
func (e *Editor) formatCompositeText(page int, path ObjectPath, index int, patch TextFormatPatch) error {
|
||||||
|
value, err := e.TargetRichText(page, ObjectPath{ID: path.ID, Annotation: path.Annotation, Children: append(slices.Clone(path.Children), index)})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := value.ApplyFormat(0, value.Len(), patch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.UpdateCompositeRichText(page, path, index, value, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateCompositePlainRichText 将可验证布局的内部文字提升为富文本并保留实例坐标
|
||||||
|
// 入参: page 页面索引, path 父路径, index 成员序号, value 富文本内容, offset 局部偏移, width 可选宽度
|
||||||
|
// 返回: error 校验或提交错误
|
||||||
|
func (e *Editor) updateCompositePlainRichText(page int, path ObjectPath, index int, value RichText, offset, width float64) error {
|
||||||
|
var metadata richTextMetadata
|
||||||
|
if err := e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
|
node, text := nodes[0], members[0].Object.TextObject
|
||||||
|
if node.object.Type != "TextObject" || !editorXMLSupported(node.span) {
|
||||||
|
return ErrNotRichText
|
||||||
|
}
|
||||||
|
text.CTM = ""
|
||||||
|
_, err := e.promoteTextValue(text)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := value.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
frame, err := text.TextFrame()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dx, dy, err := e.richPlainOffset(text)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
object := node.object
|
||||||
|
if !node.boundaryInCTM {
|
||||||
|
object = compositeBoundary(object, node.parent, true)
|
||||||
|
}
|
||||||
|
box, err := creationBox(object.TextObject.Boundary)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
matrix := TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(object.TextObject.CTM)).Multiply(TranslationMatrix(dx+offset, dy))
|
||||||
|
if width > 0 {
|
||||||
|
frame.W = width
|
||||||
|
}
|
||||||
|
next, meta, err := e.prepareRichText(Box{W: frame.W, H: frame.H}, value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
unit := richTextPlacement(meta, matrix)
|
||||||
|
unit.ID, unit.ResourceID = object.TextObject.ID, next.CompositeGraphicUnit.ResourceID
|
||||||
|
next.CompositeGraphicUnit = unit
|
||||||
|
if !node.boundaryInCTM {
|
||||||
|
next = compositeBoundary(next, node.parent, false)
|
||||||
|
}
|
||||||
|
metadata = meta
|
||||||
|
return node.update(next)
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
member, err := e.CompositeObject(page, path, index)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.putCompositeRichTextMetadata(page, member.Object, metadata, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TargetRichText 读取完整路径文字的富文本模型,普通文字仅在可验证布局时提升
|
||||||
|
// 入参: page 页面索引, path 完整对象路径
|
||||||
|
// 返回: RichText 独立模型, error 路径或不安全提升错误
|
||||||
|
func (e *Editor) TargetRichText(page int, path ObjectPath) (RichText, error) {
|
||||||
|
if len(path.Children) == 0 {
|
||||||
|
if path.Annotation != "" {
|
||||||
|
return RichText{}, ErrNotRichText
|
||||||
|
}
|
||||||
|
return e.RichText(page, path.ID)
|
||||||
|
}
|
||||||
|
index := path.Children[len(path.Children)-1]
|
||||||
|
path.Children = path.Children[:len(path.Children)-1]
|
||||||
|
member, err := e.CompositeObject(page, path, index)
|
||||||
|
if err != nil {
|
||||||
|
return RichText{}, err
|
||||||
|
}
|
||||||
|
if member.Object.Type == "TextObject" {
|
||||||
|
text := member.Object.TextObject
|
||||||
|
text.CTM = ""
|
||||||
|
return e.promoteTextValue(text)
|
||||||
|
}
|
||||||
|
value, _, _, err := e.CompositeRichText(page, member)
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
+149
-3
@@ -16,9 +16,12 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ObjectSelection 保存正文、注解及组合成员的独立快照,共享当前编辑器的不可变资源
|
// ObjectSelection 保存正文、注解及组合成员的独立快照,共享当前编辑器的不可变资源
|
||||||
@@ -863,12 +866,155 @@ func (e *Editor) ArrangeTargets(page int, paths []ObjectPath, distribute bool, d
|
|||||||
// 入参: page 页面索引, paths 完整文字路径, style 字体、字号或颜色增量
|
// 入参: page 页面索引, paths 完整文字路径, style 字体、字号或颜色增量
|
||||||
// 返回: error 路径、文字或样式错误
|
// 返回: error 路径、文字或样式错误
|
||||||
func (e *Editor) StyleTargetsText(page int, paths []ObjectPath, style TextStyle) error {
|
func (e *Editor) StyleTargetsText(page int, paths []ObjectPath, style TextStyle) error {
|
||||||
return e.editTargets(paths, func(objects, annotations []string) error {
|
patch := TextFormatPatch{}
|
||||||
|
if style.Font != "" {
|
||||||
|
patch.Font = &style.Font
|
||||||
|
}
|
||||||
|
if style.Size != 0 {
|
||||||
|
patch.Size = &style.Size
|
||||||
|
}
|
||||||
|
if style.Color != "" {
|
||||||
|
patch.Color = &style.Color
|
||||||
|
}
|
||||||
|
return e.formatTargetsText(page, paths, patch, style.Reflow)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatTargetsText 原子修改普通文字和富文本的指定格式,保留未指定的混排属性
|
||||||
|
// 入参: page 页面索引, paths 完整文字路径, patch 格式增量
|
||||||
|
// 返回: error 路径、格式或不安全重排错误
|
||||||
|
func (e *Editor) FormatTargetsText(page int, paths []ObjectPath, patch TextFormatPatch) error {
|
||||||
|
return e.formatTargetsText(page, paths, patch, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatTargetsText 统一文字选区格式事务,普通字体和颜色修改沿用定位保留路径
|
||||||
|
// 入参: page 页面索引, paths 完整路径, patch 格式增量, reflow 显式重排
|
||||||
|
// 返回: error 校验或编辑错误
|
||||||
|
func (e *Editor) formatTargetsText(page int, paths []ObjectPath, patch TextFormatPatch, reflow bool) error {
|
||||||
|
style := TextStyle{Reflow: reflow}
|
||||||
|
if patch.Font != nil {
|
||||||
|
if strings.TrimSpace(*patch.Font) == "" {
|
||||||
|
return fmt.Errorf("text requires a font")
|
||||||
|
}
|
||||||
|
style.Font = *patch.Font
|
||||||
|
}
|
||||||
|
if patch.Size != nil {
|
||||||
|
if !finite(*patch.Size) || *patch.Size <= 0 {
|
||||||
|
return fmt.Errorf("invalid text size")
|
||||||
|
}
|
||||||
|
style.Size = *patch.Size
|
||||||
|
}
|
||||||
|
if patch.Color != nil {
|
||||||
|
style.Color = *patch.Color
|
||||||
|
if style.Color == "" {
|
||||||
|
style.Color = "0 0 0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
decorated := patch.Bold != nil || patch.Italic != nil || patch.Underline != nil || patch.Strikeout != nil || patch.Shadow != nil || patch.Background != nil
|
||||||
|
return e.editTargets(paths, func(ids, annotations []string) error {
|
||||||
if len(annotations) != 0 {
|
if len(annotations) != 0 {
|
||||||
return fmt.Errorf("annotation is not a text object")
|
return fmt.Errorf("annotation is not a text object")
|
||||||
}
|
}
|
||||||
return e.StyleText(page, objects, style)
|
objects, _, err := e.selectedObjects(page, ids)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var plain []string
|
||||||
|
for _, object := range objects {
|
||||||
|
id := editorObjectID(object)
|
||||||
|
if object.Type == "TextObject" && !decorated {
|
||||||
|
plain = append(plain, id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value, err := e.richTextObject(page, object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := value.ApplyFormat(0, value.Len(), patch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := e.UpdateRichText(page, id, value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(plain) != 0 {
|
||||||
|
return e.StyleText(page, plain, style)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}, func(path ObjectPath, indexes []int) error {
|
}, func(path ObjectPath, indexes []int) error {
|
||||||
return e.StyleCompositeText(page, path, indexes, style)
|
members, err := e.CompositeObjectsAt(page, path, indexes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var plain []int
|
||||||
|
for i, member := range members {
|
||||||
|
if member.Object.Type == "TextObject" && !decorated {
|
||||||
|
plain = append(plain, indexes[i])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := e.formatCompositeText(page, path, indexes[i], patch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(plain) != 0 {
|
||||||
|
return e.StyleCompositeText(page, path, plain, style)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteTargetsTo 将选区及其关联资源写成独立单页OFD,来源关闭后仍可导入
|
||||||
|
// 不修改来源、历史或组合结构,加密来源须显式允许明文导出
|
||||||
|
// 不包含页面模板和签名,指向其他页面的跳转随单页导出移除
|
||||||
|
// 入参: ctx 取消上下文, writer 输出流, page 来源页, paths 完整对象路径, allowDecrypted 允许明文导出
|
||||||
|
// 返回: int64 写入字节数, error 路径、资源或写入错误
|
||||||
|
func (e *Editor) WriteTargetsTo(ctx context.Context, writer io.Writer, page int, paths []ObjectPath, allowDecrypted bool) (int64, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if e.encryption != nil && !allowDecrypted {
|
||||||
|
return 0, ErrEncryptionPolicyRequired
|
||||||
|
}
|
||||||
|
if len(paths) == 0 {
|
||||||
|
return 0, fmt.Errorf("no objects selected")
|
||||||
|
}
|
||||||
|
snapshot := e.transactionSnapshot()
|
||||||
|
snapshot.history, snapshot.historyIndex, snapshot.historyLimit = nil, 0, 0
|
||||||
|
snapshot.transactionChanges = nil
|
||||||
|
for id, origin := range snapshot.origins {
|
||||||
|
if origin != nil && origin.editor == e {
|
||||||
|
copy := *origin
|
||||||
|
copy.editor = &snapshot
|
||||||
|
snapshot.origins[id] = ©
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selection, err := snapshot.CaptureTargets(page, paths)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
content, err := snapshot.page(page)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
objects, annotations, err := snapshot.PasteSelection(page, selection, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
box, err := creationBox(content.Area.PhysicalBox)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
packet := NewEditor()
|
||||||
|
packet.SetRenderBackends(e.backends)
|
||||||
|
packet.SetResources(e.runtimeResources)
|
||||||
|
packet.SetFontDirs(e.fontDirs...)
|
||||||
|
packet.SetFontFS(e.fontFS...)
|
||||||
|
packet.OnWriteProgress = e.OnWriteProgress
|
||||||
|
if _, err := packet.AddPage(box.W, box.H); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
packet.pages[0].Area = content.Area
|
||||||
|
if _, _, err := packet.importSelection(ctx, &snapshot, page, objects, annotations, 0, ObjectImportOptions{AllowDecrypted: true, OnProgress: e.OnWriteProgress}, false); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return packet.WriteToWithOptions(ctx, writer, WriteOptions{})
|
||||||
|
}
|
||||||
Reference in new issue
Block a user