mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
This commit is contained in:
1 parent
0508029387
commit
d7cd3d03b5
20 files changed
+1135
-114
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="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"><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>
|
<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 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 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>
|
<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="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="infoSubject" type="text"></label>
|
||||||
<label class="form-row"><span>作者</span><input id="infoAuthor" 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>
|
<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>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|||||||
@@ -774,6 +774,8 @@ button:not(:disabled):hover {
|
|||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-custom-row input,
|
||||||
|
.gradient-stop-row input,
|
||||||
.form-row input[type="text"],
|
.form-row input[type="text"],
|
||||||
.form-row input[type="password"],
|
.form-row input[type="password"],
|
||||||
.form-row input[type="number"],
|
.form-row input[type="number"],
|
||||||
@@ -798,6 +800,56 @@ button:not(:disabled):hover {
|
|||||||
resize: vertical;
|
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 {
|
.color-input {
|
||||||
width: calc(2em + 22px);
|
width: calc(2em + 22px);
|
||||||
height: var(--control-height);
|
height: var(--control-height);
|
||||||
|
|||||||
+113
-3
@@ -213,6 +213,8 @@ const el = {
|
|||||||
infoPanel: document.querySelector("#infoPanel"),
|
infoPanel: document.querySelector("#infoPanel"),
|
||||||
infoForm: document.querySelector("#infoForm"),
|
infoForm: document.querySelector("#infoForm"),
|
||||||
infoTitle: document.querySelector("#infoTitle"),
|
infoTitle: document.querySelector("#infoTitle"),
|
||||||
|
infoCustomRows: document.querySelector("#infoCustomRows"),
|
||||||
|
infoCustomAdd: document.querySelector("#infoCustomAdd"),
|
||||||
infoAuthor: document.querySelector("#infoAuthor"),
|
infoAuthor: document.querySelector("#infoAuthor"),
|
||||||
infoSubject: document.querySelector("#infoSubject"),
|
infoSubject: document.querySelector("#infoSubject"),
|
||||||
infoCancel: document.querySelector("#infoCancel"),
|
infoCancel: document.querySelector("#infoCancel"),
|
||||||
@@ -286,6 +288,19 @@ const el = {
|
|||||||
groupObjectsButton: document.querySelector("#groupObjectsButton"),
|
groupObjectsButton: document.querySelector("#groupObjectsButton"),
|
||||||
ungroupObjectButton: document.querySelector("#ungroupObjectButton"),
|
ungroupObjectButton: document.querySelector("#ungroupObjectButton"),
|
||||||
objectOpacity: document.querySelector("#objectOpacity"),
|
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"),
|
objectStrokeFields: document.querySelector("#objectStrokeFields"),
|
||||||
objectDash: document.querySelector("#objectDash"),
|
objectDash: document.querySelector("#objectDash"),
|
||||||
objectDashRow: document.querySelector("#objectDashRow"),
|
objectDashRow: document.querySelector("#objectDashRow"),
|
||||||
@@ -590,14 +605,41 @@ editorClick(el.paragraphButton, () => {
|
|||||||
el.paragraphCancel.addEventListener("click", () => el.paragraphPanel.close());
|
el.paragraphCancel.addEventListener("click", () => el.paragraphPanel.close());
|
||||||
editorClick(el.infoButton, () => {
|
editorClick(el.infoButton, () => {
|
||||||
for (const key of ["Title", "Author", "Subject"]) el[`info${key}`].value = state.doc[key.toLowerCase()] || "";
|
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.infoStatus.textContent = "";
|
||||||
el.infoPanel.showModal();
|
el.infoPanel.showModal();
|
||||||
});
|
});
|
||||||
el.infoCancel.addEventListener("click", () => el.infoPanel.close());
|
el.infoCancel.addEventListener("click", () => el.infoPanel.close());
|
||||||
|
el.infoCustomAdd.addEventListener("click", () => addCustomDataRow());
|
||||||
el.infoForm.addEventListener("submit", async event => {
|
el.infoForm.addEventListener("submit", async event => {
|
||||||
event.preventDefault();
|
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.annotationClose.addEventListener("click", () => el.annotationNote.close());
|
||||||
el.annotationNote.addEventListener("close", () => { state.annotationEdit = null; });
|
el.annotationNote.addEventListener("close", () => { state.annotationEdit = null; });
|
||||||
el.penTool.addEventListener("change", () => {
|
el.penTool.addEventListener("change", () => {
|
||||||
@@ -947,12 +989,22 @@ el.objectBoundsForm.addEventListener("submit", async event => {
|
|||||||
if (saved) el.objectBoundsPanel.close();
|
if (saved) el.objectBoundsPanel.close();
|
||||||
});
|
});
|
||||||
el.objectStyleCancel.addEventListener("click", () => el.objectStylePanel.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.objectDash.addEventListener("change", () => {
|
||||||
el.objectDashRow.hidden = el.objectDash.value !== "custom";
|
el.objectDashRow.hidden = el.objectDash.value !== "custom";
|
||||||
el.objectDashPattern.required = !el.objectDashRow.hidden;
|
el.objectDashPattern.required = !el.objectDashRow.hidden;
|
||||||
});
|
});
|
||||||
el.objectStyleForm.addEventListener("submit", async event => {
|
el.objectStyleForm.addEventListener("submit", async event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (!el.objectGradientFields.hidden && el.gradientKind.value !== "keep" && el.gradientStops.children.length < 2) {
|
||||||
|
el.objectStyleStatus.textContent = "至少保留两个色标";
|
||||||
|
return;
|
||||||
|
}
|
||||||
const style = readObjectStyle();
|
const style = readObjectStyle();
|
||||||
const item = canvasEditor.selected;
|
const item = canvasEditor.selected;
|
||||||
if (await changeDocument("ofdgoStyleObjects", { ...item, id: canvasEditor.items().map(member => member.id) }, style)) el.objectStylePanel.close();
|
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.copyStyleButton.disabled = items.length !== 1 || !canCopyStyle(items[0]);
|
||||||
el.pasteStyleButton.disabled = !state.styleClipboard || !items.every(item => item.type === state.styleClipboard && canCopyStyle(item));
|
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 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", "");
|
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.value = alpha === null ? "" : String(Math.round((1 - alpha / 255) * 10000) / 100);
|
||||||
el.objectOpacity.placeholder = alpha === null ? "混合" : "";
|
el.objectOpacity.placeholder = alpha === null ? "混合" : "";
|
||||||
@@ -1401,13 +1468,32 @@ function openObjectStyle() {
|
|||||||
el.objectDashOffset.value = shared("dashOffset", 0) ?? "";
|
el.objectDashOffset.value = shared("dashOffset", 0) ?? "";
|
||||||
el.objectCap.value = shared("cap", "") || (shared("cap", "") === null ? "mixed" : "Butt");
|
el.objectCap.value = shared("cap", "") || (shared("cap", "") === null ? "mixed" : "Butt");
|
||||||
el.objectJoin.value = shared("join", "") || (shared("join", "") === null ? "mixed" : "Miter");
|
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.objectStyleStatus.textContent = "";
|
||||||
el.objectStylePanel.showModal();
|
el.objectStylePanel.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
function readObjectStyle() {
|
function readObjectStyle() {
|
||||||
const style = {}, changed = key => el[key].value !== state.styleOriginal[key];
|
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 (changed("objectOpacity") && el.objectOpacity.value !== "") style.alpha = Math.round((1 - Number(el.objectOpacity.value) / 100) * 255);
|
||||||
if (!el.objectStrokeFields.hidden) {
|
if (!el.objectStrokeFields.hidden) {
|
||||||
if (changed("objectDash") || changed("objectDashPattern")) {
|
if (changed("objectDash") || changed("objectDashPattern")) {
|
||||||
@@ -1421,6 +1507,30 @@ function readObjectStyle() {
|
|||||||
return style;
|
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) {
|
function openOutlinePanel(action) {
|
||||||
state.outlineAction = action;
|
state.outlineAction = action;
|
||||||
let outline = null, items = state.doc.outlines || [];
|
let outline = null, items = state.doc.outlines || [];
|
||||||
@@ -2439,7 +2549,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
}
|
}
|
||||||
setEditorInfo(doc);
|
setEditorInfo(doc);
|
||||||
if (name === "ofdgoUpdateInfo") {
|
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();
|
renderMeta();
|
||||||
updateControls();
|
updateControls();
|
||||||
rememberEditorView(before, revision);
|
rememberEditorView(before, revision);
|
||||||
|
|||||||
+105
-4
@@ -1712,8 +1712,10 @@ func editorCapabilities(capability ofdgo.ObjectCapabilities, kind string) map[st
|
|||||||
// 入参: item 前端对象, object 有效样式, paint 是否可改色, scale 描边页面倍率
|
// 入参: item 前端对象, object 有效样式, paint 是否可改色, scale 描边页面倍率
|
||||||
func editorAppearance(item map[string]any, object ofdgo.GraphicObject, paint bool, scale float64) {
|
func editorAppearance(item map[string]any, object ofdgo.GraphicObject, paint bool, scale float64) {
|
||||||
var alpha *int
|
var alpha *int
|
||||||
|
var fill *ofdgo.FillColor
|
||||||
switch object.Type {
|
switch object.Type {
|
||||||
case "TextObject":
|
case "TextObject":
|
||||||
|
fill = object.TextObject.FillColor
|
||||||
alpha = object.TextObject.Alpha
|
alpha = object.TextObject.Alpha
|
||||||
if paint {
|
if paint {
|
||||||
item["color"] = editorColorHex(object.TextObject.FillColor)
|
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
|
item["imageBorder"] = object.ImageObject.Border != nil
|
||||||
case "PathObject":
|
case "PathObject":
|
||||||
path := object.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
|
alpha = path.Alpha
|
||||||
item["dashPattern"], item["cap"], item["join"], item["dashOffset"] = path.DashPattern, path.Cap, path.Join, 0.0
|
item["dashPattern"], item["cap"], item["join"], item["dashOffset"] = path.DashPattern, path.Cap, path.Join, 0.0
|
||||||
if path.DashOffset != nil {
|
if path.DashOffset != nil {
|
||||||
@@ -1740,6 +1746,10 @@ func editorAppearance(item map[string]any, object ofdgo.GraphicObject, paint boo
|
|||||||
if alpha != nil {
|
if alpha != nil {
|
||||||
item["alpha"] = *alpha
|
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 解析画布复合对象路径
|
// compositePath 解析画布复合对象路径
|
||||||
@@ -2889,13 +2899,70 @@ func changeOutline(args []js.Value) (any, error) {
|
|||||||
return editorOutlineInfo{info.(editorInfo), path}, nil
|
return editorOutlineInfo{info.(editorInfo), path}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// styleObjects 原子更新对象透明度和路径描边属性
|
// styleObjects 原子更新对象外观和图案布局
|
||||||
// 入参: args 页面索引、对象标识数组及样式,省略字段保持原值
|
// 入参: args 页面索引、对象标识数组及样式,省略字段保持原值
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
func styleObjects(args []js.Value) (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 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 读取高级面板提交的外观字段,省略字段保持原值
|
// objectStyle 读取高级面板提交的外观字段,省略字段保持原值
|
||||||
@@ -2903,6 +2970,14 @@ func styleObjects(args []js.Value) (any, error) {
|
|||||||
// 返回: ofdgo.ObjectStyle 库层样式
|
// 返回: ofdgo.ObjectStyle 库层样式
|
||||||
func objectStyle(value js.Value) ofdgo.ObjectStyle {
|
func objectStyle(value js.Value) ofdgo.ObjectStyle {
|
||||||
style := 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() {
|
if field := value.Get("alpha"); !field.IsUndefined() {
|
||||||
v := field.Int()
|
v := field.Int()
|
||||||
style.Alpha = &v
|
style.Alpha = &v
|
||||||
@@ -3026,13 +3101,39 @@ func createDocument(args []js.Value) (any, error) {
|
|||||||
return previewEditor(editor, args[3].Bool())
|
return previewEditor(editor, args[3].Bool())
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateInfo 修改标题、作者和主题,保留其他元数据及创建程序标识
|
// updateInfo 修改文档信息和自定义字段,保留其他元数据及创建程序标识
|
||||||
// 入参: args 标题、作者、主题
|
// 入参: args 标题、作者、主题、可选自定义字段JSON
|
||||||
// 返回: any 编辑状态, error 错误信息
|
// 返回: any 编辑状态, error 错误信息
|
||||||
func updateInfo(args []js.Value) (any, error) {
|
func updateInfo(args []js.Value) (any, error) {
|
||||||
return changeObjects(func() error {
|
return changeObjects(func() error {
|
||||||
info := currentEditor.Info
|
info := currentEditor.Info
|
||||||
info.Title, info.Author, info.Subject = args[0].String(), args[1].String(), args[2].String()
|
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)
|
currentEditor.SetInfo(info)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ type DocumentInfo struct {
|
|||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Author string `json:"author"`
|
Author string `json:"author"`
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
|
CustomData []ofdgo.CustomData `json:"customData,omitempty"`
|
||||||
CreationDate string `json:"creationDate"`
|
CreationDate string `json:"creationDate"`
|
||||||
ModDate string `json:"modDate"`
|
ModDate string `json:"modDate"`
|
||||||
PageCount int `json:"pageCount"`
|
PageCount int `json:"pageCount"`
|
||||||
@@ -450,6 +451,9 @@ func (s *Session) Summary() DocumentInfo {
|
|||||||
info.Title = docInfo.Title
|
info.Title = docInfo.Title
|
||||||
info.Author = docInfo.Author
|
info.Author = docInfo.Author
|
||||||
info.Subject = docInfo.Subject
|
info.Subject = docInfo.Subject
|
||||||
|
if docInfo.CustomDatas != nil {
|
||||||
|
info.CustomData = append([]ofdgo.CustomData(nil), docInfo.CustomDatas.CustomData...)
|
||||||
|
}
|
||||||
info.CreationDate = docInfo.CreationDate
|
info.CreationDate = docInfo.CreationDate
|
||||||
info.ModDate = docInfo.ModDate
|
info.ModDate = docInfo.ModDate
|
||||||
}
|
}
|
||||||
@@ -523,6 +527,9 @@ func (s *Session) Info() DocumentInfo {
|
|||||||
info.Title = docInfo.Title
|
info.Title = docInfo.Title
|
||||||
info.Author = docInfo.Author
|
info.Author = docInfo.Author
|
||||||
info.Subject = docInfo.Subject
|
info.Subject = docInfo.Subject
|
||||||
|
if docInfo.CustomDatas != nil {
|
||||||
|
info.CustomData = append([]ofdgo.CustomData(nil), docInfo.CustomDatas.CustomData...)
|
||||||
|
}
|
||||||
info.CreationDate = docInfo.CreationDate
|
info.CreationDate = docInfo.CreationDate
|
||||||
info.ModDate = docInfo.ModDate
|
info.ModDate = docInfo.ModDate
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -783,7 +783,9 @@ func (e *Editor) prepareObject(id string, object GraphicObject) (GraphicObject,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if obj.Border != nil {
|
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
|
obj.ID = id
|
||||||
boundary, ctm = obj.Boundary, obj.CTM
|
boundary, ctm = obj.Boundary, obj.CTM
|
||||||
@@ -802,8 +804,13 @@ func (e *Editor) prepareObject(id string, object GraphicObject) (GraphicObject,
|
|||||||
} else if object.Type == "ImageObject" {
|
} else if object.Type == "ImageObject" {
|
||||||
object.ImageObject.CTM = fmt.Sprintf("%s 0 0 %s 0 0", ofdNumber(box.W), ofdNumber(box.H))
|
object.ImageObject.CTM = fmt.Sprintf("%s 0 0 %s 0 0", ofdNumber(box.W), ofdNumber(box.H))
|
||||||
}
|
}
|
||||||
if drawParam != "" || len(actions) != 0 {
|
if drawParam != "" {
|
||||||
return GraphicObject{}, fmt.Errorf("draw parameter references and actions are not supported for creation")
|
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 {
|
if err := e.validateObjectClips(clips); err != nil {
|
||||||
return GraphicObject{}, err
|
return GraphicObject{}, err
|
||||||
|
|||||||
+194
-3
@@ -23,6 +23,70 @@ import (
|
|||||||
"strings"
|
"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及调色板,不修改原颜色
|
// Color 解析当前文档的纯色,支持RGB、灰度、CMYK及调色板,不修改原颜色
|
||||||
// 入参: value 颜色,nil表示默认黑色
|
// 入参: value 颜色,nil表示默认黑色
|
||||||
// 返回: color.NRGBA 非预乘颜色, error 错误信息
|
// 返回: color.NRGBA 非预乘颜色, error 错误信息
|
||||||
@@ -141,9 +205,42 @@ func (e *Editor) RGBColor(value color.NRGBA) (*FillColor, error) {
|
|||||||
return result, nil
|
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 判断文字与路径颜色能否在保留其他字段的前提下修改
|
// editorPaintable 判断文字与路径颜色能否在保留其他字段的前提下修改
|
||||||
// 入参: object 图形对象
|
// 入参: object 图形对象
|
||||||
// 返回: bool 是否支持纯色编辑
|
// 返回: bool 是否支持绘制颜色编辑
|
||||||
func (e *Editor) editorPaintable(object GraphicObject) bool {
|
func (e *Editor) editorPaintable(object GraphicObject) bool {
|
||||||
var fill, stroke *FillColor
|
var fill, stroke *FillColor
|
||||||
switch object.Type {
|
switch object.Type {
|
||||||
@@ -155,17 +252,111 @@ func (e *Editor) editorPaintable(object GraphicObject) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, value := range []*FillColor{fill, stroke} {
|
for _, value := range []*FillColor{fill, stroke} {
|
||||||
if _, err := e.Color(value); err != nil {
|
if err := e.editorColor(value); err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
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 颜色
|
// 入参: value 颜色
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) editorColor(value *FillColor) error {
|
func (e *Editor) editorColor(value *FillColor) error {
|
||||||
|
if value == nil || value.AxialShd == nil && value.RadialShd == nil {
|
||||||
_, err := e.Color(value)
|
_, err := e.Color(value)
|
||||||
return err
|
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.ResourceID] = true
|
||||||
used[composite.DrawParam] = true
|
used[composite.DrawParam] = true
|
||||||
for _, object := range composite.Objects {
|
for _, object := range composite.Objects {
|
||||||
var fill, stroke *FillColor
|
collectObjectReferences(object, used)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,8 +93,10 @@ func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []Gra
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if node.object.Type == "PathObject" {
|
if node.object.Type == "PathObject" {
|
||||||
_, styleErr := e.resolveEditorStyle(node.object, e.copiedLayerStyle(source))
|
style, styleErr := e.resolveEditorStyle(node.object, e.copiedLayerStyle(source))
|
||||||
if styleErr != nil && editReason(styleErr) == EditUnsupportedColor || !e.editorPaintable(node.object) {
|
_, 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)
|
node, err = e.wrapCompositePath(node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)})
|
return e.addEditorDrawParam(DrawParam{LineWidth: defaultPathLineWidth, Cap: "Butt", Join: "Miter", MiterLimit: defaultMiterLimit, DashOffset: &zero, dashPatternSet: true, FillColor: fill, StrokeColor: (*StrokeColor)(fill)})
|
||||||
}
|
}
|
||||||
|
|
||||||
// addEditorDrawParam 注册完整有效绘制参数,相同外观复用资源
|
// addEditorDrawParam 注册已校验的绘制参数,相同定义复用资源
|
||||||
// 入参: draw 不含继承关系的有效参数
|
// 入参: draw 绘制参数
|
||||||
// 返回: string 资源标识, error 错误信息
|
// 返回: string 资源标识, error 错误信息
|
||||||
func (e *Editor) addEditorDrawParam(draw DrawParam) (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 {
|
for _, resource := range e.resources {
|
||||||
if resource.draw != nil {
|
if resource.draw != nil {
|
||||||
previous := *resource.draw
|
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.root("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}})
|
||||||
x.start("DrawParams", nil)
|
x.start("DrawParams", nil)
|
||||||
attrs := ofdAttrs{}
|
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.add(pair[0], pair[1])
|
||||||
}
|
}
|
||||||
|
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})
|
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "DashPattern"}, Value: draw.DashPattern})
|
||||||
|
}
|
||||||
if draw.DashOffset != nil {
|
if draw.DashOffset != nil {
|
||||||
attrs.add("DashOffset", ofdNumber(*draw.DashOffset))
|
attrs.add("DashOffset", ofdNumber(*draw.DashOffset))
|
||||||
}
|
}
|
||||||
|
|||||||
+82
-3
@@ -27,6 +27,88 @@ type AnnotationLink struct {
|
|||||||
Dest *Dest
|
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 读取注解中唯一的点击跳转,书签目标保持原始动作
|
// AnnotationLink 读取注解中唯一的点击跳转,书签目标保持原始动作
|
||||||
// 入参: page 页面索引, id 注解标识
|
// 入参: page 页面索引, id 注解标识
|
||||||
// 返回: Action 跳转动作, error 错误信息
|
// 返回: Action 跳转动作, error 错误信息
|
||||||
@@ -201,9 +283,6 @@ func (e *Editor) UpdateAnnotationLink(page int, id string, target AnnotationLink
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
position := objects[0].open
|
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))}})
|
data = editorPatchXML(data, []editorXMLPatch{{position, position, bytes.TrimPrefix(actions, []byte(xml.Header))}})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+79
-16
@@ -18,6 +18,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ func (e *Editor) sourceInfoXML() ([]byte, error) {
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorCustomDatas 更新自定义字段,保留容器扩展与同名字段的未知属性和子节点
|
// editorCustomDatas 更新自定义字段,保留原字段及容器的扩展内容
|
||||||
// 入参: data 原XML, info 文档信息节点, values 新字段
|
// 入参: data 原XML, info 文档信息节点, values 新字段
|
||||||
// 返回: []byte 修改后的XML, error 编码错误
|
// 返回: []byte 修改后的XML, error 编码错误
|
||||||
func editorCustomDatas(data []byte, info *editorXML, values *CustomDatas) ([]byte, 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
|
var encoded [][]byte
|
||||||
|
used := make(map[*editorXML]bool)
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
matches := byName[item.Name]
|
if item.sourceIndex > 0 && item.sourceIndex <= len(nodes) {
|
||||||
if len(matches) == 0 {
|
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 {
|
entry, err := xml.Marshal(struct {
|
||||||
XMLName xml.Name
|
XMLName xml.Name
|
||||||
CustomData
|
CustomData
|
||||||
@@ -95,26 +113,30 @@ func editorCustomDatas(data []byte, info *editorXML, values *CustomDatas) ([]byt
|
|||||||
encoded = append(encoded, entry)
|
encoded = append(encoded, entry)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
node := matches[0]
|
|
||||||
byName[item.Name] = matches[1:]
|
|
||||||
var original CustomData
|
var original CustomData
|
||||||
if err := xml.Unmarshal(data[node.start:node.end], &original); err != nil {
|
if err := xml.Unmarshal(data[node.start:node.end], &original); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if original.Value == item.Value {
|
if original.Name != item.Name {
|
||||||
encoded = append(encoded, data[node.start:node.end])
|
entry, err := editorXMLAttribute(data, node, "Name", item.Name)
|
||||||
continue
|
if err != nil {
|
||||||
}
|
|
||||||
var content bytes.Buffer
|
|
||||||
if err := xml.EscapeText(&content, []byte(item.Value)); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, child := range node.children {
|
renamed, err := parseEditorXML(entry)
|
||||||
content.Write(data[child.start:child.end])
|
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
|
||||||
|
}
|
||||||
|
entry, err := editorCustomDataValue(data, node, original.Value, item.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
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)
|
encoded = append(encoded, entry)
|
||||||
}
|
}
|
||||||
if container == nil {
|
if container == nil {
|
||||||
@@ -147,3 +169,44 @@ func editorCustomDatas(data []byte, info *editorXML, values *CustomDatas) ([]byt
|
|||||||
}
|
}
|
||||||
return editorPatchXML(data, patches), nil
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if info.CustomDatas != nil {
|
||||||
|
for i := range info.CustomDatas.CustomData {
|
||||||
|
info.CustomDatas.CustomData[i].sourceIndex = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
e := NewEditor()
|
e := NewEditor()
|
||||||
e.encryption = r.encryption
|
e.encryption = r.encryption
|
||||||
e.Info, e.maxID = cloneEditorData(*info), maximum
|
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)
|
all.Copy = editorXMLCopyable(node) && (origin.reason == nil || object.Type == "PathObject" && editReason(origin.reason) == EditUnsupportedColor)
|
||||||
if origin.reason != nil {
|
if origin.reason != nil {
|
||||||
all.Update, all.Reflow, all.ReplaceFont, all.Paint = false, false, false, false
|
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)
|
all.Reason, all.ReasonCode = origin.reason.Error(), editReason(origin.reason)
|
||||||
} else if !editorXMLSupported(node) {
|
} else if !editorXMLSupported(node) {
|
||||||
all.Update, all.Reflow, all.ReplaceFont = false, false, false
|
all.Update, all.Reflow, all.ReplaceFont = false, false, false
|
||||||
@@ -632,6 +638,14 @@ func editorPreservedObject(before, after GraphicObject) bool {
|
|||||||
// 返回: GraphicObject 独立副本, error 错误信息
|
// 返回: GraphicObject 独立副本, error 错误信息
|
||||||
func (e *Editor) prepareCopiedObject(id string, object GraphicObject) (GraphicObject, error) {
|
func (e *Editor) prepareCopiedObject(id string, object GraphicObject) (GraphicObject, error) {
|
||||||
origin := e.snapshotOrigin(object)
|
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
|
object.origin = nil
|
||||||
if origin == nil {
|
if origin == nil {
|
||||||
return e.prepareObject(id, object)
|
return e.prepareObject(id, object)
|
||||||
|
|||||||
+74
-1
@@ -21,6 +21,37 @@ import (
|
|||||||
"strings"
|
"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 解析原文档的绘制参数继承链,缺失或循环引用不进入编辑快照
|
// editorDrawParam 解析原文档的绘制参数继承链,缺失或循环引用不进入编辑快照
|
||||||
// 入参: id 绘制参数标识, visited 已访问标识
|
// 入参: id 绘制参数标识, visited 已访问标识
|
||||||
// 返回: *DrawParam 合并参数, error 错误信息
|
// 返回: *DrawParam 合并参数, error 错误信息
|
||||||
@@ -121,7 +152,7 @@ func (e *Editor) resolveEditorStyleDefaults(object GraphicObject, base *DrawPara
|
|||||||
obj.FillColor, obj.StrokeColor = style.FillColor, style.StrokeColor
|
obj.FillColor, obj.StrokeColor = style.FillColor, style.StrokeColor
|
||||||
}
|
}
|
||||||
for _, color := range []*FillColor{style.FillColor, (*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)}
|
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
|
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 校验路径描边尺寸、端点和虚线
|
// validateEditorStroke 校验路径描边尺寸、端点和虚线
|
||||||
// 入参: path 路径对象
|
// 入参: path 路径对象
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
|
|||||||
+48
-9
@@ -18,6 +18,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
@@ -112,13 +113,22 @@ func (e *Editor) UpdateTextContent(page int, id, value string, style TextStyle)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !capability.ReplaceFont {
|
if !capability.ReplaceFont && (style != (TextStyle{}) || !capability.Transform) {
|
||||||
return capability.editError()
|
return capability.editError()
|
||||||
}
|
}
|
||||||
object, err := e.Object(page, id)
|
object, err := e.Object(page, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
updates, err := e.styleTextObjects([]GraphicObject{object}, style)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -136,20 +146,18 @@ func (e *Editor) UpdateTextContent(page int, id, value string, style TextStyle)
|
|||||||
return e.updateObjects(page, updates, true)
|
return e.updateObjects(page, updates, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RewriteText 保留普通横向原文的字形定位,不修改对象样式或文档
|
// RewriteText 保留原文的字形定位,不修改对象样式或文档
|
||||||
// 等长替换保留各字原位;单段增删保留前缀和后缀步进,新增字符沿用局部字距
|
// 等长替换保留各字原位;单段增删保留前缀和后缀步进,新增字符沿用局部字距
|
||||||
// 不跨定位段增删或自动生成新行,复杂字形映射需单独处理
|
// 方向、缩放和修饰原样保留;复杂布局仅支持等长替换,合并字形需显式重排
|
||||||
// 入参: obj 原文字对象, value 新内容
|
// 入参: obj 原文字对象, value 新内容
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) RewriteText(obj *TextObject, value string) 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")
|
value = strings.ReplaceAll(strings.ReplaceAll(value, "\r\n", "\n"), "\r", "\n")
|
||||||
if !utf8.ValidString(value) || strings.Contains(value, "\t") || strings.Trim(value, "\n") == "" {
|
if !utf8.ValidString(value) || strings.Contains(value, "\t") || strings.Trim(value, "\n") == "" {
|
||||||
return fmt.Errorf("text must contain UTF-8 characters without tabs")
|
return fmt.Errorf("text must contain UTF-8 characters without tabs")
|
||||||
}
|
}
|
||||||
check := cloneEditorData(*obj)
|
check := cloneEditorData(*obj)
|
||||||
|
check.VScale, check.Decoration = 0, ""
|
||||||
if err := e.prepareText(&check); err != nil {
|
if err := e.prepareText(&check); err != nil {
|
||||||
var missing *MissingGlyphError
|
var missing *MissingGlyphError
|
||||||
if !errors.As(err, &missing) {
|
if !errors.As(err, &missing) {
|
||||||
@@ -160,8 +168,16 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
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 {
|
if char != '\n' && sfnt.GlyphIndex(char) == 0 {
|
||||||
missing = append(missing, char)
|
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 {
|
if err := missingGlyphError(obj.Font, missing); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
old, next := []rune(obj.Text()), []rune(value)
|
|
||||||
codes := append([]TextCode(nil), obj.TextCode...)
|
codes := append([]TextCode(nil), obj.TextCode...)
|
||||||
|
transforms := append([]CGTransform(nil), obj.CGTransform...)
|
||||||
if len(old) == len(next) {
|
if len(old) == len(next) {
|
||||||
offset := 0
|
offset := 0
|
||||||
for i := range codes {
|
for i := range codes {
|
||||||
@@ -240,10 +256,33 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
|
|||||||
}
|
}
|
||||||
check = *obj
|
check = *obj
|
||||||
check.TextCode = codes
|
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 {
|
if err := validateEditorGeometry(GraphicObject{Type: "TextObject", TextObject: check}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
obj.TextCode, obj.layout = codes, nil
|
obj.TextCode, obj.CGTransform, obj.layout = codes, transforms, nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,11 +49,13 @@ type FontShaper interface {
|
|||||||
ShapeText(value string, size float64) ([]ShapedGlyph, error)
|
ShapeText(value string, size float64) ([]ShapedGlyph, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextShapeOptions 指定单一方向文字的塑形参数,不执行双向段落分析或字体回退
|
// TextShapeOptions 指定文字塑形参数,Bidi启用双向及脚本分段,不执行字体回退
|
||||||
// Direction为空或ltr时从左向右,rtl时从右向左;Script为四字母ISO15924代码,空值自动检测
|
// Direction为空或ltr时从左向右,rtl时从右向左;Script为四字母ISO15924代码,空值自动检测
|
||||||
|
// Bidi启用时Direction指定段落方向,各文字片段按双向算法排列
|
||||||
// Language为语言标签,Features为逗号分隔的全局OpenType特性,如kern=0,liga=1,不接受字符区间
|
// Language为语言标签,Features为逗号分隔的全局OpenType特性,如kern=0,liga=1,不接受字符区间
|
||||||
// 零值保持原有FontShaper行为,非零值要求FontShaperOptions能力
|
// 零值保持原有FontShaper行为,非零值要求FontShaperOptions能力
|
||||||
type TextShapeOptions struct {
|
type TextShapeOptions struct {
|
||||||
|
Bidi bool
|
||||||
Direction string
|
Direction string
|
||||||
Script string
|
Script string
|
||||||
Language string
|
Language string
|
||||||
@@ -68,9 +70,11 @@ type FontShaperOptions interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ShapedGlyph 保存塑形后的字形、原文簇和基线定位
|
// ShapedGlyph 保存塑形后的字形、原文簇和基线定位
|
||||||
|
// Bidi模式下VisualOrder为簇从左到右的零起始顺序,同簇字形共用顺序
|
||||||
type ShapedGlyph struct {
|
type ShapedGlyph struct {
|
||||||
Glyph uint16
|
Glyph uint16
|
||||||
Cluster int
|
Cluster int
|
||||||
|
VisualOrder int
|
||||||
X, Y, Advance float64
|
X, Y, Advance float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+82
-2
@@ -18,6 +18,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"cmp"
|
"cmp"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -25,9 +26,12 @@ import (
|
|||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/go-text/typesetting/di"
|
||||||
typefont "github.com/go-text/typesetting/font"
|
typefont "github.com/go-text/typesetting/font"
|
||||||
"github.com/go-text/typesetting/harfbuzz"
|
"github.com/go-text/typesetting/harfbuzz"
|
||||||
"github.com/go-text/typesetting/language"
|
"github.com/go-text/typesetting/language"
|
||||||
|
"github.com/go-text/typesetting/shaping"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
xlanguage "golang.org/x/text/language"
|
xlanguage "golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,6 +40,7 @@ type fontShaper struct {
|
|||||||
metrics FontMetrics
|
metrics FontMetrics
|
||||||
shapeMu sync.Mutex
|
shapeMu sync.Mutex
|
||||||
shapeFont *harfbuzz.Font
|
shapeFont *harfbuzz.Font
|
||||||
|
shapeFace *typefont.Face
|
||||||
shapeBuffer *harfbuzz.Buffer
|
shapeBuffer *harfbuzz.Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,8 +51,7 @@ func (f *fontShaper) ShapeText(value string, size float64) ([]ShapedGlyph, error
|
|||||||
return f.ShapeTextWithOptions(value, size, TextShapeOptions{})
|
return f.ShapeTextWithOptions(value, size, TextShapeOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShapeTextWithOptions 使用现有纯Go塑形引擎处理单一方向原文,保留逻辑簇和视觉位置
|
// ShapeTextWithOptions 使用纯Go塑形引擎保留逻辑簇和视觉位置,可显式启用双向分段
|
||||||
// 不执行混合方向分段,调用方应将双向段落拆为单向文字对象
|
|
||||||
// 入参: value 单行原文, size 毫米字号, options 塑形选项
|
// 入参: value 单行原文, size 毫米字号, options 塑形选项
|
||||||
// 返回: []ShapedGlyph 定位字形, error 字符或选项错误
|
// 返回: []ShapedGlyph 定位字形, error 字符或选项错误
|
||||||
func (f *fontShaper) ShapeTextWithOptions(value string, size float64, options TextShapeOptions) ([]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)
|
return nil, fmt.Errorf("shaping font: %w", err)
|
||||||
}
|
}
|
||||||
f.shapeFont = harfbuzz.NewFont(face)
|
f.shapeFont = harfbuzz.NewFont(face)
|
||||||
|
f.shapeFace = face
|
||||||
f.shapeBuffer = harfbuzz.NewBuffer()
|
f.shapeBuffer = harfbuzz.NewBuffer()
|
||||||
}
|
}
|
||||||
|
if options.Bidi {
|
||||||
|
return f.shapeBidiText([]rune(value), size, props, features)
|
||||||
|
}
|
||||||
buffer := f.shapeBuffer
|
buffer := f.shapeBuffer
|
||||||
buffer.Clear()
|
buffer.Clear()
|
||||||
buffer.Props = props
|
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) })
|
slices.SortStableFunc(result, func(a, b ShapedGlyph) int { return cmp.Compare(a.Cluster, b.Cluster) })
|
||||||
return result, nil
|
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
|
||||||
|
}
|
||||||
@@ -54,4 +54,5 @@ type CustomDatas struct {
|
|||||||
type CustomData struct {
|
type CustomData struct {
|
||||||
Name string `xml:"Name,attr"`
|
Name string `xml:"Name,attr"`
|
||||||
Value string `xml:",chardata"`
|
Value string `xml:",chardata"`
|
||||||
|
sourceIndex int
|
||||||
}
|
}
|
||||||
+15
-7
@@ -224,6 +224,19 @@ func (r *Renderer) parseShdColor(segments []ShdSegment, alpha *int) color.Color
|
|||||||
// 返回: []ColorStop 后端无关的渐变分段
|
// 返回: []ColorStop 后端无关的渐变分段
|
||||||
func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []ColorStop {
|
func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []ColorStop {
|
||||||
var gradient []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
|
position, step := 0.0, 0.0
|
||||||
for i, segment := range segments {
|
for i, segment := range segments {
|
||||||
if !segment.positionMissing {
|
if !segment.positionMissing {
|
||||||
@@ -242,15 +255,10 @@ func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []ColorStop
|
|||||||
}
|
}
|
||||||
step = (end - position) / float64(next-i)
|
step = (end - position) / float64(next-i)
|
||||||
}
|
}
|
||||||
offset := position
|
positions[i] = position
|
||||||
position += step
|
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 positions
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return gradient
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// colorToRGBA 转换颜色对象
|
// 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))}
|
code := TextCode{X: ofdNumber(x), Y: ofdNumber(y), Value: escapeOFDText(string(chars))}
|
||||||
var dx, dy []string
|
var dx, dy []string
|
||||||
var lastX, lastY float64
|
var lastX, lastY float64
|
||||||
|
var visualShifts map[int]float64
|
||||||
|
if options.Shaping.Bidi {
|
||||||
|
visualShifts = bidiClusterShifts(glyphs, extra)
|
||||||
|
}
|
||||||
for j, glyph := range glyphs {
|
for j, glyph := range glyphs {
|
||||||
shift := extra[glyph.Cluster]
|
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
|
shift = extra[len(chars)] - extra[glyph.Cluster] - options.LetterSpacing
|
||||||
}
|
}
|
||||||
@@ -162,6 +168,37 @@ func layoutShapedText(obj *TextObject, value string, options TextLayout, metrics
|
|||||||
return nil
|
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 使用固定选项塑形单行原文
|
// ShapeText 使用固定选项塑形单行原文
|
||||||
// 入参: value 原文, size 毫米字号
|
// 入参: value 原文, size 毫米字号
|
||||||
// 返回: []ShapedGlyph 字形, error 塑形错误
|
// 返回: []ShapedGlyph 字形, error 塑形错误
|
||||||
|
|||||||
+177
-35
@@ -375,45 +375,15 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
|
|||||||
for _, layer := range page.Content.Layer {
|
for _, layer := range page.Content.Layer {
|
||||||
for _, object := range layer.Objects {
|
for _, object := range layer.Objects {
|
||||||
before, exists := original[editorObjectID(object)]
|
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)) {
|
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 {
|
for _, name := range origin.page.original.PageRes {
|
||||||
files[e.source.reader.ResPath(resolveResourcePath(origin.page.ref.BaseLoc, "", name))] = true
|
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) {
|
func (x *ofdXML) object(object GraphicObject, root bool) {
|
||||||
var attrs ofdAttrs
|
var attrs ofdAttrs
|
||||||
var fill, stroke *FillColor
|
var fill, stroke *FillColor
|
||||||
|
var actions []Action
|
||||||
if root {
|
if root {
|
||||||
attrs.add("xmlns:ofd", ofdNamespace)
|
attrs.add("xmlns:ofd", ofdNamespace)
|
||||||
}
|
}
|
||||||
@@ -676,6 +647,8 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
|
|||||||
attrs.add("Font", obj.Font)
|
attrs.add("Font", obj.Font)
|
||||||
attrs.number("Size", obj.Size)
|
attrs.number("Size", obj.Size)
|
||||||
attrs.number("HScale", obj.HScale)
|
attrs.number("HScale", obj.HScale)
|
||||||
|
attrs.number("VScale", obj.VScale)
|
||||||
|
attrs.add("Decoration", obj.Decoration)
|
||||||
attrs.number("Weight", float64(obj.Weight))
|
attrs.number("Weight", float64(obj.Weight))
|
||||||
attrs.number("ReadDirection", float64(obj.ReadDirection))
|
attrs.number("ReadDirection", float64(obj.ReadDirection))
|
||||||
attrs.number("CharDirection", float64(obj.CharDirection))
|
attrs.number("CharDirection", float64(obj.CharDirection))
|
||||||
@@ -690,6 +663,7 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
|
|||||||
attrs.flag("Fill", obj.Fill)
|
attrs.flag("Fill", obj.Fill)
|
||||||
attrs.alpha(obj.Alpha)
|
attrs.alpha(obj.Alpha)
|
||||||
fill, stroke = obj.FillColor, (*FillColor)(obj.StrokeColor)
|
fill, stroke = obj.FillColor, (*FillColor)(obj.StrokeColor)
|
||||||
|
actions = obj.Actions
|
||||||
case "PathObject", "Path":
|
case "PathObject", "Path":
|
||||||
obj := object.PathObject
|
obj := object.PathObject
|
||||||
attrs.add("ID", obj.ID)
|
attrs.add("ID", obj.ID)
|
||||||
@@ -714,6 +688,7 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
|
|||||||
attrs.flag("Fill", obj.Fill)
|
attrs.flag("Fill", obj.Fill)
|
||||||
attrs.alpha(obj.Alpha)
|
attrs.alpha(obj.Alpha)
|
||||||
fill, stroke = obj.FillColor, (*FillColor)(obj.StrokeColor)
|
fill, stroke = obj.FillColor, (*FillColor)(obj.StrokeColor)
|
||||||
|
actions = obj.Actions
|
||||||
case "ImageObject":
|
case "ImageObject":
|
||||||
obj := object.ImageObject
|
obj := object.ImageObject
|
||||||
attrs.add("ID", obj.ID)
|
attrs.add("ID", obj.ID)
|
||||||
@@ -723,6 +698,7 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
|
|||||||
attrs.add("ImageMask", obj.ImageMask)
|
attrs.add("ImageMask", obj.ImageMask)
|
||||||
attrs.flag("Visible", obj.Visible)
|
attrs.flag("Visible", obj.Visible)
|
||||||
attrs.alpha(obj.Alpha)
|
attrs.alpha(obj.Alpha)
|
||||||
|
actions = obj.Actions
|
||||||
case "CompositeObject", "CompositeGraphicUnit":
|
case "CompositeObject", "CompositeGraphicUnit":
|
||||||
obj := object.CompositeGraphicUnit
|
obj := object.CompositeGraphicUnit
|
||||||
attrs.add("ID", obj.ID)
|
attrs.add("ID", obj.ID)
|
||||||
@@ -732,10 +708,13 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
|
|||||||
attrs.add("DrawParam", obj.DrawParam)
|
attrs.add("DrawParam", obj.DrawParam)
|
||||||
attrs.flag("Visible", obj.Visible)
|
attrs.flag("Visible", obj.Visible)
|
||||||
attrs.alpha(obj.Alpha)
|
attrs.alpha(obj.Alpha)
|
||||||
|
actions = obj.Actions
|
||||||
}
|
}
|
||||||
x.start(object.Type, attrs)
|
x.start(object.Type, attrs)
|
||||||
|
x.actions(actions)
|
||||||
if object.Type == "ImageObject" {
|
if object.Type == "ImageObject" {
|
||||||
x.clips(object.ImageObject.Clips)
|
x.clips(object.ImageObject.Clips)
|
||||||
|
x.border(object.ImageObject.Border)
|
||||||
} else if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
|
} else if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
|
||||||
x.clips(object.CompositeGraphicUnit.Clips)
|
x.clips(object.CompositeGraphicUnit.Clips)
|
||||||
} else if object.Type == "PathObject" || object.Type == "Path" {
|
} 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})
|
x.err = x.encoder.EncodeElement(content, xml.StartElement{Name: xml.Name{Local: "ofd:TextCode"}, Attr: attrs})
|
||||||
}
|
}
|
||||||
|
|
||||||
// color 写出基本颜色
|
// color 写出纯色、渐变和图案,保留颜色空间与透明度
|
||||||
// 入参: name 节点名, color 颜色
|
// 入参: name 节点名, color 颜色
|
||||||
func (x *ofdXML) color(name string, color *FillColor) {
|
func (x *ofdXML) color(name string, color *FillColor) {
|
||||||
if color == nil {
|
if color == nil {
|
||||||
@@ -830,5 +809,168 @@ func (x *ofdXML) color(name string, color *FillColor) {
|
|||||||
}
|
}
|
||||||
attrs.alpha(color.Alpha)
|
attrs.alpha(color.Alpha)
|
||||||
x.start(name, attrs)
|
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)
|
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