mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
This commit is contained in:
1 parent
bf27afea13
commit
f924f008a3
9 files changed
+92
-16
No files matched your search
@@ -273,12 +273,14 @@ a:focus-visible {
|
||||
gap: 12px;
|
||||
padding: 6px 8px;
|
||||
line-height: 1.4;
|
||||
overflow-wrap: anywhere;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.font-listbox [role="option"] > span:first-child {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.font-option-source {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CanvasEditor, canEditObject, objectEditReason } from "./ofdgo_edit.js";
|
||||
import { CanvasEditor, canEditObject, missingGlyphMessage, objectEditReason } from "./ofdgo_edit.js";
|
||||
import { FontManager, FontPicker } from "./ofdgo_font.js";
|
||||
|
||||
const MM_TO_PX = 96 / 25.4;
|
||||
@@ -1727,7 +1727,7 @@ async function loadWASM() {
|
||||
if (data.ok) {
|
||||
request.resolve(data.data);
|
||||
} else {
|
||||
const err = new Error(data.error);
|
||||
const err = new Error(missingGlyphMessage(data.missingGlyphs) || data.error);
|
||||
if (data.canceled) {
|
||||
err.name = "AbortError";
|
||||
}
|
||||
|
||||
@@ -122,13 +122,21 @@ export function canEditObject(item, capability) {
|
||||
return Boolean(item) && (item.items || [item]).every(object => Boolean(object.capabilities?.[capability]));
|
||||
}
|
||||
|
||||
export function missingGlyphMessage(diagnostic) {
|
||||
if (!diagnostic) return "";
|
||||
const characters = Array.from(diagnostic.characters);
|
||||
const sample = characters.slice(0, 8).map(char => /[\p{C}\p{Z}\p{M}]/u.test(char)
|
||||
? `U+${char.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}` : char).join(" ");
|
||||
return `字体缺字:${sample}${characters.length > 8 ? ` 等 ${characters.length} 字` : ""},改选字体`;
|
||||
}
|
||||
|
||||
export function objectEditReason(item) {
|
||||
const reasons = (item?.items || (item ? [item] : [])).map(object => {
|
||||
const reason = object.capabilities?.reason || "";
|
||||
if (!reason) return "";
|
||||
let message = "对象特性暂不支持";
|
||||
if (reason.includes("embedded font")) message = "缺少内嵌字体";
|
||||
else if (reason.includes("does not contain")) message = "字体缺字";
|
||||
if (object.capabilities.missingGlyphs) message = missingGlyphMessage(object.capabilities.missingGlyphs);
|
||||
else if (reason.includes("embedded font")) message = "缺少内嵌字体";
|
||||
else if (reason.includes("color") || reason.includes("RGB")) message = "颜色样式暂不支持";
|
||||
else if (reason.includes("draw parameter")) message = "绘制参数暂不支持";
|
||||
else if (reason.includes("layer")) message = "图层特性暂不支持";
|
||||
|
||||
@@ -80,7 +80,7 @@ export class FontPicker {
|
||||
label.textContent = source;
|
||||
option.append(label);
|
||||
}
|
||||
option.title = font.name || font.fullName;
|
||||
option.title = [...new Set([font.fullName, font.name].filter(Boolean))].join(" · ");
|
||||
this.list.append(option);
|
||||
}
|
||||
if (!this.matches.length) {
|
||||
@@ -100,9 +100,9 @@ export class FontPicker {
|
||||
|
||||
position() {
|
||||
if (!this.open) return;
|
||||
const rect = this.input.getBoundingClientRect();
|
||||
const rect = this.input.parentElement.getBoundingClientRect();
|
||||
if (rect.right <= 0 || rect.left >= window.innerWidth) { this.close(); return; }
|
||||
const width = Math.min(Math.max(rect.width, 260), window.innerWidth - 16);
|
||||
const width = Math.min(rect.width, 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` });
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
@@ -80,6 +81,7 @@ type apiResult struct {
|
||||
OK bool `json:"ok"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Data any `json:"data,omitempty"`
|
||||
MissingGlyphs *ofdgo.MissingGlyphError `json:"missingGlyphs,omitempty"`
|
||||
}
|
||||
|
||||
// RunWASM 注册浏览器WASM接口并阻塞运行
|
||||
@@ -167,7 +169,9 @@ func registerExportCallback(name string, fn func([]js.Value) (any, error)) {
|
||||
func callbackResult(fn func([]js.Value) (any, error), args []js.Value) any {
|
||||
data, err := safeCall(fn, args)
|
||||
if err != nil {
|
||||
return encodeResult(apiResult{OK: false, Error: err.Error()})
|
||||
result := apiResult{Error: err.Error()}
|
||||
errors.As(err, &result.MissingGlyphs)
|
||||
return encodeResult(result)
|
||||
}
|
||||
if result, ok := data.(js.Value); ok {
|
||||
return result
|
||||
@@ -698,6 +702,9 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
||||
}
|
||||
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}}
|
||||
if missing := capability.MissingGlyphs; missing != nil {
|
||||
item["capabilities"].(map[string]any)["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters}
|
||||
}
|
||||
if object.Type == "ImageObject" && capability.Update {
|
||||
full, err := object.ImageObject.ImageBounds()
|
||||
if err != nil {
|
||||
|
||||
+12
-3
@@ -858,16 +858,21 @@ func (e *Editor) LayoutText(obj *TextObject, value string, options TextLayout) e
|
||||
lineHeight = math.Max(obj.Size, float64(int(ascender)-int(descender)+int(gap))*unit)
|
||||
}
|
||||
var codes []TextCode
|
||||
var missing []rune
|
||||
for _, paragraph := range strings.Split(value, "\n") {
|
||||
runes := []rune(paragraph)
|
||||
advances := make([]float64, len(runes))
|
||||
for i, char := range runes {
|
||||
glyph := sfnt.GlyphIndex(char)
|
||||
if glyph == 0 {
|
||||
return fmt.Errorf("font does not contain U+%04X", char)
|
||||
missing = append(missing, char)
|
||||
continue
|
||||
}
|
||||
advances[i] = float64(sfnt.GlyphAdvance(glyph)) * unit * hScale
|
||||
}
|
||||
if len(missing) != 0 {
|
||||
continue
|
||||
}
|
||||
spaceTextAdvances(runes, advances, options.LetterSpacing)
|
||||
for _, advance := range advances {
|
||||
if !finite(advance) {
|
||||
@@ -884,6 +889,9 @@ func (e *Editor) LayoutText(obj *TextObject, value string, options TextLayout) e
|
||||
codes = append(codes, TextCode{X: ofdNumber(x), Y: ofdNumber(y), DeltaX: deltas, Value: escapeOFDText(string(runes[line[0]:line[1]]))})
|
||||
}
|
||||
}
|
||||
if err := missingGlyphError(obj.Font, missing); err != nil {
|
||||
return err
|
||||
}
|
||||
obj.TextCode = codes
|
||||
obj.layout = nil
|
||||
if options != (TextLayout{}) || e.objectOrigin(obj.ID) != nil {
|
||||
@@ -918,6 +926,7 @@ func (e *Editor) prepareText(obj *TextObject) error {
|
||||
}
|
||||
}
|
||||
obj.TextCode = append([]TextCode(nil), obj.TextCode...)
|
||||
var missing []rune
|
||||
x, y := "", ""
|
||||
for i := range obj.TextCode {
|
||||
code := &obj.TextCode[i]
|
||||
@@ -939,7 +948,7 @@ func (e *Editor) prepareText(obj *TextObject) error {
|
||||
runes := textCodeRunes(code.Value)
|
||||
for _, char := range runes {
|
||||
if sfnt.GlyphIndex(char) == 0 {
|
||||
return fmt.Errorf("font does not contain U+%04X", char)
|
||||
missing = append(missing, char)
|
||||
}
|
||||
}
|
||||
for _, delta := range []string{code.DeltaX, code.DeltaY} {
|
||||
@@ -957,7 +966,7 @@ func (e *Editor) prepareText(obj *TextObject) error {
|
||||
if strings.Trim(obj.Text(), "\n") == "" {
|
||||
return fmt.Errorf("text codes are empty")
|
||||
}
|
||||
return nil
|
||||
return missingGlyphError(obj.Font, missing)
|
||||
}
|
||||
|
||||
// creationDeltas 校验文字位移数组及g重复编码
|
||||
|
||||
@@ -57,7 +57,7 @@ func (e *Editor) Objects(page int, ids []string) ([]GraphicObject, error) {
|
||||
return nil, err
|
||||
}
|
||||
if !capability.Copy {
|
||||
return nil, fmt.Errorf("object %q cannot be copied: %s", editorObjectID(object), capability.Reason)
|
||||
return nil, fmt.Errorf("object %q cannot be copied: %w", editorObjectID(object), capability.editError())
|
||||
}
|
||||
objects[i], err = cloneEditorObject(object)
|
||||
if err != nil {
|
||||
@@ -362,7 +362,7 @@ func (e *Editor) updateObjects(page int, objects []GraphicObject, geometry bool)
|
||||
return err
|
||||
}
|
||||
if !capability.Transform || !geometry && !e.sourceRGB() {
|
||||
return fmt.Errorf("object %q is read-only for this operation: %s", ids[i], capability.Reason)
|
||||
return fmt.Errorf("object %q is read-only for this operation: %w", ids[i], capability.editError())
|
||||
}
|
||||
}
|
||||
if geometry && e.originalPage(page) {
|
||||
@@ -498,7 +498,7 @@ func (e *Editor) DeleteObjects(page int, ids []string) error {
|
||||
return err
|
||||
}
|
||||
if !capability.Delete {
|
||||
return fmt.Errorf("object %q cannot be deleted: %s", id, capability.Reason)
|
||||
return fmt.Errorf("object %q cannot be deleted: %w", id, capability.editError())
|
||||
}
|
||||
}
|
||||
layers := copyEditorPage(e.pages[page]).Content.Layer
|
||||
@@ -568,7 +568,7 @@ func (e *Editor) objectBounds(page int, objects []GraphicObject) ([]Box, error)
|
||||
return nil, err
|
||||
}
|
||||
if !capability.Arrange {
|
||||
return nil, fmt.Errorf("object %q cannot be arranged: %s", editorObjectID(object), capability.Reason)
|
||||
return nil, fmt.Errorf("object %q cannot be arranged: %w", editorObjectID(object), capability.editError())
|
||||
}
|
||||
}
|
||||
reader, err := e.Reader()
|
||||
|
||||
@@ -17,6 +17,7 @@ package ofdgo
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
@@ -59,6 +60,7 @@ type editorSourcePage struct {
|
||||
// ObjectCapabilities 已有对象可执行的操作,Reason说明受限原因
|
||||
// Update表示完整替换内容,ReplaceFont表示可显式替换文字字体,Transform表示保留文字定位的几何变换,Order仅限同一图层或末级页块
|
||||
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复
|
||||
// MissingGlyphs提供缺字导致操作受限时的结构化诊断
|
||||
type ObjectCapabilities struct {
|
||||
Update bool
|
||||
ReplaceFont bool
|
||||
@@ -70,6 +72,16 @@ type ObjectCapabilities struct {
|
||||
Delete bool
|
||||
Order bool
|
||||
Reason string
|
||||
MissingGlyphs *MissingGlyphError
|
||||
}
|
||||
|
||||
// editError 将操作受限原因转为错误,保留可供调用方识别的缺字诊断
|
||||
// 返回: error 受限原因
|
||||
func (c ObjectCapabilities) editError() error {
|
||||
if c.MissingGlyphs != nil {
|
||||
return c.MissingGlyphs
|
||||
}
|
||||
return errors.New(c.Reason)
|
||||
}
|
||||
|
||||
// PageCapabilities 页面可执行的操作,Insert表示可以添加RGB对象;新增空白页面始终可用
|
||||
@@ -351,10 +363,12 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
|
||||
all.Order = editorContainerOrderable(node.parent)
|
||||
if _, err := e.prepareObject(id, object); err != nil {
|
||||
all.Update, all.Reflow, all.Copy, all.Reason = false, false, false, err.Error()
|
||||
errors.As(err, &all.MissingGlyphs)
|
||||
}
|
||||
if !e.sourceRGB() {
|
||||
all.ReplaceFont = false
|
||||
all.Update, all.Reflow, all.Copy, all.Reason = false, false, false, "document uses a non-RGB default color space"
|
||||
all.MissingGlyphs = nil
|
||||
}
|
||||
if object.Type == "TextObject" {
|
||||
all.Arrange = all.Update
|
||||
|
||||
@@ -15,11 +15,47 @@
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-text/typesetting/segmenter"
|
||||
)
|
||||
|
||||
// MissingGlyphError 字体缺字错误,FontID为OFD字体资源标识,Characters按首次出现顺序保留不重复的缺失字符
|
||||
// 可通过errors.As从编辑错误中取得,不表示字体缺失或文件损坏
|
||||
type MissingGlyphError struct {
|
||||
FontID string `json:"fontID"`
|
||||
Characters string `json:"characters"`
|
||||
}
|
||||
|
||||
// Error 返回缺失字符的Unicode码点
|
||||
// 返回: string 错误描述
|
||||
func (e *MissingGlyphError) Error() string {
|
||||
var codes []string
|
||||
for _, char := range e.Characters {
|
||||
codes = append(codes, fmt.Sprintf("U+%04X", char))
|
||||
}
|
||||
return fmt.Sprintf("font %q does not contain %s", e.FontID, strings.Join(codes, ", "))
|
||||
}
|
||||
|
||||
// missingGlyphError 汇总缺失字符,无缺字时返回nil
|
||||
// 入参: id 字体资源标识, characters 缺失字符
|
||||
// 返回: error 缺字诊断
|
||||
func missingGlyphError(id string, characters []rune) error {
|
||||
if len(characters) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[rune]bool)
|
||||
var value strings.Builder
|
||||
for _, char := range characters {
|
||||
if !seen[char] {
|
||||
seen[char] = true
|
||||
value.WriteRune(char)
|
||||
}
|
||||
}
|
||||
return &MissingGlyphError{FontID: id, Characters: value.String()}
|
||||
}
|
||||
|
||||
// TextLayout 本地横向段落选项,Wrap按CTM变换前的边界宽度折行,Align为left、center、right或justify
|
||||
// LineHeight为毫米单位的基线间距,0使用字体度量;LetterSpacing为字素间的附加毫米间距,可为负
|
||||
// 零值保持显式换行和左对齐
|
||||
|
||||
Reference in new issue
Block a user