mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
This commit is contained in:
1 parent
0508029387
commit
d7cd3d03b5
20 files changed
+1158
-137
No files matched your search
@@ -184,6 +184,31 @@
|
||||
<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>
|
||||
<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="objectGradientFields" hidden>
|
||||
<summary>渐变</summary>
|
||||
<div class="form-pair">
|
||||
<div class="form-row"><label for="gradientTarget">作用</label><select id="gradientTarget"><option value="fill">填充</option><option value="stroke">描边</option></select></div>
|
||||
<div class="form-row"><label for="gradientKind">类型</label><select id="gradientKind"><option value="keep">保持</option><option value="linear">线性</option><option value="radial">径向</option></select></div>
|
||||
</div>
|
||||
<div id="gradientControls" hidden>
|
||||
<div class="form-row"><label for="gradientAngle">角度</label><div class="form-unit"><input id="gradientAngle" type="number" step="any" value="0"><span>°</span></div></div>
|
||||
<div class="gradient-stop-row gradient-stop-head"><span>颜色</span><span>位置%</span><span>透明%</span><span></span></div>
|
||||
<div id="gradientStops"></div>
|
||||
<button id="gradientAdd" class="small-button" type="button" title="添加色标" aria-label="添加色标">+</button>
|
||||
</div>
|
||||
</details>
|
||||
<details id="objectPatternFields" hidden>
|
||||
<summary>图案</summary>
|
||||
<div class="form-pair">
|
||||
<div class="form-row"><label for="patternWidth">宽度</label><div class="form-unit"><input id="patternWidth" type="number" min="0" step="any"><span>mm</span></div></div>
|
||||
<div class="form-row"><label for="patternHeight">高度</label><div class="form-unit"><input id="patternHeight" type="number" min="0" step="any"><span>mm</span></div></div>
|
||||
</div>
|
||||
<div class="form-pair">
|
||||
<div class="form-row"><label for="patternXStep">横距</label><div class="form-unit"><input id="patternXStep" type="number" min="0" step="any"><span>mm</span></div></div>
|
||||
<div class="form-row"><label for="patternYStep">纵距</label><div class="form-unit"><input id="patternYStep" type="number" min="0" step="any"><span>mm</span></div></div>
|
||||
</div>
|
||||
<div class="form-row"><label for="patternCTM">矩阵</label><input id="patternCTM" type="text" spellcheck="false" autocomplete="off"></div>
|
||||
</details>
|
||||
<div id="objectStrokeFields">
|
||||
<div class="form-row"><label for="objectDash">线型</label><select id="objectDash"><option value="mixed" hidden>混合</option><option value="solid">实线</option><option value="dash">虚线</option><option value="dot">点线</option><option value="dashdot">点划</option><option value="custom">自定</option></select></div>
|
||||
<div id="objectDashRow" class="form-row" hidden><label for="objectDashPattern">间隔</label><div class="form-unit"><input id="objectDashPattern" type="text" autocomplete="off" spellcheck="false"><span>mm</span></div></div>
|
||||
@@ -267,6 +292,7 @@
|
||||
<label class="form-row"><span>标题</span><input id="infoTitle" type="text" autofocus></label>
|
||||
<label class="form-row"><span>主题</span><input id="infoSubject" type="text"></label>
|
||||
<label class="form-row"><span>作者</span><input id="infoAuthor" type="text"></label>
|
||||
<details class="info-custom"><summary>扩展</summary><div id="infoCustomRows"></div><button id="infoCustomAdd" class="small-button" type="button" title="添加字段" aria-label="添加字段">+</button></details>
|
||||
<div class="form-actions"><div id="infoStatus" class="form-status" role="status"></div><button id="infoCancel" class="button" type="button">取消</button><button class="button" type="submit">确定</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
@@ -774,6 +774,8 @@ button:not(:disabled):hover {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.info-custom-row input,
|
||||
.gradient-stop-row input,
|
||||
.form-row input[type="text"],
|
||||
.form-row input[type="password"],
|
||||
.form-row input[type="number"],
|
||||
@@ -798,6 +800,56 @@ button:not(:disabled):hover {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-dialog details {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-dialog details > summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-dialog details[open] > summary {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
#infoCustomRows {
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.info-custom-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) var(--control-height);
|
||||
gap: 6px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
#infoCustomRows > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.gradient-stop-row {
|
||||
display: grid;
|
||||
grid-template-columns: 44px minmax(0, 1fr) minmax(0, 1fr) var(--control-height);
|
||||
gap: 6px;
|
||||
margin: 6px 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gradient-stop-head {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#gradientStops {
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#infoCustomRows :focus-visible,
|
||||
#gradientStops :focus-visible {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.color-input {
|
||||
width: calc(2em + 22px);
|
||||
height: var(--control-height);
|
||||
|
||||
+113
-3
@@ -213,6 +213,8 @@ const el = {
|
||||
infoPanel: document.querySelector("#infoPanel"),
|
||||
infoForm: document.querySelector("#infoForm"),
|
||||
infoTitle: document.querySelector("#infoTitle"),
|
||||
infoCustomRows: document.querySelector("#infoCustomRows"),
|
||||
infoCustomAdd: document.querySelector("#infoCustomAdd"),
|
||||
infoAuthor: document.querySelector("#infoAuthor"),
|
||||
infoSubject: document.querySelector("#infoSubject"),
|
||||
infoCancel: document.querySelector("#infoCancel"),
|
||||
@@ -286,6 +288,19 @@ const el = {
|
||||
groupObjectsButton: document.querySelector("#groupObjectsButton"),
|
||||
ungroupObjectButton: document.querySelector("#ungroupObjectButton"),
|
||||
objectOpacity: document.querySelector("#objectOpacity"),
|
||||
objectGradientFields: document.querySelector("#objectGradientFields"),
|
||||
gradientTarget: document.querySelector("#gradientTarget"),
|
||||
gradientKind: document.querySelector("#gradientKind"),
|
||||
gradientControls: document.querySelector("#gradientControls"),
|
||||
gradientAngle: document.querySelector("#gradientAngle"),
|
||||
gradientStops: document.querySelector("#gradientStops"),
|
||||
gradientAdd: document.querySelector("#gradientAdd"),
|
||||
objectPatternFields: document.querySelector("#objectPatternFields"),
|
||||
patternWidth: document.querySelector("#patternWidth"),
|
||||
patternHeight: document.querySelector("#patternHeight"),
|
||||
patternXStep: document.querySelector("#patternXStep"),
|
||||
patternYStep: document.querySelector("#patternYStep"),
|
||||
patternCTM: document.querySelector("#patternCTM"),
|
||||
objectStrokeFields: document.querySelector("#objectStrokeFields"),
|
||||
objectDash: document.querySelector("#objectDash"),
|
||||
objectDashRow: document.querySelector("#objectDashRow"),
|
||||
@@ -590,14 +605,41 @@ editorClick(el.paragraphButton, () => {
|
||||
el.paragraphCancel.addEventListener("click", () => el.paragraphPanel.close());
|
||||
editorClick(el.infoButton, () => {
|
||||
for (const key of ["Title", "Author", "Subject"]) el[`info${key}`].value = state.doc[key.toLowerCase()] || "";
|
||||
el.infoCustomRows.replaceChildren();
|
||||
for (const [index, field] of (state.doc.customData || []).entries()) addCustomDataRow(field, index);
|
||||
el.infoStatus.textContent = "";
|
||||
el.infoPanel.showModal();
|
||||
});
|
||||
el.infoCancel.addEventListener("click", () => el.infoPanel.close());
|
||||
el.infoCustomAdd.addEventListener("click", () => addCustomDataRow());
|
||||
el.infoForm.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
if (await changeDocument("ofdgoUpdateInfo", null, el.infoTitle.value, el.infoAuthor.value, el.infoSubject.value)) el.infoPanel.close();
|
||||
const fields = [...el.infoCustomRows.children].map(row => ({Name: row.children[0].value, Value: row.children[1].value, Source: row.sourceIndex}));
|
||||
if (await changeDocument("ofdgoUpdateInfo", null, el.infoTitle.value, el.infoAuthor.value, el.infoSubject.value, JSON.stringify(fields))) el.infoPanel.close();
|
||||
});
|
||||
|
||||
function addCustomDataRow(field = {Name: "", Value: ""}, sourceIndex) {
|
||||
const row = document.createElement("div");
|
||||
row.sourceIndex = sourceIndex;
|
||||
row.className = "info-custom-row";
|
||||
for (const [key, label] of [["Name", "名称"], ["Value", "内容"]]) {
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.value = field[key];
|
||||
input.setAttribute("aria-label", label);
|
||||
input.placeholder = label;
|
||||
row.append(input);
|
||||
}
|
||||
const remove = document.createElement("button");
|
||||
remove.type = "button";
|
||||
remove.className = "small-button";
|
||||
remove.textContent = "×";
|
||||
remove.title = "删除字段";
|
||||
remove.setAttribute("aria-label", "删除字段");
|
||||
remove.addEventListener("click", () => row.remove());
|
||||
row.append(remove);
|
||||
el.infoCustomRows.append(row);
|
||||
}
|
||||
el.annotationClose.addEventListener("click", () => el.annotationNote.close());
|
||||
el.annotationNote.addEventListener("close", () => { state.annotationEdit = null; });
|
||||
el.penTool.addEventListener("change", () => {
|
||||
@@ -947,12 +989,22 @@ el.objectBoundsForm.addEventListener("submit", async event => {
|
||||
if (saved) el.objectBoundsPanel.close();
|
||||
});
|
||||
el.objectStyleCancel.addEventListener("click", () => el.objectStylePanel.close());
|
||||
el.gradientKind.addEventListener("change", () => {
|
||||
el.gradientControls.hidden = el.gradientKind.value === "keep";
|
||||
el.gradientAngle.disabled = el.gradientKind.value !== "linear";
|
||||
for (const input of el.gradientStops.querySelectorAll("input")) input.disabled = el.gradientKind.value === "keep";
|
||||
});
|
||||
el.gradientAdd.addEventListener("click", () => addGradientStop(50, "#808080", 0));
|
||||
el.objectDash.addEventListener("change", () => {
|
||||
el.objectDashRow.hidden = el.objectDash.value !== "custom";
|
||||
el.objectDashPattern.required = !el.objectDashRow.hidden;
|
||||
});
|
||||
el.objectStyleForm.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
if (!el.objectGradientFields.hidden && el.gradientKind.value !== "keep" && el.gradientStops.children.length < 2) {
|
||||
el.objectStyleStatus.textContent = "至少保留两个色标";
|
||||
return;
|
||||
}
|
||||
const style = readObjectStyle();
|
||||
const item = canvasEditor.selected;
|
||||
if (await changeDocument("ofdgoStyleObjects", { ...item, id: canvasEditor.items().map(member => member.id) }, style)) el.objectStylePanel.close();
|
||||
@@ -1390,6 +1442,21 @@ 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;
|
||||
const pattern = items.length === 1 && !items[0].scoped ? items[0].fillPattern : null;
|
||||
el.objectGradientFields.hidden = items.length !== 1 || items[0].scoped || items[0].type !== "PathObject" || !items[0].paintSize || !canEditObject(items[0], "paint");
|
||||
el.gradientKind.value = "keep";
|
||||
el.gradientTarget.value = "fill";
|
||||
el.gradientControls.hidden = true;
|
||||
el.gradientAngle.value = "0";
|
||||
el.gradientAngle.disabled = true;
|
||||
el.gradientStops.replaceChildren();
|
||||
addGradientStop(0, "#000000", 0);
|
||||
addGradientStop(100, "#ffffff", 0);
|
||||
el.objectPatternFields.hidden = !pattern;
|
||||
for (const [input, key] of [[el.patternWidth, "width"], [el.patternHeight, "height"], [el.patternXStep, "xStep"], [el.patternYStep, "yStep"], [el.patternCTM, "ctm"]]) {
|
||||
input.value = pattern?.[key] ?? "";
|
||||
input.disabled = !pattern;
|
||||
}
|
||||
const alpha = items.some(item => item.alphaMixed) ? null : shared("alpha", 255), dash = shared("dashPattern", "");
|
||||
el.objectOpacity.value = alpha === null ? "" : String(Math.round((1 - alpha / 255) * 10000) / 100);
|
||||
el.objectOpacity.placeholder = alpha === null ? "混合" : "";
|
||||
@@ -1401,13 +1468,32 @@ function openObjectStyle() {
|
||||
el.objectDashOffset.value = shared("dashOffset", 0) ?? "";
|
||||
el.objectCap.value = shared("cap", "") || (shared("cap", "") === null ? "mixed" : "Butt");
|
||||
el.objectJoin.value = shared("join", "") || (shared("join", "") === null ? "mixed" : "Miter");
|
||||
state.styleOriginal = Object.fromEntries(["objectOpacity", "objectDash", "objectDashPattern", "objectDashOffset", "objectCap", "objectJoin"].map(key => [key, el[key].value]));
|
||||
state.styleOriginal = Object.fromEntries(["objectOpacity", "objectDash", "objectDashPattern", "objectDashOffset", "objectCap", "objectJoin", "patternWidth", "patternHeight", "patternXStep", "patternYStep", "patternCTM"].map(key => [key, el[key].value]));
|
||||
el.objectStyleStatus.textContent = "";
|
||||
el.objectStylePanel.showModal();
|
||||
}
|
||||
|
||||
function readObjectStyle() {
|
||||
const style = {}, changed = key => el[key].value !== state.styleOriginal[key];
|
||||
if (!el.objectGradientFields.hidden && el.gradientKind.value !== "keep") {
|
||||
const [width, height] = canvasEditor.selected.paintSize;
|
||||
const Segment = [...el.gradientStops.children].map(row => ({Position: Number(row.children[1].value) / 100, Color: {Value: row.children[0].value, Alpha: Math.round(255 * (1 - Number(row.children[2].value) / 100))}})).sort((a, b) => a.Position - b.Position);
|
||||
const angle = Number(el.gradientAngle.value) * Math.PI / 180, x = Math.cos(angle), y = Math.sin(angle);
|
||||
const length = Math.abs(width * x) + Math.abs(height * y);
|
||||
const paint = el.gradientKind.value === "linear"
|
||||
? {AxialShd: {StartPoint: `${width / 2 - x * length / 2} ${height / 2 - y * length / 2}`, EndPoint: `${width / 2 + x * length / 2} ${height / 2 + y * length / 2}`, Extend: "3", Segment}}
|
||||
: {RadialShd: {StartPoint: `${width / 2} ${height / 2}`, EndPoint: `${width / 2} ${height / 2}`, EndRadius: Math.max(width, height) / 2, Extend: "3", Segment}};
|
||||
const target = el.gradientTarget.value;
|
||||
style[`${target}Paint`] = JSON.stringify(paint);
|
||||
style[target] = true;
|
||||
}
|
||||
if (!el.objectPatternFields.hidden) {
|
||||
const pattern = {};
|
||||
for (const [input, key] of [["patternWidth", "Width"], ["patternHeight", "Height"], ["patternXStep", "XStep"], ["patternYStep", "YStep"], ["patternCTM", "CTM"]]) {
|
||||
if (changed(input)) pattern[key] = key === "CTM" ? el[input].value.trim() : Number(el[input].value);
|
||||
}
|
||||
if (Object.keys(pattern).length) style.patternStyle = JSON.stringify(pattern);
|
||||
}
|
||||
if (changed("objectOpacity") && el.objectOpacity.value !== "") style.alpha = Math.round((1 - Number(el.objectOpacity.value) / 100) * 255);
|
||||
if (!el.objectStrokeFields.hidden) {
|
||||
if (changed("objectDash") || changed("objectDashPattern")) {
|
||||
@@ -1421,6 +1507,30 @@ function readObjectStyle() {
|
||||
return style;
|
||||
}
|
||||
|
||||
function addGradientStop(position, color, transparency) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "gradient-stop-row";
|
||||
for (const [type, value, label] of [["color", color, "色标颜色"], ["number", position, "色标位置"], ["number", transparency, "色标透明度"]]) {
|
||||
const input = document.createElement("input");
|
||||
input.type = type;
|
||||
input.value = value;
|
||||
input.disabled = el.gradientKind.value === "keep";
|
||||
input.setAttribute("aria-label", label);
|
||||
input.title = label;
|
||||
if (type === "number") { input.min = "0"; input.max = "100"; input.step = "any"; input.required = true; }
|
||||
row.append(input);
|
||||
}
|
||||
const remove = document.createElement("button");
|
||||
remove.type = "button";
|
||||
remove.className = "small-button";
|
||||
remove.textContent = "×";
|
||||
remove.title = "删除色标";
|
||||
remove.setAttribute("aria-label", "删除色标");
|
||||
remove.addEventListener("click", () => row.remove());
|
||||
row.append(remove);
|
||||
el.gradientStops.append(row);
|
||||
}
|
||||
|
||||
function openOutlinePanel(action) {
|
||||
state.outlineAction = action;
|
||||
let outline = null, items = state.doc.outlines || [];
|
||||
@@ -2439,7 +2549,7 @@ async function changeDocument(name, item, ...args) {
|
||||
}
|
||||
setEditorInfo(doc);
|
||||
if (name === "ofdgoUpdateInfo") {
|
||||
Object.assign(state.doc, { title: doc.title, author: doc.author, subject: doc.subject });
|
||||
Object.assign(state.doc, { title: doc.title, author: doc.author, subject: doc.subject, customData: doc.customData });
|
||||
renderMeta();
|
||||
updateControls();
|
||||
rememberEditorView(before, revision);
|
||||
|
||||
+105
-4
@@ -1712,8 +1712,10 @@ func editorCapabilities(capability ofdgo.ObjectCapabilities, kind string) map[st
|
||||
// 入参: item 前端对象, object 有效样式, paint 是否可改色, scale 描边页面倍率
|
||||
func editorAppearance(item map[string]any, object ofdgo.GraphicObject, paint bool, scale float64) {
|
||||
var alpha *int
|
||||
var fill *ofdgo.FillColor
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
fill = object.TextObject.FillColor
|
||||
alpha = object.TextObject.Alpha
|
||||
if paint {
|
||||
item["color"] = editorColorHex(object.TextObject.FillColor)
|
||||
@@ -1723,6 +1725,10 @@ func editorAppearance(item map[string]any, object ofdgo.GraphicObject, paint boo
|
||||
item["imageBorder"] = object.ImageObject.Border != nil
|
||||
case "PathObject":
|
||||
path := object.PathObject
|
||||
fill = path.FillColor
|
||||
if box, err := ofdgo.ParseBox(path.Boundary); err == nil {
|
||||
item["paintSize"] = []any{box.W, box.H}
|
||||
}
|
||||
alpha = path.Alpha
|
||||
item["dashPattern"], item["cap"], item["join"], item["dashOffset"] = path.DashPattern, path.Cap, path.Join, 0.0
|
||||
if path.DashOffset != nil {
|
||||
@@ -1740,6 +1746,10 @@ func editorAppearance(item map[string]any, object ofdgo.GraphicObject, paint boo
|
||||
if alpha != nil {
|
||||
item["alpha"] = *alpha
|
||||
}
|
||||
if fill != nil && fill.Pattern != nil {
|
||||
p := fill.Pattern
|
||||
item["fillPattern"] = map[string]any{"width": p.Width, "height": p.Height, "xStep": p.XStep, "yStep": p.YStep, "ctm": p.CTM}
|
||||
}
|
||||
}
|
||||
|
||||
// compositePath 解析画布复合对象路径
|
||||
@@ -2889,20 +2899,85 @@ func changeOutline(args []js.Value) (any, error) {
|
||||
return editorOutlineInfo{info.(editorInfo), path}, nil
|
||||
}
|
||||
|
||||
// styleObjects 原子更新对象透明度和路径描边属性
|
||||
// styleObjects 原子更新对象外观和图案布局
|
||||
// 入参: args 页面索引、对象标识数组及样式,省略字段保持原值
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func styleObjects(args []js.Value) (any, error) {
|
||||
style := objectStyle(args[2])
|
||||
var pattern *ofdgo.PatternStyle
|
||||
if field := args[2].Get("patternStyle"); !field.IsUndefined() {
|
||||
if err := json.Unmarshal([]byte(field.String()), &pattern); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if field := args[2].Get("fillPaint"); !field.IsUndefined() {
|
||||
if err := json.Unmarshal([]byte(field.String()), &style.FillColor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if field := args[2].Get("strokePaint"); !field.IsUndefined() {
|
||||
if err := json.Unmarshal([]byte(field.String()), &style.StrokeColor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return changeObjects(func() error {
|
||||
return currentEditor.StyleObjects(args[0].Int(), stringsFromJS(args[1]), objectStyle(args[2]))
|
||||
return currentEditor.Transaction(func(editor *ofdgo.Editor) error {
|
||||
page, ids := args[0].Int(), stringsFromJS(args[1])
|
||||
for _, paint := range []*ofdgo.FillColor{style.FillColor, (*ofdgo.FillColor)(style.StrokeColor)} {
|
||||
if err := resolveStylePaint(paint); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := editor.StyleObjects(page, ids, style); err != nil {
|
||||
return err
|
||||
}
|
||||
if pattern != nil {
|
||||
return editor.StylePatterns(page, ids, false, *pattern)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// resolveStylePaint 将渐变色标中的浏览器色值转换为文档颜色空间
|
||||
// 入参: paint 颜色数据
|
||||
// 返回: error 错误信息
|
||||
func resolveStylePaint(paint *ofdgo.FillColor) error {
|
||||
if paint == nil {
|
||||
return nil
|
||||
}
|
||||
var segments []ofdgo.ShdSegment
|
||||
if paint.AxialShd != nil {
|
||||
segments = paint.AxialShd.Segment
|
||||
} else if paint.RadialShd != nil {
|
||||
segments = paint.RadialShd.Segment
|
||||
}
|
||||
for i := range segments {
|
||||
stop := &segments[i].Color
|
||||
if strings.HasPrefix(stop.Value, "#") {
|
||||
color := ofdgo.FillColor{Alpha: stop.Alpha}
|
||||
if err := setEditorColor(&color, stop.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
*stop = ofdgo.ShdColor{Value: color.Value, ColorSpace: color.ColorSpace, Alpha: color.Alpha}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// objectStyle 读取高级面板提交的外观字段,省略字段保持原值
|
||||
// 入参: value 前端样式
|
||||
// 返回: ofdgo.ObjectStyle 库层样式
|
||||
func objectStyle(value js.Value) ofdgo.ObjectStyle {
|
||||
style := ofdgo.ObjectStyle{}
|
||||
if field := value.Get("fill"); !field.IsUndefined() {
|
||||
v := field.Bool()
|
||||
style.Fill = &v
|
||||
}
|
||||
if field := value.Get("stroke"); !field.IsUndefined() {
|
||||
v := field.Bool()
|
||||
style.Stroke = &v
|
||||
}
|
||||
if field := value.Get("alpha"); !field.IsUndefined() {
|
||||
v := field.Int()
|
||||
style.Alpha = &v
|
||||
@@ -3026,13 +3101,39 @@ func createDocument(args []js.Value) (any, error) {
|
||||
return previewEditor(editor, args[3].Bool())
|
||||
}
|
||||
|
||||
// updateInfo 修改标题、作者和主题,保留其他元数据及创建程序标识
|
||||
// 入参: args 标题、作者、主题
|
||||
// updateInfo 修改文档信息和自定义字段,保留其他元数据及创建程序标识
|
||||
// 入参: args 标题、作者、主题、可选自定义字段JSON
|
||||
// 返回: any 编辑状态, error 错误信息
|
||||
func updateInfo(args []js.Value) (any, error) {
|
||||
return changeObjects(func() error {
|
||||
info := currentEditor.Info
|
||||
info.Title, info.Author, info.Subject = args[0].String(), args[1].String(), args[2].String()
|
||||
if len(args) > 3 {
|
||||
var submitted []struct {
|
||||
ofdgo.CustomData
|
||||
Source *int
|
||||
}
|
||||
if err := json.Unmarshal([]byte(args[3].String()), &submitted); err != nil {
|
||||
return err
|
||||
}
|
||||
var previous []ofdgo.CustomData
|
||||
if info.CustomDatas != nil {
|
||||
previous = info.CustomDatas.CustomData
|
||||
}
|
||||
fields := make([]ofdgo.CustomData, len(submitted))
|
||||
for i, field := range submitted {
|
||||
if field.Source != nil {
|
||||
if *field.Source < 0 || *field.Source >= len(previous) {
|
||||
return fmt.Errorf("invalid custom data source")
|
||||
}
|
||||
fields[i] = previous[*field.Source]
|
||||
}
|
||||
fields[i].Name, fields[i].Value = field.Name, field.Value
|
||||
}
|
||||
if !slices.Equal(previous, fields) {
|
||||
info.CustomDatas = &ofdgo.CustomDatas{CustomData: fields}
|
||||
}
|
||||
}
|
||||
currentEditor.SetInfo(info)
|
||||
return nil
|
||||
})
|
||||
|
||||
+26
-19
@@ -67,25 +67,26 @@ type pageBoxInfo struct {
|
||||
|
||||
// DocumentInfo 文档信息
|
||||
type DocumentInfo struct {
|
||||
Encryption EncryptionInfo `json:"encryption"`
|
||||
Version string `json:"version"`
|
||||
DocType string `json:"docType"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
Subject string `json:"subject"`
|
||||
CreationDate string `json:"creationDate"`
|
||||
ModDate string `json:"modDate"`
|
||||
PageCount int `json:"pageCount"`
|
||||
FontCount int `json:"fontCount"`
|
||||
SignatureCount int `json:"signatureCount"`
|
||||
SignatureError string `json:"signatureError,omitempty"`
|
||||
AttachmentError string `json:"attachmentError,omitempty"`
|
||||
Attachments []AttachmentInfo `json:"attachments,omitempty"`
|
||||
Fonts []FontInfo `json:"fonts"`
|
||||
Signatures []SignatureInfo `json:"signatures"`
|
||||
Pages []PageInfo `json:"pages"`
|
||||
Outlines []OutlineInfo `json:"outlines,omitempty"`
|
||||
DetailsPending bool `json:"detailsPending,omitempty"`
|
||||
Encryption EncryptionInfo `json:"encryption"`
|
||||
Version string `json:"version"`
|
||||
DocType string `json:"docType"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
Subject string `json:"subject"`
|
||||
CustomData []ofdgo.CustomData `json:"customData,omitempty"`
|
||||
CreationDate string `json:"creationDate"`
|
||||
ModDate string `json:"modDate"`
|
||||
PageCount int `json:"pageCount"`
|
||||
FontCount int `json:"fontCount"`
|
||||
SignatureCount int `json:"signatureCount"`
|
||||
SignatureError string `json:"signatureError,omitempty"`
|
||||
AttachmentError string `json:"attachmentError,omitempty"`
|
||||
Attachments []AttachmentInfo `json:"attachments,omitempty"`
|
||||
Fonts []FontInfo `json:"fonts"`
|
||||
Signatures []SignatureInfo `json:"signatures"`
|
||||
Pages []PageInfo `json:"pages"`
|
||||
Outlines []OutlineInfo `json:"outlines,omitempty"`
|
||||
DetailsPending bool `json:"detailsPending,omitempty"`
|
||||
}
|
||||
|
||||
// OutlineInfo 目录节点信息
|
||||
@@ -450,6 +451,9 @@ func (s *Session) Summary() DocumentInfo {
|
||||
info.Title = docInfo.Title
|
||||
info.Author = docInfo.Author
|
||||
info.Subject = docInfo.Subject
|
||||
if docInfo.CustomDatas != nil {
|
||||
info.CustomData = append([]ofdgo.CustomData(nil), docInfo.CustomDatas.CustomData...)
|
||||
}
|
||||
info.CreationDate = docInfo.CreationDate
|
||||
info.ModDate = docInfo.ModDate
|
||||
}
|
||||
@@ -523,6 +527,9 @@ func (s *Session) Info() DocumentInfo {
|
||||
info.Title = docInfo.Title
|
||||
info.Author = docInfo.Author
|
||||
info.Subject = docInfo.Subject
|
||||
if docInfo.CustomDatas != nil {
|
||||
info.CustomData = append([]ofdgo.CustomData(nil), docInfo.CustomDatas.CustomData...)
|
||||
}
|
||||
info.CreationDate = docInfo.CreationDate
|
||||
info.ModDate = docInfo.ModDate
|
||||
}
|
||||
|
||||
+10
-3
@@ -783,7 +783,9 @@ func (e *Editor) prepareObject(id string, object GraphicObject) (GraphicObject,
|
||||
}
|
||||
}
|
||||
if obj.Border != nil {
|
||||
return GraphicObject{}, fmt.Errorf("image borders are not supported for creation")
|
||||
if err := e.validateImageBorder(obj.Border); err != nil {
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
}
|
||||
obj.ID = id
|
||||
boundary, ctm = obj.Boundary, obj.CTM
|
||||
@@ -802,8 +804,13 @@ func (e *Editor) prepareObject(id string, object GraphicObject) (GraphicObject,
|
||||
} else if object.Type == "ImageObject" {
|
||||
object.ImageObject.CTM = fmt.Sprintf("%s 0 0 %s 0 0", ofdNumber(box.W), ofdNumber(box.H))
|
||||
}
|
||||
if drawParam != "" || len(actions) != 0 {
|
||||
return GraphicObject{}, fmt.Errorf("draw parameter references and actions are not supported for creation")
|
||||
if drawParam != "" {
|
||||
if _, err := e.editorDrawParam(drawParam, make(map[string]bool)); err != nil {
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
}
|
||||
if err := validateObjectActions(actions); err != nil {
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
if err := e.validateObjectClips(clips); err != nil {
|
||||
return GraphicObject{}, err
|
||||
|
||||
+196
-5
@@ -23,6 +23,70 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PatternStyle 图案尺寸、步进和变换增量,nil字段保持原值,单位为毫米
|
||||
type PatternStyle struct {
|
||||
Width *float64
|
||||
Height *float64
|
||||
XStep *float64
|
||||
YStep *float64
|
||||
CTM *string
|
||||
}
|
||||
|
||||
// StylePatterns 修改已有图案布局,保留单元内容、资源引用和未知扩展
|
||||
// 入参: page 页面索引, ids 对象标识, stroke 是否修改描边, style 图案布局增量
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) StylePatterns(page int, ids []string, stroke bool, style PatternStyle) error {
|
||||
for _, size := range []*float64{style.Width, style.Height} {
|
||||
if size != nil && (!finite(*size) || *size <= 0) {
|
||||
return fmt.Errorf("pattern dimensions must be positive and finite")
|
||||
}
|
||||
}
|
||||
for _, step := range []*float64{style.XStep, style.YStep} {
|
||||
if step != nil && (!finite(*step) || *step < 0) {
|
||||
return fmt.Errorf("pattern steps must be nonnegative and finite")
|
||||
}
|
||||
}
|
||||
if style.CTM != nil && *style.CTM != "" {
|
||||
if _, err := creationNumbers(*style.CTM, 6); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
objects, _, err := e.selectedObjects(page, ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range objects {
|
||||
objects[i] = cloneEditorData(objects[i])
|
||||
object := &objects[i]
|
||||
var fill *FillColor
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
fill = object.TextObject.FillColor
|
||||
if stroke {
|
||||
fill = (*FillColor)(object.TextObject.StrokeColor)
|
||||
}
|
||||
case "PathObject", "Path":
|
||||
fill = object.PathObject.FillColor
|
||||
if stroke {
|
||||
fill = (*FillColor)(object.PathObject.StrokeColor)
|
||||
}
|
||||
}
|
||||
if fill == nil || fill.Pattern == nil {
|
||||
return fmt.Errorf("object %q does not use a pattern", ids[i])
|
||||
}
|
||||
pattern := fill.Pattern
|
||||
for _, pair := range []struct{ value, target *float64 }{{style.Width, &pattern.Width}, {style.Height, &pattern.Height}, {style.XStep, &pattern.XStep}, {style.YStep, &pattern.YStep}} {
|
||||
if pair.value != nil {
|
||||
*pair.target = *pair.value
|
||||
}
|
||||
}
|
||||
if style.CTM != nil {
|
||||
pattern.CTM = *style.CTM
|
||||
}
|
||||
}
|
||||
return e.updateObjects(page, objects, true)
|
||||
}
|
||||
|
||||
// Color 解析当前文档的纯色,支持RGB、灰度、CMYK及调色板,不修改原颜色
|
||||
// 入参: value 颜色,nil表示默认黑色
|
||||
// 返回: color.NRGBA 非预乘颜色, error 错误信息
|
||||
@@ -141,9 +205,42 @@ func (e *Editor) RGBColor(value color.NRGBA) (*FillColor, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PaintObjects 更新文字或路径的填充或描边,保留定位、其他样式和原文扩展
|
||||
// 入参: page 页面索引, ids 对象标识, paint 绘制颜色,nil恢复默认, stroke 是否修改描边
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) PaintObjects(page int, ids []string, paint *FillColor, stroke bool) error {
|
||||
if err := e.editorColor(paint); err != nil {
|
||||
return err
|
||||
}
|
||||
objects, _, err := e.selectedObjects(page, ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range objects {
|
||||
object := &objects[i]
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
if stroke {
|
||||
object.TextObject.StrokeColor = (*StrokeColor)(cloneEditorData(paint))
|
||||
} else {
|
||||
object.TextObject.FillColor = cloneEditorData(paint)
|
||||
}
|
||||
case "PathObject":
|
||||
if stroke {
|
||||
object.PathObject.StrokeColor = (*StrokeColor)(cloneEditorData(paint))
|
||||
} else {
|
||||
object.PathObject.FillColor = cloneEditorData(paint)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("object %q does not have text or path paint", editorObjectID(*object))
|
||||
}
|
||||
}
|
||||
return e.updateObjects(page, objects, true)
|
||||
}
|
||||
|
||||
// editorPaintable 判断文字与路径颜色能否在保留其他字段的前提下修改
|
||||
// 入参: object 图形对象
|
||||
// 返回: bool 是否支持纯色编辑
|
||||
// 返回: bool 是否支持绘制颜色编辑
|
||||
func (e *Editor) editorPaintable(object GraphicObject) bool {
|
||||
var fill, stroke *FillColor
|
||||
switch object.Type {
|
||||
@@ -155,17 +252,111 @@ func (e *Editor) editorPaintable(object GraphicObject) bool {
|
||||
return false
|
||||
}
|
||||
for _, value := range []*FillColor{fill, stroke} {
|
||||
if _, err := e.Color(value); err != nil {
|
||||
if err := e.editorColor(value); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// editorColor 校验当前文档的颜色空间和分量
|
||||
// collectPaintReferences 收集纯色、渐变和图案引用的资源
|
||||
// 入参: paint 绘制颜色, used 引用集合
|
||||
func collectPaintReferences(paint *FillColor, used map[string]bool) {
|
||||
if paint == nil {
|
||||
return
|
||||
}
|
||||
used[paint.ColorSpace] = true
|
||||
var segments []ShdSegment
|
||||
if paint.AxialShd != nil {
|
||||
segments = paint.AxialShd.Segment
|
||||
}
|
||||
if paint.RadialShd != nil {
|
||||
segments = paint.RadialShd.Segment
|
||||
}
|
||||
for _, segment := range segments {
|
||||
used[segment.Color.ColorSpace] = true
|
||||
}
|
||||
if paint.Pattern != nil {
|
||||
for _, object := range paint.Pattern.CellContent.Objects {
|
||||
collectObjectReferences(object, used)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collectObjectReferences 收集对象和嵌套绘制内容引用的资源
|
||||
// 入参: object 图形对象, used 引用集合
|
||||
func collectObjectReferences(object GraphicObject, used map[string]bool) {
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
used[object.TextObject.Font], used[object.TextObject.DrawParam] = true, true
|
||||
collectPaintReferences(object.TextObject.FillColor, used)
|
||||
collectPaintReferences((*FillColor)(object.TextObject.StrokeColor), used)
|
||||
case "PathObject", "Path":
|
||||
used[object.PathObject.DrawParam] = true
|
||||
collectPaintReferences(object.PathObject.FillColor, used)
|
||||
collectPaintReferences((*FillColor)(object.PathObject.StrokeColor), used)
|
||||
case "ImageObject":
|
||||
used[object.ImageObject.ResourceID], used[object.ImageObject.ImageMask] = true, true
|
||||
if object.ImageObject.Border != nil {
|
||||
collectPaintReferences((*FillColor)(object.ImageObject.Border.BorderColor), used)
|
||||
}
|
||||
case "CompositeObject", "CompositeGraphicUnit":
|
||||
collectCompositeReferences(object.CompositeGraphicUnit, used)
|
||||
}
|
||||
}
|
||||
|
||||
// editorColor 校验当前文档的纯色和渐变
|
||||
// 入参: value 颜色
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) editorColor(value *FillColor) error {
|
||||
_, err := e.Color(value)
|
||||
return err
|
||||
if value == nil || value.AxialShd == nil && value.RadialShd == nil {
|
||||
_, err := e.Color(value)
|
||||
return err
|
||||
}
|
||||
if value.unsupported || value.Pattern != nil || value.AxialShd != nil && value.RadialShd != nil || value.Value != "" || value.Index != nil {
|
||||
return fmt.Errorf("specify one paint source")
|
||||
}
|
||||
if value.Alpha != nil && (*value.Alpha < 0 || *value.Alpha > 255) {
|
||||
return fmt.Errorf("color alpha must be between 0 and 255")
|
||||
}
|
||||
var mapType, extend, start, end string
|
||||
var unit float64
|
||||
var segments []ShdSegment
|
||||
if node := value.AxialShd; node != nil {
|
||||
mapType, unit, extend, start, end, segments = node.MapType, node.MapUnit, node.Extend, node.StartPoint, node.EndPoint, node.Segment
|
||||
} else {
|
||||
node := value.RadialShd
|
||||
mapType, unit, extend, start, end, segments = node.MapType, node.MapUnit, node.Extend, node.StartPoint, node.EndPoint, node.Segment
|
||||
if !finite(node.StartRadius) || !finite(node.EndRadius) || node.StartRadius < 0 || node.EndRadius < 0 || !finite(node.Eccentricity) || node.Eccentricity < 0 || node.Eccentricity >= 1 || !finite(node.Angle) {
|
||||
return fmt.Errorf("invalid radial gradient geometry")
|
||||
}
|
||||
}
|
||||
if !slices.Contains([]string{"", "Direct", "Repeat", "Reflect"}, mapType) || !finite(unit) || unit < 0 || (mapType == "Repeat" || mapType == "Reflect") && unit == 0 || !slices.Contains([]string{"", "0", "1", "2", "3"}, extend) {
|
||||
return fmt.Errorf("invalid gradient mapping")
|
||||
}
|
||||
for _, point := range []string{start, end} {
|
||||
if _, err := creationNumbers(point, 2); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(segments) < 2 {
|
||||
return fmt.Errorf("gradient requires at least two stops")
|
||||
}
|
||||
previous := -1.0
|
||||
positions := gradientPositions(segments)
|
||||
for index, segment := range segments {
|
||||
position := positions[index]
|
||||
if !finite(position) || position < previous || position < 0 || position > 1 {
|
||||
return fmt.Errorf("gradient stops must be ordered within zero and one")
|
||||
}
|
||||
previous = position
|
||||
color := segment.Color
|
||||
if color.ColorSpace == "" {
|
||||
color.ColorSpace = value.ColorSpace
|
||||
}
|
||||
if _, err := e.Color(&FillColor{Value: color.Value, Index: color.Index, ColorSpace: color.ColorSpace, Alpha: color.Alpha}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1068,25 +1068,6 @@ func collectCompositeReferences(composite CompositeGraphicUnit, used map[string]
|
||||
used[composite.ResourceID] = true
|
||||
used[composite.DrawParam] = true
|
||||
for _, object := range composite.Objects {
|
||||
var fill, stroke *FillColor
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
used[object.TextObject.Font] = true
|
||||
used[object.TextObject.DrawParam] = true
|
||||
fill, stroke = object.TextObject.FillColor, (*FillColor)(object.TextObject.StrokeColor)
|
||||
case "PathObject":
|
||||
used[object.PathObject.DrawParam] = true
|
||||
fill, stroke = object.PathObject.FillColor, (*FillColor)(object.PathObject.StrokeColor)
|
||||
case "ImageObject":
|
||||
used[object.ImageObject.ResourceID] = true
|
||||
used[object.ImageObject.ImageMask] = true
|
||||
case "CompositeObject", "CompositeGraphicUnit":
|
||||
collectCompositeReferences(object.CompositeGraphicUnit, used)
|
||||
}
|
||||
for _, color := range []*FillColor{fill, stroke} {
|
||||
if color != nil {
|
||||
used[color.ColorSpace] = true
|
||||
}
|
||||
}
|
||||
collectObjectReferences(object, used)
|
||||
}
|
||||
}
|
||||
@@ -93,8 +93,10 @@ func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []Gra
|
||||
return err
|
||||
}
|
||||
if node.object.Type == "PathObject" {
|
||||
_, styleErr := e.resolveEditorStyle(node.object, e.copiedLayerStyle(source))
|
||||
if styleErr != nil && editReason(styleErr) == EditUnsupportedColor || !e.editorPaintable(node.object) {
|
||||
style, styleErr := e.resolveEditorStyle(node.object, e.copiedLayerStyle(source))
|
||||
_, fillErr := e.Color(style.PathObject.FillColor)
|
||||
_, strokeErr := e.Color((*FillColor)(style.PathObject.StrokeColor))
|
||||
if styleErr != nil && editReason(styleErr) == EditUnsupportedColor || fillErr != nil || strokeErr != nil {
|
||||
node, err = e.wrapCompositePath(node)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -235,11 +237,11 @@ func (e *Editor) neutralDrawParam() (string, error) {
|
||||
return e.addEditorDrawParam(DrawParam{LineWidth: defaultPathLineWidth, Cap: "Butt", Join: "Miter", MiterLimit: defaultMiterLimit, DashOffset: &zero, dashPatternSet: true, FillColor: fill, StrokeColor: (*StrokeColor)(fill)})
|
||||
}
|
||||
|
||||
// addEditorDrawParam 注册完整有效绘制参数,相同外观复用资源
|
||||
// 入参: draw 不含继承关系的有效参数
|
||||
// addEditorDrawParam 注册已校验的绘制参数,相同定义复用资源
|
||||
// 入参: draw 绘制参数
|
||||
// 返回: string 资源标识, error 错误信息
|
||||
func (e *Editor) addEditorDrawParam(draw DrawParam) (string, error) {
|
||||
draw.ID, draw.Relative, draw.ResourceID, draw.BaseLoc, draw.Link = "", "", "", "", ""
|
||||
draw.ID, draw.ResourceID, draw.BaseLoc, draw.Link = "", "", "", ""
|
||||
for _, resource := range e.resources {
|
||||
if resource.draw != nil {
|
||||
previous := *resource.draw
|
||||
@@ -257,10 +259,14 @@ func (e *Editor) addEditorDrawParam(draw DrawParam) (string, error) {
|
||||
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)}, {"Font", draw.Font}} {
|
||||
for _, pair := range [][2]string{{"ID", draw.ID}, {"Relative", draw.Relative}, {"Cap", draw.Cap}, {"Join", draw.Join}, {"Font", draw.Font}} {
|
||||
attrs.add(pair[0], pair[1])
|
||||
}
|
||||
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "DashPattern"}, Value: draw.DashPattern})
|
||||
attrs.number("LineWidth", draw.LineWidth)
|
||||
attrs.number("MiterLimit", draw.MiterLimit)
|
||||
if draw.Relative == "" || draw.dashPatternSet || draw.DashPattern != "" {
|
||||
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "DashPattern"}, Value: draw.DashPattern})
|
||||
}
|
||||
if draw.DashOffset != nil {
|
||||
attrs.add("DashOffset", ofdNumber(*draw.DashOffset))
|
||||
}
|
||||
|
||||
+82
-3
@@ -27,6 +27,88 @@ type AnnotationLink struct {
|
||||
Dest *Dest
|
||||
}
|
||||
|
||||
// SetObjectActions 原子替换普通对象的链接动作,空列表移除动作
|
||||
// 入参: page 页面索引, ids 对象标识, actions 链接动作
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) SetObjectActions(page int, ids []string, actions []Action) error {
|
||||
if err := validateObjectActions(actions); err != nil {
|
||||
return err
|
||||
}
|
||||
objects, _, err := e.selectedObjects(page, ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range objects {
|
||||
switch objects[i].Type {
|
||||
case "TextObject":
|
||||
objects[i].TextObject.Actions = cloneEditorData(actions)
|
||||
case "PathObject", "Path":
|
||||
objects[i].PathObject.Actions = cloneEditorData(actions)
|
||||
case "ImageObject":
|
||||
objects[i].ImageObject.Actions = cloneEditorData(actions)
|
||||
case "CompositeObject", "CompositeGraphicUnit":
|
||||
objects[i].CompositeGraphicUnit.Actions = cloneEditorData(actions)
|
||||
default:
|
||||
return fmt.Errorf("object %q cannot contain actions", ids[i])
|
||||
}
|
||||
}
|
||||
return e.updateObjects(page, objects, true)
|
||||
}
|
||||
|
||||
// validateObjectActions 校验新建对象的链接动作,目标不必可达
|
||||
// 入参: actions 动作列表
|
||||
// 返回: error 错误信息
|
||||
func validateObjectActions(actions []Action) error {
|
||||
for _, action := range actions {
|
||||
if !slices.Contains([]string{"CLICK", "DO", "PO"}, action.Event) {
|
||||
return fmt.Errorf("unsupported action event %q", action.Event)
|
||||
}
|
||||
if action.GotoA != nil || action.Sound != nil || action.Movie != nil {
|
||||
return fmt.Errorf("new attachment and multimedia actions require resource registration")
|
||||
}
|
||||
if (action.URI == nil) == (action.Goto == nil) {
|
||||
return fmt.Errorf("specify one URI or goto action")
|
||||
}
|
||||
if action.Goto != nil {
|
||||
if (action.Goto.Dest == nil) == (action.Goto.Bookmark == nil) {
|
||||
return fmt.Errorf("specify one destination or bookmark")
|
||||
}
|
||||
if action.Goto.Dest != nil {
|
||||
if _, err := annotationLinkXML(AnnotationLink{Dest: action.Goto.Dest}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if action.Region != nil {
|
||||
for _, area := range action.Region.Area {
|
||||
if _, err := creationNumbers(area.Start, 2); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, command := range area.Command {
|
||||
var points []string
|
||||
switch command.Type {
|
||||
case "Line":
|
||||
points = []string{command.Point1}
|
||||
case "QuadraticBezier":
|
||||
points = []string{command.Point1, command.Point2}
|
||||
case "CubicBezier":
|
||||
points = []string{command.Point1, command.Point2, command.Point3}
|
||||
case "Close":
|
||||
default:
|
||||
return fmt.Errorf("unsupported new action region command %q", command.Type)
|
||||
}
|
||||
for _, point := range points {
|
||||
if _, err := creationNumbers(point, 2); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AnnotationLink 读取注解中唯一的点击跳转,书签目标保持原始动作
|
||||
// 入参: page 页面索引, id 注解标识
|
||||
// 返回: Action 跳转动作, error 错误信息
|
||||
@@ -201,9 +283,6 @@ func (e *Editor) UpdateAnnotationLink(page int, id string, target AnnotationLink
|
||||
return nil, err
|
||||
}
|
||||
position := objects[0].open
|
||||
if clips := objects[0].child("Clips"); clips != nil {
|
||||
position = clips.end
|
||||
}
|
||||
data = editorPatchXML(data, []editorXMLPatch{{position, position, bytes.TrimPrefix(actions, []byte(xml.Header))}})
|
||||
}
|
||||
}
|
||||
|
||||
+78
-15
@@ -18,6 +18,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
@@ -62,7 +63,7 @@ func (e *Editor) sourceInfoXML() ([]byte, error) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// editorCustomDatas 更新自定义字段,保留容器扩展与同名字段的未知属性和子节点
|
||||
// editorCustomDatas 更新自定义字段,保留原字段及容器的扩展内容
|
||||
// 入参: data 原XML, info 文档信息节点, values 新字段
|
||||
// 返回: []byte 修改后的XML, error 编码错误
|
||||
func editorCustomDatas(data []byte, info *editorXML, values *CustomDatas) ([]byte, error) {
|
||||
@@ -82,9 +83,26 @@ func editorCustomDatas(data []byte, info *editorXML, values *CustomDatas) ([]byt
|
||||
}
|
||||
}
|
||||
var encoded [][]byte
|
||||
used := make(map[*editorXML]bool)
|
||||
for _, item := range items {
|
||||
matches := byName[item.Name]
|
||||
if len(matches) == 0 {
|
||||
if item.sourceIndex > 0 && item.sourceIndex <= len(nodes) {
|
||||
used[nodes[item.sourceIndex-1]] = true
|
||||
}
|
||||
}
|
||||
for _, item := range items {
|
||||
var node *editorXML
|
||||
if item.sourceIndex > 0 && item.sourceIndex <= len(nodes) {
|
||||
node = nodes[item.sourceIndex-1]
|
||||
} else {
|
||||
for _, candidate := range byName[item.Name] {
|
||||
if !used[candidate] {
|
||||
node = candidate
|
||||
used[node] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if node == nil {
|
||||
entry, err := xml.Marshal(struct {
|
||||
XMLName xml.Name
|
||||
CustomData
|
||||
@@ -95,26 +113,30 @@ func editorCustomDatas(data []byte, info *editorXML, values *CustomDatas) ([]byt
|
||||
encoded = append(encoded, entry)
|
||||
continue
|
||||
}
|
||||
node := matches[0]
|
||||
byName[item.Name] = matches[1:]
|
||||
var original CustomData
|
||||
if err := xml.Unmarshal(data[node.start:node.end], &original); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if original.Value == item.Value {
|
||||
encoded = append(encoded, data[node.start:node.end])
|
||||
if original.Name != item.Name {
|
||||
entry, err := editorXMLAttribute(data, node, "Name", item.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
renamed, err := parseEditorXML(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updated, err := editorCustomDataValue(entry, renamed, original.Value, item.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encoded = append(encoded, updated)
|
||||
continue
|
||||
}
|
||||
var content bytes.Buffer
|
||||
if err := xml.EscapeText(&content, []byte(item.Value)); err != nil {
|
||||
entry, err := editorCustomDataValue(data, node, original.Value, item.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, child := range node.children {
|
||||
content.Write(data[child.start:child.end])
|
||||
}
|
||||
patch := editorXMLContent(data, node, content.Bytes())
|
||||
entry := append(bytes.Clone(data[node.start:patch.start]), patch.data...)
|
||||
entry = append(entry, data[patch.end:node.end]...)
|
||||
encoded = append(encoded, entry)
|
||||
}
|
||||
if container == nil {
|
||||
@@ -147,3 +169,44 @@ func editorCustomDatas(data []byte, info *editorXML, values *CustomDatas) ([]byt
|
||||
}
|
||||
return editorPatchXML(data, patches), nil
|
||||
}
|
||||
|
||||
// editorCustomDataValue 修改字段文本,保留扩展子节点和注释
|
||||
// 入参: data 原XML, node 字段节点, before 原值, after 新值
|
||||
// 返回: []byte 字段XML, error 编码错误
|
||||
func editorCustomDataValue(data []byte, node *editorXML, before, after string) ([]byte, error) {
|
||||
if before == after {
|
||||
return data[node.start:node.end], nil
|
||||
}
|
||||
var content bytes.Buffer
|
||||
if err := xml.EscapeText(&content, []byte(after)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if node.open < node.close {
|
||||
inner := data[node.open:node.close]
|
||||
decoder := xml.NewDecoder(bytes.NewReader(inner))
|
||||
depth := 0
|
||||
for {
|
||||
start := decoder.InputOffset()
|
||||
token, err := decoder.RawToken()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, text := token.(xml.CharData)
|
||||
if !text || depth != 0 {
|
||||
content.Write(inner[start:decoder.InputOffset()])
|
||||
}
|
||||
switch token.(type) {
|
||||
case xml.StartElement:
|
||||
depth++
|
||||
case xml.EndElement:
|
||||
depth--
|
||||
}
|
||||
}
|
||||
}
|
||||
patch := editorXMLContent(data, node, content.Bytes())
|
||||
entry := append(bytes.Clone(data[node.start:patch.start]), patch.data...)
|
||||
return append(entry, data[patch.end:node.end]...), nil
|
||||
}
|
||||
@@ -133,6 +133,11 @@ func (r *Reader) Editor() (*Editor, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.CustomDatas != nil {
|
||||
for i := range info.CustomDatas.CustomData {
|
||||
info.CustomDatas.CustomData[i].sourceIndex = i + 1
|
||||
}
|
||||
}
|
||||
e := NewEditor()
|
||||
e.encryption = r.encryption
|
||||
e.Info, e.maxID = cloneEditorData(*info), maximum
|
||||
@@ -527,6 +532,7 @@ func (e *Editor) objectCapabilities(object GraphicObject, orderable map[*editorX
|
||||
all.Copy = editorXMLCopyable(node) && (origin.reason == nil || object.Type == "PathObject" && editReason(origin.reason) == EditUnsupportedColor)
|
||||
if origin.reason != nil {
|
||||
all.Update, all.Reflow, all.ReplaceFont, all.Paint = false, false, false, false
|
||||
all.Paint = (object.Type == "TextObject" || object.Type == "PathObject") && editReason(origin.reason) == EditUnsupportedColor
|
||||
all.Reason, all.ReasonCode = origin.reason.Error(), editReason(origin.reason)
|
||||
} else if !editorXMLSupported(node) {
|
||||
all.Update, all.Reflow, all.ReplaceFont = false, false, false
|
||||
@@ -632,6 +638,14 @@ func editorPreservedObject(before, after GraphicObject) bool {
|
||||
// 返回: GraphicObject 独立副本, error 错误信息
|
||||
func (e *Editor) prepareCopiedObject(id string, object GraphicObject) (GraphicObject, error) {
|
||||
origin := e.snapshotOrigin(object)
|
||||
if origin == nil && object.origin != nil {
|
||||
references := make(map[string]bool)
|
||||
collectObjectReferences(object, references)
|
||||
delete(references, "")
|
||||
if len(references) != 0 {
|
||||
return GraphicObject{}, fmt.Errorf("copying a foreign snapshot requires importing its resources")
|
||||
}
|
||||
}
|
||||
object.origin = nil
|
||||
if origin == nil {
|
||||
return e.prepareObject(id, object)
|
||||
|
||||
+74
-1
@@ -21,6 +21,37 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AddDrawParam 注册绘制参数,保留继承引用,未引用的资源不写入文档
|
||||
// 入参: draw 绘制参数,ID由编辑器分配
|
||||
// 返回: string 资源标识, error 错误信息
|
||||
func (e *Editor) AddDrawParam(draw DrawParam) (string, error) {
|
||||
if draw.ResourceID != "" || draw.BaseLoc != "" || draw.Link != "" {
|
||||
return "", fmt.Errorf("external draw parameter references cannot be registered directly")
|
||||
}
|
||||
base, err := e.editorDrawParam(draw.Relative, make(map[string]bool))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
effective := mergeDrawParam(*base, &draw)
|
||||
if err := validateEditorStroke(PathObject{LineWidth: draw.LineWidth, Cap: draw.Cap, Join: draw.Join, MiterLimit: draw.MiterLimit, DashPattern: draw.DashPattern, DashOffset: draw.DashOffset}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !finite(draw.Size) || draw.Size < 0 || draw.Weight < 0 || draw.Weight > 900 || draw.Weight%100 != 0 {
|
||||
return "", fmt.Errorf("invalid draw parameter font size or weight")
|
||||
}
|
||||
if effective.Font != "" {
|
||||
if _, err := e.editorFont(effective.Font); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
for _, paint := range []*FillColor{draw.FillColor, (*FillColor)(draw.StrokeColor)} {
|
||||
if err := e.editorColor(paint); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return e.addEditorDrawParam(cloneEditorData(draw))
|
||||
}
|
||||
|
||||
// editorDrawParam 解析原文档的绘制参数继承链,缺失或循环引用不进入编辑快照
|
||||
// 入参: id 绘制参数标识, visited 已访问标识
|
||||
// 返回: *DrawParam 合并参数, error 错误信息
|
||||
@@ -121,7 +152,7 @@ func (e *Editor) resolveEditorStyleDefaults(object GraphicObject, base *DrawPara
|
||||
obj.FillColor, obj.StrokeColor = style.FillColor, style.StrokeColor
|
||||
}
|
||||
for _, color := range []*FillColor{style.FillColor, (*FillColor)(style.StrokeColor)} {
|
||||
if err := e.editorColor(color); err != nil {
|
||||
if _, err := e.Color(color); err != nil {
|
||||
return GraphicObject{}, &EditError{Code: EditUnsupportedColor, Err: fmt.Errorf("unsupported draw parameter color: %w", err)}
|
||||
}
|
||||
}
|
||||
@@ -413,6 +444,48 @@ func editorStrokeScale(ctm string) float64 {
|
||||
return 1
|
||||
}
|
||||
|
||||
// SetImageBorders 原子替换图片边框,nil移除边框
|
||||
// 入参: page 页面索引, ids 图片标识, border 边框样式
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) SetImageBorders(page int, ids []string, border *ImageBorder) error {
|
||||
if err := e.validateImageBorder(border); err != nil {
|
||||
return err
|
||||
}
|
||||
objects, _, err := e.selectedObjects(page, ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range objects {
|
||||
if objects[i].Type != "ImageObject" {
|
||||
return fmt.Errorf("object %q is not an image", ids[i])
|
||||
}
|
||||
objects[i].ImageObject.Border = cloneEditorData(border)
|
||||
}
|
||||
return e.updateObjects(page, objects, true)
|
||||
}
|
||||
|
||||
// validateImageBorder 校验图片边框的线型、圆角和绘制颜色
|
||||
// 入参: border 边框
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) validateImageBorder(border *ImageBorder) error {
|
||||
if border == nil {
|
||||
return nil
|
||||
}
|
||||
for _, radius := range []float64{border.HorizonalCornerRadius, border.VerticalCornerRadius} {
|
||||
if !finite(radius) || radius < 0 {
|
||||
return fmt.Errorf("image corner radius must be finite and nonnegative")
|
||||
}
|
||||
}
|
||||
stroke := PathObject{DashPattern: border.DashPattern, DashOffset: &border.DashOffset}
|
||||
if border.LineWidth != nil {
|
||||
stroke.LineWidth = *border.LineWidth
|
||||
}
|
||||
if err := validateEditorStroke(stroke); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.editorColor((*FillColor)(border.BorderColor))
|
||||
}
|
||||
|
||||
// validateEditorStroke 校验路径描边尺寸、端点和虚线
|
||||
// 入参: path 路径对象
|
||||
// 返回: error 错误信息
|
||||
|
||||
+48
-9
@@ -18,6 +18,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -112,13 +113,22 @@ func (e *Editor) UpdateTextContent(page int, id, value string, style TextStyle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !capability.ReplaceFont {
|
||||
if !capability.ReplaceFont && (style != (TextStyle{}) || !capability.Transform) {
|
||||
return capability.editError()
|
||||
}
|
||||
object, err := e.Object(page, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !capability.LayoutKnown && style == (TextStyle{}) {
|
||||
if object.Type != "TextObject" {
|
||||
return fmt.Errorf("object %q is not text", id)
|
||||
}
|
||||
if err := e.RewriteText(&object.TextObject, value); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.updateObjects(page, []GraphicObject{object}, true)
|
||||
}
|
||||
updates, err := e.styleTextObjects([]GraphicObject{object}, style)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -136,20 +146,18 @@ func (e *Editor) UpdateTextContent(page int, id, value string, style TextStyle)
|
||||
return e.updateObjects(page, updates, true)
|
||||
}
|
||||
|
||||
// RewriteText 保留普通横向原文的字形定位,不修改对象样式或文档
|
||||
// RewriteText 保留原文的字形定位,不修改对象样式或文档
|
||||
// 等长替换保留各字原位;单段增删保留前缀和后缀步进,新增字符沿用局部字距
|
||||
// 不跨定位段增删或自动生成新行,复杂字形映射需单独处理
|
||||
// 方向、缩放和修饰原样保留;复杂布局仅支持等长替换,合并字形需显式重排
|
||||
// 入参: obj 原文字对象, value 新内容
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) RewriteText(obj *TextObject, value string) error {
|
||||
if obj.ReadDirection != 0 || obj.CharDirection != 0 || len(obj.CGTransform) != 0 || obj.VScale != 0 || obj.Decoration != "" {
|
||||
return &EditError{Code: EditUnsupportedObject, Err: fmt.Errorf("text positioning is not supported for content editing")}
|
||||
}
|
||||
value = strings.ReplaceAll(strings.ReplaceAll(value, "\r\n", "\n"), "\r", "\n")
|
||||
if !utf8.ValidString(value) || strings.Contains(value, "\t") || strings.Trim(value, "\n") == "" {
|
||||
return fmt.Errorf("text must contain UTF-8 characters without tabs")
|
||||
}
|
||||
check := cloneEditorData(*obj)
|
||||
check.VScale, check.Decoration = 0, ""
|
||||
if err := e.prepareText(&check); err != nil {
|
||||
var missing *MissingGlyphError
|
||||
if !errors.As(err, &missing) {
|
||||
@@ -160,8 +168,16 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
old, next := []rune(obj.Text()), []rune(value)
|
||||
complex := obj.ReadDirection != 0 || obj.CharDirection != 0 || len(obj.CGTransform) != 0 || obj.VScale != 0 || obj.Decoration != ""
|
||||
if complex && len(old) != len(next) {
|
||||
return &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("changing positioned text length requires explicit layout")}
|
||||
}
|
||||
var missing []rune
|
||||
for _, char := range value {
|
||||
for i, char := range next {
|
||||
if len(old) == len(next) && old[i] == char {
|
||||
continue
|
||||
}
|
||||
if char != '\n' && sfnt.GlyphIndex(char) == 0 {
|
||||
missing = append(missing, char)
|
||||
}
|
||||
@@ -169,8 +185,8 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
|
||||
if err := missingGlyphError(obj.Font, missing); err != nil {
|
||||
return err
|
||||
}
|
||||
old, next := []rune(obj.Text()), []rune(value)
|
||||
codes := append([]TextCode(nil), obj.TextCode...)
|
||||
transforms := append([]CGTransform(nil), obj.CGTransform...)
|
||||
if len(old) == len(next) {
|
||||
offset := 0
|
||||
for i := range codes {
|
||||
@@ -240,10 +256,33 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
|
||||
}
|
||||
check = *obj
|
||||
check.TextCode = codes
|
||||
position := 0
|
||||
for i, code := range codes {
|
||||
before, after := textCodeRunes(obj.TextCode[i].Value), textCodeRunes(code.Value)
|
||||
if len(before) == len(after) {
|
||||
for j, char := range after {
|
||||
if before[j] == char {
|
||||
continue
|
||||
}
|
||||
for k := range transforms {
|
||||
transform := &transforms[k]
|
||||
if position+j < transform.CodePosition || position+j >= transform.CodePosition+transform.CodeCount {
|
||||
continue
|
||||
}
|
||||
if transform.CodeCount != 1 || transform.GlyphCount != 1 {
|
||||
return &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("changing a combined glyph requires explicit layout")}
|
||||
}
|
||||
transform.Glyphs = strconv.Itoa(int(sfnt.GlyphIndex(char)))
|
||||
}
|
||||
}
|
||||
}
|
||||
position += len(after)
|
||||
}
|
||||
check.CGTransform = transforms
|
||||
if err := validateEditorGeometry(GraphicObject{Type: "TextObject", TextObject: check}); err != nil {
|
||||
return err
|
||||
}
|
||||
obj.TextCode, obj.layout = codes, nil
|
||||
obj.TextCode, obj.CGTransform, obj.layout = codes, transforms, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -49,11 +49,13 @@ type FontShaper interface {
|
||||
ShapeText(value string, size float64) ([]ShapedGlyph, error)
|
||||
}
|
||||
|
||||
// TextShapeOptions 指定单一方向文字的塑形参数,不执行双向段落分析或字体回退
|
||||
// TextShapeOptions 指定文字塑形参数,Bidi启用双向及脚本分段,不执行字体回退
|
||||
// Direction为空或ltr时从左向右,rtl时从右向左;Script为四字母ISO15924代码,空值自动检测
|
||||
// Bidi启用时Direction指定段落方向,各文字片段按双向算法排列
|
||||
// Language为语言标签,Features为逗号分隔的全局OpenType特性,如kern=0,liga=1,不接受字符区间
|
||||
// 零值保持原有FontShaper行为,非零值要求FontShaperOptions能力
|
||||
type TextShapeOptions struct {
|
||||
Bidi bool
|
||||
Direction string
|
||||
Script string
|
||||
Language string
|
||||
@@ -68,9 +70,11 @@ type FontShaperOptions interface {
|
||||
}
|
||||
|
||||
// ShapedGlyph 保存塑形后的字形、原文簇和基线定位
|
||||
// Bidi模式下VisualOrder为簇从左到右的零起始顺序,同簇字形共用顺序
|
||||
type ShapedGlyph struct {
|
||||
Glyph uint16
|
||||
Cluster int
|
||||
VisualOrder int
|
||||
X, Y, Advance float64
|
||||
}
|
||||
|
||||
|
||||
+82
-2
@@ -18,6 +18,7 @@ import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -25,9 +26,12 @@ import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/go-text/typesetting/di"
|
||||
typefont "github.com/go-text/typesetting/font"
|
||||
"github.com/go-text/typesetting/harfbuzz"
|
||||
"github.com/go-text/typesetting/language"
|
||||
"github.com/go-text/typesetting/shaping"
|
||||
"golang.org/x/image/math/fixed"
|
||||
xlanguage "golang.org/x/text/language"
|
||||
)
|
||||
|
||||
@@ -36,6 +40,7 @@ type fontShaper struct {
|
||||
metrics FontMetrics
|
||||
shapeMu sync.Mutex
|
||||
shapeFont *harfbuzz.Font
|
||||
shapeFace *typefont.Face
|
||||
shapeBuffer *harfbuzz.Buffer
|
||||
}
|
||||
|
||||
@@ -46,8 +51,7 @@ func (f *fontShaper) ShapeText(value string, size float64) ([]ShapedGlyph, error
|
||||
return f.ShapeTextWithOptions(value, size, TextShapeOptions{})
|
||||
}
|
||||
|
||||
// ShapeTextWithOptions 使用现有纯Go塑形引擎处理单一方向原文,保留逻辑簇和视觉位置
|
||||
// 不执行混合方向分段,调用方应将双向段落拆为单向文字对象
|
||||
// ShapeTextWithOptions 使用纯Go塑形引擎保留逻辑簇和视觉位置,可显式启用双向分段
|
||||
// 入参: value 单行原文, size 毫米字号, options 塑形选项
|
||||
// 返回: []ShapedGlyph 定位字形, error 字符或选项错误
|
||||
func (f *fontShaper) ShapeTextWithOptions(value string, size float64, options TextShapeOptions) ([]ShapedGlyph, error) {
|
||||
@@ -114,8 +118,12 @@ func (f *fontShaper) ShapeTextWithOptions(value string, size float64, options Te
|
||||
return nil, fmt.Errorf("shaping font: %w", err)
|
||||
}
|
||||
f.shapeFont = harfbuzz.NewFont(face)
|
||||
f.shapeFace = face
|
||||
f.shapeBuffer = harfbuzz.NewBuffer()
|
||||
}
|
||||
if options.Bidi {
|
||||
return f.shapeBidiText([]rune(value), size, props, features)
|
||||
}
|
||||
buffer := f.shapeBuffer
|
||||
buffer.Clear()
|
||||
buffer.Props = props
|
||||
@@ -143,3 +151,75 @@ func (f *fontShaper) ShapeTextWithOptions(value string, size float64, options Te
|
||||
slices.SortStableFunc(result, func(a, b ShapedGlyph) int { return cmp.Compare(a.Cluster, b.Cluster) })
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// shapingFace 固定双向排版的字体来源,不隐式替换字体
|
||||
type shapingFace struct{ face *typefont.Face }
|
||||
|
||||
// ResolveFace 返回指定字体
|
||||
// 入参: char 字符
|
||||
// 返回: *typefont.Face 字体
|
||||
func (f shapingFace) ResolveFace(char rune) *typefont.Face { return f.face }
|
||||
|
||||
// shapeBidiText 使用现有分段和行排列引擎生成单行双向字形
|
||||
// 入参: runes 原文, size 毫米字号, props 段落属性, features 字体特性
|
||||
// 返回: []ShapedGlyph 字形, error 错误信息
|
||||
func (f *fontShaper) shapeBidiText(runes []rune, size float64, props harfbuzz.SegmentProperties, features []harfbuzz.Feature) ([]ShapedGlyph, error) {
|
||||
if len(runes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
direction := di.DirectionLTR
|
||||
if props.Direction == harfbuzz.RightToLeft {
|
||||
direction = di.DirectionRTL
|
||||
}
|
||||
input := shaping.Input{Text: runes, RunEnd: len(runes), Direction: direction, Face: f.shapeFace, Size: fixed.I(int(f.metrics.UnitsPerEm())), Script: props.Script, Language: props.Language}
|
||||
for _, feature := range features {
|
||||
input.FontFeatures = append(input.FontFeatures, shaping.FontFeature{Tag: feature.Tag, Value: feature.Value})
|
||||
}
|
||||
var segmenter shaping.Segmenter
|
||||
var shaper shaping.HarfbuzzShaper
|
||||
var wrapper shaping.LineWrapper
|
||||
var runs []shaping.Output
|
||||
var advance int64
|
||||
for _, part := range segmenter.Split(input, shapingFace{f.shapeFace}) {
|
||||
if props.Script != 0 {
|
||||
part.Script = props.Script
|
||||
}
|
||||
if props.Language != "" {
|
||||
part.Language = props.Language
|
||||
}
|
||||
run := shaper.Shape(part)
|
||||
for _, glyph := range run.Glyphs {
|
||||
advance += int64(glyph.Advance)
|
||||
}
|
||||
if advance < 0 || advance >= math.MaxInt32 {
|
||||
return nil, fmt.Errorf("shaped line exceeds coordinate range")
|
||||
}
|
||||
runs = append(runs, run)
|
||||
}
|
||||
lines, truncated := wrapper.WrapParagraphF(shaping.WrapConfig{Direction: direction, DisableTrailingWhitespaceTrim: true}, fixed.Int26_6(math.MaxInt32), runes, shaping.NewSliceIterator(runs))
|
||||
if truncated != 0 || len(lines) != 1 {
|
||||
return nil, fmt.Errorf("shaping requires a single text line")
|
||||
}
|
||||
slices.SortFunc(lines[0], func(a, b shaping.Output) int { return cmp.Compare(a.VisualIndex, b.VisualIndex) })
|
||||
unit := size / float64(f.metrics.UnitsPerEm()) / 64
|
||||
var result []ShapedGlyph
|
||||
var x float64
|
||||
visual := make(map[int]int)
|
||||
for _, run := range lines[0] {
|
||||
for _, glyph := range run.Glyphs {
|
||||
if glyph.GlyphID >= typefont.GID(f.metrics.NumGlyphs()) {
|
||||
return nil, fmt.Errorf("invalid shaped glyph index %d", glyph.GlyphID)
|
||||
}
|
||||
cluster := glyph.TextIndex()
|
||||
order, exists := visual[cluster]
|
||||
if !exists {
|
||||
order = len(visual)
|
||||
visual[cluster] = order
|
||||
}
|
||||
result = append(result, ShapedGlyph{Glyph: uint16(glyph.GlyphID), Cluster: cluster, VisualOrder: order, X: (x + float64(glyph.XOffset)) * unit, Y: -float64(glyph.YOffset) * unit, Advance: float64(glyph.Advance) * unit})
|
||||
x += float64(glyph.Advance)
|
||||
}
|
||||
}
|
||||
slices.SortStableFunc(result, func(a, b ShapedGlyph) int { return cmp.Compare(a.Cluster, b.Cluster) })
|
||||
return result, nil
|
||||
}
|
||||
+3
-2
@@ -52,6 +52,7 @@ type CustomDatas struct {
|
||||
|
||||
// CustomData 自定义数据项
|
||||
type CustomData struct {
|
||||
Name string `xml:"Name,attr"`
|
||||
Value string `xml:",chardata"`
|
||||
Name string `xml:"Name,attr"`
|
||||
Value string `xml:",chardata"`
|
||||
sourceIndex int
|
||||
}
|
||||
+15
-7
@@ -224,6 +224,19 @@ func (r *Renderer) parseShdColor(segments []ShdSegment, alpha *int) color.Color
|
||||
// 返回: []ColorStop 后端无关的渐变分段
|
||||
func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []ColorStop {
|
||||
var gradient []ColorStop
|
||||
positions := gradientPositions(segments)
|
||||
for i, segment := range segments {
|
||||
segmentAlpha := mergeAlpha(segment.Color.Alpha, alpha)
|
||||
gradient = append(gradient, ColorStop{Offset: positions[i], Color: r.ResolveColor(segment.Color.Value, segment.Color.Index, segment.Color.ColorSpace, segmentAlpha)})
|
||||
}
|
||||
return gradient
|
||||
}
|
||||
|
||||
// gradientPositions 按显式锚点补全渐变分段位置,保留原顺序
|
||||
// 入参: segments 渐变分段
|
||||
// 返回: []float64 分段位置
|
||||
func gradientPositions(segments []ShdSegment) []float64 {
|
||||
positions := make([]float64, len(segments))
|
||||
position, step := 0.0, 0.0
|
||||
for i, segment := range segments {
|
||||
if !segment.positionMissing {
|
||||
@@ -242,15 +255,10 @@ func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []ColorStop
|
||||
}
|
||||
step = (end - position) / float64(next-i)
|
||||
}
|
||||
offset := position
|
||||
positions[i] = position
|
||||
position += step
|
||||
segmentAlpha := mergeAlpha(segment.Color.Alpha, alpha)
|
||||
gradient = append(gradient, ColorStop{Offset: offset, Color: r.ResolveColor(segment.Color.Value, segment.Color.Index, segment.Color.ColorSpace, segmentAlpha)})
|
||||
}
|
||||
if len(gradient) == 0 {
|
||||
return nil
|
||||
}
|
||||
return gradient
|
||||
return positions
|
||||
}
|
||||
|
||||
// colorToRGBA 转换颜色对象
|
||||
|
||||
+38
-1
@@ -119,9 +119,15 @@ func layoutShapedText(obj *TextObject, value string, options TextLayout, metrics
|
||||
code := TextCode{X: ofdNumber(x), Y: ofdNumber(y), Value: escapeOFDText(string(chars))}
|
||||
var dx, dy []string
|
||||
var lastX, lastY float64
|
||||
var visualShifts map[int]float64
|
||||
if options.Shaping.Bidi {
|
||||
visualShifts = bidiClusterShifts(glyphs, extra)
|
||||
}
|
||||
for j, glyph := range glyphs {
|
||||
shift := extra[glyph.Cluster]
|
||||
if options.Shaping.Direction == "rtl" {
|
||||
if options.Shaping.Bidi {
|
||||
shift = visualShifts[glyph.Cluster]
|
||||
} else if options.Shaping.Direction == "rtl" {
|
||||
// 逻辑簇后的间距位于视觉左侧,整行不计末尾字距
|
||||
shift = extra[len(chars)] - extra[glyph.Cluster] - options.LetterSpacing
|
||||
}
|
||||
@@ -162,6 +168,37 @@ func layoutShapedText(obj *TextObject, value string, options TextLayout, metrics
|
||||
return nil
|
||||
}
|
||||
|
||||
// bidiClusterShifts 按视觉簇顺序累计字距和两端对齐产生的偏移
|
||||
// 入参: glyphs 逻辑顺序字形, extra 逻辑字符累计偏移
|
||||
// 返回: map[int]float64 各簇的视觉偏移
|
||||
func bidiClusterShifts(glyphs []ShapedGlyph, extra []float64) map[int]float64 {
|
||||
type cluster struct {
|
||||
index, order int
|
||||
extra float64
|
||||
}
|
||||
var clusters []cluster
|
||||
for i := 0; i < len(glyphs); {
|
||||
end := i + 1
|
||||
for end < len(glyphs) && glyphs[end].Cluster == glyphs[i].Cluster {
|
||||
end++
|
||||
}
|
||||
next := len(extra) - 1
|
||||
if end < len(glyphs) {
|
||||
next = glyphs[end].Cluster
|
||||
}
|
||||
clusters = append(clusters, cluster{glyphs[i].Cluster, glyphs[i].VisualOrder, extra[next] - extra[glyphs[i].Cluster]})
|
||||
i = end
|
||||
}
|
||||
slices.SortFunc(clusters, func(a, b cluster) int { return a.order - b.order })
|
||||
shifts := make(map[int]float64, len(clusters))
|
||||
var offset float64
|
||||
for _, cluster := range clusters {
|
||||
shifts[cluster.index] = offset
|
||||
offset += cluster.extra
|
||||
}
|
||||
return shifts
|
||||
}
|
||||
|
||||
// ShapeText 使用固定选项塑形单行原文
|
||||
// 入参: value 原文, size 毫米字号
|
||||
// 返回: []ShapedGlyph 字形, error 塑形错误
|
||||
|
||||
+177
-35
@@ -375,45 +375,15 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
|
||||
for _, layer := range page.Content.Layer {
|
||||
for _, object := range layer.Objects {
|
||||
before, exists := original[editorObjectID(object)]
|
||||
collectObjectReferences(object, used)
|
||||
if e.source != nil && (!exists || !reflect.DeepEqual(before, object)) {
|
||||
collectObjectReferences(object, promoted)
|
||||
}
|
||||
if origin := e.objectOrigin(editorObjectID(object)); origin != nil && origin.page != nil && (!exists || before.Type != object.Type || !reflect.DeepEqual(before.CompositeGraphicUnit, object.CompositeGraphicUnit)) {
|
||||
for _, name := range origin.page.original.PageRes {
|
||||
files[e.source.reader.ResPath(resolveResourcePath(origin.page.ref.BaseLoc, "", name))] = true
|
||||
}
|
||||
}
|
||||
for _, value := range []*FillColor{object.TextObject.FillColor, (*FillColor)(object.TextObject.StrokeColor), object.PathObject.FillColor, (*FillColor)(object.PathObject.StrokeColor)} {
|
||||
if value != nil && value.ColorSpace != "" {
|
||||
used[value.ColorSpace] = true
|
||||
if e.source != nil && (!exists || !reflect.DeepEqual(before, object)) {
|
||||
promoted[value.ColorSpace] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
switch object.Type {
|
||||
case "CompositeObject", "CompositeGraphicUnit":
|
||||
collectCompositeReferences(object.CompositeGraphicUnit, used)
|
||||
if e.source != nil && (!exists || !reflect.DeepEqual(before.CompositeGraphicUnit, object.CompositeGraphicUnit)) {
|
||||
collectCompositeReferences(object.CompositeGraphicUnit, promoted)
|
||||
}
|
||||
case "TextObject":
|
||||
used[object.TextObject.DrawParam] = true
|
||||
used[object.TextObject.Font] = true
|
||||
if e.source != nil && (!exists || before.Type != object.Type || before.TextObject.Font != object.TextObject.Font) {
|
||||
promoted[object.TextObject.Font] = true
|
||||
}
|
||||
case "ImageObject":
|
||||
used[object.ImageObject.ResourceID] = true
|
||||
if e.source != nil && (!exists || before.Type != object.Type || before.ImageObject.ResourceID != object.ImageObject.ResourceID) {
|
||||
promoted[object.ImageObject.ResourceID] = true
|
||||
}
|
||||
if object.ImageObject.ImageMask != "" {
|
||||
used[object.ImageObject.ImageMask] = true
|
||||
if e.source != nil && (!exists || before.Type != object.Type || before.ImageObject.ImageMask != object.ImageObject.ImageMask) {
|
||||
promoted[object.ImageObject.ImageMask] = true
|
||||
}
|
||||
}
|
||||
case "PathObject":
|
||||
used[object.PathObject.DrawParam] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -663,6 +633,7 @@ func encodeOFDXML(encode func(*ofdXML)) ([]byte, error) {
|
||||
func (x *ofdXML) object(object GraphicObject, root bool) {
|
||||
var attrs ofdAttrs
|
||||
var fill, stroke *FillColor
|
||||
var actions []Action
|
||||
if root {
|
||||
attrs.add("xmlns:ofd", ofdNamespace)
|
||||
}
|
||||
@@ -676,6 +647,8 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
|
||||
attrs.add("Font", obj.Font)
|
||||
attrs.number("Size", obj.Size)
|
||||
attrs.number("HScale", obj.HScale)
|
||||
attrs.number("VScale", obj.VScale)
|
||||
attrs.add("Decoration", obj.Decoration)
|
||||
attrs.number("Weight", float64(obj.Weight))
|
||||
attrs.number("ReadDirection", float64(obj.ReadDirection))
|
||||
attrs.number("CharDirection", float64(obj.CharDirection))
|
||||
@@ -690,6 +663,7 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
|
||||
attrs.flag("Fill", obj.Fill)
|
||||
attrs.alpha(obj.Alpha)
|
||||
fill, stroke = obj.FillColor, (*FillColor)(obj.StrokeColor)
|
||||
actions = obj.Actions
|
||||
case "PathObject", "Path":
|
||||
obj := object.PathObject
|
||||
attrs.add("ID", obj.ID)
|
||||
@@ -714,6 +688,7 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
|
||||
attrs.flag("Fill", obj.Fill)
|
||||
attrs.alpha(obj.Alpha)
|
||||
fill, stroke = obj.FillColor, (*FillColor)(obj.StrokeColor)
|
||||
actions = obj.Actions
|
||||
case "ImageObject":
|
||||
obj := object.ImageObject
|
||||
attrs.add("ID", obj.ID)
|
||||
@@ -723,6 +698,7 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
|
||||
attrs.add("ImageMask", obj.ImageMask)
|
||||
attrs.flag("Visible", obj.Visible)
|
||||
attrs.alpha(obj.Alpha)
|
||||
actions = obj.Actions
|
||||
case "CompositeObject", "CompositeGraphicUnit":
|
||||
obj := object.CompositeGraphicUnit
|
||||
attrs.add("ID", obj.ID)
|
||||
@@ -732,10 +708,13 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
|
||||
attrs.add("DrawParam", obj.DrawParam)
|
||||
attrs.flag("Visible", obj.Visible)
|
||||
attrs.alpha(obj.Alpha)
|
||||
actions = obj.Actions
|
||||
}
|
||||
x.start(object.Type, attrs)
|
||||
x.actions(actions)
|
||||
if object.Type == "ImageObject" {
|
||||
x.clips(object.ImageObject.Clips)
|
||||
x.border(object.ImageObject.Border)
|
||||
} else if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
|
||||
x.clips(object.CompositeGraphicUnit.Clips)
|
||||
} else if object.Type == "PathObject" || object.Type == "Path" {
|
||||
@@ -816,7 +795,7 @@ func (x *ofdXML) textCode(value string, attrs ofdAttrs) {
|
||||
x.err = x.encoder.EncodeElement(content, xml.StartElement{Name: xml.Name{Local: "ofd:TextCode"}, Attr: attrs})
|
||||
}
|
||||
|
||||
// color 写出基本颜色
|
||||
// color 写出纯色、渐变和图案,保留颜色空间与透明度
|
||||
// 入参: name 节点名, color 颜色
|
||||
func (x *ofdXML) color(name string, color *FillColor) {
|
||||
if color == nil {
|
||||
@@ -830,5 +809,168 @@ func (x *ofdXML) color(name string, color *FillColor) {
|
||||
}
|
||||
attrs.alpha(color.Alpha)
|
||||
x.start(name, attrs)
|
||||
if color.AxialShd != nil {
|
||||
shading := color.AxialShd
|
||||
x.shading("AxialShd", shadingAttrs(shading.MapType, shading.MapUnit, shading.Extend, shading.StartPoint, shading.EndPoint), shading.Segment)
|
||||
}
|
||||
if color.RadialShd != nil {
|
||||
shading := color.RadialShd
|
||||
attrs := shadingAttrs(shading.MapType, shading.MapUnit, shading.Extend, shading.StartPoint, shading.EndPoint)
|
||||
attrs.add("StartRadius", ofdNumber(shading.StartRadius))
|
||||
attrs.add("EndRadius", ofdNumber(shading.EndRadius))
|
||||
attrs.number("Eccentricity", shading.Eccentricity)
|
||||
attrs.number("Angle", shading.Angle)
|
||||
x.shading("RadialShd", attrs, shading.Segment)
|
||||
}
|
||||
if color.Pattern != nil {
|
||||
x.pattern(color.Pattern)
|
||||
}
|
||||
x.end(name)
|
||||
}
|
||||
|
||||
// shadingAttrs 编码轴向和径向渐变的共同属性
|
||||
// 入参: mapType 映射方式, mapUnit 映射周期, extend 延伸方式, start、end 渐变端点
|
||||
// 返回: ofdAttrs 属性集合
|
||||
func shadingAttrs(mapType string, mapUnit float64, extend, start, end string) ofdAttrs {
|
||||
var attrs ofdAttrs
|
||||
attrs.add("MapType", mapType)
|
||||
attrs.number("MapUnit", mapUnit)
|
||||
attrs.add("Extend", extend)
|
||||
attrs.add("StartPoint", start)
|
||||
attrs.add("EndPoint", end)
|
||||
return attrs
|
||||
}
|
||||
|
||||
// shading 写出渐变分段,保留未显式设置的位置
|
||||
// 入参: name 渐变类型, attrs 属性, segments 色标
|
||||
func (x *ofdXML) shading(name string, attrs ofdAttrs, segments []ShdSegment) {
|
||||
x.start(name, attrs)
|
||||
for _, segment := range segments {
|
||||
var attrs ofdAttrs
|
||||
if !segment.positionMissing {
|
||||
attrs.add("Position", ofdNumber(segment.Position))
|
||||
}
|
||||
x.start("Segment", attrs)
|
||||
x.element("Color", segment.Color)
|
||||
x.end("Segment")
|
||||
}
|
||||
x.end(name)
|
||||
}
|
||||
|
||||
// element 写出具有标准XML标签的子树,显式声明OFD命名空间
|
||||
// 入参: name 元素名称, value 元素内容
|
||||
func (x *ofdXML) element(name string, value any) {
|
||||
if x.err == nil {
|
||||
x.err = x.encoder.EncodeElement(value, xml.StartElement{Name: xml.Name{Space: ofdNamespace, Local: name}})
|
||||
}
|
||||
}
|
||||
|
||||
// border 写出图片边框及其绘制颜色
|
||||
// 入参: border 边框,nil不输出节点
|
||||
func (x *ofdXML) border(border *ImageBorder) {
|
||||
if border == nil {
|
||||
return
|
||||
}
|
||||
var attrs ofdAttrs
|
||||
if border.LineWidth != nil {
|
||||
attrs.add("LineWidth", ofdNumber(*border.LineWidth))
|
||||
}
|
||||
attrs.number("HorizonalCornerRadius", border.HorizonalCornerRadius)
|
||||
attrs.number("VerticalCornerRadius", border.VerticalCornerRadius)
|
||||
attrs.number("DashOffset", border.DashOffset)
|
||||
attrs.add("DashPattern", border.DashPattern)
|
||||
x.start("Border", attrs)
|
||||
x.color("BorderColor", (*FillColor)(border.BorderColor))
|
||||
x.end("Border")
|
||||
}
|
||||
|
||||
// pattern 写出图案单元,保留对象绘制顺序
|
||||
// 入参: pattern 图案
|
||||
func (x *ofdXML) pattern(pattern *Pattern) {
|
||||
var attrs ofdAttrs
|
||||
attrs.number("Width", pattern.Width)
|
||||
attrs.number("Height", pattern.Height)
|
||||
attrs.number("XStep", pattern.XStep)
|
||||
attrs.number("YStep", pattern.YStep)
|
||||
attrs.add("ReflectMethod", pattern.ReflectMethod)
|
||||
attrs.add("RelativeTo", pattern.RelativeTo)
|
||||
attrs.add("CTM", pattern.CTM)
|
||||
x.start("Pattern", attrs)
|
||||
x.start("CellContent", nil)
|
||||
for _, object := range pattern.CellContent.Objects {
|
||||
x.object(object, false)
|
||||
}
|
||||
x.end("CellContent")
|
||||
x.end("Pattern")
|
||||
}
|
||||
|
||||
// actions 写出对象动作及精确点击区域,不检查链接可达性
|
||||
// 入参: actions 动作列表
|
||||
func (x *ofdXML) actions(actions []Action) {
|
||||
if len(actions) == 0 {
|
||||
return
|
||||
}
|
||||
x.start("Actions", nil)
|
||||
for _, action := range actions {
|
||||
var attrs ofdAttrs
|
||||
attrs.add("Event", action.Event)
|
||||
x.start("Action", attrs)
|
||||
if action.Region != nil {
|
||||
x.start("Region", nil)
|
||||
for _, area := range action.Region.Area {
|
||||
x.start("Area", ofdAttrs{{Name: xml.Name{Local: "Start"}, Value: area.Start}})
|
||||
for _, command := range area.Command {
|
||||
var attrs ofdAttrs
|
||||
for _, pair := range [][2]string{{"Point1", command.Point1}, {"Point2", command.Point2}, {"Point3", command.Point3}, {"EllipseSize", command.EllipseSize}, {"RotationAngle", command.RotationAngle}, {"LargeArc", command.LargeArc}, {"SweepDirection", command.SweepDirection}, {"EndPoint", command.EndPoint}} {
|
||||
attrs.add(pair[0], pair[1])
|
||||
}
|
||||
x.start(command.Type, attrs)
|
||||
x.end(command.Type)
|
||||
}
|
||||
x.end("Area")
|
||||
}
|
||||
x.end("Region")
|
||||
}
|
||||
if action.Goto != nil {
|
||||
x.start("Goto", nil)
|
||||
if dest := action.Goto.Dest; dest != nil {
|
||||
attrs := ofdAttrs{{Name: xml.Name{Local: "Type"}, Value: dest.Type}, {Name: xml.Name{Local: "PageID"}, Value: dest.PageID}}
|
||||
switch dest.Type {
|
||||
case "XYZ":
|
||||
attrs.add("Left", ofdNumber(dest.Left))
|
||||
attrs.add("Top", ofdNumber(dest.Top))
|
||||
attrs.add("Zoom", ofdNumber(dest.Zoom))
|
||||
case "FitH":
|
||||
attrs.add("Top", ofdNumber(dest.Top))
|
||||
case "FitV":
|
||||
attrs.add("Left", ofdNumber(dest.Left))
|
||||
case "FitR":
|
||||
attrs.add("Left", ofdNumber(dest.Left))
|
||||
attrs.add("Right", ofdNumber(dest.Right))
|
||||
attrs.add("Top", ofdNumber(dest.Top))
|
||||
attrs.add("Bottom", ofdNumber(dest.Bottom))
|
||||
}
|
||||
x.start("Dest", attrs)
|
||||
x.end("Dest")
|
||||
}
|
||||
if action.Goto.Bookmark != nil {
|
||||
x.element("Bookmark", action.Goto.Bookmark)
|
||||
}
|
||||
x.end("Goto")
|
||||
}
|
||||
if action.URI != nil {
|
||||
x.element("URI", action.URI)
|
||||
}
|
||||
if action.GotoA != nil {
|
||||
x.element("GotoA", action.GotoA)
|
||||
}
|
||||
if action.Sound != nil {
|
||||
x.element("Sound", action.Sound)
|
||||
}
|
||||
if action.Movie != nil {
|
||||
x.element("Movie", action.Movie)
|
||||
}
|
||||
x.end("Action")
|
||||
}
|
||||
x.end("Actions")
|
||||
}
|
||||
Reference in new issue
Block a user