mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
1 parent
8a1caf7952
commit
0aed2a5609
8 files changed
+246
-156
No files matched your search
@@ -132,6 +132,7 @@
|
|||||||
</select>
|
</select>
|
||||||
<label class="editor-field"><span>行距</span><span class="editor-number"><input id="textLineHeight" type="number" min="0" step="any" placeholder="自动" aria-label="行距" title="行距,0为自动" disabled><span>pt</span></span></label>
|
<label class="editor-field"><span>行距</span><span class="editor-number"><input id="textLineHeight" type="number" min="0" step="any" placeholder="自动" aria-label="行距" title="行距,0为自动" disabled><span>pt</span></span></label>
|
||||||
<button id="textWrap" class="button" type="button" aria-label="自动折行" aria-pressed="false" disabled>折行</button>
|
<button id="textWrap" class="button" type="button" aria-label="自动折行" aria-pressed="false" disabled>折行</button>
|
||||||
|
<button id="textReflow" class="button" type="button" title="重新排版" hidden>重排</button>
|
||||||
<label class="editor-field"><span>字距</span><span class="editor-number"><input id="textSpacing" type="number" step="any" placeholder="0" aria-label="字距" title="字距" disabled><span>pt</span></span></label>
|
<label class="editor-field"><span>字距</span><span class="editor-number"><input id="textSpacing" type="number" step="any" placeholder="0" aria-label="字距" title="字距" disabled><span>pt</span></span></label>
|
||||||
<button id="paragraphButton" class="editor-group-action" type="button" title="段落缩进" aria-label="段落缩进" aria-haspopup="dialog" aria-controls="paragraphPanel" disabled>段落</button>
|
<button id="paragraphButton" class="editor-group-action" type="button" title="段落缩进" aria-label="段落缩进" aria-haspopup="dialog" aria-controls="paragraphPanel" disabled>段落</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -482,14 +483,6 @@
|
|||||||
<dialog id="objectPicker" class="form-dialog" aria-label="选取对象">
|
<dialog id="objectPicker" class="form-dialog" aria-label="选取对象">
|
||||||
<form method="dialog"><div id="objectPickerList" class="object-picker-list"></div><div class="form-actions"><button class="button" type="submit">关闭</button></div></form>
|
<form method="dialog"><div id="objectPickerList" class="object-picker-list"></div><div class="form-actions"><button class="button" type="submit">关闭</button></div></form>
|
||||||
</dialog>
|
</dialog>
|
||||||
<dialog id="sourceTextPanel" class="form-dialog" aria-label="保留排版">
|
|
||||||
<form id="sourceTextForm">
|
|
||||||
<h2 class="form-title">保留排版</h2>
|
|
||||||
<div class="form-row"><label for="sourceTextValue">内容</label><textarea id="sourceTextValue" rows="3" dir="auto" aria-label="文字内容" required></textarea></div>
|
|
||||||
<div id="sourceTextPreview" aria-label="文字预览"></div>
|
|
||||||
<div class="form-actions"><span id="sourceTextStatus" class="form-status" role="status"></span><button id="sourceTextCancel" class="button" type="button">取消</button><button class="button primary" type="submit">确定</button></div>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
<dialog id="annotationNote" class="form-dialog" aria-label="注解">
|
<dialog id="annotationNote" class="form-dialog" aria-label="注解">
|
||||||
<form id="annotationForm" method="dialog">
|
<form id="annotationForm" method="dialog">
|
||||||
<div id="annotationDetail" class="annotation-detail"></div>
|
<div id="annotationDetail" class="annotation-detail"></div>
|
||||||
|
|||||||
+15
-20
@@ -1092,26 +1092,6 @@ button:not(:disabled):hover {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#sourceTextPanel {
|
|
||||||
width: 440px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sourceTextValue {
|
|
||||||
min-height: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sourceTextPreview {
|
|
||||||
height: 160px;
|
|
||||||
margin-top: 12px;
|
|
||||||
background: #fff;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sourceTextPreview svg {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.seal-position-options,
|
.seal-position-options,
|
||||||
.seal-position-options label {
|
.seal-position-options label {
|
||||||
@@ -2871,6 +2851,21 @@ button.outline-link:hover {
|
|||||||
text-indent: inherit;
|
text-indent: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-text span[data-glyph] {
|
||||||
|
position: relative;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-text span[data-glyph]::after {
|
||||||
|
content: attr(data-glyph);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
-webkit-text-fill-color: currentColor;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
.edit-text:focus-visible {
|
.edit-text:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-62
@@ -356,12 +356,6 @@ const el = {
|
|||||||
objectHeight: document.querySelector("#objectHeight"),
|
objectHeight: document.querySelector("#objectHeight"),
|
||||||
objectAspect: document.querySelector("#objectAspect"),
|
objectAspect: document.querySelector("#objectAspect"),
|
||||||
objectStylePanel: document.querySelector("#objectStylePanel"),
|
objectStylePanel: document.querySelector("#objectStylePanel"),
|
||||||
sourceTextPanel: document.querySelector("#sourceTextPanel"),
|
|
||||||
sourceTextForm: document.querySelector("#sourceTextForm"),
|
|
||||||
sourceTextValue: document.querySelector("#sourceTextValue"),
|
|
||||||
sourceTextPreview: document.querySelector("#sourceTextPreview"),
|
|
||||||
sourceTextStatus: document.querySelector("#sourceTextStatus"),
|
|
||||||
sourceTextCancel: document.querySelector("#sourceTextCancel"),
|
|
||||||
objectStyleForm: document.querySelector("#objectStyleForm"),
|
objectStyleForm: document.querySelector("#objectStyleForm"),
|
||||||
groupObjectsButton: document.querySelector("#groupObjectsButton"),
|
groupObjectsButton: document.querySelector("#groupObjectsButton"),
|
||||||
ungroupObjectButton: document.querySelector("#ungroupObjectButton"),
|
ungroupObjectButton: document.querySelector("#ungroupObjectButton"),
|
||||||
@@ -603,7 +597,7 @@ const annotationFields = Object.fromEntries(["CreateForm", "CreateCancel", "Cont
|
|||||||
el.editorTools.addEventListener("scroll", () => fontPicker.position());
|
el.editorTools.addEventListener("scroll", () => fontPicker.position());
|
||||||
|
|
||||||
const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
||||||
textControls: [el.textFont, el.textFontToggle, el.textFontAdd, el.textFontList, el.textSize, el.textColor, textBackground, ...textFormatButtons, el.undoButton, el.redoButton, el.saveButton, el.fontInput],
|
textControls: [el.textFont, el.textFontToggle, el.textFontAdd, el.textFontList, el.textSize, el.textColor, textBackground, ...textFormatButtons, el.undoButton, el.redoButton, el.saveButton, el.fontInput, document.querySelector("#textReflow")],
|
||||||
keepTextFocus: () => fontPicker.open || canvasEditor.input?.fontDialog,
|
keepTextFocus: () => fontPicker.open || canvasEditor.input?.fontDialog,
|
||||||
textClipboardScope: () => `${state.wasmSeq}:${state.fileName}:${state.doc?.documents?.[state.doc.documentIndex || 0]?.root}:${state.doc?.documentIndex || 0}:${state.doc?.documentVersion || ""}`,
|
textClipboardScope: () => `${state.wasmSeq}:${state.fileName}:${state.doc?.documents?.[state.doc.documentIndex || 0]?.root}:${state.doc?.documentIndex || 0}:${state.doc?.documentVersion || ""}`,
|
||||||
busy: () => document.body.hasAttribute("aria-busy"),
|
busy: () => document.body.hasAttribute("aria-busy"),
|
||||||
@@ -673,20 +667,21 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
|||||||
onDrawText: beginCanvasText,
|
onDrawText: beginCanvasText,
|
||||||
onDrawAnnotation: placeAnnotation,
|
onDrawAnnotation: placeAnnotation,
|
||||||
onDrawInk: (index, points, style, pressure) => changeDocument("ofdgoInsertInk", null, index, JSON.stringify(points), style.lineWidth, pressure, style.strokeColor),
|
onDrawInk: (index, points, style, pressure) => changeDocument("ofdgoInsertInk", null, index, JSON.stringify(points), style.lineWidth, pressure, style.strokeColor),
|
||||||
onPreviewText: (item, value, font, size) => callWASM("ofdgoPreviewText", item.index, item.id, value, font || null, size),
|
onPreviewText: (item, value, font, size, reflow) => callWASM("ofdgoPreviewText", item.index, item.id, value, font || null, size, !!reflow),
|
||||||
onCommitRichText: saveRichText,
|
onCommitRichText: saveRichText,
|
||||||
onPreviewRichText: validateRichText,
|
onPreviewRichText: validateRichText,
|
||||||
onTextError: (message, previous) => {
|
onTextError: (message, previous) => {
|
||||||
if (message) setStatus(message);
|
if (message) setStatus(message);
|
||||||
else if (el.statusText.textContent === previous) setStatus(state.doc ? pageStatus(state.pageIndex, state.doc.pageCount) : STATUS.ready);
|
else if (el.statusText.textContent === previous) setStatus(state.doc ? pageStatus(state.pageIndex, state.doc.pageCount) : STATUS.ready);
|
||||||
},
|
},
|
||||||
onCommitText: (item, value, fontData, color = null) => item.draft
|
onCommitText: (item, value, fontData, color = null, reflow = false) => item.draft
|
||||||
? changeDocument("ofdgoInsertText", null, item.index, value, fontData || item.fontData, item.x, item.y, item.size, item.color, item.width, item.wrap, item.align, item.paragraphHeight, item.letterSpacing, ...textIndents(item))
|
? changeDocument("ofdgoInsertText", null, item.index, value, fontData || item.fontData, item.x, item.y, item.size, item.color, item.width, item.wrap, item.align, item.paragraphHeight, item.letterSpacing, ...textIndents(item))
|
||||||
: changeDocument("ofdgoUpdateText", item, value, fontData || null, item.size, color),
|
: changeDocument("ofdgoUpdateText", item, value, fontData || null, item.size, color, reflow),
|
||||||
onTextChange: () => {
|
onTextChange: () => {
|
||||||
const editing = canvasEditor.input;
|
const editing = canvasEditor.input;
|
||||||
updatePendingChanges();
|
updatePendingChanges();
|
||||||
if (editing?.rich) updateObjectControls(canvasEditor.selected);
|
if (editing?.rich || editing?.source) updateObjectControls(canvasEditor.selected);
|
||||||
|
if (editing?.source) updateTextFonts(canvasEditor.selected, true);
|
||||||
if (!editing) {
|
if (!editing) {
|
||||||
updateObjectControls(canvasEditor.selected);
|
updateObjectControls(canvasEditor.selected);
|
||||||
updateTextFonts(canvasEditor.selected, true);
|
updateTextFonts(canvasEditor.selected, true);
|
||||||
@@ -1120,7 +1115,7 @@ el.resetCropButton.addEventListener("click", () => {
|
|||||||
});
|
});
|
||||||
for (const [button, direction, command] of [[el.undoButton, -1, "ofdgoUndo"], [el.redoButton, 1, "ofdgoRedo"]]) {
|
for (const [button, direction, command] of [[el.undoButton, -1, "ofdgoUndo"], [el.redoButton, 1, "ofdgoRedo"]]) {
|
||||||
button.addEventListener("click", async () => {
|
button.addEventListener("click", async () => {
|
||||||
if (canvasEditor.input?.rich) { canvasEditor.textUndo(direction); return; }
|
if (canvasEditor.input?.rich || canvasEditor.input?.source) { canvasEditor.textUndo(direction); return; }
|
||||||
if (!await canvasEditor.commitNudge() || !await canvasEditor.commitText() || !await canvasEditor.commitCrop()) return;
|
if (!await canvasEditor.commitNudge() || !await canvasEditor.commitText() || !await canvasEditor.commitCrop()) return;
|
||||||
await changeDocument(command);
|
await changeDocument(command);
|
||||||
});
|
});
|
||||||
@@ -1279,22 +1274,7 @@ 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.sourceTextCancel.addEventListener("click", () => el.sourceTextPanel.close());
|
document.querySelector("#textReflow").addEventListener("click", reflowCanvasText);
|
||||||
el.sourceTextValue.addEventListener("input", () => {
|
|
||||||
clearTimeout(state.sourceTextTimer);
|
|
||||||
state.sourceTextTimer = setTimeout(previewPositionedText, 150);
|
|
||||||
});
|
|
||||||
el.sourceTextPanel.addEventListener("close", () => {
|
|
||||||
clearTimeout(state.sourceTextTimer);
|
|
||||||
state.sourceText = null;
|
|
||||||
el.sourceTextPreview.replaceChildren();
|
|
||||||
});
|
|
||||||
el.sourceTextForm.addEventListener("submit", async event => {
|
|
||||||
event.preventDefault();
|
|
||||||
const item = state.sourceText?.item;
|
|
||||||
if (!item) return;
|
|
||||||
if (await changeDocument("ofdgoUpdateText", item, el.sourceTextValue.value, null, item.size, null)) el.sourceTextPanel.close();
|
|
||||||
});
|
|
||||||
el.objectLinkKind.addEventListener("change", updateObjectLinkFields);
|
el.objectLinkKind.addEventListener("change", updateObjectLinkFields);
|
||||||
el.gradientKind.addEventListener("change", () => {
|
el.gradientKind.addEventListener("change", () => {
|
||||||
el.gradientControls.hidden = el.gradientKind.value === "keep";
|
el.gradientControls.hidden = el.gradientKind.value === "keep";
|
||||||
@@ -1831,7 +1811,7 @@ function handleKeyDown(event) {
|
|||||||
|
|
||||||
function formDialogOpen() {
|
function formDialogOpen() {
|
||||||
return Boolean(sealPreview || resourceManager.pick) || settingsElements.Panel.open || fontBindingElements.Panel.open || el.signPanel.open || el.verifyPanel.open || el.credentialsPanel.open || el.encryptionPanel.open || batchElements.Panel.open || el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open || el.documentPanel.open
|
return Boolean(sealPreview || resourceManager.pick) || settingsElements.Panel.open || fontBindingElements.Panel.open || el.signPanel.open || el.verifyPanel.open || el.credentialsPanel.open || el.encryptionPanel.open || batchElements.Panel.open || el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open || el.documentPanel.open
|
||||||
|| resourceElements.Panel.open || el.structureDialog.open || el.versionPanel.open || el.batchPagesPanel.open || el.objectStylePanel.open || el.sourceTextPanel.open || el.outlinePanel.open || el.objectBoundsPanel.open || el.annotationNote.open || el.annotationCreate.open || el.objectPicker.open;
|
|| resourceElements.Panel.open || el.structureDialog.open || el.versionPanel.open || el.batchPagesPanel.open || el.objectStylePanel.open || el.outlinePanel.open || el.objectBoundsPanel.open || el.annotationNote.open || el.annotationCreate.open || el.objectPicker.open;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setObjectDimension(input, value) {
|
function setObjectDimension(input, value) {
|
||||||
@@ -2908,18 +2888,7 @@ async function editCanvasObject(item) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (item.type === "TextObject") {
|
if (item.type === "TextObject") {
|
||||||
if (!canEditObject(item, "textContent")) {
|
if (!canEditObject(item, "textContent") && !canEditObject(item, "rewriteText")) return;
|
||||||
if (canEditObject(item, "rewriteText")) {
|
|
||||||
state.sourceText = { item, openSeq: state.openSeq };
|
|
||||||
el.sourceTextValue.value = item.text;
|
|
||||||
el.sourceTextStatus.textContent = "";
|
|
||||||
el.sourceTextPreview.replaceChildren();
|
|
||||||
el.sourceTextPanel.showModal();
|
|
||||||
el.sourceTextValue.focus();
|
|
||||||
await previewPositionedText();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (!canEditObject(item, item.type === "ImageObject" ? "replaceImage" : "update")) return;
|
} else if (!canEditObject(item, item.type === "ImageObject" ? "replaceImage" : "update")) return;
|
||||||
if (item.type === "PathObject") {
|
if (item.type === "PathObject") {
|
||||||
el.shapeWidth.focus();
|
el.shapeWidth.focus();
|
||||||
@@ -2931,7 +2900,7 @@ async function editCanvasObject(item) {
|
|||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
const data = await callWASM("ofdgoEditorFont", item.font);
|
const data = await callWASM("ofdgoEditorFont", item.font, !canEditObject(item, "layoutKnown"));
|
||||||
const face = new FontFace(`ofdgo-edit-${item.font}`, data.bytes, fontFaceStyle(data.bytes));
|
const face = new FontFace(`ofdgo-edit-${item.font}`, data.bytes, fontFaceStyle(data.bytes));
|
||||||
await face.load();
|
await face.load();
|
||||||
let rich = null;
|
let rich = null;
|
||||||
@@ -2954,22 +2923,6 @@ async function editCanvasObject(item) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function previewPositionedText() {
|
|
||||||
const request = state.sourceText, value = el.sourceTextValue.value;
|
|
||||||
if (!request || !el.sourceTextPanel.open) return;
|
|
||||||
try {
|
|
||||||
const svg = await callWASM("ofdgoPreviewPositionedText", request.item.index, request.item.id, value);
|
|
||||||
if (state.sourceText !== request || request.openSeq !== state.openSeq || el.sourceTextValue.value !== value) return;
|
|
||||||
el.sourceTextPreview.replaceChildren(parseSVG(svg));
|
|
||||||
el.sourceTextStatus.textContent = "";
|
|
||||||
} catch (error) {
|
|
||||||
if (state.sourceText === request && request.openSeq === state.openSeq && el.sourceTextValue.value === value) {
|
|
||||||
el.sourceTextPreview.replaceChildren();
|
|
||||||
el.sourceTextStatus.textContent = error.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mountEditorObjects(index, page, surface) {
|
function mountEditorObjects(index, page, surface) {
|
||||||
const scope = state.composite?.index === index ? state.composite : null;
|
const scope = state.composite?.index === index ? state.composite : null;
|
||||||
canvasEditor.mount(index, { ...page, objects: scope ? scope.objects : page.objects.filter(item => state.renderAnnotations || item.type !== "Annotation") }, surface);
|
canvasEditor.mount(index, { ...page, objects: scope ? scope.objects : page.objects.filter(item => state.renderAnnotations || item.type !== "Annotation") }, surface);
|
||||||
@@ -3108,6 +3061,16 @@ async function ensureRichInput() {
|
|||||||
return canvasEditor.promoteText(rich);
|
return canvasEditor.promoteText(rich);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function reflowCanvasText() {
|
||||||
|
const editing = canvasEditor.input;
|
||||||
|
if (document.body.hasAttribute("aria-busy") || !editing?.source || editing.composing || editing.saving || !canEditObject(editing.item, "reflow")) return;
|
||||||
|
if (!window.confirm("将重新排版原文,是否继续?")) return;
|
||||||
|
editing.reflow = true;
|
||||||
|
canvasEditor.recordSourceText();
|
||||||
|
await canvasEditor.previewSourceText();
|
||||||
|
updateObjectControls(canvasEditor.selected, true);
|
||||||
|
}
|
||||||
|
|
||||||
async function changeRangeFormat(key, value) {
|
async function changeRangeFormat(key, value) {
|
||||||
if (!state.ready || document.body.hasAttribute("aria-busy") || canvasEditor.input?.composing) return;
|
if (!state.ready || document.body.hasAttribute("aria-busy") || canvasEditor.input?.composing) return;
|
||||||
try {
|
try {
|
||||||
@@ -3480,6 +3443,7 @@ async function changeTextStyle(color) {
|
|||||||
try { if (!await ensureRichInput()) return; } catch (error) { showError(error, false); return; }
|
try { if (!await ensureRichInput()) return; } catch (error) { showError(error, false); return; }
|
||||||
}
|
}
|
||||||
if (canvasEditor.input) {
|
if (canvasEditor.input) {
|
||||||
|
if (!color && canvasEditor.input.source && !canvasEditor.input.reflow && size !== item.size && !confirmTextReflow(item)) return;
|
||||||
canvasEditor.setTextStyle(color ? { color: selectedColor } : { size });
|
canvasEditor.setTextStyle(color ? { color: selectedColor } : { size });
|
||||||
updateObjectControls(canvasEditor.selected, true);
|
updateObjectControls(canvasEditor.selected, true);
|
||||||
return;
|
return;
|
||||||
@@ -3782,8 +3746,6 @@ async function changeDocument(name, item, ...args) {
|
|||||||
el.batchPagesStatus.textContent = err.message;
|
el.batchPagesStatus.textContent = err.message;
|
||||||
} else if (el.objectStylePanel.open) {
|
} else if (el.objectStylePanel.open) {
|
||||||
el.objectStyleStatus.textContent = err.message;
|
el.objectStyleStatus.textContent = err.message;
|
||||||
} else if (el.sourceTextPanel.open) {
|
|
||||||
el.sourceTextStatus.textContent = err.message;
|
|
||||||
} else if (el.objectBoundsPanel.open) {
|
} else if (el.objectBoundsPanel.open) {
|
||||||
el.objectBoundsStatus.textContent = err.message;
|
el.objectBoundsStatus.textContent = err.message;
|
||||||
} else if (el.outlinePanel.open) {
|
} else if (el.outlinePanel.open) {
|
||||||
@@ -4583,7 +4545,6 @@ async function openOFD(file, remote = null) {
|
|||||||
el.importPanel.close();
|
el.importPanel.close();
|
||||||
el.batchPagesPanel.close();
|
el.batchPagesPanel.close();
|
||||||
el.objectStylePanel.close();
|
el.objectStylePanel.close();
|
||||||
el.sourceTextPanel.close();
|
|
||||||
el.objectBoundsPanel.close();
|
el.objectBoundsPanel.close();
|
||||||
el.outlinePanel.close();
|
el.outlinePanel.close();
|
||||||
el.structureDialog.close();
|
el.structureDialog.close();
|
||||||
@@ -11799,12 +11760,16 @@ function updateObjectControls(item, reset = false) {
|
|||||||
textBackground.classList.toggle("mixed-color", Boolean((canvasEditor.input?.rich || item?.richText) && text.background === undefined));
|
textBackground.classList.toggle("mixed-color", Boolean((canvasEditor.input?.rich || item?.richText) && text.background === undefined));
|
||||||
if (text.background) textBackground.value = text.background;
|
if (text.background) textBackground.value = text.background;
|
||||||
document.querySelector("#textHighlight").style.setProperty("--highlight-color", textBackground.value);
|
document.querySelector("#textHighlight").style.setProperty("--highlight-color", textBackground.value);
|
||||||
const richHistory = canvasEditor.input?.rich;
|
const richHistory = canvasEditor.input?.rich || canvasEditor.input?.history;
|
||||||
if (richHistory) {
|
if (richHistory) {
|
||||||
el.undoButton.disabled = textDisabled || richHistory.index === 0;
|
el.undoButton.disabled = textDisabled || richHistory.index === 0;
|
||||||
el.redoButton.disabled = textDisabled || richHistory.index === richHistory.entries.length - 1;
|
el.redoButton.disabled = textDisabled || richHistory.index === richHistory.entries.length - 1;
|
||||||
}
|
}
|
||||||
el.textAlign.disabled = el.textWrap.disabled = el.textLineHeight.disabled = el.textSpacing.disabled = el.textSize.disabled || Boolean(item?.items || item?.draft) || Boolean(canvasEditor.input);
|
el.textAlign.disabled = el.textWrap.disabled = el.textLineHeight.disabled = el.textSpacing.disabled = el.textSize.disabled || Boolean(item?.items || item?.draft) || Boolean(canvasEditor.input);
|
||||||
|
const reflow = document.querySelector("#textReflow"), positioned = canvasEditor.input?.source && !canEditObject(item, "layoutKnown");
|
||||||
|
reflow.hidden = !positioned;
|
||||||
|
el.textWrap.hidden = !!positioned;
|
||||||
|
reflow.disabled = textDisabled || !canEditObject(item, "reflow") || !!canvasEditor.input?.reflow;
|
||||||
el.paragraphButton.disabled = el.textAlign.disabled;
|
el.paragraphButton.disabled = el.textAlign.disabled;
|
||||||
const layoutKnown = text === state.textDefaults || Boolean(item?.draft) || canEditObject(item, "layoutKnown");
|
const layoutKnown = text === state.textDefaults || Boolean(item?.draft) || canEditObject(item, "layoutKnown");
|
||||||
el.textAlign.value = layoutKnown ? text.align || "left" : "";
|
el.textAlign.value = layoutKnown ? text.align || "left" : "";
|
||||||
|
|||||||
+38
-16
@@ -1536,7 +1536,7 @@ export class CanvasEditor {
|
|||||||
const editing = this.input;
|
const editing = this.input;
|
||||||
if (rich) editing.rich = new RichTextSession(this, rich.model, rich.faces, rich.fonts);
|
if (rich) editing.rich = new RichTextSession(this, rich.model, rich.faces, rich.fonts);
|
||||||
if (rich && !rich.model.layout.lineHeight) input.style.lineHeight = "normal";
|
if (rich && !rich.model.layout.lineHeight) input.style.lineHeight = "normal";
|
||||||
if (source) editing.history = { entries: [{ value: item.text }], index: 0 };
|
if (source) editing.history = { entries: [{ value: item.text, face, fontChoice: item.fontChoice }], index: 0 };
|
||||||
item.artwork?.classList.add("edit-text-source");
|
item.artwork?.classList.add("edit-text-source");
|
||||||
item.node.classList.add("edit-text-source");
|
item.node.classList.add("edit-text-source");
|
||||||
item.surface.append(input);
|
item.surface.append(input);
|
||||||
@@ -1631,7 +1631,8 @@ export class CanvasEditor {
|
|||||||
if (editing.rich) editing.rich.render();
|
if (editing.rich) editing.rich.render();
|
||||||
input.focus({ preventScroll: true });
|
input.focus({ preventScroll: true });
|
||||||
if (!item.draft) document.getSelection().selectAllChildren(input);
|
if (!item.draft) document.getSelection().selectAllChildren(input);
|
||||||
if (editing.rich) { editing.rich.selection(); this.options.onTextChange(); }
|
if (editing.rich) editing.rich.selection();
|
||||||
|
this.options.onTextChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
textUndo(direction) {
|
textUndo(direction) {
|
||||||
@@ -1715,8 +1716,10 @@ export class CanvasEditor {
|
|||||||
const editing = this.input;
|
const editing = this.input;
|
||||||
if (!editing?.history || editing.composing) return;
|
if (!editing?.history || editing.composing) return;
|
||||||
const { history, input } = editing, value = editTextValue(input);
|
const { history, input } = editing, value = editTextValue(input);
|
||||||
if (history.entries[history.index].value === value) return;
|
const previous = history.entries[history.index];
|
||||||
history.entries.splice(++history.index, Infinity, { value, caret: editTextSelection(input) });
|
if (previous.value === value && previous.fontData === editing.fontData && previous.style === editing.style && !!previous.reflow === !!editing.reflow) return;
|
||||||
|
history.entries.splice(++history.index, Infinity, { value, caret: editTextSelection(input), face: editing.face,
|
||||||
|
fontData: editing.fontData, fontChoice: editing.fontChoice, style: editing.style, reflow: editing.reflow });
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreSourceText(direction) {
|
restoreSourceText(direction) {
|
||||||
@@ -1726,6 +1729,14 @@ export class CanvasEditor {
|
|||||||
if (index < 0 || index >= history.entries.length) return;
|
if (index < 0 || index >= history.entries.length) return;
|
||||||
history.index = index;
|
history.index = index;
|
||||||
const entry = history.entries[index], fragment = document.createDocumentFragment(), positions = [];
|
const entry = history.entries[index], fragment = document.createDocumentFragment(), positions = [];
|
||||||
|
if (entry.face) {
|
||||||
|
document.fonts.delete(editing.face);
|
||||||
|
document.fonts.add(entry.face);
|
||||||
|
Object.assign(editing, { face: entry.face, fontData: entry.fontData, fontChoice: entry.fontChoice, style: entry.style, reflow: entry.reflow });
|
||||||
|
editing.input.style.fontFamily = `"${entry.face.family}"`;
|
||||||
|
editing.input.style.fontSize = `${(entry.style?.size || editing.item.size) * PX_PER_MM}px`;
|
||||||
|
editing.input.style.color = entry.style?.color || editing.item.color;
|
||||||
|
}
|
||||||
let position = 0;
|
let position = 0;
|
||||||
for (const text of entry.value.split("\n")) {
|
for (const text of entry.value.split("\n")) {
|
||||||
const paragraph = document.createElement("div");
|
const paragraph = document.createElement("div");
|
||||||
@@ -1745,11 +1756,11 @@ export class CanvasEditor {
|
|||||||
async previewSourceText() {
|
async previewSourceText() {
|
||||||
const editing = this.input;
|
const editing = this.input;
|
||||||
if (!editing?.source || editing.composing || editing.saving) return;
|
if (!editing?.source || editing.composing || editing.saving) return;
|
||||||
const value = editTextValue(editing.input), font = editing.fontData, size = editing.style?.size || editing.item.size;
|
const value = editTextValue(editing.input), font = editing.fontData, size = editing.style?.size || editing.item.size, reflow = !!editing.reflow;
|
||||||
const current = () => this.input === editing && !editing.rich && !editing.composing && !editing.saving && editing.fontData === font
|
const current = () => this.input === editing && !editing.rich && !editing.composing && !editing.saving && editing.fontData === font
|
||||||
&& (editing.style?.size || editing.item.size) === size && editTextValue(editing.input) === value;
|
&& (editing.style?.size || editing.item.size) === size && !!editing.reflow === reflow && editTextValue(editing.input) === value;
|
||||||
try {
|
try {
|
||||||
const source = await this.options.onPreviewText(editing.item, value, font, size);
|
const source = await this.options.onPreviewText(editing.item, value, font, size, reflow);
|
||||||
if (!current()) return;
|
if (!current()) return;
|
||||||
this.setTextError(null);
|
this.setTextError(null);
|
||||||
this.paintSourceText(source);
|
this.paintSourceText(source);
|
||||||
@@ -1768,7 +1779,8 @@ export class CanvasEditor {
|
|||||||
const metrics = context.measureText("Mg"), baseline = (size + metrics.fontBoundingBoxAscent - metrics.fontBoundingBoxDescent) / 2;
|
const metrics = context.measureText("Mg"), baseline = (size + metrics.fontBoundingBoxAscent - metrics.fontBoundingBoxDescent) / 2;
|
||||||
Object.assign(input.style, { lineHeight: `${size}px`, fontWeight: String(source.weight || 400), fontStyle: source.italic ? "italic" : "normal" });
|
Object.assign(input.style, { lineHeight: `${size}px`, fontWeight: String(source.weight || 400), fontStyle: source.italic ? "italic" : "normal" });
|
||||||
const fragment = document.createDocumentFragment(), positions = [];
|
const fragment = document.createDocumentFragment(), positions = [];
|
||||||
let line, lineIndex = -1, naturalX = 0, position = 0;
|
let line, lineIndex = -1, naturalX = 0, position = 0, glyphIndex = 0;
|
||||||
|
const angle = (source.rotation || 0) * Math.PI / 180, cos = Math.cos(angle), sin = Math.sin(angle);
|
||||||
let left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity;
|
let left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity;
|
||||||
for (const run of source.runs) {
|
for (const run of source.runs) {
|
||||||
if (!line || run.line) {
|
if (!line || run.line) {
|
||||||
@@ -1790,15 +1802,23 @@ export class CanvasEditor {
|
|||||||
y = run.positions[index][1] * PX_PER_MM;
|
y = run.positions[index][1] * PX_PER_MM;
|
||||||
const span = document.createElement("span"), node = document.createTextNode(char);
|
const span = document.createElement("span"), node = document.createTextNode(char);
|
||||||
span.append(node);
|
span.append(node);
|
||||||
Object.assign(span.style, { display: "inline-block", opacity: String((item.alpha ?? 255) / 255), transformOrigin: "left top", transform: `translate(${x - naturalX}px,${y - baseline - lineIndex * size}px) scaleX(${source.scale})` });
|
const glyph = source.glyphs?.[glyphIndex++];
|
||||||
|
if (glyph) span.dataset.glyph = glyph;
|
||||||
|
Object.assign(span.style, { display: "inline-block", opacity: String((item.alpha ?? 255) / 255), transformOrigin: `left ${baseline}px`, transform: `translate(${x - naturalX}px,${y - baseline - lineIndex * size}px) rotate(${source.rotation || 0}deg) scale(${source.scale},${source.vscale || 1})` });
|
||||||
line.append(span);
|
line.append(span);
|
||||||
positions.push({ node, start: position, end: position + char.length });
|
positions.push({ node, start: position, end: position + char.length });
|
||||||
position += char.length;
|
position += char.length;
|
||||||
const advance = context.measureText(char).width;
|
const advance = context.measureText(glyph || char).width;
|
||||||
left = Math.min(left, x);
|
if (glyph) span.style.width = `${advance}px`;
|
||||||
top = Math.min(top, y - baseline);
|
for (const dx of [0, advance * source.scale]) {
|
||||||
right = Math.max(right, x + advance * source.scale);
|
for (const dy of [-baseline * (source.vscale || 1), (size - baseline) * (source.vscale || 1)]) {
|
||||||
bottom = Math.max(bottom, y + size - baseline);
|
const px = x + dx * cos - dy * sin, py = y + dx * sin + dy * cos;
|
||||||
|
left = Math.min(left, px);
|
||||||
|
top = Math.min(top, py);
|
||||||
|
right = Math.max(right, px);
|
||||||
|
bottom = Math.max(bottom, py);
|
||||||
|
}
|
||||||
|
}
|
||||||
naturalX += advance;
|
naturalX += advance;
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
@@ -1827,6 +1847,7 @@ export class CanvasEditor {
|
|||||||
editing.face = face;
|
editing.face = face;
|
||||||
editing.fontData = data;
|
editing.fontData = data;
|
||||||
editing.input.style.fontFamily = `"${face.family}"`;
|
editing.input.style.fontFamily = `"${face.family}"`;
|
||||||
|
this.recordSourceText();
|
||||||
this.options.onTextChange();
|
this.options.onTextChange();
|
||||||
editing.input.focus({ preventScroll: true });
|
editing.input.focus({ preventScroll: true });
|
||||||
this.previewSourceText();
|
this.previewSourceText();
|
||||||
@@ -1841,6 +1862,7 @@ export class CanvasEditor {
|
|||||||
const item = { ...editing.item, ...editing.style };
|
const item = { ...editing.item, ...editing.style };
|
||||||
editing.input.style.fontSize = `${item.size * PX_PER_MM}px`;
|
editing.input.style.fontSize = `${item.size * PX_PER_MM}px`;
|
||||||
editing.input.style.color = item.color;
|
editing.input.style.color = item.color;
|
||||||
|
this.recordSourceText();
|
||||||
this.fitTextInput();
|
this.fitTextInput();
|
||||||
this.options.onTextChange();
|
this.options.onTextChange();
|
||||||
if ("size" in values) this.previewSourceText();
|
if ("size" in values) this.previewSourceText();
|
||||||
@@ -1852,7 +1874,7 @@ export class CanvasEditor {
|
|||||||
if (editing.rich) return Boolean(editing.refreshPending) || (!editing.item.draft || Boolean(editing.rich.text.trim())) && editing.rich.changed;
|
if (editing.rich) return Boolean(editing.refreshPending) || (!editing.item.draft || Boolean(editing.rich.text.trim())) && editing.rich.changed;
|
||||||
const value = editTextValue(editing.input);
|
const value = editTextValue(editing.input);
|
||||||
return (!editing.item.draft || Boolean(value.trim()))
|
return (!editing.item.draft || Boolean(value.trim()))
|
||||||
&& (value !== editing.item.text || Boolean(editing.fontData || editing.style));
|
&& (value !== editing.item.text || Boolean(editing.fontData || editing.style || editing.reflow));
|
||||||
}
|
}
|
||||||
|
|
||||||
async commitText() {
|
async commitText() {
|
||||||
@@ -1874,7 +1896,7 @@ export class CanvasEditor {
|
|||||||
let saved = false;
|
let saved = false;
|
||||||
try {
|
try {
|
||||||
saved = editing.rich ? await this.options.onCommitRichText(editing.item, editing.rich.model, editing.rich.fonts)
|
saved = editing.rich ? await this.options.onCommitRichText(editing.item, editing.rich.model, editing.rich.fonts)
|
||||||
: await this.options.onCommitText(editing.style ? { ...editing.item, ...editing.style } : editing.item, value, editing.fontData, editing.style?.color ?? null);
|
: await this.options.onCommitText(editing.style ? { ...editing.item, ...editing.style } : editing.item, value, editing.fontData, editing.style?.color ?? null, !!editing.reflow);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.input === editing) this.setTextError(error);
|
if (this.input === editing) this.setTextError(error);
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-36
@@ -1428,7 +1428,7 @@ func resourceLocations(args []js.Value) (any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fontPreview 使用当前文档和版本的字形轮廓生成独立预览
|
// fontPreview 使用当前文档和版本的字形轮廓生成独立预览
|
||||||
// 入参: args 字体资源标识
|
// 入参: args 字体资源标识及可选字形映射标记
|
||||||
// 返回: any SVG数据, error 字体或绘制错误
|
// 返回: any SVG数据, error 字体或绘制错误
|
||||||
func fontPreview(args []js.Value) (any, error) {
|
func fontPreview(args []js.Value) (any, error) {
|
||||||
if currentSession == nil || len(args) != 1 {
|
if currentSession == nil || len(args) != 1 {
|
||||||
@@ -3009,6 +3009,9 @@ func changeCompositeText(page int, path ofdgo.ObjectPath, indexes []int, operati
|
|||||||
args = args[1:]
|
args = args[1:]
|
||||||
}
|
}
|
||||||
style := ofdgo.TextStyle{Size: args[1].Float()}
|
style := ofdgo.TextStyle{Size: args[1].Float()}
|
||||||
|
if len(args) > 3 {
|
||||||
|
style.Reflow = args[3].Bool()
|
||||||
|
}
|
||||||
if !args[0].IsNull() {
|
if !args[0].IsNull() {
|
||||||
data, err := bytesFromJS(args[0])
|
data, err := bytesFromJS(args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3061,6 +3064,12 @@ func editorFont(args []js.Value) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(args) > 1 && args[1].Bool() {
|
||||||
|
_, data, _, _, err = ofdgo.FixFontDataAggressive(data, true, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return successResult(map[string]any{"bytes": bytesToJS(data)}), nil
|
return successResult(map[string]any{"bytes": bytesToJS(data)}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3090,7 +3099,7 @@ func editorObjectAt(page int, key string) (ofdgo.GraphicObject, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// previewText 生成保留原定位的输入预览,不修改文档或历史
|
// previewText 生成保留原定位的输入预览,不修改文档或历史
|
||||||
// 入参: args 页码、对象路径、新文字、可选字体数据和字号
|
// 入参: args 页码、对象路径、新文字、可选字体数据、字号及重排标记
|
||||||
// 返回: any 定位段及字体样式, error 错误信息
|
// 返回: any 定位段及字体样式, error 错误信息
|
||||||
func previewText(args []js.Value) (any, error) {
|
func previewText(args []js.Value) (any, error) {
|
||||||
key := args[1].String()
|
key := args[1].String()
|
||||||
@@ -3103,18 +3112,23 @@ func previewText(args []js.Value) (any, error) {
|
|||||||
return nil, fmt.Errorf("object is not text")
|
return nil, fmt.Errorf("object is not text")
|
||||||
}
|
}
|
||||||
editor := currentEditor
|
editor := currentEditor
|
||||||
|
fontID := ""
|
||||||
if len(args) > 3 && !args[3].IsNull() {
|
if len(args) > 3 && !args[3].IsNull() {
|
||||||
data, err := bytesFromJS(args[3])
|
data, err := bytesFromJS(args[3])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
editor = ofdgo.NewEditor()
|
editor = ofdgo.NewEditor()
|
||||||
text.Font, err = editor.AddFont(FontFile{Data: data}, 0)
|
fontID, err = editor.AddFont(FontFile{Data: data}, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
text.Font = ""
|
||||||
}
|
}
|
||||||
if len(args) > 4 && args[4].Float() != text.Size {
|
if len(args) > 4 && args[4].Float() != text.Size || len(args) > 5 && args[5].Bool() {
|
||||||
|
if fontID != "" {
|
||||||
|
text.Font = fontID
|
||||||
|
}
|
||||||
var x, y float64
|
var x, y float64
|
||||||
if strings.Contains(key, "/") {
|
if strings.Contains(key, "/") {
|
||||||
x, _ = strconv.ParseFloat(text.TextCode[0].X, 64)
|
x, _ = strconv.ParseFloat(text.TextCode[0].X, 64)
|
||||||
@@ -3134,7 +3148,7 @@ func previewText(args []js.Value) (any, error) {
|
|||||||
code.X, code.Y = fmt.Sprint(cx+x), fmt.Sprint(cy+y-baseline)
|
code.X, code.Y = fmt.Sprint(cx+x), fmt.Sprint(cy+y-baseline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if err := editor.RewriteText(&text, args[2].String()); err != nil {
|
} else if err := editor.RewriteTextWithFont(&text, args[2].String(), fontID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var runs []any
|
var runs []any
|
||||||
@@ -3166,7 +3180,22 @@ func previewText(args []js.Value) (any, error) {
|
|||||||
if scale == 0 {
|
if scale == 0 {
|
||||||
scale = 1
|
scale = 1
|
||||||
}
|
}
|
||||||
return map[string]any{"runs": runs, "scale": scale, "weight": text.Weight, "italic": text.Italic}, nil
|
glyphs := make(map[int]string)
|
||||||
|
if fontID == "" {
|
||||||
|
for _, transform := range text.CGTransform {
|
||||||
|
if transform.CodeCount != transform.GlyphCount {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i, value := range strings.Fields(transform.Glyphs) {
|
||||||
|
glyph, err := strconv.ParseUint(value, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
glyphs[transform.CodePosition+i] = string(rune(0xF0000 + glyph))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map[string]any{"runs": runs, "glyphs": glyphs, "scale": scale, "vscale": text.VScale, "rotation": text.CharDirection, "weight": text.Weight, "italic": text.Italic}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// previewPositionedText 预览保留定位的文字替换,不修改编辑器或撤销记录
|
// previewPositionedText 预览保留定位的文字替换,不修改编辑器或撤销记录
|
||||||
@@ -3204,78 +3233,71 @@ func previewPositionedText(args []js.Value) (any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// updateText 修改文字,null内容保留定位且允许替换字体,null字体数据和颜色沿用对象属性
|
// updateText 修改文字,null内容保留定位且允许替换字体,null字体数据和颜色沿用对象属性
|
||||||
// 入参: args 页码、对象标识、内容、字体数据、字号和颜色
|
// 入参: args 页码、对象标识、内容、字体数据、字号、颜色及可选重排标记
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
func updateText(args []js.Value) (any, error) {
|
func updateText(args []js.Value) (any, error) {
|
||||||
if currentEditor == nil {
|
return changeObjects(func() error { return applyTextUpdate(args) })
|
||||||
return nil, fmt.Errorf("no document is being edited")
|
}
|
||||||
}
|
|
||||||
|
// applyTextUpdate 在当前事务中修改文字及字体,不刷新会话
|
||||||
|
// 入参: args 页码、对象标识、内容、字体数据、字号、颜色及可选重排标记
|
||||||
|
// 返回: error 字体、排版或提交错误
|
||||||
|
func applyTextUpdate(args []js.Value) error {
|
||||||
page, id := args[0].Int(), args[1].String()
|
page, id := args[0].Int(), args[1].String()
|
||||||
object, err := currentEditor.Object(page, id)
|
object, err := currentEditor.Object(page, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if object.Type != "TextObject" {
|
if object.Type != "TextObject" {
|
||||||
return nil, fmt.Errorf("object %q is not text", id)
|
return fmt.Errorf("object %q is not text", id)
|
||||||
}
|
}
|
||||||
if !args[3].IsNull() {
|
if !args[3].IsNull() {
|
||||||
data, err := bytesFromJS(args[3])
|
data, err := bytesFromJS(args[3])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
value := object.TextObject.Text()
|
value := object.TextObject.Text()
|
||||||
if !args[2].IsNull() {
|
if !args[2].IsNull() {
|
||||||
value = args[2].String()
|
value = args[2].String()
|
||||||
}
|
}
|
||||||
if err := checkFontGlyphs(data, value); err != nil {
|
if err := checkFontGlyphs(data, value); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
object.TextObject.Font, err = currentEditor.AddFont(FontFile{Data: data}, 0)
|
object.TextObject.Font, err = currentEditor.AddFont(FontFile{Data: data}, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !args[2].IsNull() {
|
if !args[2].IsNull() {
|
||||||
if args[4].Float() == object.TextObject.Size {
|
if args[4].Float() == object.TextObject.Size || len(args) > 6 && args[6].Bool() {
|
||||||
var style ofdgo.TextStyle
|
style := ofdgo.TextStyle{Reflow: len(args) > 6 && args[6].Bool()}
|
||||||
|
if style.Reflow {
|
||||||
|
style.Size = args[4].Float()
|
||||||
|
}
|
||||||
if !args[3].IsNull() {
|
if !args[3].IsNull() {
|
||||||
style.Font = object.TextObject.Font
|
style.Font = object.TextObject.Font
|
||||||
}
|
}
|
||||||
if !args[5].IsNull() {
|
if !args[5].IsNull() {
|
||||||
var fill ofdgo.FillColor
|
var fill ofdgo.FillColor
|
||||||
if err := setEditorColor(&fill, args[5].String()); err != nil {
|
if err := setEditorColor(&fill, args[5].String()); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
style.Color = fill.Value
|
style.Color = fill.Value
|
||||||
}
|
}
|
||||||
revision := currentEditor.Revision()
|
return currentEditor.UpdateTextContent(page, id, args[2].String(), style)
|
||||||
if err := currentEditor.UpdateTextContent(page, id, args[2].String(), style); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if currentEditor.Revision() == revision {
|
|
||||||
return editorSummary(), nil
|
|
||||||
}
|
|
||||||
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
|
||||||
}
|
}
|
||||||
object.TextObject.Size = args[4].Float()
|
object.TextObject.Size = args[4].Float()
|
||||||
_, layout := object.TextObject.TextLayout()
|
_, layout := object.TextObject.TextLayout()
|
||||||
if err := currentEditor.LayoutText(&object.TextObject, args[2].String(), layout); err != nil {
|
if err := currentEditor.LayoutText(&object.TextObject, args[2].String(), layout); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !args[5].IsNull() {
|
if !args[5].IsNull() {
|
||||||
if err := setTextColor(&object.TextObject, args[5].String()); err != nil {
|
if err := setTextColor(&object.TextObject, args[5].String()); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
revision := currentEditor.Revision()
|
return currentEditor.UpdateObject(page, id, object)
|
||||||
if err := currentEditor.UpdateObject(page, id, object); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if currentEditor.Revision() == revision {
|
|
||||||
return editorSummary(), nil
|
|
||||||
}
|
|
||||||
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkFontGlyphs 只检查候选字体,不注册资源或改变文档
|
// checkFontGlyphs 只检查候选字体,不注册资源或改变文档
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ func (e *Editor) changeCompositeText(page int, path ObjectPath, indexes []int, v
|
|||||||
// 入参: node 原文节点, member 有效快照, value 新内容, style 样式增量, options 段落选项, frame 文字框调整
|
// 入参: node 原文节点, member 有效快照, value 新内容, style 样式增量, options 段落选项, frame 文字框调整
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) updateCompositeText(node *editorCompositeNode, member CompositeMember, value *string, style TextStyle, options *TextLayout, frame *Box) error {
|
func (e *Editor) updateCompositeText(node *editorCompositeNode, member CompositeMember, value *string, style TextStyle, options *TextLayout, frame *Box) error {
|
||||||
|
if style.Reflow {
|
||||||
|
options = &TextLayout{AutoBidi: true}
|
||||||
|
}
|
||||||
rewriteOnly := value != nil && options == nil && frame == nil && style.Color == "" && (style.Font == "" || style.Font == member.Object.TextObject.Font) && (style.Size == 0 || style.Size == member.Object.TextObject.Size)
|
rewriteOnly := value != nil && options == nil && frame == nil && style.Color == "" && (style.Font == "" || style.Font == member.Object.TextObject.Font) && (style.Size == 0 || style.Size == member.Object.TextObject.Size)
|
||||||
if node.object.Type != "TextObject" || !member.Capabilities.ReplaceFont && !(rewriteOnly && member.Capabilities.RewriteText) {
|
if node.object.Type != "TextObject" || !member.Capabilities.ReplaceFont && !(rewriteOnly && member.Capabilities.RewriteText) {
|
||||||
return fmt.Errorf("composite text style is not supported")
|
return fmt.Errorf("composite text style is not supported")
|
||||||
@@ -81,7 +84,11 @@ func (e *Editor) updateCompositeText(node *editorCompositeNode, member Composite
|
|||||||
original := content
|
original := content
|
||||||
if style.Font != "" {
|
if style.Font != "" {
|
||||||
if state.layout == nil && options == nil && (style.Size == 0 || style.Size == text.Size) {
|
if state.layout == nil && options == nil && (style.Size == 0 || style.Size == text.Size) {
|
||||||
if err := e.replaceTextFont(&text, style.Font); err != nil {
|
replacement := content
|
||||||
|
if value != nil {
|
||||||
|
replacement = *value
|
||||||
|
}
|
||||||
|
if err := e.RewriteTextWithFont(&text, replacement, style.Font); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -628,7 +628,6 @@ func (e *Editor) objectCapabilities(object GraphicObject, orderable map[*editorX
|
|||||||
}
|
}
|
||||||
if object.Type == "TextObject" {
|
if object.Type == "TextObject" {
|
||||||
all.TextContent = all.Update && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0 && (all.LayoutKnown || len(object.TextObject.CGTransform) == 0)
|
all.TextContent = all.Update && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0 && (all.LayoutKnown || len(object.TextObject.CGTransform) == 0)
|
||||||
all.Reflow = all.Reflow && (all.LayoutKnown || len(object.TextObject.CGTransform) == 0)
|
|
||||||
all.RewriteText = all.TextContent
|
all.RewriteText = all.TextContent
|
||||||
if !all.RewriteText {
|
if !all.RewriteText {
|
||||||
text := object.TextObject
|
text := object.TextObject
|
||||||
|
|||||||
+99
-12
@@ -55,10 +55,12 @@ type TextLayout struct {
|
|||||||
// TextStyle 文字样式增量
|
// TextStyle 文字样式增量
|
||||||
// Font为空时保留原字体,Size单位为毫米且为0时保留原字号
|
// Font为空时保留原字体,Size单位为毫米且为0时保留原字号
|
||||||
// Color为OFD的RGB分量字符串,空值保留原颜色,修改时保留原透明度
|
// Color为OFD的RGB分量字符串,空值保留原颜色,修改时保留原透明度
|
||||||
|
// Reflow显式重新排版,默认保留原文定位
|
||||||
type TextStyle struct {
|
type TextStyle struct {
|
||||||
Font string
|
Font string
|
||||||
Size float64
|
Size float64
|
||||||
Color string
|
Color string
|
||||||
|
Reflow bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// textLayout 保存编辑中的原文与选项,不写入OFD,也不参与渲染
|
// textLayout 保存编辑中的原文与选项,不写入OFD,也不参与渲染
|
||||||
@@ -140,11 +142,37 @@ func (e *Editor) UpdateTextContent(page int, id, value string, style TextStyle)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
reflow := style.Reflow
|
||||||
|
if reflow {
|
||||||
|
if object.TextObject.ReadDirection != 0 || object.TextObject.CharDirection != 0 {
|
||||||
|
return &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("paragraph layout requires horizontal text")}
|
||||||
|
}
|
||||||
|
if style.Font != "" {
|
||||||
|
object.TextObject.Font = style.Font
|
||||||
|
}
|
||||||
|
if style.Size != 0 {
|
||||||
|
object.TextObject.Size = style.Size
|
||||||
|
}
|
||||||
|
if err := e.LayoutText(&object.TextObject, value, TextLayout{AutoBidi: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
style.Font, style.Size = "", 0
|
||||||
|
style.Reflow = false
|
||||||
|
}
|
||||||
|
if !capability.LayoutKnown && !reflow && style.Font != "" {
|
||||||
|
if err := e.RewriteTextWithFont(&object.TextObject, value, style.Font); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
style.Font = ""
|
||||||
|
}
|
||||||
updates, err := e.styleTextObjects([]GraphicObject{object}, style)
|
updates, err := e.styleTextObjects([]GraphicObject{object}, style)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
text := &updates[0].TextObject
|
text := &updates[0].TextObject
|
||||||
|
if reflow {
|
||||||
|
return e.updateObjects(page, updates, true)
|
||||||
|
}
|
||||||
if capability.LayoutKnown {
|
if capability.LayoutKnown {
|
||||||
_, layout := text.TextLayout()
|
_, layout := text.TextLayout()
|
||||||
err = e.LayoutText(text, value, layout)
|
err = e.LayoutText(text, value, layout)
|
||||||
@@ -283,10 +311,12 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
|
|||||||
}
|
}
|
||||||
if mapping < len(transforms) && position+j >= transforms[mapping].CodePosition {
|
if mapping < len(transforms) && position+j >= transforms[mapping].CodePosition {
|
||||||
transform := &transforms[mapping]
|
transform := &transforms[mapping]
|
||||||
if transform.CodeCount != 1 || transform.GlyphCount != 1 {
|
if transform.CodeCount != transform.GlyphCount {
|
||||||
return &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("changing a combined glyph requires explicit layout")}
|
return &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("changing a combined glyph requires explicit layout")}
|
||||||
}
|
}
|
||||||
transform.Glyphs = strconv.Itoa(int(sfnt.GlyphIndex(char)))
|
glyphs := strings.Fields(transform.Glyphs)
|
||||||
|
glyphs[position+j-transform.CodePosition] = strconv.Itoa(int(sfnt.GlyphIndex(char)))
|
||||||
|
transform.Glyphs = strings.Join(glyphs, " ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,6 +330,52 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RewriteTextWithFont 更换字体并修改定位文字,保留原始坐标及逐字映射,失败时不改变对象
|
||||||
|
// 入参: obj 文字对象, value 新内容, fontID 目标字体,空值沿用原字体
|
||||||
|
// 返回: error 字体、定位或字形错误
|
||||||
|
func (e *Editor) RewriteTextWithFont(obj *TextObject, value, fontID string) error {
|
||||||
|
value = strings.ReplaceAll(strings.ReplaceAll(value, "\r\n", "\n"), "\r", "\n")
|
||||||
|
if fontID == "" || fontID == obj.Font {
|
||||||
|
return e.RewriteText(obj, value)
|
||||||
|
}
|
||||||
|
text := cloneEditorData(*obj)
|
||||||
|
if err := validateTextGlyphs(text, 1<<16); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, transform := range text.CGTransform {
|
||||||
|
if transform.CodeCount != transform.GlyphCount {
|
||||||
|
return &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("changing the font of combined glyphs requires explicit layout")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(text.CGTransform) != 0 && len([]rune(text.Text())) != len([]rune(value)) {
|
||||||
|
return &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("changing positioned text length requires explicit layout")}
|
||||||
|
}
|
||||||
|
transforms := text.CGTransform
|
||||||
|
text.Font, text.CGTransform = fontID, nil
|
||||||
|
if err := e.RewriteText(&text, value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
font, err := e.editorFont(fontID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var missing []rune
|
||||||
|
for _, char := range text.Text() {
|
||||||
|
if char != '\n' && font.GlyphIndex(char) == 0 {
|
||||||
|
missing = append(missing, char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := missingGlyphError(fontID, missing); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
text.Font, text.CGTransform = obj.Font, transforms
|
||||||
|
if err := e.replaceTextFont(&text, fontID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*obj = text
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// TextLayout 获取最近一次排版的原文与选项,区分软换行和显式换行
|
// TextLayout 获取最近一次排版的原文与选项,区分软换行和显式换行
|
||||||
// 信息仅在当前编辑过程保留,读取OFD时返回定位后的文字和默认选项
|
// 信息仅在当前编辑过程保留,读取OFD时返回定位后的文字和默认选项
|
||||||
// 返回: string 原文, TextLayout 排版选项
|
// 返回: string 原文, TextLayout 排版选项
|
||||||
@@ -393,6 +469,13 @@ func (e *Editor) styleTextObjects(objects []GraphicObject, style TextStyle) ([]G
|
|||||||
text := &updates[i].TextObject
|
text := &updates[i].TextObject
|
||||||
value, layout := text.TextLayout()
|
value, layout := text.TextLayout()
|
||||||
known := e.objectOrigin(id) == nil || text.layout != nil
|
known := e.objectOrigin(id) == nil || text.layout != nil
|
||||||
|
if style.Reflow {
|
||||||
|
if text.ReadDirection != 0 || text.CharDirection != 0 {
|
||||||
|
return nil, &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("paragraph layout requires horizontal text")}
|
||||||
|
}
|
||||||
|
known = true
|
||||||
|
layout.AutoBidi = true
|
||||||
|
}
|
||||||
fontChanged := style.Font != "" && style.Font != text.Font
|
fontChanged := style.Font != "" && style.Font != text.Font
|
||||||
sizeChanged := style.Size != 0 && style.Size != text.Size
|
sizeChanged := style.Size != 0 && style.Size != text.Size
|
||||||
if sizeChanged && !known {
|
if sizeChanged && !known {
|
||||||
@@ -408,7 +491,7 @@ func (e *Editor) styleTextObjects(objects []GraphicObject, style TextStyle) ([]G
|
|||||||
if sizeChanged {
|
if sizeChanged {
|
||||||
text.Size = style.Size
|
text.Size = style.Size
|
||||||
}
|
}
|
||||||
if known && (fontChanged || sizeChanged) {
|
if known && (fontChanged || sizeChanged || style.Reflow) {
|
||||||
if err := e.LayoutText(text, value, layout); err != nil {
|
if err := e.LayoutText(text, value, layout); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -458,15 +541,19 @@ func (e *Editor) replaceTextFont(text *TextObject, id string) error {
|
|||||||
var missing []rune
|
var missing []rune
|
||||||
for i := range transforms {
|
for i := range transforms {
|
||||||
transform := &transforms[i]
|
transform := &transforms[i]
|
||||||
if transform.CodeCount != 1 || transform.GlyphCount != 1 {
|
if transform.CodeCount != transform.GlyphCount {
|
||||||
return &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("changing the font of combined glyphs requires explicit layout")}
|
return &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("changing the font of combined glyphs requires explicit layout")}
|
||||||
}
|
}
|
||||||
char := chars[transform.CodePosition]
|
glyphs := make([]string, transform.CodeCount)
|
||||||
glyph := font.GlyphIndex(char)
|
for j := range glyphs {
|
||||||
if glyph == 0 {
|
char := chars[transform.CodePosition+j]
|
||||||
missing = append(missing, char)
|
glyph := font.GlyphIndex(char)
|
||||||
|
if glyph == 0 {
|
||||||
|
missing = append(missing, char)
|
||||||
|
}
|
||||||
|
glyphs[j] = strconv.Itoa(int(glyph))
|
||||||
}
|
}
|
||||||
transform.Glyphs = strconv.Itoa(int(glyph))
|
transform.Glyphs = strings.Join(glyphs, " ")
|
||||||
}
|
}
|
||||||
if err := missingGlyphError(id, missing); err != nil {
|
if err := missingGlyphError(id, missing); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
Reference in new issue
Block a user