mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
1 parent
fa8fec9830
commit
0f86c8d727
17 files changed
+842
-127
No files matched your search
@@ -1425,6 +1425,10 @@ button.outline-link:hover {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-text-source {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.edit-text {
|
.edit-text {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|||||||
+23
-18
@@ -402,6 +402,7 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
|||||||
const editing = canvasEditor.input;
|
const editing = canvasEditor.input;
|
||||||
updatePendingChanges();
|
updatePendingChanges();
|
||||||
if (!editing) {
|
if (!editing) {
|
||||||
|
updateObjectControls(canvasEditor.selected);
|
||||||
updateTextFonts(canvasEditor.selected, true);
|
updateTextFonts(canvasEditor.selected, true);
|
||||||
syncSelection();
|
syncSelection();
|
||||||
if (state.fontRenderPending) window.setTimeout(refreshPendingFonts, 0);
|
if (state.fontRenderPending) window.setTimeout(refreshPendingFonts, 0);
|
||||||
@@ -568,6 +569,7 @@ el.resetCropButton.addEventListener("click", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const item = canvasEditor.selected;
|
const item = canvasEditor.selected;
|
||||||
|
if (item?.scoped) return changeDocument("ofdgoResetCompositeCrop", item);
|
||||||
if (item?.imageBounds) return canvasEditor.options.onCrop(item, item.imageBounds);
|
if (item?.imageBounds) return canvasEditor.options.onCrop(item, item.imageBounds);
|
||||||
});
|
});
|
||||||
editorClick(el.undoButton, () => changeDocument("ofdgoUndo"));
|
editorClick(el.undoButton, () => changeDocument("ofdgoUndo"));
|
||||||
@@ -1212,7 +1214,6 @@ function rememberEditorView(before, revision, reindex = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pageCan(capability, index = state.pageIndex) {
|
function pageCan(capability, index = state.pageIndex) {
|
||||||
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];
|
||||||
return Boolean(state.editorInfo?.pageCapabilities?.[index] & bit);
|
return Boolean(state.editorInfo?.pageCapabilities?.[index] & bit);
|
||||||
}
|
}
|
||||||
@@ -1639,7 +1640,7 @@ async function insertObject(event) {
|
|||||||
const doc = item?.scoped
|
const doc = item?.scoped
|
||||||
? await callWASM("ofdgoChangeCompositeObjects", item.index, state.composite.key, [item.id], "image", data)
|
? await callWASM("ofdgoChangeCompositeObjects", item.index, state.composite.key, [item.id], "image", data)
|
||||||
: item ? await callWASM("ofdgoReplaceImage", item.index, item.id, data, el.insertFit.value)
|
: item ? await callWASM("ofdgoReplaceImage", item.index, item.id, data, el.insertFit.value)
|
||||||
: await callWASM("ofdgoInsertImage", index, data, x, y, page.width - x * 2);
|
: await callWASM("ofdgoInsertImage", index, data, x, y, page.width - x * 2, state.composite?.key || "");
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1649,13 +1650,13 @@ async function insertObject(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setEditorInfo(doc);
|
setEditorInfo(doc);
|
||||||
canvasEditor.pendingSelection = item ? null : { index };
|
canvasEditor.pendingSelection = item ? null : { index, ids: doc.selectedIDs };
|
||||||
state.selectObjects = true;
|
state.selectObjects = true;
|
||||||
setPan(false);
|
setPan(false);
|
||||||
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);
|
rememberEditorView(before, revision, !item && !!state.composite);
|
||||||
el.viewerPanel.focus({ preventScroll: true });
|
el.viewerPanel.focus({ preventScroll: true });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1858,10 +1859,12 @@ 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",
|
ofdgoUpdateText: "text", ofdgoStyleText: "textStyle", ofdgoCropImage: "crop", ofdgoLayoutText: "layout", ofdgoFitImage: "fit", ofdgoResetCompositeCrop: "resetCrop",
|
||||||
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);
|
||||||
|
const inserting = ["ofdgoInsertShape", "ofdgoInsertText", "ofdgoInsertImage"].includes(name);
|
||||||
|
if (scope && inserting) args.push(scope.key);
|
||||||
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)
|
||||||
@@ -1874,7 +1877,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
}
|
}
|
||||||
const view = name === "ofdgoUndo" ? state.editorViews.get(revision) : name === "ofdgoRedo" ? state.editorViews.get(doc.revision) : null;
|
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)
|
const reindex = !!scope && (scoped && ["ofdgoDeleteObject", "ofdgoDeleteObjects", "ofdgoCopyObjects", "ofdgoOrderObjects"].includes(name)
|
||||||
|| name === "ofdgoPasteObjects" && !!args[4]);
|
|| inserting || name === "ofdgoPasteObjects" && !!args[4]);
|
||||||
const reindexed = restoring ? view?.reindex && view.before : reindex && before;
|
const reindexed = restoring ? view?.reindex && view.before : reindex && before;
|
||||||
const clipboard = state.objectClipboard;
|
const clipboard = state.objectClipboard;
|
||||||
if (reindexed && clipboard?.scope?.startsWith(reindexed.scope + "/") && clipboard.page === reindexed.page) state.objectClipboard = null;
|
if (reindexed && clipboard?.scope?.startsWith(reindexed.scope + "/") && clipboard.page === reindexed.page) state.objectClipboard = null;
|
||||||
@@ -1882,7 +1885,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
const restoredIndex = location ? doc.pages.findIndex(page => page.id === location.page) : -1;
|
const restoredIndex = location ? doc.pages.findIndex(page => page.id === location.page) : -1;
|
||||||
if (restoredIndex >= 0) {
|
if (restoredIndex >= 0) {
|
||||||
state.composite = location.scope ? { index: restoredIndex, key: location.scope, objects: [] } : null;
|
state.composite = location.scope ? { index: restoredIndex, key: location.scope, objects: [] } : null;
|
||||||
} else if (!item && !(name === "ofdgoPasteObjects" && args[4])) {
|
} else if (!item && !inserting && !(name === "ofdgoPasteObjects" && args[4])) {
|
||||||
resetCompositeScope();
|
resetCompositeScope();
|
||||||
}
|
}
|
||||||
if (restoring && JSON.stringify(doc.outlines) !== JSON.stringify(state.doc.outlines)) {
|
if (restoring && JSON.stringify(doc.outlines) !== JSON.stringify(state.doc.outlines)) {
|
||||||
@@ -3752,7 +3755,8 @@ 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, scope: state.composite?.key || "", 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,
|
||||||
|
compound: items.some(item => item.type === "CompositeObject" || item.type === "CompositeGraphicUnit") };
|
||||||
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;
|
||||||
@@ -3807,13 +3811,14 @@ 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) {
|
const scope = state.composite?.key || "";
|
||||||
setStatus("请回到原编辑范围粘贴");
|
if (clipboard.compound && (scope || clipboard.scope) && (scope !== clipboard.scope || page.id !== clipboard.page)) {
|
||||||
|
setStatus("复合对象请在原范围粘贴");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!clipboard.scope && !pageCan("insert")) return;
|
if (!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.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]) {
|
||||||
@@ -3821,8 +3826,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, clipboard.scope || "")) {
|
if (await changeDocument("ofdgoPasteObjects", null, index, token, offset.x, offset.y, state.composite?.key || "")) {
|
||||||
clipboard.page = page.id;
|
clipboard.targetPage = page.id;
|
||||||
clipboard.x = offset.x;
|
clipboard.x = offset.x;
|
||||||
clipboard.y = offset.y;
|
clipboard.y = offset.y;
|
||||||
clipboard.cut = false;
|
clipboard.cut = false;
|
||||||
@@ -5605,25 +5610,25 @@ function updateObjectControls(item, reset = false) {
|
|||||||
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, "cropImage");
|
el.cropImageButton.disabled = disabled || item.type !== "ImageObject" || !canEditObject(item, "cropImage");
|
||||||
el.imageFit.disabled = el.cropImageButton.disabled || cropping || Boolean(item.scoped);
|
el.imageFit.disabled = el.cropImageButton.disabled || cropping || Boolean(item.scoped && !canEditObject(item, "fitImage"));
|
||||||
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.scoped || !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.cropped : !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"));
|
||||||
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(state.composite && !selection) || Boolean(item?.items && !selection);
|
const textDisabled = !state.editing || !state.ready || state.exporting || Boolean(item?.items && !selection);
|
||||||
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) && !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 || state.composite);
|
el.textAlign.disabled = el.textWrap.disabled = el.textLineHeight.disabled = el.textSpacing.disabled = el.textSize.disabled || Boolean(item?.items || item?.draft) || Boolean(canvasEditor.input);
|
||||||
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" : "";
|
||||||
|
|||||||
@@ -1024,6 +1024,7 @@ export class CanvasEditor {
|
|||||||
});
|
});
|
||||||
this.input = { input, item, face, fontChoice: item.fontChoice };
|
this.input = { input, item, face, fontChoice: item.fontChoice };
|
||||||
const editing = this.input;
|
const editing = this.input;
|
||||||
|
item.artwork?.classList.add("edit-text-source");
|
||||||
item.surface.append(input);
|
item.surface.append(input);
|
||||||
input.addEventListener("input", () => this.options.onTextChange());
|
input.addEventListener("input", () => this.options.onTextChange());
|
||||||
input.addEventListener("compositionstart", () => { editing.composing = true; });
|
input.addEventListener("compositionstart", () => { editing.composing = true; });
|
||||||
@@ -1141,6 +1142,7 @@ export class CanvasEditor {
|
|||||||
this.input = null;
|
this.input = null;
|
||||||
if (editing) {
|
if (editing) {
|
||||||
editing.input.remove();
|
editing.input.remove();
|
||||||
|
editing.item.artwork?.classList.remove("edit-text-source");
|
||||||
document.fonts.delete(editing.face);
|
document.fonts.delete(editing.face);
|
||||||
if (editing.item.draft && this.selected === editing.item) this.select(null);
|
if (editing.item.draft && this.selected === editing.item) this.select(null);
|
||||||
this.options.onTextChange();
|
this.options.onTextChange();
|
||||||
|
|||||||
+92
-22
@@ -833,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, "cropImage": capability.CropImage, "enter": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
|
"replaceImage": capability.ReplaceImage, "cropImage": capability.CropImage, "fitImage": capability.FitImage, "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}
|
||||||
}
|
}
|
||||||
@@ -925,17 +925,35 @@ func compositeObjects(args []js.Value) (any, error) {
|
|||||||
editorAppearance(item, member.Object, member.Capabilities.Paint, member.StrokeScale)
|
editorAppearance(item, member.Object, member.Capabilities.Paint, member.StrokeScale)
|
||||||
if member.Object.Type == "TextObject" {
|
if member.Object.Type == "TextObject" {
|
||||||
object := member.Object.TextObject
|
object := member.Object.TextObject
|
||||||
item["text"], _ = object.TextLayout()
|
value, layout := object.TextLayout()
|
||||||
|
item["text"] = value
|
||||||
|
item["wrap"], item["align"], item["paragraphHeight"] = layout.Wrap, layout.Align, layout.LineHeight
|
||||||
|
item["letterSpacing"] = layout.LetterSpacing
|
||||||
|
item["leftIndent"], item["rightIndent"], item["firstLineIndent"] = layout.LeftIndent, layout.RightIndent, layout.FirstLineIndent
|
||||||
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)
|
item["bounds"] = editorBox(box)
|
||||||
if member.Capabilities.Reflow {
|
if member.Capabilities.Reflow {
|
||||||
inverse, _ := member.Matrix.Invert()
|
inverse, _ := member.Matrix.Invert()
|
||||||
local := inverse.TransformBox(box)
|
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)}
|
width := math.Max(object.Size, local.X+local.W)
|
||||||
|
if layout.Wrap {
|
||||||
|
frame, err := object.TextFrame()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
width = frame.W
|
||||||
|
}
|
||||||
|
frame := ofdgo.Box{W: width, H: math.Max(object.Size, local.Y+local.H)}
|
||||||
|
item["textFrame"] = map[string]any{"width": frame.W, "height": frame.H, "matrix": editorMatrix(member.Matrix)}
|
||||||
|
if layout.Wrap {
|
||||||
|
bounds := member.Matrix.TransformBox(frame)
|
||||||
|
item["x"], item["y"], item["width"], item["height"] = bounds.X, bounds.Y, bounds.W, bounds.H
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if member.Capabilities.CropImage {
|
if member.Capabilities.CropImage {
|
||||||
item["imageBounds"] = editorBox(box)
|
item["imageBounds"] = editorBox(box)
|
||||||
|
item["cropped"] = member.Cropped
|
||||||
}
|
}
|
||||||
objects = append(objects, item)
|
objects = append(objects, item)
|
||||||
}
|
}
|
||||||
@@ -970,6 +988,31 @@ func changeCompositeObjects(args []js.Value) (any, error) {
|
|||||||
return err
|
return err
|
||||||
case "text", "textStyle":
|
case "text", "textStyle":
|
||||||
return changeCompositeText(page, path, indexes, operation, args[4:])
|
return changeCompositeText(page, path, indexes, operation, args[4:])
|
||||||
|
case "layout":
|
||||||
|
if len(indexes) != 1 {
|
||||||
|
return fmt.Errorf("text layout requires one member")
|
||||||
|
}
|
||||||
|
layout := ofdgo.TextLayout{Wrap: args[6].Bool(), Align: args[7].String(), LineHeight: args[8].Float(), LetterSpacing: args[9].Float(), LeftIndent: args[10].Float(), RightIndent: args[11].Float(), FirstLineIndent: args[12].Float()}
|
||||||
|
if !args[4].IsNull() {
|
||||||
|
return currentEditor.ResizeCompositeTextFrame(page, path, indexes[0], args[4].Float(), args[5].Float(), layout)
|
||||||
|
}
|
||||||
|
members, err := currentEditor.CompositeObjects(page, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if indexes[0] >= len(members) {
|
||||||
|
return fmt.Errorf("text member is unavailable")
|
||||||
|
}
|
||||||
|
value, _ := members[indexes[0]].Object.TextObject.TextLayout()
|
||||||
|
return currentEditor.LayoutCompositeText(page, path, indexes[0], value, layout)
|
||||||
|
case "fit", "resetCrop":
|
||||||
|
if len(indexes) != 1 {
|
||||||
|
return fmt.Errorf("image operation requires one member")
|
||||||
|
}
|
||||||
|
if operation == "fit" {
|
||||||
|
return currentEditor.FitCompositeImage(page, path, indexes[0], args[4].String())
|
||||||
|
}
|
||||||
|
return currentEditor.ResetCompositeImageCrop(page, path, indexes[0])
|
||||||
case "crop":
|
case "crop":
|
||||||
if len(indexes) != 1 {
|
if len(indexes) != 1 {
|
||||||
return fmt.Errorf("image crop requires one member")
|
return fmt.Errorf("image crop requires one member")
|
||||||
@@ -1273,16 +1316,22 @@ 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 {
|
key := ""
|
||||||
if len(args) < 5 || args[4].String() == "" {
|
if len(args) > 4 {
|
||||||
return nil, fmt.Errorf("paste requires the original composite scope")
|
key = args[4].String()
|
||||||
}
|
}
|
||||||
key := args[4].String()
|
page, dx, dy := args[0].Int(), args[2].Float(), args[3].Float()
|
||||||
|
if key != "" {
|
||||||
path, err := compositePath(key)
|
path, err := compositePath(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
indexes, err := currentEditor.PasteCompositeObjects(args[0].Int(), path, copiedObjects.composite, args[2].Float(), args[3].Float())
|
var indexes []int
|
||||||
|
if copiedObjects.composite != nil {
|
||||||
|
indexes, err = currentEditor.PasteCompositeObjects(page, path, copiedObjects.composite, dx, dy)
|
||||||
|
} else {
|
||||||
|
indexes, err = currentEditor.CopyObjectsToComposite(page, path, copiedObjects.objects, dx, dy)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1293,10 +1342,15 @@ func pasteObjects(args []js.Value) (any, error) {
|
|||||||
}
|
}
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
if len(args) > 4 && args[4].String() != "" {
|
if copiedObjects.composite != nil {
|
||||||
return nil, fmt.Errorf("top-level objects cannot be pasted into a composite")
|
ids, err := currentEditor.PasteCompositeSelection(page, copiedObjects.composite, dx, dy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
info, err := previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
||||||
|
return editorSelectionInfo{info, ids}, err
|
||||||
}
|
}
|
||||||
return copyEditorObjects(args[0].Int(), copiedObjects.objects, args[2].Float(), args[3].Float())
|
return copyEditorObjects(page, copiedObjects.objects, dx, dy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// copyEditorObjects 复制快照并更新预览,返回新对象标识
|
// copyEditorObjects 复制快照并更新预览,返回新对象标识
|
||||||
@@ -1961,10 +2015,7 @@ func insertShape(args []js.Value) (any, error) {
|
|||||||
if err := setPathStyle(&path, args[6:]); err != nil {
|
if err := setPathStyle(&path, args[6:]); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, err := currentEditor.AddObject(args[0].Int(), ofdgo.GraphicObject{Type: "PathObject", PathObject: path}); err != nil {
|
return insertEditorObject(args[0].Int(), ofdgo.GraphicObject{Type: "PathObject", PathObject: path}, args[11:])
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// updatePathStyle 原子更新选中路径的绘制样式,保留未修改属性
|
// updatePathStyle 原子更新选中路径的绘制样式,保留未修改属性
|
||||||
@@ -2031,10 +2082,7 @@ func insertText(args []js.Value) (any, error) {
|
|||||||
if err := setTextColor(&object.TextObject, args[6].String()); err != nil {
|
if err := setTextColor(&object.TextObject, args[6].String()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, err := currentEditor.AddObject(page, object); err != nil {
|
return insertEditorObject(page, object, args[15:])
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// layoutText 调整文字框和段落排版,保留软换行前的原文
|
// layoutText 调整文字框和段落排版,保留软换行前的原文
|
||||||
@@ -2274,10 +2322,32 @@ func insertImage(args []js.Value) (any, error) {
|
|||||||
object := ofdgo.GraphicObject{Type: "ImageObject", ImageObject: ofdgo.ImageObject{
|
object := ofdgo.GraphicObject{Type: "ImageObject", ImageObject: ofdgo.ImageObject{
|
||||||
ResourceID: resourceID, Boundary: fmt.Sprintf("%g %g %g %g", box.X, box.Y, box.W, box.H),
|
ResourceID: resourceID, Boundary: fmt.Sprintf("%g %g %g %g", box.X, box.Y, box.W, box.H),
|
||||||
}}
|
}}
|
||||||
if _, err := currentEditor.AddObject(args[0].Int(), object); err != nil {
|
return insertEditorObject(args[0].Int(), object, args[5:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertEditorObject 复用顶层和内部范围插入流程,返回稳定选区
|
||||||
|
// 入参: page 页码, object 新对象, scope 可选父路径
|
||||||
|
// 返回: any 文档信息及新选区, error 错误信息
|
||||||
|
func insertEditorObject(page int, object ofdgo.GraphicObject, scope []js.Value) (any, error) {
|
||||||
|
var id string
|
||||||
|
var err error
|
||||||
|
if len(scope) > 0 && scope[0].String() != "" {
|
||||||
|
key := scope[0].String()
|
||||||
|
path, parseErr := compositePath(key)
|
||||||
|
if parseErr != nil {
|
||||||
|
return nil, parseErr
|
||||||
|
}
|
||||||
|
var index int
|
||||||
|
index, err = currentEditor.AddCompositeObject(page, path, object)
|
||||||
|
id = fmt.Sprintf("%s/%d", key, index)
|
||||||
|
} else {
|
||||||
|
id, err = currentEditor.AddObject(page, object)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
info, err := previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
||||||
|
return editorSelectionInfo{info, []string{id}}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorOperationProgress 节流长操作检查点并向浏览器让出执行,保证阶段开始和结束可取消
|
// editorOperationProgress 节流长操作检查点并向浏览器让出执行,保证阶段开始和结束可取消
|
||||||
|
|||||||
@@ -75,6 +75,17 @@ type editorResource struct {
|
|||||||
composite string
|
composite string
|
||||||
references []string
|
references []string
|
||||||
subset *editorFontSubset
|
subset *editorFontSubset
|
||||||
|
states map[string]editorCompositeState
|
||||||
|
draw *DrawParam
|
||||||
|
}
|
||||||
|
|
||||||
|
// definition 获取独立定义资源的标识
|
||||||
|
// 返回: string 复合资源或绘制参数标识
|
||||||
|
func (r editorResource) definition() string {
|
||||||
|
if r.draw != nil {
|
||||||
|
return r.draw.ID
|
||||||
|
}
|
||||||
|
return r.composite
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorResourceKey 资源内容和字体集合索引
|
// editorResourceKey 资源内容和字体集合索引
|
||||||
|
|||||||
+137
-27
@@ -21,6 +21,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"math"
|
"math"
|
||||||
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
@@ -45,6 +46,7 @@ type CompositeMember struct {
|
|||||||
Capabilities ObjectCapabilities
|
Capabilities ObjectCapabilities
|
||||||
Position ObjectPosition
|
Position ObjectPosition
|
||||||
Transform bool
|
Transform bool
|
||||||
|
Cropped bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorCompositeNode 保留原始XML及实例上下文,不修改共享资源
|
// editorCompositeNode 保留原始XML及实例上下文,不修改共享资源
|
||||||
@@ -58,6 +60,7 @@ type editorCompositeNode struct {
|
|||||||
drawParams []string
|
drawParams []string
|
||||||
clip *canvas.Path
|
clip *canvas.Path
|
||||||
visible bool
|
visible bool
|
||||||
|
alpha *int
|
||||||
ref *editorCompositeNode
|
ref *editorCompositeNode
|
||||||
children []*editorCompositeNode
|
children []*editorCompositeNode
|
||||||
expanded bool
|
expanded bool
|
||||||
@@ -66,6 +69,81 @@ type editorCompositeNode struct {
|
|||||||
owner *editorCompositeNode
|
owner *editorCompositeNode
|
||||||
patches []editorXMLPatch
|
patches []editorXMLPatch
|
||||||
index int
|
index int
|
||||||
|
states map[string]editorCompositeState
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorCompositeState 保存内部对象的会话排版及新增裁剪起点,不写入OFD
|
||||||
|
type editorCompositeState struct {
|
||||||
|
layout *textLayout
|
||||||
|
crop *editorCompositeCrop
|
||||||
|
origin [2]float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorCompositeCrop 记录编辑前的裁剪数量及节点存在性
|
||||||
|
type editorCompositeCrop struct {
|
||||||
|
count int
|
||||||
|
exists bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// setStates 独立保存当前XML子树的会话信息,共享不可变记录
|
||||||
|
// 入参: states 来源记录
|
||||||
|
func (n *editorCompositeNode) setStates(states map[string]editorCompositeState) {
|
||||||
|
n.states = make(map[string]editorCompositeState)
|
||||||
|
var visit func(*editorXML)
|
||||||
|
visit = func(node *editorXML) {
|
||||||
|
if state, ok := states[node.attr("ID")]; ok {
|
||||||
|
n.states[node.attr("ID")] = state
|
||||||
|
}
|
||||||
|
for _, child := range node.children {
|
||||||
|
visit(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visit(n.node)
|
||||||
|
n.object.TextObject.layout = n.states[editorObjectID(n.object)].layout
|
||||||
|
}
|
||||||
|
|
||||||
|
// setState 替换当前对象的会话记录并计入本次事务
|
||||||
|
// 入参: state 新记录
|
||||||
|
func (n *editorCompositeNode) setState(state editorCompositeState) {
|
||||||
|
id := editorObjectID(n.object)
|
||||||
|
if reflect.DeepEqual(n.states[id], state) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n.states == nil {
|
||||||
|
n.states = make(map[string]editorCompositeState)
|
||||||
|
}
|
||||||
|
if state == (editorCompositeState{}) {
|
||||||
|
delete(n.states, id)
|
||||||
|
} else {
|
||||||
|
n.states[id] = state
|
||||||
|
}
|
||||||
|
n.object.TextObject.layout = state.layout
|
||||||
|
n.changed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// remapCompositeStates 随XML标识重映射会话记录,仅保留当前子树的对象
|
||||||
|
// 入参: states 原记录, ids 标识映射
|
||||||
|
// 返回: map[string]editorCompositeState 新记录
|
||||||
|
func remapCompositeStates(states map[string]editorCompositeState, ids map[string]string) map[string]editorCompositeState {
|
||||||
|
var result map[string]editorCompositeState
|
||||||
|
for id, next := range ids {
|
||||||
|
if state, ok := states[id]; ok {
|
||||||
|
if result == nil {
|
||||||
|
result = make(map[string]editorCompositeState)
|
||||||
|
}
|
||||||
|
result[next] = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeCompositeStates 清除子树旧记录,再由写入结果合并新记录
|
||||||
|
// 入参: states 目标记录, node 子树根节点
|
||||||
|
func removeCompositeStates(states map[string]editorCompositeState, node *editorXML) {
|
||||||
|
delete(states, node.attr("ID"))
|
||||||
|
for _, child := range node.children {
|
||||||
|
removeCompositeStates(states, child)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompositeObjects 枚举复合对象的直接可绘制成员,资源引用层不占用路径层级
|
// CompositeObjects 枚举复合对象的直接可绘制成员,资源引用层不占用路径层级
|
||||||
@@ -124,25 +202,29 @@ func (e *Editor) ResizeCompositeObjects(page int, path ObjectPath, indexes []int
|
|||||||
if _, err := creationBox(editorBoxString(box)); err != nil {
|
if _, err := creationBox(editorBoxString(box)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
members, err := e.CompositeObjects(page, path)
|
return e.editCompositeObjects(page, path, indexes, func(renderer *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
if err != nil {
|
var before Box
|
||||||
return err
|
for _, member := range members {
|
||||||
}
|
if !member.Capabilities.Transform {
|
||||||
var before Box
|
return member.Capabilities.editError()
|
||||||
for _, i := range indexes {
|
}
|
||||||
if i < 0 || i >= len(members) {
|
before = unionTextBox(before, member.Bounds)
|
||||||
return fmt.Errorf("invalid composite selection")
|
|
||||||
}
|
}
|
||||||
before = unionTextBox(before, members[i].Bounds)
|
if before.W <= 0 || before.H <= 0 {
|
||||||
}
|
return fmt.Errorf("selection has no visible bounds")
|
||||||
if before.W <= 0 || before.H <= 0 {
|
}
|
||||||
return fmt.Errorf("selection has no visible bounds")
|
sx, sy := box.W/before.W, box.H/before.H
|
||||||
}
|
if math.Abs(sx-sy) > 1e-9*math.Max(sx, sy) {
|
||||||
sx, sy := box.W/before.W, box.H/before.H
|
return fmt.Errorf("composite members require proportional resizing")
|
||||||
if math.Abs(sx-sy) > 1e-9*math.Max(sx, sy) {
|
}
|
||||||
return fmt.Errorf("composite members require proportional resizing")
|
matrix := Matrix{a: sx, d: sx, e: box.X - before.X*sx, f: box.Y - before.Y*sx}
|
||||||
}
|
for _, node := range nodes {
|
||||||
return e.TransformCompositeObjects(page, path, indexes, box.X-before.X*sx, box.Y-before.Y*sx, sx)
|
if err := e.transformCompositeMember(renderer, node, matrix); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// compositeOrientation 将旋转或镜像中心移至选区中心
|
// compositeOrientation 将旋转或镜像中心移至选区中心
|
||||||
@@ -177,6 +259,7 @@ func (e *Editor) compositeScope(page int, path ObjectPath) (*Reader, *Renderer,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
root.setStates(object.CompositeGraphicUnit.states)
|
||||||
reader, err := e.Reader()
|
reader, err := e.Reader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
@@ -191,7 +274,7 @@ func (e *Editor) compositeScope(page int, path ObjectPath) (*Reader, *Renderer,
|
|||||||
scope := root
|
scope := root
|
||||||
for depth := 0; ; depth++ {
|
for depth := 0; ; depth++ {
|
||||||
var members []*editorCompositeNode
|
var members []*editorCompositeNode
|
||||||
members, err = scope.members(reader, renderer, make(map[string]bool))
|
members, err = e.compositeMembers(scope, reader, renderer, make(map[string]bool))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -237,10 +320,10 @@ func newEditorCompositeNode(data []byte) (*editorCompositeNode, error) {
|
|||||||
return &editorCompositeNode{data: data, node: node, object: object}, err
|
return &editorCompositeNode{data: data, node: node, object: object}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// members 展开当前复合实例的资源引用与直接绘制成员
|
// compositeMembers 展开当前复合实例的资源引用与直接绘制成员
|
||||||
// 入参: reader 资源读取器, renderer 渲染上下文, visiting 当前资源链
|
// 入参: n 当前节点, reader 资源读取器, renderer 渲染上下文, visiting 当前资源链
|
||||||
// 返回: []*editorCompositeNode 成员, error 错误信息
|
// 返回: []*editorCompositeNode 成员, error 错误信息
|
||||||
func (n *editorCompositeNode) members(reader *Reader, renderer *Renderer, visiting map[string]bool) ([]*editorCompositeNode, error) {
|
func (e *Editor) compositeMembers(n *editorCompositeNode, reader *Reader, renderer *Renderer, visiting map[string]bool) ([]*editorCompositeNode, error) {
|
||||||
if n.object.Type != "CompositeObject" && n.object.Type != "CompositeGraphicUnit" {
|
if n.object.Type != "CompositeObject" && n.object.Type != "CompositeGraphicUnit" {
|
||||||
return nil, fmt.Errorf("object is not composite")
|
return nil, fmt.Errorf("object is not composite")
|
||||||
}
|
}
|
||||||
@@ -259,6 +342,7 @@ func (n *editorCompositeNode) members(reader *Reader, renderer *Renderer, visiti
|
|||||||
defaults := renderer.drawParamDefaults(c.DrawParam, n.defaults)
|
defaults := renderer.drawParamDefaults(c.DrawParam, n.defaults)
|
||||||
drawParams := append(slices.Clone(n.drawParams), c.DrawParam)
|
drawParams := append(slices.Clone(n.drawParams), c.DrawParam)
|
||||||
visible := n.visible && (c.Visible == nil || *c.Visible)
|
visible := n.visible && (c.Visible == nil || *c.Visible)
|
||||||
|
alpha := mergeAlpha(n.alpha, c.Alpha)
|
||||||
if c.ResourceID != "" {
|
if c.ResourceID != "" {
|
||||||
data, err := reader.readFile(reader.resourceFiles[c.ResourceID])
|
data, err := reader.readFile(reader.resourceFiles[c.ResourceID])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -288,8 +372,17 @@ func (n *editorCompositeNode) members(reader *Reader, renderer *Renderer, visiti
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
var states map[string]editorCompositeState
|
||||||
|
for _, resource := range e.resources {
|
||||||
|
if resource.composite == c.ResourceID {
|
||||||
|
states = resource.states
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n.ref.setStates(states)
|
||||||
n.ref.parent, n.ref.boundaryInCTM, n.ref.defaults, n.ref.clip, n.ref.visible = matrix, true, defaults, clip, visible
|
n.ref.parent, n.ref.boundaryInCTM, n.ref.defaults, n.ref.clip, n.ref.visible = matrix, true, defaults, clip, visible
|
||||||
n.ref.drawParams = drawParams
|
n.ref.drawParams = drawParams
|
||||||
|
n.ref.alpha = alpha
|
||||||
}
|
}
|
||||||
var collect func(*editorXML) error
|
var collect func(*editorXML) error
|
||||||
collect = func(container *editorXML) error {
|
collect = func(container *editorXML) error {
|
||||||
@@ -308,8 +401,10 @@ func (n *editorCompositeNode) members(reader *Reader, renderer *Renderer, visiti
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
item.setStates(n.states)
|
||||||
item.parent, item.boundaryInCTM, item.defaults, item.clip, item.visible, item.span = matrix, n.boundaryInCTM, defaults, clip, visible, child
|
item.parent, item.boundaryInCTM, item.defaults, item.clip, item.visible, item.span = matrix, n.boundaryInCTM, defaults, clip, visible, child
|
||||||
item.drawParams = drawParams
|
item.drawParams = drawParams
|
||||||
|
item.alpha = alpha
|
||||||
item.owner = n
|
item.owner = n
|
||||||
n.children = append(n.children, item)
|
n.children = append(n.children, item)
|
||||||
}
|
}
|
||||||
@@ -328,7 +423,7 @@ func (n *editorCompositeNode) members(reader *Reader, renderer *Renderer, visiti
|
|||||||
return nil, fmt.Errorf("cyclic composite resource %q", id)
|
return nil, fmt.Errorf("cyclic composite resource %q", id)
|
||||||
}
|
}
|
||||||
visiting[id] = true
|
visiting[id] = true
|
||||||
members, err := n.ref.members(reader, renderer, visiting)
|
members, err := e.compositeMembers(n.ref, reader, renderer, visiting)
|
||||||
delete(visiting, id)
|
delete(visiting, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -390,16 +485,18 @@ func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorComp
|
|||||||
errors.As(err, &capability.MissingGlyphs)
|
errors.As(err, &capability.MissingGlyphs)
|
||||||
} else if _, err := text.TextFrame(); err == nil {
|
} else if _, err := text.TextFrame(); err == nil {
|
||||||
capability.Reflow = text.ReadDirection == 0 && text.CharDirection == 0
|
capability.Reflow = text.ReadDirection == 0 && text.CharDirection == 0
|
||||||
|
capability.LayoutKnown = text.layout != nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if transform && node.object.Type == "ImageObject" && bounds.box.W > 0 && bounds.box.H > 0 {
|
if transform && node.object.Type == "ImageObject" && bounds.box.W > 0 && bounds.box.H > 0 {
|
||||||
clips := node.object.ImageObject.Clips
|
clips := node.object.ImageObject.Clips
|
||||||
_, capability.CropImage = node.matrix(clips == nil || clips.TransFlag == nil || *clips.TransFlag).Invert()
|
_, capability.CropImage = node.matrix(clips == nil || clips.TransFlag == nil || *clips.TransFlag).Invert()
|
||||||
|
capability.FitImage = capability.CropImage && axisAlignedMatrix(NewMatrix(node.object.ImageObject.CTM))
|
||||||
}
|
}
|
||||||
_, 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, Matrix: node.matrix(true), 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, Cropped: node.states[editorObjectID(node.object)].crop != nil}
|
||||||
position.Index++
|
position.Index++
|
||||||
positions[node.span.parent] = position
|
positions[node.span.parent] = position
|
||||||
}
|
}
|
||||||
@@ -613,6 +710,8 @@ func (e *Editor) editCompositeScope(page int, path ObjectPath, edit func(*Render
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
next.setStates(root.states)
|
||||||
|
next.object.CompositeGraphicUnit.states = next.states
|
||||||
origin := e.objectOrigin(path.ID)
|
origin := e.objectOrigin(path.ID)
|
||||||
next.node.parent = origin.node.parent
|
next.node.parent = origin.node.parent
|
||||||
updated := &editorObjectOrigin{page: origin.page, data: data, node: next.node, object: next.object}
|
updated := &editorObjectOrigin{page: origin.page, data: data, node: next.node, object: next.object}
|
||||||
@@ -745,6 +844,10 @@ func compositeBoundary(object GraphicObject, parent Matrix, inward bool) Graphic
|
|||||||
// 返回: []byte 新片段, bool 是否变化, error 错误信息
|
// 返回: []byte 新片段, bool 是否变化, error 错误信息
|
||||||
func (e *Editor) writeCompositeNode(node *editorCompositeNode) ([]byte, bool, error) {
|
func (e *Editor) writeCompositeNode(node *editorCompositeNode) ([]byte, bool, error) {
|
||||||
patches := slices.Clone(node.patches)
|
patches := slices.Clone(node.patches)
|
||||||
|
states := maps.Clone(node.states)
|
||||||
|
if states == nil {
|
||||||
|
states = make(map[string]editorCompositeState)
|
||||||
|
}
|
||||||
for _, child := range node.children {
|
for _, child := range node.children {
|
||||||
data, changed, err := e.writeCompositeNode(child)
|
data, changed, err := e.writeCompositeNode(child)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -752,17 +855,20 @@ func (e *Editor) writeCompositeNode(node *editorCompositeNode) ([]byte, bool, er
|
|||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
patches = append(patches, editorXMLPatch{child.span.start, child.span.end, data})
|
patches = append(patches, editorXMLPatch{child.span.start, child.span.end, data})
|
||||||
|
removeCompositeStates(states, child.node)
|
||||||
|
maps.Copy(states, child.states)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data := editorPatchXML(node.data, patches)
|
data := editorPatchXML(node.data, patches)
|
||||||
changed := node.changed || len(patches) != 0
|
changed := node.changed || len(patches) != 0
|
||||||
|
node.states = states
|
||||||
if node.ref != nil {
|
if node.ref != nil {
|
||||||
fragment, dirty, err := e.writeCompositeNode(node.ref)
|
fragment, dirty, err := e.writeCompositeNode(node.ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
if dirty {
|
if dirty {
|
||||||
id, err := e.addCompositeResource(fragment)
|
id, err := e.addCompositeResource(fragment, node.ref.states)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
@@ -783,9 +889,9 @@ func (e *Editor) writeCompositeNode(node *editorCompositeNode) ([]byte, bool, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addCompositeResource 注册独立复合资源并重映射内部标识,保留未知XML
|
// addCompositeResource 注册独立复合资源并重映射内部标识,保留未知XML
|
||||||
// 入参: data 复合资源片段
|
// 入参: data 复合资源片段, states 内部会话记录
|
||||||
// 返回: string 新资源标识, error 错误信息
|
// 返回: string 新资源标识, error 错误信息
|
||||||
func (e *Editor) addCompositeResource(data []byte) (string, error) {
|
func (e *Editor) addCompositeResource(data []byte, states map[string]editorCompositeState) (string, error) {
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXML(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -814,6 +920,7 @@ func (e *Editor) addCompositeResource(data []byte) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
resource.states = remapCompositeStates(states, ids)
|
||||||
e.resources = append(e.resources, resource)
|
e.resources = append(e.resources, resource)
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
@@ -848,13 +955,16 @@ func (e *Editor) compositeResource(id string, data []byte) (editorResource, erro
|
|||||||
// 入参: composite 复合对象, used 引用集合
|
// 入参: composite 复合对象, used 引用集合
|
||||||
func collectCompositeReferences(composite CompositeGraphicUnit, used map[string]bool) {
|
func collectCompositeReferences(composite CompositeGraphicUnit, used map[string]bool) {
|
||||||
used[composite.ResourceID] = true
|
used[composite.ResourceID] = true
|
||||||
|
used[composite.DrawParam] = true
|
||||||
for _, object := range composite.Objects {
|
for _, object := range composite.Objects {
|
||||||
var fill, stroke *FillColor
|
var fill, stroke *FillColor
|
||||||
switch object.Type {
|
switch object.Type {
|
||||||
case "TextObject":
|
case "TextObject":
|
||||||
used[object.TextObject.Font] = true
|
used[object.TextObject.Font] = true
|
||||||
|
used[object.TextObject.DrawParam] = true
|
||||||
fill, stroke = object.TextObject.FillColor, (*FillColor)(object.TextObject.StrokeColor)
|
fill, stroke = object.TextObject.FillColor, (*FillColor)(object.TextObject.StrokeColor)
|
||||||
case "PathObject":
|
case "PathObject":
|
||||||
|
used[object.PathObject.DrawParam] = true
|
||||||
fill, stroke = object.PathObject.FillColor, (*FillColor)(object.PathObject.StrokeColor)
|
fill, stroke = object.PathObject.FillColor, (*FillColor)(object.PathObject.StrokeColor)
|
||||||
case "ImageObject":
|
case "ImageObject":
|
||||||
used[object.ImageObject.ResourceID] = true
|
used[object.ImageObject.ResourceID] = true
|
||||||
|
|||||||
+153
-10
@@ -15,12 +15,17 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tdewolff/canvas"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CompositeSelection 同一内部容器的独立选区快照,共享不可变字体和图片资源
|
// CompositeSelection 同一内部容器的独立选区快照,共享不可变字体和图片资源
|
||||||
// 仅可粘贴回当前Editor的原范围,源对象修改或删除不改变快照,不用于跨文档交换
|
// 可在当前Editor内粘贴,源对象修改或删除不改变快照,不用于跨文档交换
|
||||||
// 父路径的祖先成员增删或排序后需重新捕获,避免使用已失效的序号路径
|
// 父路径的祖先成员增删或排序后需重新捕获,避免使用已失效的序号路径
|
||||||
type CompositeSelection struct {
|
type CompositeSelection struct {
|
||||||
editor *Editor
|
editor *Editor
|
||||||
@@ -29,6 +34,135 @@ type CompositeSelection struct {
|
|||||||
references int
|
references int
|
||||||
containers []int
|
containers []int
|
||||||
nodes []*editorCompositeNode
|
nodes []*editorCompositeNode
|
||||||
|
source *editorSourcePage
|
||||||
|
}
|
||||||
|
|
||||||
|
// PasteCompositeSelection 将内部基本对象快照粘贴到页面顶层,保持源继承外观和裁剪
|
||||||
|
// 复合成员需留在原范围操作,不将未知嵌套结构强制扁平化
|
||||||
|
// 入参: page 目标页, selection 当前编辑器内捕获的快照, dx、dy 页面位移
|
||||||
|
// 返回: []string 新对象标识, error 错误信息
|
||||||
|
func (e *Editor) PasteCompositeSelection(page int, selection *CompositeSelection, dx, dy float64) ([]string, error) {
|
||||||
|
if !finite(dx) || !finite(dy) {
|
||||||
|
return nil, fmt.Errorf("paste requires finite offsets")
|
||||||
|
}
|
||||||
|
if _, err := e.page(page); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
objects, err := e.compositeSelectionObjects(selection)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return e.CopyObjects(page, objects, dx, dy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// compositeSelectionObjects 固定内部基本对象的页面几何和父裁剪,保留原XML扩展
|
||||||
|
// 入参: selection 独立内部快照
|
||||||
|
// 返回: []GraphicObject 可复制到其他范围的快照, error 错误信息
|
||||||
|
func (e *Editor) compositeSelectionObjects(selection *CompositeSelection) ([]GraphicObject, error) {
|
||||||
|
if selection == nil || selection.editor != e {
|
||||||
|
return nil, fmt.Errorf("composite selection belongs to another editor")
|
||||||
|
}
|
||||||
|
for _, node := range selection.nodes {
|
||||||
|
if node.object.Type == "CompositeObject" || node.object.Type == "CompositeGraphicUnit" {
|
||||||
|
return nil, fmt.Errorf("composite groups require their original editing scope")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.prepareSourceIDs(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reader, err := e.Reader()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
renderer := NewRenderer(reader, WithFontFS(e.fontFS...))
|
||||||
|
var objects []GraphicObject
|
||||||
|
for _, source := range selection.nodes {
|
||||||
|
copy, err := newEditorCompositeNode(source.data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
copy.setStates(source.states)
|
||||||
|
object, err := e.compositeMemberStyle(source)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
object = mergeGraphicObjectAlpha(object, source.alpha)
|
||||||
|
if !source.visible || source.clip != nil && source.clip.Empty() {
|
||||||
|
visible := false
|
||||||
|
switch object.Type {
|
||||||
|
case "TextObject":
|
||||||
|
object.TextObject.Visible = &visible
|
||||||
|
case "PathObject":
|
||||||
|
object.PathObject.Visible = &visible
|
||||||
|
case "ImageObject":
|
||||||
|
object.ImageObject.Visible = &visible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !source.boundaryInCTM {
|
||||||
|
object = compositeBoundary(object, source.parent, true)
|
||||||
|
}
|
||||||
|
if err := copy.update(object); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
copy.parent, copy.boundaryInCTM = IdentityMatrix, true
|
||||||
|
if err := e.transformCompositeMember(renderer, copy, source.parent); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := e.isolateCompositeStyle(copy); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if source.clip != nil && !source.clip.Empty() {
|
||||||
|
shape := compositeClipPath(source.clip)
|
||||||
|
clips := *editorObjectClips(©.object)
|
||||||
|
matrix, ok := copy.matrix(clips == nil || clips.TransFlag == nil || *clips.TransFlag).Invert()
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("composite clip transform is not invertible")
|
||||||
|
}
|
||||||
|
if err := appendCompositeClip(copy, shape, matrix, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
copy.node.parent = &editorXML{name: xml.Name{Local: "Layer"}, parent: &editorXML{name: xml.Name{Local: "Content"}}}
|
||||||
|
object, err = e.resolveEditorStyle(copy.object, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
object.origin = &editorObjectOrigin{page: selection.source, data: copy.data, node: copy.node, object: copy.object}
|
||||||
|
objects = append(objects, object)
|
||||||
|
}
|
||||||
|
return objects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// compositeClipPath 将已解析的父裁剪转换为标准紧缩路径,使用页面毫米坐标
|
||||||
|
// 入参: path 画布裁剪
|
||||||
|
// 返回: PathObject 标准裁剪路径
|
||||||
|
func compositeClipPath(path *canvas.Path) PathObject {
|
||||||
|
path = path.Copy().Transform(canvas.Matrix{{1, 0, 0}, {0, -1, 0}}).ReplaceArcs()
|
||||||
|
data := path.Data()
|
||||||
|
var value strings.Builder
|
||||||
|
for i := 0; i < len(data); {
|
||||||
|
switch data[i] {
|
||||||
|
case canvas.MoveToCmd, canvas.LineToCmd:
|
||||||
|
command := "L"
|
||||||
|
if data[i] == canvas.MoveToCmd {
|
||||||
|
command = "M"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&value, "%s %s %s ", command, ofdNumber(data[i+1]), ofdNumber(data[i+2]))
|
||||||
|
i += 4
|
||||||
|
case canvas.QuadToCmd:
|
||||||
|
fmt.Fprintf(&value, "Q %s %s %s %s ", ofdNumber(data[i+1]), ofdNumber(data[i+2]), ofdNumber(data[i+3]), ofdNumber(data[i+4]))
|
||||||
|
i += 6
|
||||||
|
case canvas.CubeToCmd:
|
||||||
|
fmt.Fprintf(&value, "B %s %s %s %s %s %s ", ofdNumber(data[i+1]), ofdNumber(data[i+2]), ofdNumber(data[i+3]), ofdNumber(data[i+4]), ofdNumber(data[i+5]), ofdNumber(data[i+6]))
|
||||||
|
i += 8
|
||||||
|
case canvas.CloseCmd:
|
||||||
|
value.WriteString("C ")
|
||||||
|
i += 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fill, stroke := true, false
|
||||||
|
return PathObject{Boundary: "0 0 1 1", Fill: &fill, Stroke: &stroke, AbbreviatedData: strings.TrimSpace(value.String())}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CaptureCompositeObjects 捕获内部选区,不修改文档或分配资源
|
// CaptureCompositeObjects 捕获内部选区,不修改文档或分配资源
|
||||||
@@ -48,7 +182,7 @@ func (e *Editor) CaptureCompositeObjects(page int, path ObjectPath, indexes []in
|
|||||||
members := e.measureCompositeMembers(renderer, nodes)
|
members := e.measureCompositeMembers(renderer, nodes)
|
||||||
var owner *editorCompositeNode
|
var owner *editorCompositeNode
|
||||||
var container *editorXML
|
var container *editorXML
|
||||||
selection := &CompositeSelection{editor: e, page: e.pages[page].ID, path: ObjectPath{ID: path.ID, Children: slices.Clone(path.Children)}}
|
selection := &CompositeSelection{editor: e, page: e.pages[page].ID, path: ObjectPath{ID: path.ID, Children: slices.Clone(path.Children)}, source: e.objectOrigin(path.ID).page}
|
||||||
for j, i := range indexes {
|
for j, i := range indexes {
|
||||||
if i < 0 || i >= len(nodes) || j > 0 && indexes[j-1] == i || !members[i].Capabilities.Copy {
|
if i < 0 || i >= len(nodes) || j > 0 && indexes[j-1] == i || !members[i].Capabilities.Copy {
|
||||||
return nil, fmt.Errorf("invalid composite copy selection")
|
return nil, fmt.Errorf("invalid composite copy selection")
|
||||||
@@ -76,22 +210,30 @@ func (e *Editor) CaptureCompositeObjects(page int, path ObjectPath, indexes []in
|
|||||||
return selection, nil
|
return selection, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PasteCompositeObjects 将快照粘贴至原内部容器末尾,不依赖源成员当前的序号或存在性
|
// PasteCompositeObjects 将快照粘贴至内部范围末尾,不依赖源成员当前的序号或存在性
|
||||||
// 位移使用当前父变换下的页面毫米,父容器和继承样式沿用当前范围,提交一次撤销记录
|
// 同范围沿用原容器和当前继承样式,跨范围固定基本对象的源外观,位移使用页面毫米
|
||||||
|
// 目标范围的父透明度及裁剪仍作用于新成员,全部成员提交一次撤销记录
|
||||||
// 入参: page 页面索引, path 父复合路径, selection 捕获的快照, dx、dy 页面位移
|
// 入参: page 页面索引, path 父复合路径, selection 捕获的快照, dx、dy 页面位移
|
||||||
// 返回: []int 新成员序号, error 错误信息
|
// 返回: []int 新成员序号, error 错误信息
|
||||||
func (e *Editor) PasteCompositeObjects(page int, path ObjectPath, selection *CompositeSelection, dx, dy float64) ([]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) {
|
if selection == nil || selection.editor != e {
|
||||||
return nil, fmt.Errorf("paste requires the original composite scope")
|
return nil, fmt.Errorf("composite selection belongs to another editor")
|
||||||
}
|
}
|
||||||
if !finite(dx) || !finite(dy) {
|
if !finite(dx) || !finite(dy) {
|
||||||
return nil, fmt.Errorf("paste requires finite offsets")
|
return nil, fmt.Errorf("paste requires finite offsets")
|
||||||
}
|
}
|
||||||
|
if _, err := e.page(page); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if e.pages[page].ID != selection.page || path.ID != selection.path.ID || !slices.Equal(path.Children, selection.path.Children) {
|
||||||
|
objects, err := e.compositeSelectionObjects(selection)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return e.CopyObjectsToComposite(page, path, objects, dx, dy)
|
||||||
|
}
|
||||||
var result []int
|
var result []int
|
||||||
err := e.editCompositeScope(page, path, func(renderer *Renderer, root *editorCompositeNode, _ []*editorCompositeNode) error {
|
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)
|
scope := root.loadedScope(path.Children)
|
||||||
owner := scope
|
owner := scope
|
||||||
for i := 0; i < selection.references; i++ {
|
for i := 0; i < selection.references; i++ {
|
||||||
@@ -116,10 +258,11 @@ func (e *Editor) PasteCompositeObjects(page int, path ObjectPath, selection *Com
|
|||||||
copy.parent, copy.boundaryInCTM = parent, owner.boundaryInCTM
|
copy.parent, copy.boundaryInCTM = parent, owner.boundaryInCTM
|
||||||
nodes[i] = ©
|
nodes[i] = ©
|
||||||
}
|
}
|
||||||
data, err := e.copyCompositeNodes(renderer, nodes, dx, dy)
|
data, states, err := e.copyCompositeNodes(renderer, nodes, dx, dy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
maps.Copy(owner.states, states)
|
||||||
next, reached := 0, false
|
next, reached := 0, false
|
||||||
var count func(*editorCompositeNode)
|
var count func(*editorCompositeNode)
|
||||||
count = func(node *editorCompositeNode) {
|
count = func(node *editorCompositeNode) {
|
||||||
|
|||||||
+122
-22
@@ -70,38 +70,138 @@ func (e *Editor) CropCompositeImage(page int, path ObjectPath, index int, box Bo
|
|||||||
}
|
}
|
||||||
fill, stroke := true, false
|
fill, stroke := true, false
|
||||||
shape.Fill, shape.Stroke = &fill, &stroke
|
shape.Fill, shape.Stroke = &fill, &stroke
|
||||||
data, err := encodeOFDXML(func(x *ofdXML) {
|
return appendCompositeClip(node, shape, inverse, true)
|
||||||
x.clips(&Clips{Clip: []Clip{{Area: []ClipArea{{CTM: inverse.String(), Path: []PathObject{shape}}}}}})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// appendCompositeClip 追加独立交集裁剪并记录会话还原起点,保留原始裁剪XML
|
||||||
|
// 入参: node 对象节点, shape 裁剪路径, matrix 路径到裁剪坐标的变换, track 是否记录还原起点
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func appendCompositeClip(node *editorCompositeNode, shape PathObject, matrix Matrix, track bool) error {
|
||||||
|
state := node.states[editorObjectID(node.object)]
|
||||||
|
if track && state.crop == nil {
|
||||||
|
state.crop = &editorCompositeCrop{exists: node.node.child("Clips") != nil}
|
||||||
|
if clips := node.object.ImageObject.Clips; clips != nil {
|
||||||
|
state.crop.count = len(clips.Clip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data, err := encodeOFDXML(func(x *ofdXML) {
|
||||||
|
x.clips(&Clips{Clip: []Clip{{Area: []ClipArea{{CTM: matrix.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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
data = bytes.TrimPrefix(data, []byte(xml.Header))
|
content := append(bytes.Clone(node.data[original.open:original.close]), clip...)
|
||||||
root, err := parseEditorXML(data)
|
patch = editorXMLContent(node.data, original, content)
|
||||||
|
} else {
|
||||||
|
data, err = editorXMLStandalone(data, root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var patch editorXMLPatch
|
content := append(data, node.data[node.node.open:node.node.close]...)
|
||||||
if original := node.node.child("Clips"); original != nil {
|
patch = editorXMLContent(node.data, node.node, content)
|
||||||
clip, err := editorXMLStandalone(data[root.children[0].start:root.children[0].end], root.children[0])
|
}
|
||||||
if err != nil {
|
updated, err := newEditorCompositeNode(editorPatchXML(node.data, []editorXMLPatch{patch}))
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
node.data, node.node, node.object, node.changed = updated.data, updated.node, updated.object, true
|
||||||
|
node.setState(state)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetCompositeImageCrop 仅移除本次编辑追加的裁剪,保留输入文件中的裁剪、蒙版和其他属性
|
||||||
|
// 入参: page 页面索引, path 父路径, index 图片序号
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) ResetCompositeImageCrop(page int, path ObjectPath, index int) error {
|
||||||
|
return e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, _ []CompositeMember) error {
|
||||||
|
return resetCompositeImageCrop(nodes[0])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// resetCompositeImageCrop 恢复图片原始裁剪节点,不还原后续变换和资源替换
|
||||||
|
// 入参: node 图片节点
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func resetCompositeImageCrop(node *editorCompositeNode) error {
|
||||||
|
if node.object.Type != "ImageObject" {
|
||||||
|
return fmt.Errorf("composite member is not an image")
|
||||||
|
}
|
||||||
|
state := node.states[editorObjectID(node.object)]
|
||||||
|
if state.crop == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
clips := node.node.child("Clips")
|
||||||
|
var patches []editorXMLPatch
|
||||||
|
if !state.crop.exists {
|
||||||
|
patches = append(patches, editorXMLPatch{clips.start, clips.end, nil})
|
||||||
|
} else {
|
||||||
|
count := 0
|
||||||
|
for _, child := range clips.children {
|
||||||
|
if child.name.Local == "Clip" {
|
||||||
|
if count >= state.crop.count {
|
||||||
|
patches = append(patches, editorXMLPatch{child.start, child.end, nil})
|
||||||
|
}
|
||||||
|
count++
|
||||||
}
|
}
|
||||||
content := append(bytes.Clone(node.data[original.open:original.close]), clip...)
|
}
|
||||||
patch = editorXMLContent(node.data, original, content)
|
}
|
||||||
} else {
|
updated, err := newEditorCompositeNode(editorPatchXML(node.data, patches))
|
||||||
data, err = editorXMLStandalone(data, root)
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
}
|
||||||
}
|
node.data, node.node, node.object, node.changed = updated.data, updated.node, updated.object, true
|
||||||
content := append(data, node.data[node.node.open:node.node.close]...)
|
state.crop = nil
|
||||||
patch = editorXMLContent(node.data, node.node, content)
|
node.setState(state)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FitCompositeImage 按原像素比例适应或填充内部图片框,保留原始裁剪和蒙版
|
||||||
|
// 入参: page 页面索引, path 父路径, index 图片序号, mode 为contain或cover
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) FitCompositeImage(page int, path ObjectPath, index int, mode string) error {
|
||||||
|
return e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
|
node := nodes[0]
|
||||||
|
if !members[0].Capabilities.FitImage {
|
||||||
|
return fmt.Errorf("composite image fitting is not supported")
|
||||||
|
}
|
||||||
|
if err := resetCompositeImageCrop(node); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
object := cloneEditorData(node.object)
|
||||||
|
if err := e.fitImage(&object.ImageObject, mode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
object.ImageObject.Clips = node.object.ImageObject.Clips
|
||||||
|
if err := node.update(object); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if mode == "contain" {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
updated, err := newEditorCompositeNode(editorPatchXML(node.data, []editorXMLPatch{patch}))
|
box, _ := ParseBox(object.ImageObject.Boundary)
|
||||||
|
shape, err := NewShape(ShapeRectangle, Box{W: box.W, H: box.H})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
fill, stroke := true, false
|
||||||
|
shape.Fill, shape.Stroke = &fill, &stroke
|
||||||
|
frame := node.parent.Multiply(TranslationMatrix(box.X, box.Y))
|
||||||
|
if !node.boundaryInCTM {
|
||||||
|
frame = TranslationMatrix(box.X, box.Y).Multiply(node.parent)
|
||||||
}
|
}
|
||||||
node.data, node.node, node.object, node.changed = updated.data, updated.node, updated.object, true
|
clips := object.ImageObject.Clips
|
||||||
return nil
|
inverse, _ := node.matrix(clips == nil || clips.TransFlag == nil || *clips.TransFlag).Invert()
|
||||||
|
return appendCompositeClip(node, shape, inverse.Multiply(frame), true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
// 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"
|
||||||
|
"image/color"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddCompositeObject 按页面坐标向内部范围末尾添加基本对象,复用已注册资源
|
||||||
|
// 入参: page 页面索引, path 父路径, object 文字、图片或路径
|
||||||
|
// 返回: int 新成员序号, error 错误信息
|
||||||
|
func (e *Editor) AddCompositeObject(page int, path ObjectPath, object GraphicObject) (int, error) {
|
||||||
|
indexes, err := e.CopyObjectsToComposite(page, path, []GraphicObject{object}, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return indexes[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyObjectsToComposite 将页面坐标快照复制到内部范围,隔离目标继承样式并分配新标识
|
||||||
|
// 保真快照保留原文,全部对象成功后提交一次撤销记录
|
||||||
|
// 入参: page 页面索引, path 父路径, objects 当前文档快照或基本对象, dx、dy 页面位移
|
||||||
|
// 返回: []int 新成员序号, error 错误信息
|
||||||
|
func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []GraphicObject, dx, dy float64) ([]int, error) {
|
||||||
|
if !finite(dx) || !finite(dy) {
|
||||||
|
return nil, fmt.Errorf("copy requires finite offsets")
|
||||||
|
}
|
||||||
|
var result []int
|
||||||
|
err := e.editCompositeScope(page, path, func(renderer *Renderer, root *editorCompositeNode, members []*editorCompositeNode) error {
|
||||||
|
if len(objects) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
owner := root.loadedScope(path.Children)
|
||||||
|
box, _ := ParseBox(owner.object.CompositeGraphicUnit.Boundary)
|
||||||
|
parent := owner.parent.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(NewMatrix(owner.object.CompositeGraphicUnit.CTM))
|
||||||
|
inverse, ok := parent.Invert()
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("composite insertion requires an invertible transform")
|
||||||
|
}
|
||||||
|
next := len(members)
|
||||||
|
if container := owner.node.child("Content"); container != nil {
|
||||||
|
next = 0
|
||||||
|
for _, member := range members {
|
||||||
|
if member.owner != owner || member.span.start < container.close {
|
||||||
|
next++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var content []byte
|
||||||
|
for _, source := range objects {
|
||||||
|
if source.Type == "CompositeObject" || source.Type == "CompositeGraphicUnit" {
|
||||||
|
return fmt.Errorf("composite groups require their original editing scope")
|
||||||
|
}
|
||||||
|
id := e.nextID()
|
||||||
|
object, err := e.prepareCopiedObject(id, source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
maximum := e.maxID
|
||||||
|
object, origin, err := e.copyObjectOrigin(source, object, &maximum)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.maxID = maximum
|
||||||
|
var data []byte
|
||||||
|
if origin != nil {
|
||||||
|
data, err = editorXMLObject(origin.data, origin.node, origin.object, object)
|
||||||
|
if err == nil {
|
||||||
|
data, err = editorXMLStandalone(data, origin.node)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data, err = editorObjectXML(object)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
node, err := newEditorCompositeNode(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
node.parent, node.boundaryInCTM = IdentityMatrix, true
|
||||||
|
if err := e.isolateCompositeStyle(node); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := e.transformCompositeMember(renderer, node, inverse.Multiply(TranslationMatrix(dx, dy))); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !owner.boundaryInCTM {
|
||||||
|
if err := node.update(compositeBoundary(node.object, parent, false)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if origin == nil && object.TextObject.layout != nil {
|
||||||
|
y, _ := strconv.ParseFloat(object.TextObject.TextCode[0].Y, 64)
|
||||||
|
node.setState(editorCompositeState{layout: object.TextObject.layout, origin: [2]float64{0, y}})
|
||||||
|
}
|
||||||
|
for id, state := range node.states {
|
||||||
|
owner.states[id] = state
|
||||||
|
}
|
||||||
|
content = append(content, node.data...)
|
||||||
|
result = append(result, next+len(result))
|
||||||
|
}
|
||||||
|
if container := owner.node.child("Content"); container != nil {
|
||||||
|
if container.open == container.end {
|
||||||
|
owner.patches = append(owner.patches, editorXMLContent(owner.data, container, content))
|
||||||
|
} else {
|
||||||
|
owner.patches = append(owner.patches, editorXMLPatch{container.close, container.close, content})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content, err := editorXMLContainer("Content", nil, content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
content = bytes.TrimPrefix(content, []byte(xml.Header))
|
||||||
|
if owner.node.open == owner.node.end {
|
||||||
|
owner.patches = append(owner.patches, editorXMLContent(owner.data, owner.node, content))
|
||||||
|
} else {
|
||||||
|
owner.patches = append(owner.patches, editorXMLPatch{owner.node.close, owner.node.close, content})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isolateCompositeStyle 固定基本对象的绘制参数,避免目标容器改变颜色、虚线和线帽
|
||||||
|
// 入参: node 页面坐标中的对象节点
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) isolateCompositeStyle(node *editorCompositeNode) error {
|
||||||
|
object, err := e.resolveEditorStyleDefaults(node.object, &DrawParam{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if object.Type == "TextObject" || object.Type == "PathObject" {
|
||||||
|
id, err := e.neutralDrawParam()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if object.Type == "TextObject" {
|
||||||
|
object.TextObject.DrawParam = id
|
||||||
|
} else {
|
||||||
|
object.PathObject.DrawParam = id
|
||||||
|
object.PathObject.dashPatternSet = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node.update(object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// neutralDrawParam 注册可复用的完整默认绘制参数,不改变文档默认颜色空间
|
||||||
|
// 返回: string 绘制参数标识, error 错误信息
|
||||||
|
func (e *Editor) neutralDrawParam() (string, error) {
|
||||||
|
for _, resource := range e.resources {
|
||||||
|
if resource.draw != nil {
|
||||||
|
return resource.draw.ID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.prepareSourceIDs(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
fill, err := e.RGBColor(color.NRGBA{A: 255})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
zero := 0.0
|
||||||
|
draw := &DrawParam{ID: e.nextID(), LineWidth: defaultPathLineWidth, Cap: "Butt", Join: "Miter", MiterLimit: defaultMiterLimit, DashOffset: &zero, dashPatternSet: true, FillColor: fill, StrokeColor: (*StrokeColor)(fill)}
|
||||||
|
data, err := encodeOFDXML(func(x *ofdXML) {
|
||||||
|
x.root("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}})
|
||||||
|
x.start("DrawParams", nil)
|
||||||
|
attrs := ofdAttrs{}
|
||||||
|
for _, pair := range [][2]string{{"ID", draw.ID}, {"LineWidth", ofdNumber(draw.LineWidth)}, {"Cap", draw.Cap}, {"Join", draw.Join}, {"MiterLimit", ofdNumber(draw.MiterLimit)}, {"DashOffset", "0"}} {
|
||||||
|
attrs.add(pair[0], pair[1])
|
||||||
|
}
|
||||||
|
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "DashPattern"}, Value: ""})
|
||||||
|
x.start("DrawParam", attrs)
|
||||||
|
x.color("FillColor", fill)
|
||||||
|
x.color("StrokeColor", fill)
|
||||||
|
x.end("DrawParam")
|
||||||
|
x.end("DrawParams")
|
||||||
|
x.end("Res")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
resource := editorResource{name: e.resourceDirectory() + "/DrawParam_" + draw.ID + ".xml", data: data, draw: draw}
|
||||||
|
if fill.ColorSpace != "" {
|
||||||
|
resource.references = []string{fill.ColorSpace}
|
||||||
|
}
|
||||||
|
e.resources = append(e.resources, resource)
|
||||||
|
return draw.ID, nil
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ package ofdgo
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,11 +61,12 @@ 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 })
|
||||||
data, err := e.copyCompositeNodes(renderer, nodes, dx, dy)
|
data, states, err := e.copyCompositeNodes(renderer, nodes, dx, dy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
owner := nodes[0].owner
|
owner := nodes[0].owner
|
||||||
|
maps.Copy(owner.states, states)
|
||||||
owner.patches = append(owner.patches, editorXMLPatch{container.close, container.close, data})
|
owner.patches = append(owner.patches, editorXMLPatch{container.close, container.close, data})
|
||||||
next := 0
|
next := 0
|
||||||
for _, member := range owner.children {
|
for _, member := range owner.children {
|
||||||
@@ -85,27 +87,29 @@ func (e *Editor) CopyCompositeObjects(page int, path ObjectPath, indexes []int,
|
|||||||
|
|
||||||
// copyCompositeNodes 复制成员原文并统一重映射标识,保持副本之间的引用
|
// copyCompositeNodes 复制成员原文并统一重映射标识,保持副本之间的引用
|
||||||
// 入参: renderer 渲染器, nodes 快照, dx、dy 页面位移
|
// 入参: renderer 渲染器, nodes 快照, dx、dy 页面位移
|
||||||
// 返回: []byte 副本XML, error 错误信息
|
// 返回: []byte 副本XML, map[string]editorCompositeState 会话信息, error 错误信息
|
||||||
func (e *Editor) copyCompositeNodes(renderer *Renderer, nodes []*editorCompositeNode, dx, dy float64) ([]byte, error) {
|
func (e *Editor) copyCompositeNodes(renderer *Renderer, nodes []*editorCompositeNode, dx, dy float64) ([]byte, map[string]editorCompositeState, error) {
|
||||||
ids := make(map[string]string)
|
ids := make(map[string]string)
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
if err := e.collectCompositeIDs(node.node, ids); err != nil {
|
if err := e.collectCompositeIDs(node.node, ids); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var data []byte
|
var data []byte
|
||||||
|
states := make(map[string]editorCompositeState)
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
copy := *node
|
copy := *node
|
||||||
if err := e.transformCompositeMember(renderer, ©, TranslationMatrix(dx, dy)); err != nil {
|
if err := e.transformCompositeMember(renderer, ©, TranslationMatrix(dx, dy)); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
fragment, err := editorXMLRemapIDs(copy.data, copy.node, ids)
|
fragment, err := editorXMLRemapIDs(copy.data, copy.node, ids)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
maps.Copy(states, remapCompositeStates(node.states, ids))
|
||||||
data = append(data, fragment...)
|
data = append(data, fragment...)
|
||||||
}
|
}
|
||||||
return data, nil
|
return data, states, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OrderCompositeObjects 调整同一直接容器内的绘制顺序,不改变页块和其他实例
|
// OrderCompositeObjects 调整同一直接容器内的绘制顺序,不改变页块和其他实例
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import (
|
|||||||
// 入参: page 页面索引, path 父复合路径, index 成员序号, value 文字, style 文字样式增量
|
// 入参: page 页面索引, path 父复合路径, index 成员序号, value 文字, style 文字样式增量
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) UpdateCompositeText(page int, path ObjectPath, index int, value string, style TextStyle) 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)
|
return e.changeCompositeText(page, path, []int{index}, &value, style, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StyleCompositeText 批量修改内部文字字体、字号或颜色,换字体保留原文字定位
|
// StyleCompositeText 批量修改内部文字字体、字号或颜色,换字体保留原文字定位
|
||||||
@@ -33,13 +33,28 @@ func (e *Editor) UpdateCompositeText(page int, path ObjectPath, index int, value
|
|||||||
// 入参: page 页面索引, path 父复合路径, indexes 成员序号, style 文字样式增量
|
// 入参: page 页面索引, path 父复合路径, indexes 成员序号, style 文字样式增量
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) StyleCompositeText(page int, path ObjectPath, indexes []int, style TextStyle) error {
|
func (e *Editor) StyleCompositeText(page int, path ObjectPath, indexes []int, style TextStyle) error {
|
||||||
return e.changeCompositeText(page, path, indexes, nil, style)
|
return e.changeCompositeText(page, path, indexes, nil, style, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LayoutCompositeText 显式重排内部普通文字,保留原始基线、变换、裁剪和绘制属性
|
||||||
|
// 段落选项仅保留在编辑会话中,撤销重做随对象恢复,保存为标准TextCode定位
|
||||||
|
// 入参: page 页面索引, path 父路径, index 成员序号, value 原文, options 段落选项
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) LayoutCompositeText(page int, path ObjectPath, index int, value string, options TextLayout) error {
|
||||||
|
return e.changeCompositeText(page, path, []int{index}, &value, TextStyle{}, &options, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResizeCompositeTextFrame 调整内部文字框本地宽度并按指定段落选项重排
|
||||||
|
// 入参: page 页面索引, path 父路径, index 成员序号, offset 左侧偏移, width 宽度, options 段落选项
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) ResizeCompositeTextFrame(page int, path ObjectPath, index int, offset, width float64, options TextLayout) error {
|
||||||
|
return e.changeCompositeText(page, path, []int{index}, nil, TextStyle{}, &options, &Box{X: offset, W: width})
|
||||||
}
|
}
|
||||||
|
|
||||||
// changeCompositeText 生成独立文字副本,全部校验通过后合并原文
|
// changeCompositeText 生成独立文字副本,全部校验通过后合并原文
|
||||||
// 入参: page 页面索引, path 父复合路径, indexes 成员序号, value 新内容,nil保留定位, style 样式增量
|
// 入参: page 页面索引, path 父复合路径, indexes 成员序号, value 新内容,nil保留定位, style 样式增量, options 显式段落选项, frame 可选文字框调整
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) changeCompositeText(page int, path ObjectPath, indexes []int, value *string, style TextStyle) error {
|
func (e *Editor) changeCompositeText(page int, path ObjectPath, indexes []int, value *string, style TextStyle, options *TextLayout, frame *Box) error {
|
||||||
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
for i, node := range nodes {
|
for i, node := range nodes {
|
||||||
member := members[i]
|
member := members[i]
|
||||||
@@ -48,21 +63,51 @@ func (e *Editor) changeCompositeText(page int, path ObjectPath, indexes []int, v
|
|||||||
}
|
}
|
||||||
object := cloneEditorData(node.object)
|
object := cloneEditorData(node.object)
|
||||||
text := cloneEditorData(member.Object.TextObject)
|
text := cloneEditorData(member.Object.TextObject)
|
||||||
|
state := node.states[editorObjectID(node.object)]
|
||||||
|
content, layout := text.TextLayout()
|
||||||
|
original := content
|
||||||
if style.Font != "" {
|
if style.Font != "" {
|
||||||
text.Font = style.Font
|
text.Font = style.Font
|
||||||
}
|
}
|
||||||
content := text.Text()
|
|
||||||
if value != nil {
|
if value != nil {
|
||||||
content = *value
|
content = *value
|
||||||
}
|
}
|
||||||
if content != text.Text() || style.Size != 0 && style.Size != text.Size {
|
if options != nil {
|
||||||
|
layout = *options
|
||||||
|
}
|
||||||
|
if frame != nil {
|
||||||
|
var err error
|
||||||
|
if !node.boundaryInCTM {
|
||||||
|
text = compositeBoundary(GraphicObject{Type: "TextObject", TextObject: text}, node.parent, true).TextObject
|
||||||
|
}
|
||||||
|
text, err = text.ResizeTextFrame(frame.X, frame.W)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !node.boundaryInCTM {
|
||||||
|
text = compositeBoundary(GraphicObject{Type: "TextObject", TextObject: text}, node.parent, false).TextObject
|
||||||
|
}
|
||||||
|
object.TextObject.Boundary, object.TextObject.CTM = text.Boundary, text.CTM
|
||||||
|
}
|
||||||
|
if options != nil || content != original || style.Size != 0 && style.Size != text.Size {
|
||||||
if style.Size != 0 {
|
if style.Size != 0 {
|
||||||
text.Size = style.Size
|
text.Size = style.Size
|
||||||
}
|
}
|
||||||
if err := e.layoutCompositeText(&text, content); err != nil {
|
if state.layout == nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
copy(state.origin[:], origin)
|
||||||
|
}
|
||||||
|
if err := e.layoutCompositeText(&text, content, layout, state.origin); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
object.TextObject.Size, object.TextObject.TextCode = text.Size, text.TextCode
|
object.TextObject.Size, object.TextObject.TextCode = text.Size, text.TextCode
|
||||||
|
state.layout = &textLayout{value: content, options: layout}
|
||||||
}
|
}
|
||||||
if style.Font != "" && style.Font != object.TextObject.Font {
|
if style.Font != "" && style.Font != object.TextObject.Font {
|
||||||
if err := e.prepareText(&text); err != nil {
|
if err := e.prepareText(&text); err != nil {
|
||||||
@@ -85,23 +130,17 @@ func (e *Editor) changeCompositeText(page int, path ObjectPath, indexes []int, v
|
|||||||
if err := node.update(object); err != nil {
|
if err := node.update(object); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
node.setState(state)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// layoutCompositeText 重排普通文字并保持原首行基线起点
|
// layoutCompositeText 重排普通文字并保持原首行基线起点
|
||||||
// 入参: text 有效文字样式, value 新内容
|
// 入参: text 有效文字样式, value 新内容, options 段落选项, origin 原始基线起点
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) layoutCompositeText(text *TextObject, value string) error {
|
func (e *Editor) layoutCompositeText(text *TextObject, value string, options TextLayout, origin [2]float64) error {
|
||||||
if len(text.TextCode) == 0 {
|
if err := e.LayoutText(text, value, options); err != nil {
|
||||||
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
|
return err
|
||||||
}
|
}
|
||||||
baseline, _ := strconv.ParseFloat(text.TextCode[0].Y, 64)
|
baseline, _ := strconv.ParseFloat(text.TextCode[0].Y, 64)
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ func (e *Editor) copyObjectOrigin(before, after GraphicObject, maximum *int) (Gr
|
|||||||
return GraphicObject{}, nil, err
|
return GraphicObject{}, nil, err
|
||||||
}
|
}
|
||||||
result.TextObject.layout = after.TextObject.layout
|
result.TextObject.layout = after.TextObject.layout
|
||||||
|
result.CompositeGraphicUnit.states = remapCompositeStates(after.CompositeGraphicUnit.states, ids)
|
||||||
root.parent = origin.node.parent
|
root.parent = origin.node.parent
|
||||||
return result, &editorObjectOrigin{page: origin.page, data: data, node: root, object: result}, nil
|
return result, &editorObjectOrigin{page: origin.page, data: data, node: root, object: result}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ type editorSourcePage struct {
|
|||||||
// ObjectCapabilities 已有对象可执行的操作,Reason说明受限原因
|
// ObjectCapabilities 已有对象可执行的操作,Reason说明受限原因
|
||||||
// Update表示完整替换内容,ReplaceFont表示可显式替换文字字体,Transform表示保留文字定位的几何变换,Order仅限同一图层或末级页块
|
// Update表示完整替换内容,ReplaceFont表示可显式替换文字字体,Transform表示保留文字定位的几何变换,Order仅限同一图层或末级页块
|
||||||
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复
|
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复
|
||||||
|
// FitImage表示可按原始图片比例适应或填充现有边界
|
||||||
// Paint表示可独立修改纯色填充和描边,不要求重新排版文字
|
// Paint表示可独立修改纯色填充和描边,不要求重新排版文字
|
||||||
// ReplaceImage表示可替换图片数据,CropImage表示可裁剪图片;内部裁剪与原裁剪取交集
|
// ReplaceImage表示可替换图片数据,CropImage表示可裁剪图片;内部裁剪与原裁剪取交集
|
||||||
// MissingGlyphs提供缺字导致操作受限时的结构化诊断
|
// MissingGlyphs提供缺字导致操作受限时的结构化诊断
|
||||||
@@ -70,6 +71,7 @@ type ObjectCapabilities struct {
|
|||||||
ReplaceFont bool
|
ReplaceFont bool
|
||||||
ReplaceImage bool
|
ReplaceImage bool
|
||||||
CropImage bool
|
CropImage bool
|
||||||
|
FitImage bool
|
||||||
Reflow bool
|
Reflow bool
|
||||||
LayoutKnown bool
|
LayoutKnown bool
|
||||||
Transform bool
|
Transform bool
|
||||||
@@ -360,6 +362,7 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
|
|||||||
if origin == nil {
|
if origin == nil {
|
||||||
all.ReplaceImage = object.Type == "ImageObject"
|
all.ReplaceImage = object.Type == "ImageObject"
|
||||||
all.CropImage = all.ReplaceImage
|
all.CropImage = all.ReplaceImage
|
||||||
|
all.FitImage = all.ReplaceImage && axisAlignedMatrix(NewMatrix(object.ImageObject.CTM))
|
||||||
return all, nil
|
return all, nil
|
||||||
}
|
}
|
||||||
node := origin.node
|
node := origin.node
|
||||||
@@ -391,6 +394,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
|
all.CropImage = all.ReplaceImage
|
||||||
|
all.FitImage = all.ReplaceImage && axisAlignedMatrix(NewMatrix(object.ImageObject.CTM))
|
||||||
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
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error)
|
|||||||
}
|
}
|
||||||
fonts, images, spaces, resourceFiles := e.usedResources()
|
fonts, images, spaces, resourceFiles := e.usedResources()
|
||||||
for _, resource := range e.resources {
|
for _, resource := range e.resources {
|
||||||
if resource.composite != "" && slices.Contains(resourceFiles, resource.name) {
|
if resource.definition() != "" && slices.Contains(resourceFiles, resource.name) {
|
||||||
parts[resource.name] = resource.data
|
parts[resource.name] = resource.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-1
@@ -27,7 +27,16 @@ func (e *Editor) editorDrawParam(id string, visited map[string]bool) (*DrawParam
|
|||||||
if id == "" {
|
if id == "" {
|
||||||
return &DrawParam{}, nil
|
return &DrawParam{}, nil
|
||||||
}
|
}
|
||||||
dp := e.source.reader.drawParamCache[id]
|
var dp *DrawParam
|
||||||
|
for _, resource := range e.resources {
|
||||||
|
if resource.draw != nil && resource.draw.ID == id {
|
||||||
|
dp = resource.draw
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dp == nil && e.source != nil {
|
||||||
|
dp = e.source.reader.drawParamCache[id]
|
||||||
|
}
|
||||||
if dp == nil || visited[id] {
|
if dp == nil || visited[id] {
|
||||||
return nil, &EditError{Code: EditUnsupportedStyle, Err: fmt.Errorf("invalid draw parameter reference %q", id)}
|
return nil, &EditError{Code: EditUnsupportedStyle, Err: fmt.Errorf("invalid draw parameter reference %q", id)}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,4 +120,5 @@ type CompositeGraphicUnit struct {
|
|||||||
CompositeGraphicUnit []CompositeGraphicUnit `xml:"CompositeGraphicUnit"`
|
CompositeGraphicUnit []CompositeGraphicUnit `xml:"CompositeGraphicUnit"`
|
||||||
Clips *Clips `xml:"Clips"`
|
Clips *Clips `xml:"Clips"`
|
||||||
Actions []Action `xml:"Actions>Action"`
|
Actions []Action `xml:"Actions>Action"`
|
||||||
|
states map[string]editorCompositeState
|
||||||
}
|
}
|
||||||
+5
-2
@@ -337,6 +337,7 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
|
|||||||
case "CompositeObject", "CompositeGraphicUnit":
|
case "CompositeObject", "CompositeGraphicUnit":
|
||||||
collectCompositeReferences(object.CompositeGraphicUnit, used)
|
collectCompositeReferences(object.CompositeGraphicUnit, used)
|
||||||
case "TextObject":
|
case "TextObject":
|
||||||
|
used[object.TextObject.DrawParam] = true
|
||||||
used[object.TextObject.Font] = true
|
used[object.TextObject.Font] = true
|
||||||
if e.source != nil && (!exists || before.Type != object.Type || before.TextObject.Font != object.TextObject.Font) {
|
if e.source != nil && (!exists || before.Type != object.Type || before.TextObject.Font != object.TextObject.Font) {
|
||||||
promoted[object.TextObject.Font] = true
|
promoted[object.TextObject.Font] = true
|
||||||
@@ -352,20 +353,22 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
|
|||||||
promoted[object.ImageObject.ImageMask] = true
|
promoted[object.ImageObject.ImageMask] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "PathObject":
|
||||||
|
used[object.PathObject.DrawParam] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i := len(e.resources) - 1; i >= 0; i-- {
|
for i := len(e.resources) - 1; i >= 0; i-- {
|
||||||
resource := e.resources[i]
|
resource := e.resources[i]
|
||||||
if resource.composite != "" && used[resource.composite] {
|
if resource.definition() != "" && used[resource.definition()] {
|
||||||
for _, id := range resource.references {
|
for _, id := range resource.references {
|
||||||
used[id] = true
|
used[id] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, resource := range e.resources {
|
for _, resource := range e.resources {
|
||||||
if resource.composite != "" && used[resource.composite] {
|
if resource.definition() != "" && used[resource.definition()] {
|
||||||
files[resource.name] = true
|
files[resource.name] = true
|
||||||
}
|
}
|
||||||
if resource.font != nil && used[resource.font.ID] {
|
if resource.font != nil && used[resource.font.ID] {
|
||||||
|
|||||||
Reference in new issue
Block a user