mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
This commit is contained in:
1 parent
06b5df9173
commit
697a059162
18 files changed
+692
-128
No files matched your search
@@ -183,6 +183,12 @@
|
||||
<form id="objectStyleForm">
|
||||
<div class="form-row"><span>组合</span><div class="form-format"><button id="groupObjectsButton" class="button" type="button">组合</button><button id="ungroupObjectButton" class="button" type="button">解组</button></div></div>
|
||||
<div class="form-row"><span>格式</span><div class="form-format"><button id="copyStyleButton" class="button" type="button" aria-label="复制格式">复制</button><button id="pasteStyleButton" class="button" type="button" aria-label="粘贴格式">粘贴</button></div></div>
|
||||
<details id="objectLinkFields" hidden>
|
||||
<summary>链接</summary>
|
||||
<div class="form-row"><label for="objectLinkKind">目标</label><select id="objectLinkKind"><option value="none">不设</option><option value="uri">网址</option><option value="page">页面</option><option value="keep">原有</option></select></div>
|
||||
<div class="form-row" id="objectLinkAddressRow" hidden><label for="objectLinkAddress">地址</label><input id="objectLinkAddress" type="text"></div>
|
||||
<div class="form-row" id="objectLinkPageRow" hidden><label for="objectLinkPage">页码</label><input id="objectLinkPage" type="number" min="1" step="1"></div>
|
||||
</details>
|
||||
<div class="form-row"><label for="objectOpacity">透明</label><div class="form-unit"><input id="objectOpacity" type="number" min="0" max="100" step="any" aria-label="透明度" autofocus><span>%</span></div></div>
|
||||
<details id="objectBorderFields" hidden>
|
||||
<summary>边框</summary>
|
||||
@@ -342,6 +348,14 @@
|
||||
<dialog id="objectPicker" class="form-dialog" aria-label="选取对象">
|
||||
<form method="dialog"><div id="objectPickerList" class="object-picker-list"></div><div class="form-actions"><button class="button" type="submit">关闭</button></div></form>
|
||||
</dialog>
|
||||
<dialog id="sourceTextPanel" class="form-dialog" aria-label="保留排版">
|
||||
<form id="sourceTextForm">
|
||||
<h2 class="form-title">保留排版</h2>
|
||||
<div class="form-row"><label for="sourceTextValue">内容</label><textarea id="sourceTextValue" rows="3" dir="auto" aria-label="文字内容" required></textarea></div>
|
||||
<div id="sourceTextPreview" aria-label="文字预览"></div>
|
||||
<div class="form-actions"><span id="sourceTextStatus" class="form-status" role="status"></span><button id="sourceTextCancel" class="button" type="button">取消</button><button class="button primary" type="submit">确定</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="annotationNote" class="form-dialog" aria-label="注解">
|
||||
<form id="annotationForm" method="dialog">
|
||||
<div id="annotationDetail" class="annotation-detail"></div>
|
||||
|
||||
@@ -692,6 +692,27 @@ button:not(:disabled):hover {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
#sourceTextPanel {
|
||||
width: 440px;
|
||||
}
|
||||
|
||||
#sourceTextValue {
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
#sourceTextPreview {
|
||||
height: 160px;
|
||||
margin-top: 12px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#sourceTextPreview svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.security-dialog {
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
+90
-3
@@ -284,10 +284,22 @@ const el = {
|
||||
objectHeight: document.querySelector("#objectHeight"),
|
||||
objectAspect: document.querySelector("#objectAspect"),
|
||||
objectStylePanel: document.querySelector("#objectStylePanel"),
|
||||
sourceTextPanel: document.querySelector("#sourceTextPanel"),
|
||||
sourceTextForm: document.querySelector("#sourceTextForm"),
|
||||
sourceTextValue: document.querySelector("#sourceTextValue"),
|
||||
sourceTextPreview: document.querySelector("#sourceTextPreview"),
|
||||
sourceTextStatus: document.querySelector("#sourceTextStatus"),
|
||||
sourceTextCancel: document.querySelector("#sourceTextCancel"),
|
||||
objectStyleForm: document.querySelector("#objectStyleForm"),
|
||||
groupObjectsButton: document.querySelector("#groupObjectsButton"),
|
||||
ungroupObjectButton: document.querySelector("#ungroupObjectButton"),
|
||||
objectOpacity: document.querySelector("#objectOpacity"),
|
||||
objectLinkFields: document.querySelector("#objectLinkFields"),
|
||||
objectLinkKind: document.querySelector("#objectLinkKind"),
|
||||
objectLinkAddress: document.querySelector("#objectLinkAddress"),
|
||||
objectLinkPage: document.querySelector("#objectLinkPage"),
|
||||
objectLinkAddressRow: document.querySelector("#objectLinkAddressRow"),
|
||||
objectLinkPageRow: document.querySelector("#objectLinkPageRow"),
|
||||
objectGradientFields: document.querySelector("#objectGradientFields"),
|
||||
gradientTarget: document.querySelector("#gradientTarget"),
|
||||
objectBorderFields: document.querySelector("#objectBorderFields"),
|
||||
@@ -997,6 +1009,23 @@ el.objectBoundsForm.addEventListener("submit", async event => {
|
||||
if (saved) el.objectBoundsPanel.close();
|
||||
});
|
||||
el.objectStyleCancel.addEventListener("click", () => el.objectStylePanel.close());
|
||||
el.sourceTextCancel.addEventListener("click", () => el.sourceTextPanel.close());
|
||||
el.sourceTextValue.addEventListener("input", () => {
|
||||
clearTimeout(state.sourceTextTimer);
|
||||
state.sourceTextTimer = setTimeout(previewPositionedText, 150);
|
||||
});
|
||||
el.sourceTextPanel.addEventListener("close", () => {
|
||||
clearTimeout(state.sourceTextTimer);
|
||||
state.sourceText = null;
|
||||
el.sourceTextPreview.replaceChildren();
|
||||
});
|
||||
el.sourceTextForm.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
const item = state.sourceText?.item;
|
||||
if (!item) return;
|
||||
if (await changeDocument("ofdgoUpdateText", item, el.sourceTextValue.value, null, item.size, null)) el.sourceTextPanel.close();
|
||||
});
|
||||
el.objectLinkKind.addEventListener("change", updateObjectLinkFields);
|
||||
el.gradientKind.addEventListener("change", () => {
|
||||
el.gradientControls.hidden = el.gradientKind.value === "keep";
|
||||
el.gradientAngle.disabled = el.gradientKind.value !== "linear";
|
||||
@@ -1410,7 +1439,7 @@ function handleKeyDown(event) {
|
||||
|
||||
function formDialogOpen() {
|
||||
return el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open
|
||||
|| el.batchPagesPanel.open || el.objectStylePanel.open || el.outlinePanel.open || el.objectBoundsPanel.open || el.annotationNote.open || el.annotationCreate.open || el.objectPicker.open;
|
||||
|| el.batchPagesPanel.open || el.objectStylePanel.open || el.sourceTextPanel.open || el.outlinePanel.open || el.objectBoundsPanel.open || el.annotationNote.open || el.annotationCreate.open || el.objectPicker.open;
|
||||
}
|
||||
|
||||
function setObjectDimension(input, value) {
|
||||
@@ -1458,6 +1487,16 @@ function openObjectStyle() {
|
||||
el.copyStyleButton.disabled = items.length !== 1 || !canCopyStyle(items[0]);
|
||||
el.pasteStyleButton.disabled = !state.styleClipboard || !items.every(item => item.type === state.styleClipboard && canCopyStyle(item));
|
||||
const shared = (key, fallback) => items.every(item => (item[key] ?? fallback) === (items[0][key] ?? fallback)) ? items[0][key] ?? fallback : null;
|
||||
el.objectLinkFields.hidden = items.length !== 1 || items[0].scoped;
|
||||
const link = items[0].link ? JSON.parse(items[0].link) : null;
|
||||
const targetPage = link?.Goto?.Dest ? state.doc.pages.findIndex(page => page.id === link.Goto.Dest.PageID) : -1;
|
||||
el.objectLinkKind.value = items[0].linkCount > 1 ? "keep" : link?.URI ? "uri" : targetPage >= 0 ? "page" : link ? "keep" : "none";
|
||||
el.objectLinkKind.disabled = items[0].linkCount > 1;
|
||||
el.objectLinkAddress.value = link?.URI?.URI || "";
|
||||
el.objectLinkPage.value = String(targetPage >= 0 ? targetPage + 1 : 1);
|
||||
el.objectLinkPage.max = String(state.doc?.pages.length || 1);
|
||||
state.objectLinkOriginal = { kind: el.objectLinkKind.value, address: el.objectLinkAddress.value, page: el.objectLinkPage.value, link };
|
||||
updateObjectLinkFields();
|
||||
el.objectBorderFields.hidden = items.length !== 1 || items[0].scoped || items[0].type !== "ImageObject";
|
||||
const border = items[0].borderStyle ? JSON.parse(items[0].borderStyle) : null;
|
||||
el.borderEnabled.checked = Boolean(border);
|
||||
@@ -1517,6 +1556,14 @@ function openObjectStyle() {
|
||||
|
||||
function readObjectStyle() {
|
||||
const style = {}, changed = key => el[key].value !== state.styleOriginal[key];
|
||||
if (!el.objectLinkFields.hidden) {
|
||||
const original = state.objectLinkOriginal, kind = el.objectLinkKind.value;
|
||||
if (kind !== "keep" && (kind !== original.kind || kind === "uri" && el.objectLinkAddress.value !== original.address || kind === "page" && el.objectLinkPage.value !== original.page)) {
|
||||
style.link = kind === "none" ? null : JSON.stringify(kind === "uri"
|
||||
? { URI: { ...original.link?.URI, URI: el.objectLinkAddress.value.trim() } }
|
||||
: { Dest: { ...(original.link?.Goto?.Dest || { Type: "Fit" }), PageID: state.doc.pages[Number(el.objectLinkPage.value) - 1].id } });
|
||||
}
|
||||
}
|
||||
if (!el.objectBorderFields.hidden) {
|
||||
const border = {};
|
||||
if (el.borderEnabled.checked !== state.borderOriginal.Enabled) border.Enabled = el.borderEnabled.checked;
|
||||
@@ -1587,6 +1634,16 @@ function readObjectStyle() {
|
||||
return style;
|
||||
}
|
||||
|
||||
function updateObjectLinkFields() {
|
||||
const kind = el.objectLinkKind.value, visible = !el.objectLinkFields.hidden;
|
||||
el.objectLinkAddressRow.hidden = !visible || kind !== "uri";
|
||||
el.objectLinkPageRow.hidden = !visible || kind !== "page";
|
||||
el.objectLinkAddress.disabled = el.objectLinkAddressRow.hidden;
|
||||
el.objectLinkPage.disabled = el.objectLinkPageRow.hidden;
|
||||
el.objectLinkAddress.required = visible && kind === "uri";
|
||||
el.objectLinkPage.required = visible && kind === "page";
|
||||
}
|
||||
|
||||
function gradientForm() {
|
||||
return {
|
||||
kind: el.gradientKind.value, angle: el.gradientAngle.value,
|
||||
@@ -2084,7 +2141,18 @@ async function editCanvasObject(item) {
|
||||
return;
|
||||
}
|
||||
if (item.type === "TextObject") {
|
||||
if (!canEditObject(item, "textContent")) return;
|
||||
if (!canEditObject(item, "textContent")) {
|
||||
if (!item.scoped && canEditObject(item, "rewriteText")) {
|
||||
state.sourceText = { item, openSeq: state.openSeq };
|
||||
el.sourceTextValue.value = item.text;
|
||||
el.sourceTextStatus.textContent = "";
|
||||
el.sourceTextPreview.replaceChildren();
|
||||
el.sourceTextPanel.showModal();
|
||||
el.sourceTextValue.focus();
|
||||
await previewPositionedText();
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (!canEditObject(item, item.type === "ImageObject" ? "replaceImage" : "update")) return;
|
||||
if (item.type === "PathObject") {
|
||||
el.shapeWidth.focus();
|
||||
@@ -2114,6 +2182,22 @@ async function editCanvasObject(item) {
|
||||
}
|
||||
}
|
||||
|
||||
async function previewPositionedText() {
|
||||
const request = state.sourceText, value = el.sourceTextValue.value;
|
||||
if (!request || !el.sourceTextPanel.open) return;
|
||||
try {
|
||||
const svg = await callWASM("ofdgoPreviewPositionedText", request.item.index, request.item.id, value);
|
||||
if (state.sourceText !== request || request.openSeq !== state.openSeq || el.sourceTextValue.value !== value) return;
|
||||
el.sourceTextPreview.replaceChildren(parseSVG(svg));
|
||||
el.sourceTextStatus.textContent = "";
|
||||
} catch (error) {
|
||||
if (state.sourceText === request && request.openSeq === state.openSeq && el.sourceTextValue.value === value) {
|
||||
el.sourceTextPreview.replaceChildren();
|
||||
el.sourceTextStatus.textContent = error.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mountEditorObjects(index, page, surface) {
|
||||
const scope = state.composite?.index === index ? state.composite : null;
|
||||
canvasEditor.mount(index, { ...page, objects: scope ? scope.objects : page.objects.filter(item => state.renderAnnotations || item.type !== "Annotation") }, surface);
|
||||
@@ -2714,6 +2798,8 @@ async function changeDocument(name, item, ...args) {
|
||||
el.batchPagesStatus.textContent = err.message;
|
||||
} else if (el.objectStylePanel.open) {
|
||||
el.objectStyleStatus.textContent = err.message;
|
||||
} else if (el.sourceTextPanel.open) {
|
||||
el.sourceTextStatus.textContent = err.message;
|
||||
} else if (el.objectBoundsPanel.open) {
|
||||
el.objectBoundsStatus.textContent = err.message;
|
||||
} else if (el.outlinePanel.open) {
|
||||
@@ -3275,6 +3361,7 @@ async function openOFD(file) {
|
||||
el.importPanel.close();
|
||||
el.batchPagesPanel.close();
|
||||
el.objectStylePanel.close();
|
||||
el.sourceTextPanel.close();
|
||||
el.objectBoundsPanel.close();
|
||||
el.outlinePanel.close();
|
||||
updateControls();
|
||||
@@ -7077,7 +7164,7 @@ function updateObjectControls(item, reset = false) {
|
||||
el.resetCropButton.disabled = canEditObject(item, "resetCrop") ? 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.editObjectButton.disabled = disabled || Boolean(item.items) || !canEditObject(item, "enter") && (item.type === "PathObject"
|
||||
|| !canEditObject(item, item.type === "TextObject" ? "textContent" : item.type === "ImageObject" ? "replaceImage" : "update"));
|
||||
|| !(item.type === "TextObject" && !item.scoped && canEditObject(item, "rewriteText")) && !canEditObject(item, item.type === "TextObject" ? "textContent" : item.type === "ImageObject" ? "replaceImage" : "update"));
|
||||
el.editObjectButton.textContent = canEditObject(item, "enter") ? "进入" : item?.type === "ImageObject" ? "替换" : "修改";
|
||||
el.editObjectButton.title = canEditObject(item, "enter") ? item?.type === "Annotation" ? "进入注解" : "进入组合" : item?.type === "ImageObject" ? "替换图片" : item?.type === "Annotation" ? "修改注解" : "修改对象";
|
||||
el.editObjectButton.setAttribute("aria-label", el.editObjectButton.title);
|
||||
|
||||
@@ -1186,10 +1186,12 @@ export class CanvasEditor {
|
||||
input.setAttribute("aria-label", "编辑文字");
|
||||
input.setAttribute("role", "textbox");
|
||||
input.setAttribute("aria-multiline", "true");
|
||||
input.setAttribute("dir", "auto");
|
||||
input.contentEditable = "true";
|
||||
input.spellcheck = false;
|
||||
for (const text of item.text.split("\n")) {
|
||||
const paragraph = document.createElement("div");
|
||||
paragraph.setAttribute("dir", "auto");
|
||||
if (text) paragraph.textContent = text;
|
||||
else paragraph.append(document.createElement("br"));
|
||||
input.append(paragraph);
|
||||
@@ -1206,6 +1208,7 @@ export class CanvasEditor {
|
||||
minHeight: autoSize ? "0px" : `${frame.height * PX_PER_MM + 4}px`,
|
||||
transform: `matrix(${a},${b},${c},${d},${(e + a * left) * PX_PER_MM},${(f + b * left) * PX_PER_MM})`,
|
||||
whiteSpace: item.wrap ? "pre-wrap" : "pre",
|
||||
unicodeBidi: "plaintext",
|
||||
fontFamily: `"${face.family}"`, fontSize: `${item.size * PX_PER_MM}px`,
|
||||
lineHeight: item.paragraphHeight || item.lineHeight ? `${(item.paragraphHeight || item.lineHeight) * PX_PER_MM}px` : "normal", color: item.color,
|
||||
textAlign: item.align || "left",
|
||||
|
||||
+65
-4
@@ -184,6 +184,7 @@ func RunWASM() {
|
||||
registerCallback("ofdgoReadAnnotation", readAnnotation)
|
||||
registerCallback("ofdgoInsertInk", insertInk)
|
||||
registerCallback("ofdgoStyleObjects", styleObjects)
|
||||
registerCallback("ofdgoPreviewPositionedText", previewPositionedText)
|
||||
registerCallback("ofdgoMoveOutline", moveOutline)
|
||||
registerCallback("ofdgoCaptureStyle", captureStyle)
|
||||
registerCallback("ofdgoGroupObjects", groupObjects)
|
||||
@@ -1702,6 +1703,7 @@ func editorCapabilities(capability ofdgo.ObjectCapabilities, kind string) map[st
|
||||
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, "fitImage": capability.FitImage, "resetCrop": capability.ResetCrop, "ungroup": capability.Ungroup, "stretch": capability.Stretch, "enter": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
|
||||
result["textContent"] = capability.TextContent
|
||||
result["rewriteText"] = capability.RewriteText
|
||||
if missing := capability.MissingGlyphs; missing != nil {
|
||||
result["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters}
|
||||
}
|
||||
@@ -1711,6 +1713,16 @@ func editorCapabilities(capability ofdgo.ObjectCapabilities, kind string) map[st
|
||||
// editorAppearance 提供对象自身透明度及有效绘制外观,不混入父对象透明度
|
||||
// 入参: item 前端对象, object 有效样式, paint 是否可改色, scale 描边页面倍率
|
||||
func editorAppearance(item map[string]any, object ofdgo.GraphicObject, paint bool, scale float64) {
|
||||
links := 0
|
||||
for _, action := range object.Actions() {
|
||||
if action.Event == "CLICK" && (action.URI != nil || action.Goto != nil) {
|
||||
links++
|
||||
if data, err := json.Marshal(action); err == nil {
|
||||
item["link"] = string(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
item["linkCount"] = links
|
||||
var alpha *int
|
||||
var fill, stroke *ofdgo.FillColor
|
||||
switch object.Type {
|
||||
@@ -1945,7 +1957,7 @@ func changeCompositeObjects(args []js.Value) (any, error) {
|
||||
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()}
|
||||
layout := ofdgo.TextLayout{AutoBidi: true, 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)
|
||||
}
|
||||
@@ -2224,6 +2236,40 @@ func previewText(args []js.Value) (any, error) {
|
||||
return map[string]any{"runs": runs, "scale": scale, "weight": text.Weight, "italic": text.Italic}, nil
|
||||
}
|
||||
|
||||
// previewPositionedText 预览保留定位的文字替换,不修改编辑器或撤销记录
|
||||
// 入参: args 页面、对象标识和内容
|
||||
// 返回: any SVG预览, error 错误信息
|
||||
func previewPositionedText(args []js.Value) (any, error) {
|
||||
object, err := currentEditor.Object(args[0].Int(), args[1].String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if object.Type != "TextObject" {
|
||||
return nil, fmt.Errorf("object is not text")
|
||||
}
|
||||
if err := currentEditor.RewriteText(&object.TextObject, args[2].String()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
measured, err := currentSession.Renderer.MeasureObject(object, ofdgo.MeasureOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
box := measured.Bounds
|
||||
margin := object.TextObject.Size / 4
|
||||
box.X, box.Y, box.W, box.H = box.X-margin, box.Y-margin, box.W+2*margin, box.H+2*margin
|
||||
boundary, err := ofdgo.ParseBox(object.TextObject.Boundary)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
object.TextObject.Boundary = fmt.Sprintf("%g %g %g %g", boundary.X-box.X, boundary.Y-box.Y, boundary.W, boundary.H)
|
||||
page := &ofdgo.PageContent{Area: ofdgo.PageArea{PhysicalBox: fmt.Sprintf("0 0 %g %g", box.W, box.H)}, Content: ofdgo.Content{Layer: []ofdgo.Layer{{Objects: []ofdgo.GraphicObject{object}}}}}
|
||||
var output bytes.Buffer
|
||||
if err := currentSession.Renderer.RenderToSVG(page, &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return output.String(), nil
|
||||
}
|
||||
|
||||
// updateText 修改文字,null内容保留定位且允许替换字体,null字体数据和颜色沿用对象属性
|
||||
// 入参: args 页码、对象标识、内容、字体数据、字号和颜色
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
@@ -2258,7 +2304,10 @@ func updateText(args []js.Value) (any, error) {
|
||||
}
|
||||
if !args[2].IsNull() {
|
||||
if args[4].Float() == object.TextObject.Size {
|
||||
style := ofdgo.TextStyle{Font: object.TextObject.Font}
|
||||
var style ofdgo.TextStyle
|
||||
if !args[3].IsNull() {
|
||||
style.Font = object.TextObject.Font
|
||||
}
|
||||
if !args[5].IsNull() {
|
||||
var fill ofdgo.FillColor
|
||||
if err := setEditorColor(&fill, args[5].String()); err != nil {
|
||||
@@ -2407,7 +2456,7 @@ func insertText(args []js.Value) (any, error) {
|
||||
Boundary: fmt.Sprintf("%g %g %g %g", box.X, box.Y, box.W, box.H),
|
||||
Font: fontID, Size: args[5].Float(),
|
||||
}}
|
||||
if err := currentEditor.LayoutText(&object.TextObject, args[1].String(), ofdgo.TextLayout{Wrap: args[8].Bool(), Align: args[9].String(), LineHeight: args[10].Float(), LetterSpacing: args[11].Float(), LeftIndent: args[12].Float(), RightIndent: args[13].Float(), FirstLineIndent: args[14].Float()}); err != nil {
|
||||
if err := currentEditor.LayoutText(&object.TextObject, args[1].String(), ofdgo.TextLayout{AutoBidi: true, Wrap: args[8].Bool(), Align: args[9].String(), LineHeight: args[10].Float(), LetterSpacing: args[11].Float(), LeftIndent: args[12].Float(), RightIndent: args[13].Float(), FirstLineIndent: args[14].Float()}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := setTextColor(&object.TextObject, args[6].String()); err != nil {
|
||||
@@ -2438,7 +2487,7 @@ func layoutText(args []js.Value) (any, error) {
|
||||
}
|
||||
}
|
||||
value, _ := object.TextObject.TextLayout()
|
||||
if err := currentEditor.LayoutText(&object.TextObject, value, ofdgo.TextLayout{Wrap: args[4].Bool(), Align: args[5].String(), LineHeight: args[6].Float(), LetterSpacing: args[7].Float(), LeftIndent: args[8].Float(), RightIndent: args[9].Float(), FirstLineIndent: args[10].Float()}); err != nil {
|
||||
if err := currentEditor.LayoutText(&object.TextObject, value, ofdgo.TextLayout{AutoBidi: true, Wrap: args[4].Bool(), Align: args[5].String(), LineHeight: args[6].Float(), LetterSpacing: args[7].Float(), LeftIndent: args[8].Float(), RightIndent: args[9].Float(), FirstLineIndent: args[10].Float()}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
revision := currentEditor.Revision()
|
||||
@@ -2938,6 +2987,13 @@ func changeOutline(args []js.Value) (any, error) {
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func styleObjects(args []js.Value) (any, error) {
|
||||
style := objectStyle(args[2])
|
||||
linkField := args[2].Get("link")
|
||||
var link *ofdgo.AnnotationLink
|
||||
if !linkField.IsUndefined() && !linkField.IsNull() {
|
||||
if err := json.Unmarshal([]byte(linkField.String()), &link); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var border *ofdgo.ImageBorderStyle
|
||||
if field := args[2].Get("borderStyle"); !field.IsUndefined() {
|
||||
if err := json.Unmarshal([]byte(field.String()), &border); err != nil {
|
||||
@@ -2967,6 +3023,11 @@ func styleObjects(args []js.Value) (any, error) {
|
||||
return changeObjects(func() error {
|
||||
return currentEditor.Transaction(func(editor *ofdgo.Editor) error {
|
||||
page, ids := args[0].Int(), stringsFromJS(args[1])
|
||||
if !linkField.IsUndefined() {
|
||||
if err := editor.SetObjectLink(page, ids, link); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if border != nil {
|
||||
if err := resolveStylePaint((*ofdgo.FillColor)(border.Color)); err != nil {
|
||||
return err
|
||||
|
||||
+17
-1
@@ -149,6 +149,17 @@ func backendName(backend Backend) string {
|
||||
func WithRenderBackends(backends RenderBackends) RendererOption {
|
||||
return func(r *Renderer) {
|
||||
fontsChanged := !sameBackend(r.backends.Fonts, backends.Fonts)
|
||||
for _, pair := range [][2]Backend{
|
||||
{r.backends.FontResources, backends.FontResources}, {r.backends.Fonts, backends.Fonts},
|
||||
{r.backends.Geometry, backends.Geometry}, {r.backends.Compiler, backends.Compiler},
|
||||
{r.backends.Raster, backends.Raster}, {r.backends.SVG, backends.SVG},
|
||||
{r.backends.PDF, backends.PDF}, {r.backends.EPS, backends.EPS},
|
||||
} {
|
||||
if !sameBackend(pair[0], pair[1]) {
|
||||
r.backendStates = make(map[any]any)
|
||||
break
|
||||
}
|
||||
}
|
||||
r.backends = backends
|
||||
if fontsChanged {
|
||||
r.resetFontBackendCache()
|
||||
@@ -202,7 +213,12 @@ func (r *Renderer) Backends() RenderBackends {
|
||||
// 入参: backend 光栅后端,nil禁用图像输出
|
||||
// 返回: RendererOption 渲染选项
|
||||
func WithRasterBackend(backend RasterBackend) RendererOption {
|
||||
return func(r *Renderer) { r.backends.Raster = backend }
|
||||
return func(r *Renderer) {
|
||||
if !sameBackend(r.backends.Raster, backend) {
|
||||
r.backendStates = make(map[any]any)
|
||||
}
|
||||
r.backends.Raster = backend
|
||||
}
|
||||
}
|
||||
|
||||
// CompilePage 编译与后端无关的光栅页面,可交给任意RasterBackend绘制
|
||||
|
||||
+2
-1
@@ -28,7 +28,7 @@ var (
|
||||
ErrEncryptionPolicyRequired = errors.New("explicit encryption policy required")
|
||||
)
|
||||
|
||||
// Credentials 解密凭据,UserName为空时尝试匹配用户;证书采用DER编码
|
||||
// Credentials 解密凭据,UserName用于筛选接收者,留空自动匹配,不参与口令派生;证书采用DER编码
|
||||
// Password与Decrypter分别用于口令和证书方案,私钥可由外部密码设备持有
|
||||
type Credentials struct {
|
||||
UserName string
|
||||
@@ -86,6 +86,7 @@ type EncryptionRecipient struct {
|
||||
}
|
||||
|
||||
// EncryptionOptions 设置GM/T0099加密,口令与证书方案二选一
|
||||
// UserName标记口令接收者,不参与口令派生,留空使用User
|
||||
// 口令方案应使用足够长的随机口令;标准密钥派生不提供慢速口令哈希
|
||||
type EncryptionOptions struct {
|
||||
Password []byte
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ func (e *Editor) SetFontFS(fsys ...fs.FS) {
|
||||
func (e *Editor) SetPageCompiler(compiler PageCompiler) {
|
||||
e.backends.Compiler = compiler
|
||||
if e.fontRenderer != nil {
|
||||
e.fontRenderer.backends.Compiler = compiler
|
||||
WithRenderBackends(e.backends)(e.fontRenderer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -526,7 +526,7 @@ func (e *Editor) updateObjectOrigins(page int, objects []GraphicObject, preserve
|
||||
beforeOrigins[ids[i]], afterOrigins[ids[i]] = origin, next
|
||||
}
|
||||
}
|
||||
if reflect.DeepEqual(before, after) {
|
||||
if reflect.DeepEqual(before, after) && len(afterOrigins) == 0 {
|
||||
return nil
|
||||
}
|
||||
apply := func(e *Editor, objects []GraphicObject, origins map[string]*editorObjectOrigin) {
|
||||
|
||||
+270
-100
@@ -56,6 +56,7 @@ type editorPageImport struct {
|
||||
defaultCS string
|
||||
attachments map[string][]byte
|
||||
copyPage bool
|
||||
strictLinks bool
|
||||
progress editorProgress
|
||||
}
|
||||
|
||||
@@ -69,6 +70,152 @@ type PageImportOptions struct {
|
||||
OnProgress func(stage string, completed, total int) error
|
||||
}
|
||||
|
||||
// ObjectImportOptions 跨文档对象迁移选项,位移单位为毫米
|
||||
// AllowDecrypted显式允许将加密来源导入未加密目标,进度回调返回错误时撤销整个操作
|
||||
type ObjectImportOptions struct {
|
||||
DX, DY float64
|
||||
AllowDecrypted bool
|
||||
OnProgress func(stage string, completed, total int) error
|
||||
}
|
||||
|
||||
// ImportObjects 迁移选区及引用资源,不改变来源与目标页面结构,提交一次撤销记录
|
||||
// 保留对象原文与图层继承,同页链接映射到目标页,无法迁移的跨页链接明确报错
|
||||
// 返回后可关闭来源阅读器,后续修改来源不影响副本
|
||||
// 入参: source 来源编辑器, sourcePage 来源页, ids 对象标识, page 目标页, options 迁移选项
|
||||
// 返回: []string 新对象标识, error 错误信息
|
||||
func (e *Editor) ImportObjects(source *Editor, sourcePage int, ids []string, page int, options ObjectImportOptions) ([]string, error) {
|
||||
if !finite(options.DX) || !finite(options.DY) {
|
||||
return nil, fmt.Errorf("import requires finite offsets")
|
||||
}
|
||||
if source.encryption != nil && e.encryption == nil && !options.AllowDecrypted {
|
||||
return nil, ErrEncryptionPolicyRequired
|
||||
}
|
||||
objects, err := source.Objects(sourcePage, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target, err := e.page(page)
|
||||
if err != nil || len(objects) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
if source == e {
|
||||
return e.CopyObjects(page, objects, options.DX, options.DY)
|
||||
}
|
||||
reader, err := source.Reader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc, err := reader.Doc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ref := doc.Pages.Page[sourcePage]
|
||||
progress := editorProgress(options.OnProgress)
|
||||
base, migration, err := e.prepareImport(reader, doc, map[string]bool{ref.ID: true}, false, progress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
migration.ids[ref.ID], migration.strictLinks = target.ID, true
|
||||
pageName := reader.ResPath(ref.BaseLoc)
|
||||
content, err := reader.PageContentByIndex(sourcePage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, value := range content.PageRes {
|
||||
name, err := migration.resolve(pageName, "", value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := migration.resourceIndex(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
layers := make([][]byte, len(objects))
|
||||
for i, object := range objects {
|
||||
if err := progress.report("objects", i, len(objects)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var data []byte
|
||||
var attrs ofdAttrs
|
||||
if origin := object.origin; origin != nil {
|
||||
data, err = editorXMLObject(origin.data, origin.node, origin.object, object)
|
||||
if err == nil {
|
||||
data, err = editorXMLStandalone(data, origin.node)
|
||||
}
|
||||
for parent := origin.node.parent; parent != nil; parent = parent.parent {
|
||||
if parent.name.Local == "Layer" {
|
||||
attrs.add("DrawParam", parent.attr("DrawParam"))
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data, err = editorObjectXML(object)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err = editorXMLContainer("Layer", attrs, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root, err := parseEditorXML(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
layers[i], err = migration.encode(editorImportEntry{name: pageName, data: data}, root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := migration.encodeResources(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, resource := range e.resources {
|
||||
delete(migration.files, resource.name)
|
||||
}
|
||||
next, err := migration.merge(base, nil, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var copied []string
|
||||
err = e.Transaction(func(editor *Editor) error {
|
||||
if err := progress.report("commit", 0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
editor.bindImportResources()
|
||||
editor.source, editor.maxID = next, migration.maximum
|
||||
next.idsReady = true
|
||||
imported := make([]GraphicObject, len(layers))
|
||||
for i, data := range layers {
|
||||
data, err := editorXMLContainer("Content", nil, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var content Content
|
||||
if err := xml.Unmarshal(data, &content); err != nil {
|
||||
return err
|
||||
}
|
||||
layer := content.Layer[0]
|
||||
root, err := parseEditorXML(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
object := layer.Objects[0]
|
||||
imported[i], err = editor.copiedObjectStyle(object, layer.DrawParam)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imported[i].TextObject.layout = objects[i].TextObject.layout
|
||||
imported[i].state = objects[i].state
|
||||
imported[i].CompositeGraphicUnit.states = remapCompositeStates(objects[i].CompositeGraphicUnit.states, migration.ids)
|
||||
imported[i].origin = &editorObjectOrigin{editor: editor, data: data, node: root.child("Layer").children[0], object: object}
|
||||
}
|
||||
copied, err = editor.CopyObjects(page, imported, options.DX, options.DY)
|
||||
return err
|
||||
})
|
||||
return copied, err
|
||||
}
|
||||
|
||||
// ImportPages 将来源主文档中的指定页面插入目标位置,保持输入顺序并作为一次撤销操作
|
||||
// 页面索引从0开始,at可等于当前页数;复制关联模板、注释、签章外观和实际引用的资源
|
||||
// 不导入来源元数据和目录,指向未选页面的跳转被移除;签名数据仅保留原始凭据,不代表合并后文档有效
|
||||
@@ -117,56 +264,10 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
||||
return nil, nil
|
||||
}
|
||||
progress := editorProgress(options.OnProgress)
|
||||
maximum, err := e.sourceMaxID(progress)
|
||||
base, m, err := e.prepareImport(source, doc, selected, copyPage, progress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base := e.source
|
||||
if base == nil {
|
||||
base, err = e.importBase()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
m := &editorPageImport{reader: source, doc: doc, directory: e.packageDirectory(), target: base.reader, maximum: maximum, copyPage: copyPage, progress: progress,
|
||||
paths: make(map[string]string), evidence: make(map[string]string),
|
||||
ids: make(map[string]string), pages: selected, files: make(map[string][]byte), resources: make(map[string]editorImportEntry), used: make(map[string][]byte), templates: make(map[string]TemplatePage), templateParts: make(map[string][]byte), attachments: make(map[string][]byte)}
|
||||
for _, resource := range e.resources {
|
||||
m.files[resource.name] = nil
|
||||
}
|
||||
m.documentXML, err = m.readFile(source.ResPath(source.OFD.DocBody[0].DocRoot))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.documentRoot, err = parseEditorXML(m.documentXML)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if copyPage {
|
||||
for _, page := range doc.Pages.Page {
|
||||
if !selected[page.ID] {
|
||||
m.ids[page.ID] = page.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, name := range append(slices.Clone(doc.CommonData.PublicRes), doc.CommonData.DocumentRes...) {
|
||||
if err := m.resourceIndex(source.ResPath(name)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, template := range doc.CommonData.TemplatePage {
|
||||
m.templates[template.ID] = template
|
||||
}
|
||||
if doc.CommonData.DefaultCS != 0 {
|
||||
m.defaultCS, err = m.reference(strconv.Itoa(doc.CommonData.DefaultCS))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if !e.sourceRGB() {
|
||||
m.defaultCS = m.id("defaultRGB")
|
||||
m.used["defaultRGB"] = []byte(`<ofd:ColorSpace ID="` + m.defaultCS + `" Type="RGB" BitsPerComponent="8"/>`)
|
||||
m.resources["defaultRGB"] = editorImportEntry{group: "ColorSpaces"}
|
||||
}
|
||||
refs := make([]Page, len(indexes))
|
||||
ids := make([]string, len(indexes))
|
||||
for i, index := range indexes {
|
||||
@@ -209,30 +310,9 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
||||
}
|
||||
}
|
||||
if !copyPage {
|
||||
var resourceContent []byte
|
||||
for _, group := range []string{"ColorSpaces", "DrawParams", "Fonts", "MultiMedias", "CompositeGraphicUnits"} {
|
||||
var entries []byte
|
||||
for _, id := range slices.Sorted(maps.Keys(m.used)) {
|
||||
if m.resources[id].group == group {
|
||||
entries = append(entries, m.used[id]...)
|
||||
}
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
continue
|
||||
}
|
||||
encoded, err := editorXMLContainer(group, nil, entries)
|
||||
if err != nil {
|
||||
if err := m.encodeResources(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resourceContent = append(resourceContent, encoded...)
|
||||
}
|
||||
resources, err := editorXMLContainer("Res", nil, resourceContent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(resourceContent) != 0 {
|
||||
m.resourceXML = resources
|
||||
}
|
||||
}
|
||||
for _, resource := range e.resources {
|
||||
delete(m.files, resource.name)
|
||||
@@ -281,19 +361,7 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
||||
if err := progress.report("commit", 0, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.source == nil {
|
||||
for i := range e.resources {
|
||||
resource := &e.resources[i]
|
||||
if resource.font != nil {
|
||||
resource.font = cloneEditorData(resource.font)
|
||||
resource.font.FontFile = "/" + resource.name
|
||||
}
|
||||
if resource.image != nil {
|
||||
resource.image = cloneEditorData(resource.image)
|
||||
resource.image.MediaFile = "/" + resource.name
|
||||
}
|
||||
}
|
||||
}
|
||||
e.bindImportResources()
|
||||
e.source, e.maxID = next, m.maximum
|
||||
next.idsReady = true
|
||||
e.outlines = outlines
|
||||
@@ -320,6 +388,111 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// bindImportResources 将新建文档资源路径绑定到包根,保持迁移前后的解析一致
|
||||
func (e *Editor) bindImportResources() {
|
||||
if e.source != nil {
|
||||
return
|
||||
}
|
||||
for i := range e.resources {
|
||||
resource := &e.resources[i]
|
||||
if resource.font != nil {
|
||||
resource.font = cloneEditorData(resource.font)
|
||||
resource.font.FontFile = "/" + resource.name
|
||||
}
|
||||
if resource.image != nil {
|
||||
resource.image = cloneEditorData(resource.image)
|
||||
resource.image.MediaFile = "/" + resource.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// prepareImport 建立页面和对象迁移共用的资源图,不修改目标编辑器
|
||||
// 入参: source 来源, doc 来源文档, selected 页面映射范围, copyPage 是否同文档, progress 进度
|
||||
// 返回: *editorSource 目标基础, *editorPageImport 资源图, error 错误信息
|
||||
func (e *Editor) prepareImport(source *Reader, doc *Document, selected map[string]bool, copyPage bool, progress editorProgress) (*editorSource, *editorPageImport, error) {
|
||||
maximum, err := e.sourceMaxID(progress)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
base := e.source
|
||||
if base == nil {
|
||||
base, err = e.importBase()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
m := &editorPageImport{reader: source, doc: doc, directory: e.packageDirectory(), target: base.reader, maximum: maximum, copyPage: copyPage, progress: progress,
|
||||
paths: make(map[string]string), evidence: make(map[string]string), ids: make(map[string]string), pages: selected,
|
||||
files: make(map[string][]byte), resources: make(map[string]editorImportEntry), used: make(map[string][]byte),
|
||||
templates: make(map[string]TemplatePage), templateParts: make(map[string][]byte), attachments: make(map[string][]byte)}
|
||||
for _, resource := range e.resources {
|
||||
m.files[resource.name] = nil
|
||||
}
|
||||
m.documentXML, err = m.readFile(source.ResPath(source.OFD.DocBody[0].DocRoot))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
m.documentRoot, err = parseEditorXML(m.documentXML)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if copyPage {
|
||||
for _, page := range doc.Pages.Page {
|
||||
if !selected[page.ID] {
|
||||
m.ids[page.ID] = page.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, name := range append(slices.Clone(doc.CommonData.PublicRes), doc.CommonData.DocumentRes...) {
|
||||
if err := m.resourceIndex(source.ResPath(name)); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
for _, template := range doc.CommonData.TemplatePage {
|
||||
m.templates[template.ID] = template
|
||||
}
|
||||
if doc.CommonData.DefaultCS != 0 {
|
||||
m.defaultCS, err = m.reference(strconv.Itoa(doc.CommonData.DefaultCS))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
} else if !e.sourceRGB() {
|
||||
m.defaultCS = m.id("defaultRGB")
|
||||
m.used["defaultRGB"] = []byte(`<ofd:ColorSpace ID="` + m.defaultCS + `" Type="RGB" BitsPerComponent="8"/>`)
|
||||
m.resources["defaultRGB"] = editorImportEntry{group: "ColorSpaces"}
|
||||
}
|
||||
return base, m, nil
|
||||
}
|
||||
|
||||
// encodeResources 编码本次迁移实际引用的资源,不包含未使用的资源定义
|
||||
// 返回: error 编码错误
|
||||
func (m *editorPageImport) encodeResources() error {
|
||||
var content []byte
|
||||
ids := slices.Sorted(maps.Keys(m.used))
|
||||
for _, group := range []string{"ColorSpaces", "DrawParams", "Fonts", "MultiMedias", "CompositeGraphicUnits"} {
|
||||
var entries []byte
|
||||
for _, id := range ids {
|
||||
if m.resources[id].group == group {
|
||||
entries = append(entries, m.used[id]...)
|
||||
}
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
continue
|
||||
}
|
||||
encoded, err := editorXMLContainer(group, nil, entries)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content = append(content, encoded...)
|
||||
}
|
||||
if len(content) == 0 {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
m.resourceXML, err = editorXMLContainer("Res", nil, content)
|
||||
return err
|
||||
}
|
||||
|
||||
// readFile 分块读取导入条目,允许资源解压过程中取消,不共享来源内存
|
||||
// 入参: name 包内路径
|
||||
// 返回: []byte 条目内容, error 错误信息
|
||||
@@ -738,6 +911,9 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
|
||||
bookmarks[bookmark.Name] = bookmark.Dest
|
||||
}
|
||||
if dest := gotoDest(&action, bookmarks); !m.copyPage && (dest == nil || !m.pages[dest.PageID]) {
|
||||
if m.strictLinks {
|
||||
return nil, fmt.Errorf("object link targets a page outside the imported selection")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
@@ -1073,20 +1249,24 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
|
||||
for _, id := range slices.Sorted(maps.Keys(m.templateParts)) {
|
||||
content = append(content, m.templateParts[id]...)
|
||||
}
|
||||
data = editorPatchXML(data, []editorXMLPatch{{position, position, content}})
|
||||
root, err = parseEditorXML(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
patches := []editorXMLPatch{{position, position, content}}
|
||||
patch := editorXMLPatch{common.open, common.open, editorXMLText("MaxUnitID", strconv.Itoa(m.maximum))}
|
||||
if maximum := common.child("MaxUnitID"); maximum != nil {
|
||||
patch = editorXMLContent(data, maximum, []byte(strconv.Itoa(m.maximum)))
|
||||
}
|
||||
patches = append(patches, patch)
|
||||
pages := root.child("Pages")
|
||||
if pages == nil {
|
||||
return nil, fmt.Errorf("document has no Pages")
|
||||
}
|
||||
content = bytes.Clone(m.pageParts)
|
||||
if pages.open != pages.end {
|
||||
content = append(bytes.Clone(data[pages.open:pages.close]), content...)
|
||||
if len(m.pageParts) != 0 {
|
||||
if pages.open == pages.end {
|
||||
patches = append(patches, editorXMLContent(data, pages, m.pageParts))
|
||||
} else {
|
||||
patches = append(patches, editorXMLPatch{pages.close, pages.close, m.pageParts})
|
||||
}
|
||||
data = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, pages, content)})
|
||||
}
|
||||
data = editorPatchXML(data, patches)
|
||||
if len(annotations) != 0 {
|
||||
loc, err := mergeImportIndex(base.reader, files, base.document.Annotations, path.Join(m.directory, "Annotations.xml"), "Annotations", annotations)
|
||||
if err != nil {
|
||||
@@ -1117,16 +1297,6 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
|
||||
}
|
||||
}
|
||||
}
|
||||
root, err = parseEditorXML(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
common = root.child("CommonData")
|
||||
patch := editorXMLPatch{common.open, common.open, editorXMLText("MaxUnitID", strconv.Itoa(m.maximum))}
|
||||
if maximum := common.child("MaxUnitID"); maximum != nil {
|
||||
patch = editorXMLContent(data, maximum, []byte(strconv.Itoa(m.maximum)))
|
||||
}
|
||||
data = editorPatchXML(data, []editorXMLPatch{patch})
|
||||
files[name] = data
|
||||
if len(signatures) != 0 {
|
||||
loc, err := mergeImportIndex(base.reader, files, base.document.Signatures, path.Join(m.directory, "Signs", "Signatures.xml"), "Signatures", signatures)
|
||||
|
||||
@@ -71,10 +71,12 @@ type editorSourcePage struct {
|
||||
// Paint表示可独立修改纯色填充和描边,不要求重新排版文字
|
||||
// ReplaceImage表示可替换图片数据,CropImage表示可裁剪图片;内部裁剪与原裁剪取交集
|
||||
// MissingGlyphs提供缺字导致操作受限时的结构化诊断
|
||||
// RewriteText表示可保留原定位修改,具体替换仍需校验长度、字形映射和字体覆盖
|
||||
// Ungroup表示可移除组合容器并保留内部内容,Stretch表示图片及纯图片组合可独立调整宽高
|
||||
type ObjectCapabilities struct {
|
||||
Update bool
|
||||
TextContent bool
|
||||
RewriteText bool
|
||||
Paint bool
|
||||
ReplaceFont bool
|
||||
ReplaceImage bool
|
||||
@@ -504,6 +506,7 @@ func (e *Editor) objectCapabilities(object GraphicObject, orderable map[*editorX
|
||||
origin := e.objectOrigin(id)
|
||||
if origin == nil {
|
||||
all.TextContent = all.Reflow
|
||||
all.RewriteText = object.Type == "TextObject"
|
||||
all.Stretch = object.Type == "ImageObject"
|
||||
all.ReplaceImage = object.Type == "ImageObject"
|
||||
all.CropImage = all.ReplaceImage
|
||||
@@ -551,7 +554,12 @@ func (e *Editor) objectCapabilities(object GraphicObject, orderable map[*editorX
|
||||
all.Reason, all.ReasonCode = "image actions cannot be transformed together with the border", EditUnsupportedObject
|
||||
}
|
||||
if object.Type == "TextObject" {
|
||||
all.TextContent = all.Update && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0
|
||||
all.TextContent = all.Update && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0 && (all.LayoutKnown || len(object.TextObject.CGTransform) == 0)
|
||||
all.RewriteText = all.TextContent
|
||||
if !all.RewriteText {
|
||||
text := object.TextObject
|
||||
all.RewriteText = e.RewriteText(&text, text.Text()) == nil
|
||||
}
|
||||
all.Arrange = all.Update || e.editorTextMeasurable(object.TextObject)
|
||||
if _, err := object.TextObject.TextFrame(); err != nil {
|
||||
all.Reflow = false
|
||||
|
||||
+18
-3
@@ -236,7 +236,22 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error)
|
||||
// 返回: []byte 页面XML, error 错误信息
|
||||
func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, error) {
|
||||
page := e.pages[index]
|
||||
if reflect.DeepEqual(page, *source.original) {
|
||||
unchanged := reflect.DeepEqual(page, *source.original)
|
||||
if unchanged && len(e.origins) != 0 {
|
||||
for _, layer := range page.Content.Layer {
|
||||
for _, object := range layer.Objects {
|
||||
id := editorObjectID(object)
|
||||
if origin := e.origins[id]; origin != nil && origin.node != source.nodes[id] {
|
||||
unchanged = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !unchanged {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if unchanged {
|
||||
return source.data, nil
|
||||
}
|
||||
if source.root.open == source.root.end {
|
||||
@@ -314,11 +329,11 @@ func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, err
|
||||
if exists {
|
||||
nextID := editorObjectID(next)
|
||||
origin := source.nodes[nextID]
|
||||
if origin != nil && reflect.DeepEqual(before[nextID], next) {
|
||||
current := e.objectOrigin(nextID)
|
||||
if origin != nil && current.node == origin && reflect.DeepEqual(before[nextID], next) {
|
||||
encoded = data[origin.start:origin.end]
|
||||
} else {
|
||||
var err error
|
||||
current := e.objectOrigin(nextID)
|
||||
encoded, err = editorXMLObject(current.data, current.node, current.object, next)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math"
|
||||
@@ -45,6 +46,136 @@ type ImageBorderStyle struct {
|
||||
Color *StrokeColor
|
||||
}
|
||||
|
||||
// DrawParam 读取绘制参数,resolved为true时合并继承链,返回独立副本
|
||||
// 入参: id 资源标识, resolved 是否解析继承
|
||||
// 返回: *DrawParam 绘制参数, error 错误信息
|
||||
func (e *Editor) DrawParam(id string, resolved bool) (*DrawParam, error) {
|
||||
if resolved {
|
||||
return e.editorDrawParam(id, make(map[string]bool))
|
||||
}
|
||||
for _, resource := range e.resources {
|
||||
if resource.draw != nil && resource.draw.ID == id {
|
||||
return cloneEditorData(resource.draw), nil
|
||||
}
|
||||
}
|
||||
if e.source != nil {
|
||||
if value := e.source.reader.drawParamCache[id]; value != nil {
|
||||
return cloneEditorData(value), nil
|
||||
}
|
||||
}
|
||||
return nil, &EditError{Code: EditUnsupportedStyle, Err: fmt.Errorf("draw parameter %q not found", id)}
|
||||
}
|
||||
|
||||
// SetObjectDrawParam 修改对象的绘制参数引用,保留局部覆盖和原文扩展,空标识恢复图层继承
|
||||
// 入参: page 页面索引, ids 对象标识, id 绘制参数标识
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) SetObjectDrawParam(page int, ids []string, id string) error {
|
||||
if _, err := e.editorDrawParam(id, make(map[string]bool)); err != nil {
|
||||
return err
|
||||
}
|
||||
objects, indexes, err := e.selectedObjects(page, ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
origins := make(map[string]*editorObjectOrigin)
|
||||
for i, object := range objects {
|
||||
if object.Type != "TextObject" && object.Type != "PathObject" && object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit" {
|
||||
return fmt.Errorf("object %q does not support draw parameters", ids[i])
|
||||
}
|
||||
origin := e.objectOrigin(ids[i])
|
||||
if origin != nil && origin.node.attr("DrawParam") == id {
|
||||
continue
|
||||
}
|
||||
var data []byte
|
||||
if origin != nil {
|
||||
before, resolveErr := e.resolveEditorStyle(origin.object, e.pages[page].Content.Layer[indexes[i].layer].DrawParam)
|
||||
if resolveErr != nil {
|
||||
return resolveErr
|
||||
}
|
||||
oldXML, encodeErr := editorObjectXML(before)
|
||||
if encodeErr != nil {
|
||||
return encodeErr
|
||||
}
|
||||
comparison := object
|
||||
comparison.TextObject.DrawParam, comparison.PathObject.DrawParam = "", ""
|
||||
newXML, encodeErr := editorObjectXML(comparison)
|
||||
if encodeErr != nil {
|
||||
return encodeErr
|
||||
}
|
||||
data, err = editorXMLMerge(origin.data, origin.node, oldXML, newXML)
|
||||
if err == nil {
|
||||
data, err = editorXMLStandalone(data, origin.node)
|
||||
}
|
||||
} else {
|
||||
data, err = editorObjectXML(object)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root, err := parseEditorXML(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if id == "" {
|
||||
before, encodeErr := editorXMLContainer(root.name.Local, ofdAttrs{{Name: xml.Name{Local: "DrawParam"}, Value: root.attr("DrawParam")}}, nil)
|
||||
if encodeErr != nil {
|
||||
return encodeErr
|
||||
}
|
||||
after, encodeErr := editorXMLContainer(root.name.Local, nil, nil)
|
||||
if encodeErr != nil {
|
||||
return encodeErr
|
||||
}
|
||||
data, err = editorXMLMerge(data, root, before, after)
|
||||
} else {
|
||||
data, err = editorXMLAttribute(data, root, "DrawParam", id)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
next := GraphicObject{Type: object.Type, state: object.state}
|
||||
var target any
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
target = &next.TextObject
|
||||
case "PathObject":
|
||||
target = &next.PathObject
|
||||
default:
|
||||
target = &next.CompositeGraphicUnit
|
||||
}
|
||||
if err := xml.Unmarshal(data, target); err != nil {
|
||||
return err
|
||||
}
|
||||
next, err = e.resolveEditorStyle(next, e.pages[page].Content.Layer[indexes[i].layer].DrawParam)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if next.TextObject.Font == object.TextObject.Font && next.TextObject.Size == object.TextObject.Size {
|
||||
next.TextObject.layout = object.TextObject.layout
|
||||
}
|
||||
switch next.Type {
|
||||
case "TextObject":
|
||||
next.TextObject.DrawParam = id
|
||||
case "PathObject":
|
||||
next.PathObject.DrawParam = id
|
||||
}
|
||||
next.CompositeGraphicUnit.states = object.CompositeGraphicUnit.states
|
||||
root, err = parseEditorXML(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updated := &editorObjectOrigin{editor: e, data: data, node: root, object: next}
|
||||
if origin != nil {
|
||||
updated.page, root.parent = origin.page, origin.node.parent
|
||||
} else {
|
||||
content := &editorXML{name: xml.Name{Local: "Content"}}
|
||||
root.parent = &editorXML{name: xml.Name{Local: "Layer"}, parent: content, children: []*editorXML{root}}
|
||||
content.children = []*editorXML{root.parent}
|
||||
}
|
||||
objects[i], origins[ids[i]] = next, updated
|
||||
}
|
||||
return e.updateObjectOrigins(page, objects, true, origins)
|
||||
}
|
||||
|
||||
// AddDrawParam 注册绘制参数,保留继承引用,未引用的资源不写入文档
|
||||
// 入参: draw 绘制参数,ID由编辑器分配
|
||||
// 返回: string 资源标识, error 错误信息
|
||||
|
||||
@@ -66,8 +66,9 @@ func missingGlyphError(id string, characters []rune) error {
|
||||
// Wrap按CTM变换前的边界宽度折行,Align为left、center、right或justify
|
||||
// LineHeight为毫米单位的基线间距,0使用字体度量;LetterSpacing为字素间的附加毫米间距,可为负
|
||||
// LeftIndent和RightIndent为左右缩进,FirstLineIndent为每段首行相对左缩进的偏移,单位为毫米
|
||||
// 零值保持显式换行和左对齐
|
||||
// AutoBidi自动识别双向文字并启用塑形,零值保持显式换行和左对齐
|
||||
type TextLayout struct {
|
||||
AutoBidi bool
|
||||
Shape bool
|
||||
Shaping TextShapeOptions
|
||||
Wrap bool
|
||||
@@ -256,7 +257,7 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
|
||||
}
|
||||
check = *obj
|
||||
check.TextCode = codes
|
||||
position := 0
|
||||
position, mapping := 0, 0
|
||||
for i, code := range codes {
|
||||
before, after := textCodeRunes(obj.TextCode[i].Value), textCodeRunes(code.Value)
|
||||
if len(before) == len(after) {
|
||||
@@ -264,11 +265,11 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
|
||||
if before[j] == char {
|
||||
continue
|
||||
}
|
||||
for k := range transforms {
|
||||
transform := &transforms[k]
|
||||
if position+j < transform.CodePosition || position+j >= transform.CodePosition+transform.CodeCount {
|
||||
continue
|
||||
for mapping < len(transforms) && position+j >= transforms[mapping].CodePosition+transforms[mapping].CodeCount {
|
||||
mapping++
|
||||
}
|
||||
if mapping < len(transforms) && position+j >= transforms[mapping].CodePosition {
|
||||
transform := &transforms[mapping]
|
||||
if transform.CodeCount != 1 || transform.GlyphCount != 1 {
|
||||
return &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("changing a combined glyph requires explicit layout")}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,22 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Actions 返回对象动作的独立副本,不修改原对象
|
||||
// 返回: []Action 动作列表
|
||||
func (o GraphicObject) Actions() []Action {
|
||||
switch o.Type {
|
||||
case "TextObject":
|
||||
return cloneEditorData(o.TextObject.Actions)
|
||||
case "PathObject", "Path":
|
||||
return cloneEditorData(o.PathObject.Actions)
|
||||
case "ImageObject":
|
||||
return cloneEditorData(o.ImageObject.Actions)
|
||||
case "CompositeObject", "CompositeGraphicUnit":
|
||||
return cloneEditorData(o.CompositeGraphicUnit.Actions)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析路径并区分省略的虚线样式与显式实线
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 错误信息
|
||||
|
||||
@@ -53,6 +53,14 @@ type Renderer struct {
|
||||
// RendererOption 渲染器配置选项
|
||||
type RendererOption func(*Renderer)
|
||||
|
||||
// ClearCache 释放派生字体、图片、模板与后端缓存,不改变文档和渲染配置
|
||||
func (r *Renderer) ClearCache() {
|
||||
r.backendStates = make(map[any]any)
|
||||
r.imageCache = make(map[string]image.Image)
|
||||
r.templatePageCache = make(map[string]*PageContent)
|
||||
r.resetFontCache()
|
||||
}
|
||||
|
||||
// SetFontFS 替换外部字体文件系统并重置字体缓存
|
||||
// 入参: fsys 字体文件系统
|
||||
func (r *Renderer) SetFontFS(fsys ...fs.FS) {
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/unicode/bidi"
|
||||
)
|
||||
|
||||
// LayoutText 按自有字体度量重排横向文字,保留绘制属性,可显式启用字体塑形
|
||||
@@ -76,6 +78,18 @@ func LayoutText(obj *TextObject, value string, options TextLayout, metrics FontM
|
||||
if lineHeight == 0 {
|
||||
lineHeight = math.Max(obj.Size, float64(int(ascender)+int(descender)+int(gap))*unit)
|
||||
}
|
||||
if options.AutoBidi && !options.Shaping.Bidi {
|
||||
for _, char := range value {
|
||||
properties, _ := bidi.LookupRune(char)
|
||||
switch properties.Class() {
|
||||
case bidi.R, bidi.AL, bidi.RLE, bidi.RLO, bidi.RLI, bidi.FSI:
|
||||
options.Shaping.Bidi = true
|
||||
}
|
||||
if options.Shaping.Bidi {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if options.Shape || options.Shaping != (TextShapeOptions{}) {
|
||||
return layoutShapedText(obj, value, options, metrics, width, lineHeight, hScale, float64(ascender)*unit)
|
||||
}
|
||||
|
||||
+5
-7
@@ -293,21 +293,19 @@ func validateTextGlyphs(obj TextObject, glyphCount uint16) error {
|
||||
total += len(textCodeRunes(code.Value))
|
||||
ends = append(ends, total)
|
||||
}
|
||||
previous := 0
|
||||
previous, boundary := 0, 0
|
||||
for _, transform := range obj.CGTransform {
|
||||
start, count := transform.CodePosition, transform.CodeCount
|
||||
if start < previous || count <= 0 || start >= total || count > total-start {
|
||||
return fmt.Errorf("invalid glyph character range")
|
||||
}
|
||||
end := start + count
|
||||
for _, boundary := range ends {
|
||||
if start < boundary {
|
||||
if end > boundary {
|
||||
for ends[boundary] <= start {
|
||||
boundary++
|
||||
}
|
||||
if end > ends[boundary] {
|
||||
return fmt.Errorf("glyph mapping crosses text codes")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
ids := strings.Fields(transform.Glyphs)
|
||||
if len(ids) == 0 || len(ids) != transform.GlyphCount {
|
||||
return fmt.Errorf("invalid glyph count")
|
||||
|
||||
Reference in new issue
Block a user