mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
This commit is contained in:
1 parent
d7cd3d03b5
commit
06b5df9173
13 files changed
+434
-128
No files matched your search
@@ -184,6 +184,18 @@
|
||||
<div class="form-row"><span>组合</span><div class="form-format"><button id="groupObjectsButton" class="button" type="button">组合</button><button id="ungroupObjectButton" class="button" type="button">解组</button></div></div>
|
||||
<div class="form-row"><span>格式</span><div class="form-format"><button id="copyStyleButton" class="button" type="button" aria-label="复制格式">复制</button><button id="pasteStyleButton" class="button" type="button" aria-label="粘贴格式">粘贴</button></div></div>
|
||||
<div class="form-row"><label for="objectOpacity">透明</label><div class="form-unit"><input id="objectOpacity" type="number" min="0" max="100" step="any" aria-label="透明度" autofocus><span>%</span></div></div>
|
||||
<details id="objectBorderFields" hidden>
|
||||
<summary>边框</summary>
|
||||
<div class="form-row"><label for="borderEnabled">显示</label><input id="borderEnabled" type="checkbox"></div>
|
||||
<div class="form-pair">
|
||||
<div class="form-row"><label for="borderWidth">线宽</label><div class="form-unit"><input id="borderWidth" type="number" min="0" step="any"><span>mm</span></div></div>
|
||||
<div class="form-row"><label for="borderColor">颜色</label><input id="borderColor" class="color-input" type="color"><span id="borderColorOriginal" hidden>原样</span></div>
|
||||
</div>
|
||||
<div class="form-pair">
|
||||
<div class="form-row"><label for="borderHorizontal">横径</label><div class="form-unit"><input id="borderHorizontal" type="number" min="0" step="any" aria-label="水平圆角半径"><span>mm</span></div></div>
|
||||
<div class="form-row"><label for="borderVertical">纵径</label><div class="form-unit"><input id="borderVertical" type="number" min="0" step="any" aria-label="垂直圆角半径"><span>mm</span></div></div>
|
||||
</div>
|
||||
</details>
|
||||
<details id="objectGradientFields" hidden>
|
||||
<summary>渐变</summary>
|
||||
<div class="form-pair">
|
||||
@@ -199,6 +211,7 @@
|
||||
</details>
|
||||
<details id="objectPatternFields" hidden>
|
||||
<summary>图案</summary>
|
||||
<div class="form-row"><label for="patternTarget">作用</label><select id="patternTarget"><option value="fill">填充</option><option value="stroke">描边</option></select></div>
|
||||
<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>
|
||||
|
||||
+137
-25
@@ -290,6 +290,14 @@ const el = {
|
||||
objectOpacity: document.querySelector("#objectOpacity"),
|
||||
objectGradientFields: document.querySelector("#objectGradientFields"),
|
||||
gradientTarget: document.querySelector("#gradientTarget"),
|
||||
objectBorderFields: document.querySelector("#objectBorderFields"),
|
||||
borderEnabled: document.querySelector("#borderEnabled"),
|
||||
borderWidth: document.querySelector("#borderWidth"),
|
||||
borderColor: document.querySelector("#borderColor"),
|
||||
borderColorOriginal: document.querySelector("#borderColorOriginal"),
|
||||
borderHorizontal: document.querySelector("#borderHorizontal"),
|
||||
borderVertical: document.querySelector("#borderVertical"),
|
||||
patternTarget: document.querySelector("#patternTarget"),
|
||||
gradientKind: document.querySelector("#gradientKind"),
|
||||
gradientControls: document.querySelector("#gradientControls"),
|
||||
gradientAngle: document.querySelector("#gradientAngle"),
|
||||
@@ -994,6 +1002,14 @@ el.gradientKind.addEventListener("change", () => {
|
||||
el.gradientAngle.disabled = el.gradientKind.value !== "linear";
|
||||
for (const input of el.gradientStops.querySelectorAll("input")) input.disabled = el.gradientKind.value === "keep";
|
||||
});
|
||||
el.gradientTarget.addEventListener("change", () => {
|
||||
state.gradientForms[state.gradientTarget].form = gradientForm();
|
||||
loadGradientForm(el.gradientTarget.value);
|
||||
});
|
||||
el.patternTarget.addEventListener("change", () => {
|
||||
state.patternForms[state.patternTarget] = patternForm();
|
||||
loadPatternForm(el.patternTarget.value);
|
||||
});
|
||||
el.gradientAdd.addEventListener("click", () => addGradientStop(50, "#808080", 0));
|
||||
el.objectDash.addEventListener("change", () => {
|
||||
el.objectDashRow.hidden = el.objectDash.value !== "custom";
|
||||
@@ -1442,21 +1458,47 @@ function openObjectStyle() {
|
||||
el.copyStyleButton.disabled = items.length !== 1 || !canCopyStyle(items[0]);
|
||||
el.pasteStyleButton.disabled = !state.styleClipboard || !items.every(item => item.type === state.styleClipboard && canCopyStyle(item));
|
||||
const shared = (key, fallback) => items.every(item => (item[key] ?? fallback) === (items[0][key] ?? fallback)) ? items[0][key] ?? fallback : null;
|
||||
const pattern = items.length === 1 && !items[0].scoped ? items[0].fillPattern : null;
|
||||
el.objectBorderFields.hidden = items.length !== 1 || items[0].scoped || items[0].type !== "ImageObject";
|
||||
const border = items[0].borderStyle ? JSON.parse(items[0].borderStyle) : null;
|
||||
el.borderEnabled.checked = Boolean(border);
|
||||
el.borderWidth.value = String(border?.LineWidth ?? .353);
|
||||
el.borderHorizontal.value = String(border?.HorizonalCornerRadius ?? 0);
|
||||
el.borderVertical.value = String(border?.VerticalCornerRadius ?? 0);
|
||||
el.borderColor.value = items[0].borderColor || "#000000";
|
||||
el.borderColor.disabled = Boolean(border && !items[0].borderColor);
|
||||
el.borderColor.hidden = el.borderColor.disabled;
|
||||
el.borderColorOriginal.hidden = !el.borderColor.disabled;
|
||||
state.borderOriginal = {
|
||||
Enabled: el.borderEnabled.checked, LineWidth: el.borderWidth.value,
|
||||
HorizontalRadius: el.borderHorizontal.value, VerticalRadius: el.borderVertical.value,
|
||||
Color: el.borderColor.value
|
||||
};
|
||||
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;
|
||||
state.gradientForms = {};
|
||||
for (const target of ["fill", "stroke"]) {
|
||||
const gradient = items[0][`${target}Gradient`], base = gradient ? JSON.parse(gradient.paint) : null;
|
||||
const node = base?.AxialShd || base?.RadialShd;
|
||||
const start = node?.StartPoint.split(/\s+/).map(Number), end = node?.EndPoint.split(/\s+/).map(Number);
|
||||
const form = {
|
||||
kind: base?.AxialShd ? "linear" : base?.RadialShd ? "radial" : "keep",
|
||||
angle: String(base?.AxialShd ? Math.atan2(end[1] - start[1], end[0] - start[0]) * 180 / Math.PI : 0),
|
||||
stops: gradient ? gradient.stops.map((stop, i) => ({ position: String(stop.position), color: stop.color, transparency: String(stop.transparency), source: node.Segment[i] }))
|
||||
: [{ position: "0", color: "#000000", transparency: "0" }, { position: "100", color: "#ffffff", transparency: "0" }]
|
||||
};
|
||||
state.gradientForms[target] = { base, form, original: JSON.stringify(form) };
|
||||
}
|
||||
el.gradientTarget.value = "fill";
|
||||
loadGradientForm("fill");
|
||||
state.patternForms = {};
|
||||
for (const target of ["fill", "stroke"]) {
|
||||
const pattern = items.length === 1 && !items[0].scoped ? items[0][`${target}Pattern`] : null;
|
||||
if (pattern) state.patternForms[target] = Object.fromEntries(Object.entries(pattern).map(([key, value]) => [key, String(value)]));
|
||||
}
|
||||
state.patternOriginal = JSON.stringify(state.patternForms);
|
||||
el.objectPatternFields.hidden = !Object.keys(state.patternForms).length;
|
||||
el.patternTarget.disabled = Object.keys(state.patternForms).length < 2;
|
||||
el.patternTarget.value = state.patternForms.fill ? "fill" : "stroke";
|
||||
loadPatternForm(el.patternTarget.value);
|
||||
const alpha = items.some(item => item.alphaMixed) ? null : shared("alpha", 255), dash = shared("dashPattern", "");
|
||||
el.objectOpacity.value = alpha === null ? "" : String(Math.round((1 - alpha / 255) * 10000) / 100);
|
||||
el.objectOpacity.placeholder = alpha === null ? "混合" : "";
|
||||
@@ -1468,31 +1510,69 @@ function openObjectStyle() {
|
||||
el.objectDashOffset.value = shared("dashOffset", 0) ?? "";
|
||||
el.objectCap.value = shared("cap", "") || (shared("cap", "") === null ? "mixed" : "Butt");
|
||||
el.objectJoin.value = shared("join", "") || (shared("join", "") === null ? "mixed" : "Miter");
|
||||
state.styleOriginal = Object.fromEntries(["objectOpacity", "objectDash", "objectDashPattern", "objectDashOffset", "objectCap", "objectJoin", "patternWidth", "patternHeight", "patternXStep", "patternYStep", "patternCTM"].map(key => [key, el[key].value]));
|
||||
state.styleOriginal = Object.fromEntries(["objectOpacity", "objectDash", "objectDashPattern", "objectDashOffset", "objectCap", "objectJoin"].map(key => [key, el[key].value]));
|
||||
el.objectStyleStatus.textContent = "";
|
||||
el.objectStylePanel.showModal();
|
||||
}
|
||||
|
||||
function readObjectStyle() {
|
||||
const style = {}, changed = key => el[key].value !== state.styleOriginal[key];
|
||||
if (!el.objectGradientFields.hidden && el.gradientKind.value !== "keep") {
|
||||
if (!el.objectBorderFields.hidden) {
|
||||
const border = {};
|
||||
if (el.borderEnabled.checked !== state.borderOriginal.Enabled) border.Enabled = el.borderEnabled.checked;
|
||||
if (el.borderEnabled.checked) {
|
||||
for (const [input, key] of [[el.borderWidth, "LineWidth"], [el.borderHorizontal, "HorizontalRadius"], [el.borderVertical, "VerticalRadius"]]) {
|
||||
if (input.value !== state.borderOriginal[key] || !state.borderOriginal.Enabled) border[key] = Number(input.value);
|
||||
}
|
||||
if (!el.borderColor.disabled && (el.borderColor.value !== state.borderOriginal.Color || !state.borderOriginal.Enabled)) {
|
||||
border.Color = { Value: el.borderColor.value };
|
||||
}
|
||||
}
|
||||
if (Object.keys(border).length) style.borderStyle = JSON.stringify(border);
|
||||
}
|
||||
if (!el.objectGradientFields.hidden) {
|
||||
state.gradientForms[state.gradientTarget].form = gradientForm();
|
||||
for (const target of ["fill", "stroke"]) {
|
||||
const { base, form, original } = state.gradientForms[target];
|
||||
if (form.kind === "keep" || JSON.stringify(form) === original) continue;
|
||||
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 previous = JSON.parse(original);
|
||||
const Segment = form.stops.map(stop => {
|
||||
const old = previous.stops.find(entry => entry.source && JSON.stringify(entry.source) === JSON.stringify(stop.source));
|
||||
const Color = old && old.color === stop.color ? { ...stop.source.Color } : { Value: stop.color };
|
||||
if (!old || old.transparency !== stop.transparency || old.color !== stop.color) Color.Alpha = Math.round(255 * (1 - Number(stop.transparency) / 100));
|
||||
return { Position: Number(stop.position) / 100, Color };
|
||||
}).sort((a, b) => a.Position - b.Position);
|
||||
const angle = Number(form.angle) * 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"
|
||||
let paint = form.kind === "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;
|
||||
if (base && form.kind === previous.kind) {
|
||||
paint = JSON.parse(JSON.stringify(base));
|
||||
const node = paint.AxialShd || paint.RadialShd;
|
||||
node.Segment = Segment;
|
||||
if (paint.AxialShd && form.angle !== previous.angle) {
|
||||
const start = node.StartPoint.split(/\s+/).map(Number), end = node.EndPoint.split(/\s+/).map(Number);
|
||||
const half = Math.hypot(end[0] - start[0], end[1] - start[1]) / 2, cx = (start[0] + end[0]) / 2, cy = (start[1] + end[1]) / 2;
|
||||
node.StartPoint = `${cx - x * half} ${cy - y * half}`;
|
||||
node.EndPoint = `${cx + x * half} ${cy + y * half}`;
|
||||
}
|
||||
}
|
||||
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 (!el.objectPatternFields.hidden) {
|
||||
state.patternForms[state.patternTarget] = patternForm();
|
||||
const original = JSON.parse(state.patternOriginal);
|
||||
for (const [target, form] of Object.entries(state.patternForms)) {
|
||||
const pattern = {};
|
||||
for (const [key, field] of [["width", "Width"], ["height", "Height"], ["xStep", "XStep"], ["yStep", "YStep"], ["ctm", "CTM"]]) {
|
||||
if (form[key] !== original[target][key]) pattern[field] = key === "ctm" ? form[key].trim() : Number(form[key]);
|
||||
}
|
||||
if (Object.keys(pattern).length) style[`${target}PatternStyle`] = JSON.stringify(pattern);
|
||||
}
|
||||
}
|
||||
if (changed("objectOpacity") && el.objectOpacity.value !== "") style.alpha = Math.round((1 - Number(el.objectOpacity.value) / 100) * 255);
|
||||
if (!el.objectStrokeFields.hidden) {
|
||||
@@ -1507,8 +1587,40 @@ function readObjectStyle() {
|
||||
return style;
|
||||
}
|
||||
|
||||
function addGradientStop(position, color, transparency) {
|
||||
function gradientForm() {
|
||||
return {
|
||||
kind: el.gradientKind.value, angle: el.gradientAngle.value,
|
||||
stops: [...el.gradientStops.children].map(row => ({ position: row.children[1].value, color: row.children[0].value, transparency: row.children[2].value, source: row.paintSource }))
|
||||
};
|
||||
}
|
||||
|
||||
function loadGradientForm(target) {
|
||||
state.gradientTarget = target;
|
||||
const form = state.gradientForms[target].form;
|
||||
el.gradientKind.value = form.kind;
|
||||
el.gradientAngle.value = form.angle;
|
||||
el.gradientControls.hidden = form.kind === "keep";
|
||||
el.gradientAngle.disabled = form.kind !== "linear";
|
||||
el.gradientStops.replaceChildren();
|
||||
for (const stop of form.stops) addGradientStop(stop.position, stop.color, stop.transparency, stop.source);
|
||||
}
|
||||
|
||||
function patternForm() {
|
||||
return { width: el.patternWidth.value, height: el.patternHeight.value, xStep: el.patternXStep.value, yStep: el.patternYStep.value, ctm: el.patternCTM.value };
|
||||
}
|
||||
|
||||
function loadPatternForm(target) {
|
||||
state.patternTarget = target;
|
||||
const pattern = state.patternForms[target];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function addGradientStop(position, color, transparency, source) {
|
||||
const row = document.createElement("div");
|
||||
row.paintSource = source;
|
||||
row.className = "gradient-stop-row";
|
||||
for (const [type, value, label] of [["color", color, "色标颜色"], ["number", position, "色标位置"], ["number", transparency, "色标透明度"]]) {
|
||||
const input = document.createElement("input");
|
||||
|
||||
@@ -1195,7 +1195,8 @@ export class CanvasEditor {
|
||||
input.append(paragraph);
|
||||
}
|
||||
const left = item.leftIndent || 0, right = item.rightIndent || 0;
|
||||
const frame = item.textFrame || { width: item.width, height: item.height, matrix: [1, 0, 0, 1, item.x, item.y] };
|
||||
const frame = { ...(item.textFrame || { width: item.width, height: item.height, matrix: [1, 0, 0, 1, item.x, item.y] }) };
|
||||
if (item.wrap && item.paragraphWidth != null) frame.width = item.paragraphWidth;
|
||||
const [a, b, c, d, e, f] = frame.matrix;
|
||||
const autoSize = !source && !item.wrap && (!item.align || item.align === "left");
|
||||
Object.assign(input.style, {
|
||||
|
||||
+67
-10
@@ -937,7 +937,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
||||
local := inverse.TransformBox(box)
|
||||
width := math.Max(object.TextObject.Size, local.X+local.W)
|
||||
if layout.Wrap {
|
||||
width = frame.W
|
||||
item["paragraphWidth"] = frame.W
|
||||
}
|
||||
frame = ofdgo.Box{W: width, H: math.Max(object.TextObject.Size, local.Y+local.H)}
|
||||
item["textFrame"] = map[string]any{"width": frame.W, "height": frame.H, "matrix": editorMatrix(matrix)}
|
||||
@@ -1712,10 +1712,11 @@ func editorCapabilities(capability ofdgo.ObjectCapabilities, kind string) map[st
|
||||
// 入参: item 前端对象, object 有效样式, paint 是否可改色, scale 描边页面倍率
|
||||
func editorAppearance(item map[string]any, object ofdgo.GraphicObject, paint bool, scale float64) {
|
||||
var alpha *int
|
||||
var fill *ofdgo.FillColor
|
||||
var fill, stroke *ofdgo.FillColor
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
fill = object.TextObject.FillColor
|
||||
stroke = (*ofdgo.FillColor)(object.TextObject.StrokeColor)
|
||||
alpha = object.TextObject.Alpha
|
||||
if paint {
|
||||
item["color"] = editorColorHex(object.TextObject.FillColor)
|
||||
@@ -1723,9 +1724,17 @@ func editorAppearance(item map[string]any, object ofdgo.GraphicObject, paint boo
|
||||
case "ImageObject":
|
||||
alpha = object.ImageObject.Alpha
|
||||
item["imageBorder"] = object.ImageObject.Border != nil
|
||||
item["borderColor"] = "#000000"
|
||||
if border := object.ImageObject.Border; border != nil {
|
||||
item["borderColor"] = editorColorHex((*ofdgo.FillColor)(border.BorderColor))
|
||||
}
|
||||
if data, err := json.Marshal(object.ImageObject.Border); err == nil {
|
||||
item["borderStyle"] = string(data)
|
||||
}
|
||||
case "PathObject":
|
||||
path := object.PathObject
|
||||
fill = path.FillColor
|
||||
stroke = (*ofdgo.FillColor)(path.StrokeColor)
|
||||
if box, err := ofdgo.ParseBox(path.Boundary); err == nil {
|
||||
item["paintSize"] = []any{box.W, box.H}
|
||||
}
|
||||
@@ -1746,9 +1755,34 @@ func editorAppearance(item map[string]any, object ofdgo.GraphicObject, paint boo
|
||||
if alpha != nil {
|
||||
item["alpha"] = *alpha
|
||||
}
|
||||
if fill != nil && fill.Pattern != nil {
|
||||
p := fill.Pattern
|
||||
item["fillPattern"] = map[string]any{"width": p.Width, "height": p.Height, "xStep": p.XStep, "yStep": p.YStep, "ctm": p.CTM}
|
||||
for key, paint := range map[string]*ofdgo.FillColor{"fill": fill, "stroke": stroke} {
|
||||
if paint == nil {
|
||||
continue
|
||||
}
|
||||
if p := paint.Pattern; p != nil {
|
||||
item[key+"Pattern"] = map[string]any{"width": p.Width, "height": p.Height, "xStep": p.XStep, "yStep": p.YStep, "ctm": p.CTM}
|
||||
}
|
||||
var segments []ofdgo.ShdSegment
|
||||
if paint.AxialShd != nil {
|
||||
segments = paint.AxialShd.Segment
|
||||
} else if paint.RadialShd != nil {
|
||||
segments = paint.RadialShd.Segment
|
||||
}
|
||||
if len(segments) == 0 {
|
||||
continue
|
||||
}
|
||||
stops, err := currentEditor.GradientStops(segments, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
values := make([]any, len(stops))
|
||||
for i, stop := range stops {
|
||||
c := segments[i].Color
|
||||
values[i] = map[string]any{"position": stop.Offset * 100, "color": editorColorHex(&ofdgo.FillColor{Value: c.Value, Index: c.Index, ColorSpace: c.ColorSpace}), "transparency": (1 - float64(stop.Color.A)/255) * 100}
|
||||
}
|
||||
if data, err := json.Marshal(paint); err == nil {
|
||||
item[key+"Gradient"] = map[string]any{"paint": string(data), "stops": values}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1827,7 +1861,7 @@ func compositeObjects(args []js.Value) (any, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
width = frame.W
|
||||
item["paragraphWidth"] = frame.W
|
||||
}
|
||||
frame := ofdgo.Box{W: width, H: math.Max(object.Size, local.Y+local.H)}
|
||||
item["textFrame"] = map[string]any{"width": frame.W, "height": frame.H, "matrix": editorMatrix(member.Matrix)}
|
||||
@@ -2904,11 +2938,21 @@ func changeOutline(args []js.Value) (any, error) {
|
||||
// 返回: 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() {
|
||||
var border *ofdgo.ImageBorderStyle
|
||||
if field := args[2].Get("borderStyle"); !field.IsUndefined() {
|
||||
if err := json.Unmarshal([]byte(field.String()), &border); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
patterns := make(map[bool]*ofdgo.PatternStyle)
|
||||
for key, stroke := range map[string]bool{"fillPatternStyle": false, "strokePatternStyle": true} {
|
||||
if field := args[2].Get(key); !field.IsUndefined() {
|
||||
var pattern ofdgo.PatternStyle
|
||||
if err := json.Unmarshal([]byte(field.String()), &pattern); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patterns[stroke] = &pattern
|
||||
}
|
||||
}
|
||||
if field := args[2].Get("fillPaint"); !field.IsUndefined() {
|
||||
if err := json.Unmarshal([]byte(field.String()), &style.FillColor); err != nil {
|
||||
@@ -2923,6 +2967,14 @@ func styleObjects(args []js.Value) (any, error) {
|
||||
return changeObjects(func() error {
|
||||
return currentEditor.Transaction(func(editor *ofdgo.Editor) error {
|
||||
page, ids := args[0].Int(), stringsFromJS(args[1])
|
||||
if border != nil {
|
||||
if err := resolveStylePaint((*ofdgo.FillColor)(border.Color)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := editor.StyleImageBorders(page, ids, *border); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, paint := range []*ofdgo.FillColor{style.FillColor, (*ofdgo.FillColor)(style.StrokeColor)} {
|
||||
if err := resolveStylePaint(paint); err != nil {
|
||||
return err
|
||||
@@ -2931,8 +2983,10 @@ func styleObjects(args []js.Value) (any, error) {
|
||||
if err := editor.StyleObjects(page, ids, style); err != nil {
|
||||
return err
|
||||
}
|
||||
if pattern != nil {
|
||||
return editor.StylePatterns(page, ids, false, *pattern)
|
||||
for stroke, pattern := range patterns {
|
||||
if err := editor.StylePatterns(page, ids, stroke, *pattern); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -2946,6 +3000,9 @@ func resolveStylePaint(paint *ofdgo.FillColor) error {
|
||||
if paint == nil {
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(paint.Value, "#") {
|
||||
return setEditorColor(paint, paint.Value)
|
||||
}
|
||||
var segments []ofdgo.ShdSegment
|
||||
if paint.AxialShd != nil {
|
||||
segments = paint.AxialShd.Segment
|
||||
|
||||
+11
-73
@@ -23,6 +23,7 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"io/fs"
|
||||
"maps"
|
||||
"math"
|
||||
"reflect"
|
||||
"slices"
|
||||
@@ -59,6 +60,7 @@ type Editor struct {
|
||||
serial uint64
|
||||
source *editorSource
|
||||
origins map[string]*editorObjectOrigin
|
||||
removedPages map[string]bool
|
||||
outlines []byte
|
||||
encryption *encryptionState
|
||||
}
|
||||
@@ -271,7 +273,6 @@ func (e *Editor) CopyPages(indexes []int) ([]int, error) {
|
||||
return nil, nil
|
||||
}
|
||||
at := len(e.pages)
|
||||
if e.source != nil {
|
||||
source, err := e.Reader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -285,78 +286,6 @@ func (e *Editor) CopyPages(indexes []int) ([]int, error) {
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
pages := make([]PageContent, len(indexes))
|
||||
for n, index := range indexes {
|
||||
source := &e.pages[index]
|
||||
page := &pages[n]
|
||||
*page = PageContent{Area: source.Area, Content: Content{Layer: make([]Layer, len(source.Content.Layer))}}
|
||||
for i, layer := range source.Content.Layer {
|
||||
page.Content.Layer[i] = Layer{Type: layer.Type, Objects: make([]GraphicObject, len(layer.Objects))}
|
||||
for j, object := range layer.Objects {
|
||||
copy, err := cloneEditorObject(object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page.Content.Layer[i].Objects[j] = copy
|
||||
}
|
||||
}
|
||||
}
|
||||
result := make([]int, len(pages))
|
||||
maximum := e.maxID
|
||||
origins := make(map[string]*editorObjectOrigin)
|
||||
for n := range pages {
|
||||
page := &pages[n]
|
||||
maximum++
|
||||
page.ID = strconv.Itoa(maximum)
|
||||
for i := range page.Content.Layer {
|
||||
layer := &page.Content.Layer[i]
|
||||
maximum++
|
||||
layer.ID = strconv.Itoa(maximum)
|
||||
for j := range layer.Objects {
|
||||
object := &layer.Objects[j]
|
||||
before := *object
|
||||
maximum++
|
||||
id := strconv.Itoa(maximum)
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
object.TextObject.ID = id
|
||||
case "PathObject":
|
||||
object.PathObject.ID = id
|
||||
case "ImageObject":
|
||||
object.ImageObject.ID = id
|
||||
case "CompositeObject", "CompositeGraphicUnit":
|
||||
object.CompositeGraphicUnit.ID = id
|
||||
}
|
||||
copied, origin, err := e.copyObjectOrigin(before, *object, &maximum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
*object = copied
|
||||
if origin != nil {
|
||||
origins[id] = origin
|
||||
}
|
||||
}
|
||||
}
|
||||
result[n] = at + n
|
||||
}
|
||||
e.maxID = maximum
|
||||
for id, origin := range origins {
|
||||
e.setObjectOrigin(id, origin)
|
||||
}
|
||||
e.pages = append(e.pages, pages...)
|
||||
if change := e.recordChange(); change != nil {
|
||||
for i := range pages {
|
||||
pages[i] = copyEditorPage(pages[i])
|
||||
}
|
||||
change.undo = func(e *Editor) { e.pages = e.pages[:at] }
|
||||
change.redo = func(e *Editor) {
|
||||
for _, page := range pages {
|
||||
e.pages = append(e.pages, copyEditorPage(page))
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DeletePage 删除页面,输出时清理相关目录、跳转、注释和签章位置,保留其他页面及原始签名凭据
|
||||
// 入参: index 页面索引
|
||||
@@ -386,6 +315,13 @@ func (e *Editor) DeletePages(indexes []int) error {
|
||||
saved[i] = copyEditorPage(*page)
|
||||
}
|
||||
remove := func(e *Editor) {
|
||||
e.removedPages = maps.Clone(e.removedPages)
|
||||
if e.removedPages == nil {
|
||||
e.removedPages = make(map[string]bool)
|
||||
}
|
||||
for _, page := range saved {
|
||||
e.removedPages[page.ID] = true
|
||||
}
|
||||
kept, next := e.pages[:0], 0
|
||||
for index, page := range e.pages {
|
||||
if next < len(indexes) && index == indexes[next] {
|
||||
@@ -397,8 +333,10 @@ func (e *Editor) DeletePages(indexes []int) error {
|
||||
clear(e.pages[len(kept):])
|
||||
e.pages = kept
|
||||
}
|
||||
previousRemoved := e.removedPages
|
||||
if change := e.recordChange(); change != nil {
|
||||
change.undo = func(e *Editor) {
|
||||
e.removedPages = previousRemoved
|
||||
pages := make([]PageContent, len(e.pages)+len(saved))
|
||||
kept, removed := 0, 0
|
||||
for index := range pages {
|
||||
|
||||
@@ -32,6 +32,22 @@ type PatternStyle struct {
|
||||
CTM *string
|
||||
}
|
||||
|
||||
// GradientStops 解析当前文档的渐变分段,保留顺序并合并透明度
|
||||
// 入参: segments 渐变分段, alpha 外层透明度
|
||||
// 返回: []ColorStop 后端无关的色标, error 颜色解析错误
|
||||
func (e *Editor) GradientStops(segments []ShdSegment, alpha *int) ([]ColorStop, error) {
|
||||
positions := gradientPositions(segments)
|
||||
stops := make([]ColorStop, len(segments))
|
||||
for i, segment := range segments {
|
||||
c, err := e.Color(&FillColor{Value: segment.Color.Value, Index: segment.Color.Index, ColorSpace: segment.Color.ColorSpace, Alpha: mergeAlpha(segment.Color.Alpha, alpha)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stops[i] = ColorStop{Offset: positions[i], Color: colorToRGBA(c)}
|
||||
}
|
||||
return stops, nil
|
||||
}
|
||||
|
||||
// StylePatterns 修改已有图案布局,保留单元内容、资源引用和未知扩展
|
||||
// 入参: page 页面索引, ids 对象标识, stroke 是否修改描边, style 图案布局增量
|
||||
// 返回: error 错误信息
|
||||
|
||||
@@ -61,6 +61,7 @@ func (e *Editor) transactionSnapshot() Editor {
|
||||
next.resources = slices.Clone(e.resources)
|
||||
next.fonts, next.images, next.resourceID = maps.Clone(e.fonts), maps.Clone(e.images), maps.Clone(e.resourceID)
|
||||
next.origins = maps.Clone(e.origins)
|
||||
next.removedPages = maps.Clone(e.removedPages)
|
||||
next.fontMetrics = maps.Clone(e.fontMetrics)
|
||||
if e.source != nil {
|
||||
source := *e.source
|
||||
|
||||
@@ -737,7 +737,7 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
|
||||
for _, bookmark := range m.doc.Bookmarks.Bookmark {
|
||||
bookmarks[bookmark.Name] = bookmark.Dest
|
||||
}
|
||||
if dest := gotoDest(&action, bookmarks); dest == nil || !m.pages[dest.PageID] && !m.copyPage {
|
||||
if dest := gotoDest(&action, bookmarks); !m.copyPage && (dest == nil || !m.pages[dest.PageID]) {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
@@ -759,7 +759,11 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
|
||||
value = name + ":" + value
|
||||
}
|
||||
value = m.id(value)
|
||||
case "Font", "ResourceID", "Substitution", "ImageMask", "Relative", "DrawParam", "ColorSpace", "Thumbnail", "TemplateID", "PageID", "PageRef", "RefId":
|
||||
case "PageID":
|
||||
if !m.copyPage || m.pages[value] {
|
||||
value, err = m.reference(value)
|
||||
}
|
||||
case "Font", "ResourceID", "Substitution", "ImageMask", "Relative", "DrawParam", "ColorSpace", "Thumbnail", "TemplateID", "PageRef", "RefId":
|
||||
value, err = m.reference(value)
|
||||
case "BaseLoc", "FileRef":
|
||||
loc, resolveErr := m.resolve(entry.name, "", value)
|
||||
|
||||
@@ -27,6 +27,62 @@ type AnnotationLink struct {
|
||||
Dest *Dest
|
||||
}
|
||||
|
||||
// SetObjectLink 修改对象的单个点击链接,保留其他事件和点击区域,nil移除链接
|
||||
// 入参: page 页面索引, ids 对象标识, link 链接目标,不检查可达性
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) SetObjectLink(page int, ids []string, link *AnnotationLink) error {
|
||||
if link != nil {
|
||||
if _, err := annotationLinkXML(*link); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
objects, _, err := e.selectedObjects(page, ids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range objects {
|
||||
var actions *[]Action
|
||||
switch objects[i].Type {
|
||||
case "TextObject":
|
||||
actions = &objects[i].TextObject.Actions
|
||||
case "PathObject":
|
||||
actions = &objects[i].PathObject.Actions
|
||||
case "ImageObject":
|
||||
actions = &objects[i].ImageObject.Actions
|
||||
case "CompositeObject", "CompositeGraphicUnit":
|
||||
actions = &objects[i].CompositeGraphicUnit.Actions
|
||||
default:
|
||||
return fmt.Errorf("object %q cannot contain links", ids[i])
|
||||
}
|
||||
*actions = cloneEditorData(*actions)
|
||||
index := -1
|
||||
for j, action := range *actions {
|
||||
if action.Event == "CLICK" && (action.URI != nil || action.Goto != nil) {
|
||||
if index >= 0 {
|
||||
return fmt.Errorf("object %q contains multiple click links", ids[i])
|
||||
}
|
||||
index = j
|
||||
}
|
||||
}
|
||||
if link == nil {
|
||||
if index >= 0 {
|
||||
*actions = slices.Delete(*actions, index, index+1)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if index < 0 {
|
||||
index = len(*actions)
|
||||
*actions = append(*actions, Action{Event: "CLICK"})
|
||||
}
|
||||
action := &(*actions)[index]
|
||||
action.URI, action.Goto = cloneEditorData(link.URI), nil
|
||||
if link.Dest != nil {
|
||||
action.Goto = &Goto{Dest: cloneEditorData(link.Dest)}
|
||||
}
|
||||
}
|
||||
return e.updateObjects(page, objects, true)
|
||||
}
|
||||
|
||||
// SetObjectActions 原子替换普通对象的链接动作,空列表移除动作
|
||||
// 入参: page 页面索引, ids 对象标识, actions 链接动作
|
||||
// 返回: error 错误信息
|
||||
|
||||
@@ -214,7 +214,7 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error)
|
||||
}
|
||||
parts[name] = updated
|
||||
}
|
||||
if originalCount != len(source.pages) || len(source.annotationPages) != 0 {
|
||||
if originalCount != len(source.pages) || len(source.annotationPages) != 0 || len(e.removedPages) != 0 {
|
||||
if err := e.prunePageReferences(parts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+35
-1
@@ -16,6 +16,7 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -24,7 +25,10 @@ import (
|
||||
// 入参: parts 输出覆盖条目
|
||||
// 返回: error 读取或解析错误
|
||||
func (e *Editor) prunePageReferences(parts map[string][]byte) error {
|
||||
removed := make(map[string]bool)
|
||||
removed := maps.Clone(e.removedPages)
|
||||
if removed == nil {
|
||||
removed = make(map[string]bool)
|
||||
}
|
||||
for id := range e.source.pages {
|
||||
removed[id] = true
|
||||
}
|
||||
@@ -154,6 +158,36 @@ func (e *Editor) prunePageReferences(parts map[string][]byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// pruneCreatedPageReferences 清理创作文档中被明确删除页面的直接跳转,不检查其他目标是否可达
|
||||
// 入参: data 创作XML, removed 已删除页面标识
|
||||
// 返回: []byte 更新后的XML, error 解析错误
|
||||
func pruneCreatedPageReferences(data []byte, removed map[string]bool) ([]byte, error) {
|
||||
root, err := parseEditorXML(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var patches []editorXMLPatch
|
||||
var walk func(*editorXML)
|
||||
walk = func(node *editorXML) {
|
||||
if node.name.Space != ofdNamespace {
|
||||
return
|
||||
}
|
||||
if node.name.Local == "Action" {
|
||||
if target := node.child("Goto"); target != nil {
|
||||
if dest := target.child("Dest"); dest != nil && removed[dest.attr("PageID")] {
|
||||
patches = append(patches, editorXMLPatch{start: node.start, end: node.end})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, child := range node.children {
|
||||
walk(child)
|
||||
}
|
||||
}
|
||||
walk(root)
|
||||
return editorPatchXML(data, patches), nil
|
||||
}
|
||||
|
||||
// editorPageLocation 解析标准XML引用,优先使用所在文件的相对路径
|
||||
// 入参: reader 原包, parts 覆盖条目, name 所在XML, value 路径
|
||||
// 返回: string 包内路径, error 路径不存在
|
||||
|
||||
+74
-15
@@ -21,6 +21,30 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ObjectStyle 对象外观,nil字段保持原值,颜色的Alpha为nil时保留原颜色透明度
|
||||
// StyleObjects使用对象坐标毫米,StyleCompositeObjects的LineWidth使用页面毫米
|
||||
type ObjectStyle struct {
|
||||
Alpha *int
|
||||
Fill *bool
|
||||
Stroke *bool
|
||||
FillColor *FillColor
|
||||
StrokeColor *StrokeColor
|
||||
LineWidth *float64
|
||||
DashPattern *string
|
||||
DashOffset *float64
|
||||
Cap *string
|
||||
Join *string
|
||||
}
|
||||
|
||||
// ImageBorderStyle 图片边框增量,nil字段及未指定的颜色透明度保留原值,尺寸单位为毫米
|
||||
type ImageBorderStyle struct {
|
||||
Enabled *bool
|
||||
LineWidth *float64
|
||||
HorizontalRadius *float64
|
||||
VerticalRadius *float64
|
||||
Color *StrokeColor
|
||||
}
|
||||
|
||||
// AddDrawParam 注册绘制参数,保留继承引用,未引用的资源不写入文档
|
||||
// 入参: draw 绘制参数,ID由编辑器分配
|
||||
// 返回: string 资源标识, error 错误信息
|
||||
@@ -159,21 +183,6 @@ func (e *Editor) resolveEditorStyleDefaults(object GraphicObject, base *DrawPara
|
||||
return cloneEditorObject(object)
|
||||
}
|
||||
|
||||
// ObjectStyle 对象外观,nil字段保持原值,颜色的Alpha为nil时保留原颜色透明度
|
||||
// StyleObjects使用对象坐标毫米,StyleCompositeObjects的LineWidth使用页面毫米
|
||||
type ObjectStyle struct {
|
||||
Alpha *int
|
||||
Fill *bool
|
||||
Stroke *bool
|
||||
FillColor *FillColor
|
||||
StrokeColor *StrokeColor
|
||||
LineWidth *float64
|
||||
DashPattern *string
|
||||
DashOffset *float64
|
||||
Cap *string
|
||||
Join *string
|
||||
}
|
||||
|
||||
// StyleObjects 原子更新对象透明度、文字颜色及路径填充和描边样式
|
||||
// 入参: page 页面索引, ids 对象标识, style 待修改属性
|
||||
// 返回: error 错误信息
|
||||
@@ -444,6 +453,56 @@ func editorStrokeScale(ctm string) float64 {
|
||||
return 1
|
||||
}
|
||||
|
||||
// StyleImageBorders 原子修改图片边框,保留未修改的颜色、线型及原文扩展
|
||||
// 入参: page 页面索引, ids 图片标识, style 边框增量
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) StyleImageBorders(page int, ids []string, style ImageBorderStyle) error {
|
||||
for _, value := range []*float64{style.LineWidth, style.HorizontalRadius, style.VerticalRadius} {
|
||||
if value != nil && (!finite(*value) || *value < 0) {
|
||||
return fmt.Errorf("border dimensions must be finite and nonnegative")
|
||||
}
|
||||
}
|
||||
if style.Color != nil {
|
||||
if err := e.editorColor((*FillColor)(style.Color)); 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])
|
||||
}
|
||||
if style.Enabled != nil && !*style.Enabled {
|
||||
objects[i].ImageObject.Border = nil
|
||||
continue
|
||||
}
|
||||
border := cloneEditorData(objects[i].ImageObject.Border)
|
||||
if border == nil {
|
||||
if style == (ImageBorderStyle{}) {
|
||||
continue
|
||||
}
|
||||
border = &ImageBorder{}
|
||||
}
|
||||
if style.LineWidth != nil {
|
||||
border.LineWidth = cloneEditorData(style.LineWidth)
|
||||
}
|
||||
if style.HorizontalRadius != nil {
|
||||
border.HorizonalCornerRadius = *style.HorizontalRadius
|
||||
}
|
||||
if style.VerticalRadius != nil {
|
||||
border.VerticalCornerRadius = *style.VerticalRadius
|
||||
}
|
||||
if style.Color != nil {
|
||||
border.BorderColor = (*StrokeColor)(editorStyleColor((*FillColor)(style.Color), (*FillColor)(border.BorderColor)))
|
||||
}
|
||||
objects[i].ImageObject.Border = border
|
||||
}
|
||||
return e.updateObjects(page, objects, true)
|
||||
}
|
||||
|
||||
// SetImageBorders 原子替换图片边框,nil移除边框
|
||||
// 入参: page 页面索引, ids 图片标识, border 边框样式
|
||||
// 返回: error 错误信息
|
||||
|
||||
@@ -22,9 +22,11 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"path"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -214,6 +216,19 @@ func (e *Editor) validate() error {
|
||||
// 入参: write 条目写入方法, progress 保存进度回调,预览时为nil
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) writeParts(write func(string, []byte, bool) error, progress editorProgress) error {
|
||||
if len(e.removedPages) != 0 {
|
||||
output := write
|
||||
write = func(name string, data []byte, binary bool) error {
|
||||
if !binary && strings.EqualFold(path.Ext(name), ".xml") {
|
||||
var err error
|
||||
data, err = pruneCreatedPageReferences(data, e.removedPages)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return output(name, data, binary)
|
||||
}
|
||||
}
|
||||
fonts, images, spaces, definitions := e.usedResources()
|
||||
writeXML := func(name string, encode func(*ofdXML)) error {
|
||||
data, err := encodeOFDXML(encode)
|
||||
|
||||
Reference in new issue
Block a user