mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
This commit is contained in:
1 parent
58564b0647
commit
6428a885fa
12 files changed
+462
-72
No files matched your search
@@ -100,7 +100,8 @@
|
|||||||
<span>插入</span>
|
<span>插入</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-group">
|
<div class="editor-group">
|
||||||
<div class="text-font"><select id="textFont" aria-label="字体" title="字体" disabled></select><button id="textFontAdd" class="icon-button" type="button" title="添加字体" aria-label="添加字体">+</button></div>
|
<div class="text-font"><div class="font-picker"><input id="textFont" type="text" role="combobox" aria-label="字体" title="字体" aria-autocomplete="list" aria-controls="textFontList" aria-expanded="false" autocomplete="off" spellcheck="false" disabled><button id="textFontToggle" type="button" title="选择字体" aria-label="选择字体" tabindex="-1" disabled></button></div><button id="textFontAdd" class="icon-button" type="button" title="添加字体" aria-label="添加字体">+</button></div>
|
||||||
|
<div id="textFontList" class="font-listbox" role="listbox" aria-label="字体列表" popover="manual"></div>
|
||||||
<div class="editor-number"><input id="textSize" type="number" min="1" step="any" required aria-label="字号" disabled><span>pt</span></div>
|
<div class="editor-number"><input id="textSize" type="number" min="1" step="any" required aria-label="字号" disabled><span>pt</span></div>
|
||||||
<input id="textColor" class="color-input" type="color" value="#000000" aria-label="文字颜色" title="文字颜色" disabled>
|
<input id="textColor" class="color-input" type="color" value="#000000" aria-label="文字颜色" title="文字颜色" disabled>
|
||||||
<span>文字</span>
|
<span>文字</span>
|
||||||
|
|||||||
@@ -208,6 +208,78 @@ a:focus-visible {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-picker {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
height: var(--control-height);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-picker #textFont {
|
||||||
|
padding: 0 6px;
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #1f2428;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-picker:focus-within {
|
||||||
|
outline: 2px solid var(--focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#textFontToggle {
|
||||||
|
width: 24px;
|
||||||
|
min-width: 24px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#textFontToggle::after {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-right: 1px solid currentColor;
|
||||||
|
border-bottom: 1px solid currentColor;
|
||||||
|
transform: translateY(-2px) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-listbox {
|
||||||
|
position: fixed;
|
||||||
|
margin: 0;
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--panel);
|
||||||
|
color: #1f2428;
|
||||||
|
box-shadow: 0 5px 18px rgba(20, 40, 50, 0.14);
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-listbox [role="option"] {
|
||||||
|
padding: 6px 8px;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-listbox [role="option"]:hover,
|
||||||
|
.font-listbox [aria-selected="true"] {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-listbox [aria-disabled="true"],
|
||||||
|
.font-search-empty {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-search-empty {
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
#textColor {
|
#textColor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-25
@@ -1,5 +1,5 @@
|
|||||||
import { CanvasEditor, canEditObject, objectEditReason } from "./ofdgo_edit.js";
|
import { CanvasEditor, canEditObject, objectEditReason } from "./ofdgo_edit.js";
|
||||||
import { FontManager } from "./ofdgo_font.js";
|
import { FontManager, FontPicker } from "./ofdgo_font.js";
|
||||||
|
|
||||||
const MM_TO_PX = 96 / 25.4;
|
const MM_TO_PX = 96 / 25.4;
|
||||||
const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)");
|
const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)");
|
||||||
@@ -109,6 +109,8 @@ const el = {
|
|||||||
textLineHeight: document.querySelector("#textLineHeight"),
|
textLineHeight: document.querySelector("#textLineHeight"),
|
||||||
textSpacing: document.querySelector("#textSpacing"),
|
textSpacing: document.querySelector("#textSpacing"),
|
||||||
textFont: document.querySelector("#textFont"),
|
textFont: document.querySelector("#textFont"),
|
||||||
|
textFontToggle: document.querySelector("#textFontToggle"),
|
||||||
|
textFontList: document.querySelector("#textFontList"),
|
||||||
textFontAdd: document.querySelector("#textFontAdd"),
|
textFontAdd: document.querySelector("#textFontAdd"),
|
||||||
textSize: document.querySelector("#textSize"),
|
textSize: document.querySelector("#textSize"),
|
||||||
textColor: document.querySelector("#textColor"),
|
textColor: document.querySelector("#textColor"),
|
||||||
@@ -244,9 +246,11 @@ const fontManager = new FontManager({
|
|||||||
onChange: scheduleFontSync,
|
onChange: scheduleFontSync,
|
||||||
onPermissionChange: updateFontPermissionHint,
|
onPermissionChange: updateFontPermissionHint,
|
||||||
});
|
});
|
||||||
|
const fontPicker = new FontPicker(el.textFont, el.textFontToggle, el.textFontList, changeTextFont);
|
||||||
|
el.editorTools.addEventListener("scroll", () => fontPicker.position());
|
||||||
|
|
||||||
const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
||||||
fontControls: [el.textFont, el.textFontAdd],
|
fontControls: [el.textFont, el.textFontToggle, el.textFontAdd],
|
||||||
busy: () => document.body.hasAttribute("aria-busy"),
|
busy: () => document.body.hasAttribute("aria-busy"),
|
||||||
rotation: () => state.rotation,
|
rotation: () => state.rotation,
|
||||||
canInsert: (index) => pageCan("insert", index),
|
canInsert: (index) => pageCan("insert", index),
|
||||||
@@ -341,7 +345,6 @@ editorClick(el.textWrap, () => changeParagraph(!currentTextStyle().wrap));
|
|||||||
el.textAlign.addEventListener("change", () => changeParagraph());
|
el.textAlign.addEventListener("change", () => changeParagraph());
|
||||||
el.textLineHeight.addEventListener("change", () => changeParagraph());
|
el.textLineHeight.addEventListener("change", () => changeParagraph());
|
||||||
el.textSpacing.addEventListener("change", () => changeParagraph());
|
el.textSpacing.addEventListener("change", () => changeParagraph());
|
||||||
el.textFont.addEventListener("change", changeTextFont);
|
|
||||||
el.shapeWidth.addEventListener("focus", () => { el.shapeWidth.defaultValue = el.shapeWidth.value; });
|
el.shapeWidth.addEventListener("focus", () => { el.shapeWidth.defaultValue = el.shapeWidth.value; });
|
||||||
for (const input of [el.textSize, el.shapeWidth, el.textLineHeight, el.textSpacing]) {
|
for (const input of [el.textSize, el.shapeWidth, el.textLineHeight, el.textSpacing]) {
|
||||||
input.addEventListener("keydown", (event) => {
|
input.addEventListener("keydown", (event) => {
|
||||||
@@ -942,7 +945,7 @@ async function toggleTextTool() {
|
|||||||
}
|
}
|
||||||
if (openSeq !== state.openSeq) return;
|
if (openSeq !== state.openSeq) return;
|
||||||
const style = canEditObject(canvasEditor.selected, "layoutKnown") ? currentTextStyle() : state.textDefaults;
|
const style = canEditObject(canvasEditor.selected, "layoutKnown") ? currentTextStyle() : state.textDefaults;
|
||||||
const font = state.textFonts[Number(el.textFont.value)];
|
const font = state.textFonts[Number(fontPicker.value)];
|
||||||
state.textDefaults = { type: "TextObject", size: style.size, color: style.color, wrap: style.wrap,
|
state.textDefaults = { type: "TextObject", size: style.size, color: style.color, wrap: style.wrap,
|
||||||
align: style.align || "left", paragraphHeight: style.paragraphHeight || 0, letterSpacing: style.letterSpacing || 0,
|
align: style.align || "left", paragraphHeight: style.paragraphHeight || 0, letterSpacing: style.letterSpacing || 0,
|
||||||
fontChoice: font && !font.disabled ? font : state.textDefaults.fontChoice };
|
fontChoice: font && !font.disabled ? font : state.textDefaults.fontChoice };
|
||||||
@@ -953,7 +956,7 @@ async function toggleTextTool() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function beginCanvasText(index, box, surface, page) {
|
async function beginCanvasText(index, box, surface, page) {
|
||||||
const font = state.textDefaults.fontChoice || state.textFonts[Number(el.textFont.value)];
|
const font = state.textDefaults.fontChoice || state.textFonts[Number(fontPicker.value)];
|
||||||
if (!font || font.disabled) {
|
if (!font || font.disabled) {
|
||||||
setStatus("尚未添加字体");
|
setStatus("尚未添加字体");
|
||||||
el.textFontAdd.focus();
|
el.textFontAdd.focus();
|
||||||
@@ -984,19 +987,6 @@ function editorFonts(item) {
|
|||||||
return fonts;
|
return fonts;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFontOptions(select, fonts, selected) {
|
|
||||||
const key = (font) => font?.id || font?.postscriptName;
|
|
||||||
select.replaceChildren();
|
|
||||||
fonts.forEach((font, index) => {
|
|
||||||
const option = document.createElement("option");
|
|
||||||
option.value = String(index);
|
|
||||||
option.textContent = font.fullName || font.name;
|
|
||||||
option.disabled = Boolean(font.disabled);
|
|
||||||
select.append(option);
|
|
||||||
});
|
|
||||||
select.value = fonts.length ? String(Math.max(0, fonts.findIndex(font => key(font) === key(selected)))) : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTextFonts(item, refresh = false) {
|
function updateTextFonts(item, refresh = false) {
|
||||||
const id = item?.type === "TextObject" && !item.draft ? item.font : null;
|
const id = item?.type === "TextObject" && !item.draft ? item.font : null;
|
||||||
if (refresh || state.textFontID !== id) {
|
if (refresh || state.textFontID !== id) {
|
||||||
@@ -1007,14 +997,14 @@ function updateTextFonts(item, refresh = false) {
|
|||||||
&& !state.textFonts.some(font => (font.id || font.postscriptName) === (selected.id || selected.postscriptName))) {
|
&& !state.textFonts.some(font => (font.id || font.postscriptName) === (selected.id || selected.postscriptName))) {
|
||||||
state.textFonts.unshift(selected);
|
state.textFonts.unshift(selected);
|
||||||
}
|
}
|
||||||
setFontOptions(el.textFont, state.textFonts, selected || state.textFonts[0]);
|
fontPicker.setFonts(state.textFonts, selected || state.textFonts[0]);
|
||||||
if (!id) state.textDefaults.fontChoice = state.textFonts[Number(el.textFont.value)];
|
if (!id) state.textDefaults.fontChoice = state.textFonts[Number(fontPicker.value)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeTextFont() {
|
async function changeTextFont() {
|
||||||
const item = canvasEditor.selected;
|
const item = canvasEditor.selected;
|
||||||
const font = state.textFonts[Number(el.textFont.value)];
|
const font = state.textFonts[Number(fontPicker.value)];
|
||||||
if (!state.ready || document.body.hasAttribute("aria-busy") || !font || font.disabled) return;
|
if (!state.ready || document.body.hasAttribute("aria-busy") || !font || font.disabled) return;
|
||||||
const editing = canvasEditor.input;
|
const editing = canvasEditor.input;
|
||||||
if (editing) {
|
if (editing) {
|
||||||
@@ -2992,7 +2982,7 @@ async function pasteEditorContent(event) {
|
|||||||
}
|
}
|
||||||
const value = event.clipboardData.getData("text/plain").replace(/\t/g, " ");
|
const value = event.clipboardData.getData("text/plain").replace(/\t/g, " ");
|
||||||
if (!value.trim()) return;
|
if (!value.trim()) return;
|
||||||
const font = state.textFonts[Number(el.textFont.value)];
|
const font = state.textFonts[Number(fontPicker.value)];
|
||||||
if (!font || font.disabled) {
|
if (!font || font.disabled) {
|
||||||
setStatus("尚未添加字体");
|
setStatus("尚未添加字体");
|
||||||
el.textFontAdd.focus();
|
el.textFontAdd.focus();
|
||||||
@@ -4459,7 +4449,7 @@ function updateObjectControls(item, reset = false) {
|
|||||||
el.multiSelectButton.disabled = !state.editing || !canvasEditor.enabled || !state.ready || state.exporting;
|
el.multiSelectButton.disabled = !state.editing || !canvasEditor.enabled || !state.ready || state.exporting;
|
||||||
const text = item?.type === "TextObject" ? item : state.textDefaults;
|
const text = item?.type === "TextObject" ? item : state.textDefaults;
|
||||||
const textDisabled = !state.editing || !state.ready || state.exporting || Boolean(item?.items);
|
const textDisabled = !state.editing || !state.ready || state.exporting || Boolean(item?.items);
|
||||||
el.textFont.disabled = textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "replaceFont");
|
fontPicker.setDisabled(textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "replaceFont"));
|
||||||
el.textSize.disabled = textDisabled || Boolean(item?.draft) || text !== state.textDefaults && !canEditObject(item, "reflow");
|
el.textSize.disabled = textDisabled || Boolean(item?.draft) || text !== state.textDefaults && !canEditObject(item, "reflow");
|
||||||
el.textColor.disabled = textDisabled || Boolean(item?.draft) || text !== state.textDefaults && !canEditObject(item, "update");
|
el.textColor.disabled = textDisabled || Boolean(item?.draft) || text !== state.textDefaults && !canEditObject(item, "update");
|
||||||
el.textFontAdd.disabled = !state.editing || !state.ready || state.exporting;
|
el.textFontAdd.disabled = !state.editing || !state.ready || state.exporting;
|
||||||
@@ -4495,12 +4485,12 @@ function updateObjectControls(item, reset = false) {
|
|||||||
}
|
}
|
||||||
updateDrawingControls();
|
updateDrawingControls();
|
||||||
const members = item?.items || (item ? [item] : []);
|
const members = item?.items || (item ? [item] : []);
|
||||||
const orders = members.map(member => member.order).sort((a, b) => a - b);
|
const orders = members.map(member => member.position).sort((a, b) => a - b);
|
||||||
const count = members[0]?.count || 0;
|
const count = members[0]?.count || 0;
|
||||||
const canRaise = orders.some((order, i) => order !== count - orders.length + i);
|
const canRaise = orders.some((order, i) => order !== count - orders.length + i);
|
||||||
const canLower = orders.some((order, i) => order !== i);
|
const canLower = orders.some((order, i) => order !== i);
|
||||||
el.objectOrder.disabled = disabled || cropping || count <= orders.length || !canEditObject(item, "order")
|
el.objectOrder.disabled = disabled || cropping || count <= orders.length || !canEditObject(item, "order")
|
||||||
|| members.some(member => member.layer !== members[0].layer);
|
|| members.some(member => member.container !== members[0].container);
|
||||||
for (const option of el.objectOrder.options) {
|
for (const option of el.objectOrder.options) {
|
||||||
option.disabled = el.objectOrder.disabled || (["up", "top"].includes(option.value) ? !canRaise : !canLower);
|
option.disabled = el.objectOrder.disabled || (["up", "top"].includes(option.value) ? !canRaise : !canLower);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,9 +156,9 @@ export class CanvasEditor {
|
|||||||
viewer.addEventListener("lostpointercapture", () => this.cancel());
|
viewer.addEventListener("lostpointercapture", () => this.cancel());
|
||||||
viewer.addEventListener("keydown", (event) => this.keyDown(event));
|
viewer.addEventListener("keydown", (event) => this.keyDown(event));
|
||||||
viewer.addEventListener("keyup", (event) => this.modifierChange(event));
|
viewer.addEventListener("keyup", (event) => this.modifierChange(event));
|
||||||
viewer.addEventListener("dblclick", () => {
|
viewer.addEventListener("dblclick", (event) => {
|
||||||
const item = this.selected;
|
const item = this.selected;
|
||||||
if (this.enabled && item && !item.items && !this.input && !this.options.busy()) {
|
if (!event.altKey && this.enabled && item && !item.items && !this.input && !this.options.busy()) {
|
||||||
this.options.onEdit(item);
|
this.options.onEdit(item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -361,6 +361,10 @@ export class CanvasEditor {
|
|||||||
this.startShape(event);
|
this.startShape(event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.target.dataset.corner) {
|
||||||
|
this.cycleSelection(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const node = event.target.closest(".edit-object, .edit-selection");
|
const node = event.target.closest(".edit-object, .edit-selection");
|
||||||
if (node) {
|
if (node) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -401,6 +405,19 @@ export class CanvasEditor {
|
|||||||
this.viewer.setPointerCapture(event.pointerId);
|
this.viewer.setPointerCapture(event.pointerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cycleSelection(event) {
|
||||||
|
const surface = event.target.closest(".page-surface");
|
||||||
|
const page = this.pages.get(surface);
|
||||||
|
if (!page) return;
|
||||||
|
const point = pagePoint(event.clientX, event.clientY, surface.getBoundingClientRect(), page, this.options.rotation());
|
||||||
|
const items = [...surface.querySelectorAll(".edit-object")].map(node => this.nodes.get(node)).reverse()
|
||||||
|
.filter(item => point.x >= item.x && point.x <= item.x + item.width && point.y >= item.y && point.y <= item.y + item.height);
|
||||||
|
if (!items.length) return;
|
||||||
|
event.preventDefault();
|
||||||
|
this.viewer.focus({ preventScroll: true });
|
||||||
|
this.select(items[(items.indexOf(this.selected) + 1) % items.length]);
|
||||||
|
}
|
||||||
|
|
||||||
move(event) {
|
move(event) {
|
||||||
const drag = this.drag;
|
const drag = this.drag;
|
||||||
if (!drag || drag.pointerID !== event.pointerId) {
|
if (!drag || drag.pointerID !== event.pointerId) {
|
||||||
|
|||||||
@@ -1,5 +1,141 @@
|
|||||||
const DATABASE = "ofdgo";
|
const DATABASE = "ofdgo";
|
||||||
|
|
||||||
|
export class FontPicker {
|
||||||
|
constructor(input, toggle, list, onChange) {
|
||||||
|
Object.assign(this, { input, toggle, list, onChange, fonts: [], value: "", active: -1, open: false });
|
||||||
|
input.addEventListener("input", () => this.show(input.value));
|
||||||
|
input.addEventListener("focus", () => input.select());
|
||||||
|
input.addEventListener("click", () => { if (!this.open) this.show(); });
|
||||||
|
input.addEventListener("keydown", event => this.keyDown(event));
|
||||||
|
input.addEventListener("blur", () => { this.commitExact(); this.close(); });
|
||||||
|
toggle.addEventListener("pointerdown", event => event.preventDefault());
|
||||||
|
toggle.addEventListener("click", () => {
|
||||||
|
if (this.open) this.close();
|
||||||
|
else { input.focus(); this.show(); }
|
||||||
|
});
|
||||||
|
list.addEventListener("pointerdown", event => {
|
||||||
|
if (event.target.closest("[role=option]")) event.preventDefault();
|
||||||
|
});
|
||||||
|
list.addEventListener("click", event => {
|
||||||
|
const option = event.target.closest("[role=option]");
|
||||||
|
if (option) this.choose(Number(option.dataset.index));
|
||||||
|
});
|
||||||
|
window.addEventListener("resize", () => this.close());
|
||||||
|
}
|
||||||
|
|
||||||
|
setFonts(fonts, selected) {
|
||||||
|
this.close();
|
||||||
|
this.fonts = fonts;
|
||||||
|
const key = font => font?.id || font?.postscriptName;
|
||||||
|
this.value = fonts.length ? String(Math.max(0, fonts.findIndex(font => key(font) === key(selected)))) : "";
|
||||||
|
this.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisabled(disabled) {
|
||||||
|
this.input.disabled = this.toggle.disabled = disabled;
|
||||||
|
if (disabled) this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
restore() {
|
||||||
|
const font = this.fonts[Number(this.value)];
|
||||||
|
this.input.value = font?.fullName || font?.name || "";
|
||||||
|
this.input.setAttribute("aria-expanded", String(this.open));
|
||||||
|
this.input.removeAttribute("aria-activedescendant");
|
||||||
|
}
|
||||||
|
|
||||||
|
show(query = "") {
|
||||||
|
if (this.input.disabled) return;
|
||||||
|
const normalize = value => value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/g, " ").trim();
|
||||||
|
const terms = normalize(query).split(" ").filter(Boolean);
|
||||||
|
this.matches = this.fonts.map((font, index) => ({ font, index })).filter(({ font }) => {
|
||||||
|
const name = normalize([font.fullName, font.name, font.family, font.postscriptName].filter(Boolean).join(" "));
|
||||||
|
return terms.every(term => name.includes(term));
|
||||||
|
});
|
||||||
|
this.list.replaceChildren();
|
||||||
|
for (const { font, index } of this.matches) {
|
||||||
|
const option = document.createElement("div");
|
||||||
|
option.id = `${this.list.id}-${index}`;
|
||||||
|
option.dataset.index = String(index);
|
||||||
|
option.setAttribute("role", "option");
|
||||||
|
option.setAttribute("aria-disabled", String(Boolean(font.disabled)));
|
||||||
|
option.textContent = font.fullName || font.name;
|
||||||
|
this.list.append(option);
|
||||||
|
}
|
||||||
|
if (!this.matches.length) {
|
||||||
|
const empty = document.createElement("div");
|
||||||
|
empty.className = "font-search-empty";
|
||||||
|
empty.textContent = "无匹配字体";
|
||||||
|
this.list.append(empty);
|
||||||
|
}
|
||||||
|
if (!this.open) this.list.showPopover();
|
||||||
|
this.open = true;
|
||||||
|
this.input.setAttribute("aria-expanded", "true");
|
||||||
|
this.position();
|
||||||
|
const selected = this.matches.findIndex(({ font, index }) => !font.disabled && String(index) === this.value);
|
||||||
|
this.activate(selected < 0 ? this.matches.findIndex(({ font }) => !font.disabled) : selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
position() {
|
||||||
|
if (!this.open) return;
|
||||||
|
const rect = this.input.getBoundingClientRect();
|
||||||
|
if (rect.right <= 0 || rect.left >= window.innerWidth) { this.close(); return; }
|
||||||
|
const width = Math.min(Math.max(rect.width, 260), window.innerWidth - 16);
|
||||||
|
Object.assign(this.list.style, { left: `${Math.max(8, Math.min(rect.left, window.innerWidth - width - 8))}px`,
|
||||||
|
top: `${rect.bottom + 4}px`, width: `${width}px`, maxHeight: `${Math.max(40, Math.min(280, window.innerHeight - rect.bottom - 12))}px` });
|
||||||
|
}
|
||||||
|
|
||||||
|
activate(index) {
|
||||||
|
this.active = index;
|
||||||
|
[...this.list.children].forEach((option, i) => option.setAttribute("aria-selected", String(i === index)));
|
||||||
|
const option = this.list.children[index];
|
||||||
|
if (option) {
|
||||||
|
this.input.setAttribute("aria-activedescendant", option.id);
|
||||||
|
option.scrollIntoView({ block: "nearest" });
|
||||||
|
} else this.input.removeAttribute("aria-activedescendant");
|
||||||
|
}
|
||||||
|
|
||||||
|
choose(index) {
|
||||||
|
if (!this.fonts[index] || this.fonts[index].disabled) return;
|
||||||
|
const changed = this.value !== String(index);
|
||||||
|
this.value = String(index);
|
||||||
|
this.close();
|
||||||
|
if (changed) return this.onChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
commitExact() {
|
||||||
|
const value = this.input.value.trim().toLocaleLowerCase();
|
||||||
|
const index = this.fonts.findIndex(font => !font.disabled
|
||||||
|
&& [font.fullName, font.name, font.postscriptName].some(name => name?.toLocaleLowerCase() === value));
|
||||||
|
if (index >= 0) this.choose(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (this.open) { this.open = false; this.list.hidePopover(); }
|
||||||
|
this.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
keyDown(event) {
|
||||||
|
if (event.isComposing) return;
|
||||||
|
if (!["Escape", "Enter", "ArrowDown", "ArrowUp", "Tab"].includes(event.key)) return;
|
||||||
|
event.stopPropagation();
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault();
|
||||||
|
this.close();
|
||||||
|
} else if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.open && this.active >= 0) this.choose(this.matches[this.active].index);
|
||||||
|
else { this.commitExact(); this.close(); }
|
||||||
|
} else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!this.open) { this.show(); return; }
|
||||||
|
const direction = event.key === "ArrowDown" ? 1 : -1;
|
||||||
|
for (let i = this.active + direction; i >= 0 && i < this.matches.length; i += direction) {
|
||||||
|
if (!this.matches[i].font.disabled) { this.activate(i); break; }
|
||||||
|
}
|
||||||
|
} else if (event.key === "Tab") { this.commitExact(); this.close(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class FontManager {
|
export class FontManager {
|
||||||
constructor({ onChange, onPermissionChange }) {
|
constructor({ onChange, onPermissionChange }) {
|
||||||
this.userFonts = [];
|
this.userFonts = [];
|
||||||
|
|||||||
@@ -662,7 +662,11 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if box.W > 0 && box.H > 0 {
|
if box.W > 0 && box.H > 0 {
|
||||||
item := map[string]any{"id": id, "type": object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": order, "count": len(layer.Objects), "layer": layer.ID,
|
position, err := currentEditor.ObjectPosition(index, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
item := map[string]any{"id": id, "type": object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": order, "position": position.Index, "count": position.Count, "container": position.Container,
|
||||||
"capabilities": map[string]any{"update": capability.Update, "replaceFont": capability.ReplaceFont, "reflow": capability.Reflow, "layoutKnown": capability.LayoutKnown, "transform": capability.Transform, "arrange": capability.Arrange, "copy": capability.Copy, "delete": capability.Delete, "order": capability.Order, "reason": capability.Reason}}
|
"capabilities": map[string]any{"update": capability.Update, "replaceFont": capability.ReplaceFont, "reflow": capability.Reflow, "layoutKnown": capability.LayoutKnown, "transform": capability.Transform, "arrange": capability.Arrange, "copy": capability.Copy, "delete": capability.Delete, "order": capability.Order, "reason": capability.Reason}}
|
||||||
if object.Type == "ImageObject" && capability.Update {
|
if object.Type == "ImageObject" && capability.Update {
|
||||||
full, err := object.ImageObject.ImageBounds()
|
full, err := object.ImageObject.ImageBounds()
|
||||||
|
|||||||
+13
-4
@@ -428,15 +428,17 @@ func (e *Editor) DeleteObject(page int, id string) error {
|
|||||||
return e.DeleteObjects(page, []string{id})
|
return e.DeleteObjects(page, []string{id})
|
||||||
}
|
}
|
||||||
|
|
||||||
// MoveObject 调整同页正文图层内的绘制顺序,保留对象标识和内容
|
// MoveObject 调整同一图层或末级页块内的绘制顺序,保留对象标识和内容
|
||||||
// 入参: page 页面索引, id 对象标识, to 移动后的对象索引,0为最底层,末尾为最顶层
|
// 入参: page 页面索引, id 对象标识, to 直接容器内的对象索引,0为最底层,末尾为最顶层
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) MoveObject(page int, id string, to int) error {
|
func (e *Editor) MoveObject(page int, id string, to int) error {
|
||||||
layer, from, err := e.findObject(page, id)
|
layer, from, err := e.findObject(page, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if to < 0 || to >= len(layer.Objects) {
|
_, siblings := e.objectOrderIndexes(page, layer, id)
|
||||||
|
from = slices.Index(siblings, from)
|
||||||
|
if to < 0 || to >= len(siblings) {
|
||||||
return fmt.Errorf("object index %d out of range", to)
|
return fmt.Errorf("object index %d out of range", to)
|
||||||
}
|
}
|
||||||
if from == to {
|
if from == to {
|
||||||
@@ -452,7 +454,14 @@ func (e *Editor) MoveObject(page int, id string, to int) error {
|
|||||||
layers := copyEditorPage(e.pages[page]).Content.Layer
|
layers := copyEditorPage(e.pages[page]).Content.Layer
|
||||||
for i := range layers {
|
for i := range layers {
|
||||||
if layers[i].ID == layer.ID {
|
if layers[i].ID == layer.ID {
|
||||||
moveEditorItem(layers[i].Objects, from, to)
|
objects := make([]GraphicObject, len(siblings))
|
||||||
|
for j, index := range siblings {
|
||||||
|
objects[j] = layer.Objects[index]
|
||||||
|
}
|
||||||
|
moveEditorItem(objects, from, to)
|
||||||
|
for j, index := range siblings {
|
||||||
|
layers[i].Objects[index] = objects[j]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
e.replaceLayers(page, layers)
|
e.replaceLayers(page, layers)
|
||||||
|
|||||||
+65
-6
@@ -132,7 +132,59 @@ func (e *Editor) CopyObjects(page int, objects []GraphicObject, dx, dy float64)
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OrderObjects 调整同页选区的绘制顺序,保留选中及未选中对象各自的相对顺序。
|
// ObjectPosition 对象在图层或页块直接成员中的位置,Index从0开始,Count不含嵌套页块的对象。
|
||||||
|
type ObjectPosition struct {
|
||||||
|
Layer string
|
||||||
|
Container string
|
||||||
|
Index int
|
||||||
|
Count int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectPosition 获取对象的直接容器及排序位置。
|
||||||
|
// 入参: page 页面索引, id 对象标识
|
||||||
|
// 返回: ObjectPosition 对象位置, error 错误信息
|
||||||
|
func (e *Editor) ObjectPosition(page int, id string) (ObjectPosition, error) {
|
||||||
|
layer, _, err := e.findObject(page, id)
|
||||||
|
if err != nil {
|
||||||
|
return ObjectPosition{}, err
|
||||||
|
}
|
||||||
|
parent, indexes := e.objectOrderIndexes(page, layer, id)
|
||||||
|
position := ObjectPosition{Layer: layer.ID, Container: layer.ID, Count: len(indexes)}
|
||||||
|
if parent != nil {
|
||||||
|
position.Container = parent.attr("ID")
|
||||||
|
}
|
||||||
|
for i, index := range indexes {
|
||||||
|
if editorObjectID(layer.Objects[index]) == id {
|
||||||
|
position.Index = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return position, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// objectOrderIndexes 获取同一直接容器的对象索引,不跨越图层或页块。
|
||||||
|
// 入参: page 页面索引, layer 图层, id 对象标识
|
||||||
|
// 返回: *editorXML 原始容器, []int 对象索引
|
||||||
|
func (e *Editor) objectOrderIndexes(page int, layer *Layer, id string) (*editorXML, []int) {
|
||||||
|
var nodes map[string]*editorXML
|
||||||
|
var parent *editorXML
|
||||||
|
if e.originalPage(page) {
|
||||||
|
nodes = e.source.pages[e.pages[page].ID].nodes
|
||||||
|
if node := nodes[id]; node != nil {
|
||||||
|
parent = node.parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var indexes []int
|
||||||
|
for i, object := range layer.Objects {
|
||||||
|
node := nodes[editorObjectID(object)]
|
||||||
|
if node == nil && parent == nil || node != nil && node.parent == parent {
|
||||||
|
indexes = append(indexes, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parent, indexes
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrderObjects 调整同一图层或页块内选区的绘制顺序,保留各组选中及未选中对象的相对顺序。
|
||||||
// 入参: page 页面索引, ids 对象标识, order 为up、down、top或bottom
|
// 入参: page 页面索引, ids 对象标识, order 为up、down、top或bottom
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) OrderObjects(page int, ids []string, order string) error {
|
func (e *Editor) OrderObjects(page int, ids []string, order string) error {
|
||||||
@@ -143,20 +195,25 @@ func (e *Editor) OrderObjects(page int, ids []string, order string) error {
|
|||||||
if err != nil || len(indexes) == 0 {
|
if err != nil || len(indexes) == 0 {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
layer := &e.pages[page].Content.Layer[indexes[0].layer]
|
||||||
|
_, siblings := e.objectOrderIndexes(page, layer, ids[0])
|
||||||
for i, id := range ids {
|
for i, id := range ids {
|
||||||
capability, err := e.ObjectCapabilities(page, id)
|
capability, err := e.ObjectCapabilities(page, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !capability.Order || indexes[i].layer != indexes[0].layer {
|
if !capability.Order || indexes[i].layer != indexes[0].layer || !slices.Contains(siblings, indexes[i].index) {
|
||||||
return fmt.Errorf("ordering requires editable objects in the same layer")
|
return fmt.Errorf("ordering requires editable objects in the same container")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
layers := copyEditorPage(e.pages[page]).Content.Layer
|
layers := copyEditorPage(e.pages[page]).Content.Layer
|
||||||
objects := layers[indexes[0].layer].Objects
|
objects := make([]GraphicObject, len(siblings))
|
||||||
|
for i, index := range siblings {
|
||||||
|
objects[i] = layer.Objects[index]
|
||||||
|
}
|
||||||
selected := make([]bool, len(objects))
|
selected := make([]bool, len(objects))
|
||||||
for _, index := range indexes {
|
for _, index := range indexes {
|
||||||
selected[index.index] = true
|
selected[slices.Index(siblings, index.index)] = true
|
||||||
}
|
}
|
||||||
switch order {
|
switch order {
|
||||||
case "up":
|
case "up":
|
||||||
@@ -184,7 +241,9 @@ func (e *Editor) OrderObjects(page int, ids []string, order string) error {
|
|||||||
}
|
}
|
||||||
objects = ordered
|
objects = ordered
|
||||||
}
|
}
|
||||||
layers[indexes[0].layer].Objects = objects
|
for i, index := range siblings {
|
||||||
|
layers[indexes[0].layer].Objects[index] = objects[i]
|
||||||
|
}
|
||||||
e.replaceLayers(page, layers)
|
e.replaceLayers(page, layers)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/tdewolff/font"
|
||||||
|
)
|
||||||
|
|
||||||
|
// subsetFonts 收集新增字体实际使用的字形,仅裁剪保存结果,不修改编辑资源或原文档字体。
|
||||||
|
// 返回: map[string][]byte 包内字体子集, error 错误信息
|
||||||
|
func (e *Editor) subsetFonts() (map[string][]byte, error) {
|
||||||
|
used := make(map[string]map[uint16]bool)
|
||||||
|
for _, resource := range e.resources {
|
||||||
|
if resource.font == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sfnt := e.fonts[resource.font.ID]
|
||||||
|
if sfnt.IsTrueType && !slices.ContainsFunc([]string{"fvar", "COLR", "CBDT", "sbix", "SVG "}, func(tag string) bool { return sfnt.Tables[tag] != nil }) {
|
||||||
|
used[resource.font.ID] = make(map[uint16]bool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, page := range e.pages {
|
||||||
|
for _, layer := range page.Content.Layer {
|
||||||
|
for _, object := range layer.Objects {
|
||||||
|
if object.Type != "TextObject" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
obj := object.TextObject
|
||||||
|
glyphs := used[obj.Font]
|
||||||
|
if glyphs == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, code := range obj.TextCode {
|
||||||
|
for _, char := range textCodeRunes(code.Value) {
|
||||||
|
glyphs[e.fonts[obj.Font].GlyphIndex(char)] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result := make(map[string][]byte)
|
||||||
|
for _, resource := range e.resources {
|
||||||
|
if resource.font == nil || len(used[resource.font.ID]) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
glyphs := used[resource.font.ID]
|
||||||
|
glyphs[0] = true
|
||||||
|
ids := make([]uint16, 0, len(glyphs))
|
||||||
|
for id := range glyphs {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
slices.Sort(ids)
|
||||||
|
data, err := subsetEditorFont(resource.data, ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("subset font %s: %w", resource.font.FontName, err)
|
||||||
|
}
|
||||||
|
if len(data) < len(resource.data) {
|
||||||
|
result[resource.name] = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// subsetEditorFont 裁剪TrueType字形及复合依赖,保留字符映射、名称、度量和提示指令。
|
||||||
|
// 入参: data 字体数据, glyphs 已排序的字形编号,包含0
|
||||||
|
// 返回: []byte 字体子集, error 错误信息
|
||||||
|
func subsetEditorFont(data []byte, glyphs []uint16) ([]byte, error) {
|
||||||
|
sfnt, err := font.ParseSFNT(bytes.Clone(data), 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
subset, err := sfnt.Subset(glyphs, font.SubsetOptions{Tables: []string{
|
||||||
|
"cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "glyf", "loca", "cvt ", "fpgm", "prep", "gasp",
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
subset.Tables["name"] = sfnt.Tables["name"]
|
||||||
|
clear(subset.Tables["head"][8:12])
|
||||||
|
return serializeOTF(subset.Tables)
|
||||||
|
}
|
||||||
+8
-18
@@ -57,7 +57,7 @@ type editorSourcePage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ObjectCapabilities 已有对象可执行的操作,Reason说明受限原因。
|
// ObjectCapabilities 已有对象可执行的操作,Reason说明受限原因。
|
||||||
// Update表示完整替换内容,ReplaceFont表示可显式替换文字字体,Transform表示保留文字定位的几何变换,Order仅限当前图层。
|
// Update表示完整替换内容,ReplaceFont表示可显式替换文字字体,Transform表示保留文字定位的几何变换,Order仅限同一图层或末级页块。
|
||||||
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复。
|
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复。
|
||||||
type ObjectCapabilities struct {
|
type ObjectCapabilities struct {
|
||||||
Update bool
|
Update bool
|
||||||
@@ -348,7 +348,7 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
|
|||||||
if err := validateEditorGeometry(object); err != nil {
|
if err := validateEditorGeometry(object); err != nil {
|
||||||
return ObjectCapabilities{Reason: err.Error()}, nil
|
return ObjectCapabilities{Reason: err.Error()}, nil
|
||||||
}
|
}
|
||||||
all.Order = e.sourceLayerOrderable(page, layer.ID)
|
all.Order = editorContainerOrderable(node.parent)
|
||||||
if _, err := e.prepareObject(id, object); err != nil {
|
if _, err := e.prepareObject(id, object); err != nil {
|
||||||
all.Update, all.Reflow, all.Copy, all.Reason = false, false, false, err.Error()
|
all.Update, all.Reflow, all.Copy, all.Reason = false, false, false, err.Error()
|
||||||
}
|
}
|
||||||
@@ -375,23 +375,13 @@ func (e *Editor) objectOrigin(id string) *editorObjectOrigin {
|
|||||||
return e.source.origins[id]
|
return e.source.origins[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
// sourceLayerOrderable 判断图层是否仅含可独立排序的直接对象。
|
// editorContainerOrderable 判断容器的直接对象能否在保留页块的前提下排序。
|
||||||
// 入参: page 页面索引, id 图层标识
|
// 入参: container 图层或页块
|
||||||
// 返回: bool 是否可排序
|
// 返回: bool 是否可排序
|
||||||
func (e *Editor) sourceLayerOrderable(page int, id string) bool {
|
func editorContainerOrderable(container *editorXML) bool {
|
||||||
if !e.originalPage(page) {
|
for _, child := range container.children {
|
||||||
return true
|
if !editorXMLSupported(child) {
|
||||||
}
|
return false
|
||||||
content := e.source.pages[e.pages[page].ID].root.child("Content")
|
|
||||||
if content != nil {
|
|
||||||
for _, layer := range content.children {
|
|
||||||
if layer.attr("ID") == id {
|
|
||||||
for _, child := range layer.children {
|
|
||||||
if !editorXMLSupported(child) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
+17
-11
@@ -214,17 +214,20 @@ func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, err
|
|||||||
for _, object := range original.Objects {
|
for _, object := range original.Objects {
|
||||||
before[editorObjectID(object)] = object
|
before[editorObjectID(object)] = object
|
||||||
}
|
}
|
||||||
|
ordered := make(map[*editorXML][]GraphicObject)
|
||||||
after := make(map[string]GraphicObject)
|
after := make(map[string]GraphicObject)
|
||||||
var ordered []GraphicObject
|
|
||||||
for _, object := range current.Objects {
|
for _, object := range current.Objects {
|
||||||
id := editorObjectID(object)
|
id := editorObjectID(object)
|
||||||
after[id] = object
|
after[id] = object
|
||||||
if source.nodes[id] != nil {
|
if node := source.nodes[id]; node != nil {
|
||||||
ordered = append(ordered, object)
|
ordered[node.parent] = append(ordered[node.parent], object)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
orderable := e.sourceLayerOrderable(index, original.ID)
|
positions := make(map[*editorXML]int)
|
||||||
position := 0
|
orderable := make(map[*editorXML]bool)
|
||||||
|
for parent := range ordered {
|
||||||
|
orderable[parent] = editorContainerOrderable(parent)
|
||||||
|
}
|
||||||
for _, object := range original.Objects {
|
for _, object := range original.Objects {
|
||||||
id := editorObjectID(object)
|
id := editorObjectID(object)
|
||||||
node := source.nodes[id]
|
node := source.nodes[id]
|
||||||
@@ -232,12 +235,14 @@ func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, err
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
next, exists := after[id]
|
next, exists := after[id]
|
||||||
if orderable {
|
if orderable[node.parent] {
|
||||||
exists = position < len(ordered)
|
position := positions[node.parent]
|
||||||
|
members := ordered[node.parent]
|
||||||
|
exists = position < len(members)
|
||||||
if exists {
|
if exists {
|
||||||
next = ordered[position]
|
next = members[position]
|
||||||
}
|
}
|
||||||
position++
|
positions[node.parent]++
|
||||||
}
|
}
|
||||||
var encoded []byte
|
var encoded []byte
|
||||||
if exists {
|
if exists {
|
||||||
@@ -369,13 +374,14 @@ func (e *Editor) sourceInfoXML() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// writeSource 将未修改ZIP条目直接复制到新包,逐项写入改动,不持有原资源解压副本。
|
// writeSource 将未修改ZIP条目直接复制到新包,逐项写入改动,不持有原资源解压副本。
|
||||||
// 入参: writer 输出流
|
// 入参: writer 输出流, fonts 新增字体子集
|
||||||
// 返回: int64 写入字节数, error 错误信息
|
// 返回: int64 写入字节数, error 错误信息
|
||||||
func (e *Editor) writeSource(writer io.Writer) (int64, error) {
|
func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte) (int64, error) {
|
||||||
parts, err := e.sourceParts()
|
parts, err := e.sourceParts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
maps.Copy(parts, fonts)
|
||||||
reader := e.source.reader
|
reader := e.source.reader
|
||||||
remaining := maps.Clone(reader.files)
|
remaining := maps.Clone(reader.files)
|
||||||
if remaining == nil {
|
if remaining == nil {
|
||||||
|
|||||||
+13
-4
@@ -29,19 +29,28 @@ import (
|
|||||||
|
|
||||||
const ofdNamespace = "http://www.ofdspec.org/2016"
|
const ofdNamespace = "http://www.ofdspec.org/2016"
|
||||||
|
|
||||||
// WriteTo 逐个条目写出OFD,仅包含实际引用的资源,不关闭调用方输出流,出错时应丢弃本次输出
|
// WriteTo 逐个条目写出OFD,不关闭调用方输出流,出错时应丢弃本次输出
|
||||||
|
// 新增资源仅写入实际引用的部分,静态TrueType轮廓字体按实际文字生成子集;编辑资源、原文档字体、可变及彩色字体等保持原样。
|
||||||
|
// 重开子集文档后输入未包含的文字,需要通过AddFont注册完整字体并替换原字体引用。
|
||||||
// 入参: writer 输出流
|
// 入参: writer 输出流
|
||||||
// 返回: int64 已写入字节数, error 错误信息
|
// 返回: int64 已写入字节数, error 错误信息
|
||||||
func (e *Editor) WriteTo(writer io.Writer) (int64, error) {
|
func (e *Editor) WriteTo(writer io.Writer) (int64, error) {
|
||||||
if err := e.validate(); err != nil {
|
if err := e.validate(); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
fonts, err := e.subsetFonts()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
if e.source != nil {
|
if e.source != nil {
|
||||||
return e.writeSource(writer)
|
return e.writeSource(writer, fonts)
|
||||||
}
|
}
|
||||||
output := &ofdCountingWriter{writer: writer}
|
output := &ofdCountingWriter{writer: writer}
|
||||||
archive := zip.NewWriter(output)
|
archive := zip.NewWriter(output)
|
||||||
err := e.writeParts(func(name string, data []byte, compressed bool) error {
|
err = e.writeParts(func(name string, data []byte, compressed bool) error {
|
||||||
|
if subset, ok := fonts[name]; ok {
|
||||||
|
data = subset
|
||||||
|
}
|
||||||
method := uint16(zip.Deflate)
|
method := uint16(zip.Deflate)
|
||||||
if compressed {
|
if compressed {
|
||||||
method = zip.Store
|
method = zip.Store
|
||||||
@@ -60,7 +69,7 @@ func (e *Editor) WriteTo(writer io.Writer) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reader 获取当前文档的独立内存快照,不进行ZIP压缩,后续修改不影响已有快照
|
// Reader 获取当前文档的独立内存快照,不进行ZIP压缩,后续修改不影响已有快照
|
||||||
// 返回的Reader可用于现有渲染、搜索和导出接口,仅包含实际引用的资源,二进制数据内部共享只读
|
// 返回的Reader可用于现有渲染、搜索和导出接口,新增资源仅包含实际引用的部分,二进制数据内部共享只读
|
||||||
// 返回: *Reader 阅读器, error 错误信息
|
// 返回: *Reader 阅读器, error 错误信息
|
||||||
func (e *Editor) Reader() (*Reader, error) {
|
func (e *Editor) Reader() (*Reader, error) {
|
||||||
if err := e.validate(); err != nil {
|
if err := e.validate(); err != nil {
|
||||||
|
|||||||
Reference in new issue
Block a user