mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
1 parent
1a9059429e
commit
745f39a614
4 files changed
+361
-14
No files matched your search
+39
-9
@@ -3755,6 +3755,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
refreshManagement = false;
|
refreshManagement = false;
|
||||||
if (name === "ofdgoChangeFontBinding") err.message = fontBindingError(err.message);
|
if (name === "ofdgoChangeFontBinding") err.message = fontBindingError(err.message);
|
||||||
|
if (name === "ofdgoChangeInvoice") err.message = invoiceIssues([err.message]);
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
if (settingsElements.Panel.open) {
|
if (settingsElements.Panel.open) {
|
||||||
settingsElements.Status.textContent = err.message;
|
settingsElements.Status.textContent = err.message;
|
||||||
@@ -5386,7 +5387,7 @@ function settingsButton(label, action, disabled = false, id = "", container = se
|
|||||||
if (id) button.id = `settings${id}`;
|
if (id) button.id = `settings${id}`;
|
||||||
button.addEventListener("click", async () => {
|
button.addEventListener("click", async () => {
|
||||||
try { await action(); }
|
try { await action(); }
|
||||||
catch (error) { settingsElements.Status.textContent = error.message; }
|
catch (error) { settingsElements.Status.textContent = settings.invoice ? invoiceIssues([error.message]) : error.message; }
|
||||||
});
|
});
|
||||||
container.append(button);
|
container.append(button);
|
||||||
return button;
|
return button;
|
||||||
@@ -5728,13 +5729,18 @@ function invoiceIssues(issues, details = false) {
|
|||||||
"unsupported invoice XML root or version": "暂不支持此发票格式或版本",
|
"unsupported invoice XML root or version": "暂不支持此发票格式或版本",
|
||||||
"unsupported eInvoice namespace or version": "暂不支持此发票格式或版本",
|
"unsupported eInvoice namespace or version": "暂不支持此发票格式或版本",
|
||||||
"unsupported EInvoice version": "暂不支持此发票版本",
|
"unsupported EInvoice version": "暂不支持此发票版本",
|
||||||
"EInvoice requires a unique versioned Header": "发票头信息不完整"
|
"EInvoice requires a unique versioned Header": "发票头信息不完整",
|
||||||
|
"invoice text exceeds original reference slot capacity": "原框空间不足,请缩短文字",
|
||||||
|
"invoice slot requires Font or FontData with the missing characters:": "字体缺字,请更换字体",
|
||||||
|
"invoice references cross columns or reverse reading order": "图元跨列,无法重排",
|
||||||
|
"invoice references do not form adjacent aligned text rows": "图元未对齐,无法重排"
|
||||||
};
|
};
|
||||||
return [...new Set((issues || []).map(issue => {
|
return [...new Set((issues || []).map(issue => {
|
||||||
const message = typeof issue === "string" ? issue : issue.message || issue.code;
|
const message = typeof issue === "string" ? issue : issue.message || issue.code;
|
||||||
if (typeof issue === "string") return details ? message : messages[message] || message;
|
const translated = messages[message] || Object.entries(messages).find(([prefix]) => prefix.endsWith(":") && message?.startsWith(prefix))?.[1] || message;
|
||||||
|
if (typeof issue === "string") return details ? message : translated;
|
||||||
if (details) return [issue.source?.fileLoc || issue.source?.id || issue.source?.kind, issue.source?.nodePath, issue.field, message].filter(Boolean).join(":");
|
if (details) return [issue.source?.fileLoc || issue.source?.id || issue.source?.kind, issue.source?.nodePath, issue.field, message].filter(Boolean).join(":");
|
||||||
return [issue.field, codes[issue.code] || messages[message] || message].filter(Boolean).join(":");
|
return [issue.field, codes[issue.code] || translated].filter(Boolean).join(":");
|
||||||
}).filter(Boolean))].join("\n");
|
}).filter(Boolean))].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6109,6 +6115,12 @@ function renderInvoiceFields(container, draft) {
|
|||||||
invoiceDirty(draft, "fields");
|
invoiceDirty(draft, "fields");
|
||||||
};
|
};
|
||||||
invoiceInput(container, "数值", "FactValue", change?.value ?? fact.value, value => update("value", value), !writable, "textarea");
|
invoiceInput(container, "数值", "FactValue", change?.value ?? fact.value, value => update("value", value), !writable, "textarea");
|
||||||
|
if (writable && !fresh && draft.invoice.standard === invoiceReferenceStandard) {
|
||||||
|
const fonts = state.doc.fonts || [];
|
||||||
|
const libraryFonts = fontManager.editorFonts().filter(font => !font.disabled);
|
||||||
|
settingsSelect(container, "字体", "InvoiceFactFont", [["", "原字体"], ...fonts.map(font => [font.id, font.fontName || font.familyName || font.id]), ...libraryFonts.map(font => [`file:${font.id}`, font.fullName || font.name])], change?.font || "", value => update("font", value));
|
||||||
|
settingsButton("添加", () => openFontFile(el.fontInput), false, "InvoiceFactAddFont", container);
|
||||||
|
}
|
||||||
invoiceInput(container, "空值", "FactNil", change?.nil ?? fact.nil, value => update("nil", value), !writable || draft.invoice.standard === invoiceReferenceStandard, "checkbox");
|
invoiceInput(container, "空值", "FactNil", change?.nil ?? fact.nil, value => update("nil", value), !writable || draft.invoice.standard === invoiceReferenceStandard, "checkbox");
|
||||||
if (!fresh) invoiceInput(container, "删除", "FactDelete", change?.delete || false, value => update("delete", value), !writable || draft.invoice.standard === invoiceReferenceStandard, "checkbox");
|
if (!fresh) invoiceInput(container, "删除", "FactDelete", change?.delete || false, value => update("delete", value), !writable || draft.invoice.standard === invoiceReferenceStandard, "checkbox");
|
||||||
else if (state.editing) settingsButton("删除", () => {
|
else if (state.editing) settingsButton("删除", () => {
|
||||||
@@ -6350,23 +6362,41 @@ async function applyInvoiceDraft(draft, synchronize = draft.synchronize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function prepareInvoiceRequest(draft, request) {
|
async function prepareInvoiceRequest(draft, request) {
|
||||||
if (request.operation !== "create" || draft.invoice.standard !== invoiceReferenceStandard) return true;
|
if (draft.invoice.standard !== invoiceReferenceStandard) return true;
|
||||||
if (!draft.layout.font && !draft.fontChoice?.startsWith("file:")) {
|
const creating = request.operation === "create";
|
||||||
|
const patches = (request.operation === "batch" ? request.operations : [request]).filter(operation => operation.operation === "patch");
|
||||||
|
if (!creating && !patches.length) return true;
|
||||||
|
if (creating && !draft.layout.font && !draft.fontChoice?.startsWith("file:")) {
|
||||||
invoicePanel.tab = "appearance";
|
invoicePanel.tab = "appearance";
|
||||||
draft.layoutOpen = true;
|
draft.layoutOpen = true;
|
||||||
renderDocumentSettings();
|
renderDocumentSettings();
|
||||||
throw new Error("请选择标引票面字体");
|
throw new Error("请选择标引票面字体");
|
||||||
}
|
}
|
||||||
const sequence = settings.sequence, openSeq = state.openSeq;
|
const sequence = settings.sequence, openSeq = state.openSeq;
|
||||||
|
const doc = state.doc, scope = JSON.stringify(invoiceScope());
|
||||||
const snapshot = JSON.stringify(request), fontChoice = draft.fontChoice;
|
const snapshot = JSON.stringify(request), fontChoice = draft.fontChoice;
|
||||||
|
for (const patch of patches) patch.changes = structuredClone(patch.changes);
|
||||||
settingsElements.Form.inert = true;
|
settingsElements.Form.inert = true;
|
||||||
try {
|
try {
|
||||||
if (fontChoice?.startsWith("file:")) {
|
if (creating && fontChoice?.startsWith("file:")) {
|
||||||
const font = fontManager.editorFonts().find(value => `file:${value.id}` === fontChoice && !value.disabled);
|
const font = fontManager.editorFonts().find(value => `file:${value.id}` === fontChoice && !value.disabled);
|
||||||
if (!font) throw new Error("所选字体不可用");
|
if (!font) throw new Error("所选字体不可用");
|
||||||
request.fontData = invoiceBase64(await readTextFont(font));
|
request.fontData = invoiceBase64(await readTextFont(font));
|
||||||
}
|
}
|
||||||
if (sequence !== settings.sequence || openSeq !== state.openSeq || invoiceCurrent() !== draft || !settingsElements.Panel.open || !settings.invoice) return false;
|
const fonts = new Map();
|
||||||
|
for (const patch of patches) {
|
||||||
|
for (const change of patch.changes) {
|
||||||
|
if (!change.font?.startsWith("file:")) continue;
|
||||||
|
if (!fonts.has(change.font)) {
|
||||||
|
const font = fontManager.editorFonts().find(value => `file:${value.id}` === change.font && !value.disabled);
|
||||||
|
if (!font) throw new Error("所选字体不可用");
|
||||||
|
fonts.set(change.font, invoiceBase64(await readTextFont(font)));
|
||||||
|
}
|
||||||
|
change.fontData = fonts.get(change.font);
|
||||||
|
delete change.font;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sequence !== settings.sequence || openSeq !== state.openSeq || doc !== state.doc || scope !== JSON.stringify(invoiceScope()) || invoiceCurrent() !== draft || !settingsElements.Panel.open || !settings.invoice) return false;
|
||||||
if (draft.fontChoice !== fontChoice || JSON.stringify(buildInvoiceRequest(draft)) !== snapshot) throw new Error("内容已变更,请重新应用");
|
if (draft.fontChoice !== fontChoice || JSON.stringify(buildInvoiceRequest(draft)) !== snapshot) throw new Error("内容已变更,请重新应用");
|
||||||
return true;
|
return true;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -6395,7 +6425,7 @@ function buildInvoiceRequest(draft) {
|
|||||||
else {
|
else {
|
||||||
const operations = [];
|
const operations = [];
|
||||||
if (draft.dirty.has("model")) operations.push({ operation: "set", index: draft.entry.index, invoice: draft.invoice });
|
if (draft.dirty.has("model")) operations.push({ operation: "set", index: draft.entry.index, invoice: draft.invoice });
|
||||||
if (draft.dirty.has("fields")) operations.push({ operation: "patch", index: draft.entry.index, changes: [...draft.changes.values()] });
|
if (draft.dirty.has("fields")) operations.push({ operation: "patch", index: draft.entry.index, changes: structuredClone([...draft.changes.values()]) });
|
||||||
if (draft.dirty.has("bindings")) operations.push({ operation: "bindings", index: draft.entry.index, bindings: draft.bindings });
|
if (draft.dirty.has("bindings")) operations.push({ operation: "bindings", index: draft.entry.index, bindings: draft.bindings });
|
||||||
Object.assign(request, operations.length === 1 ? operations[0] : { operation: "batch", operations });
|
Object.assign(request, operations.length === 1 ? operations[0] : { operation: "batch", operations });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InvoiceLayout 指定发票信息页的字体资源及毫米尺寸,不代表税务机关的固定票样
|
// InvoiceLayout 指定发票信息页的字体资源及毫米尺寸,不代表税务机关的固定票样
|
||||||
@@ -537,6 +538,306 @@ func invoiceLayoutText(text *TextObject, value string, metrics FontMetrics) (flo
|
|||||||
return math.Ceil((bottom-top)*1e6) / 1e6, nil
|
return math.Ceil((bottom-top)*1e6) / 1e6, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// layoutInvoiceReferenceSlots 按原图元框和基线分配变长字段,空TextCode保留原ID和原框,不借用跨列空间或改变引用
|
||||||
|
// 入参: ctx 取消上下文, bindings 有序图元槽位, before 原文字, value 新文字, font 字体覆盖, fontData 字体数据
|
||||||
|
// 返回: error 引用、字体、布局或容量错误
|
||||||
|
func (e *Editor) layoutInvoiceReferenceSlots(ctx context.Context, bindings []InvoiceBinding, before, value, font string, fontData []byte) error {
|
||||||
|
if len(bindings) == 0 || !utf8.ValidString(value) || !invoiceWriteText(value) || strings.ContainsAny(value, "\r\n\t") {
|
||||||
|
return fmt.Errorf("invoice slot layout requires valid single-flow text and existing references")
|
||||||
|
}
|
||||||
|
type slot struct {
|
||||||
|
binding InvoiceBinding
|
||||||
|
object GraphicObject
|
||||||
|
box Box
|
||||||
|
x, y float64
|
||||||
|
right float64
|
||||||
|
visible Box
|
||||||
|
}
|
||||||
|
slots := make([]slot, len(bindings))
|
||||||
|
mappings := make(map[string]map[rune]uint16)
|
||||||
|
var original strings.Builder
|
||||||
|
for i, binding := range bindings {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if binding.Page != bindings[0].Page {
|
||||||
|
return fmt.Errorf("invoice slot layout cannot infer flow across pages")
|
||||||
|
}
|
||||||
|
page, err := e.page(binding.Page)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
matches := 0
|
||||||
|
for _, layer := range page.Content.Layer {
|
||||||
|
for _, object := range layer.Objects {
|
||||||
|
if sameResourceID(editorObjectID(object), binding.ObjectID) {
|
||||||
|
matches++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
object, err := e.Object(binding.Page, binding.ObjectID)
|
||||||
|
if err != nil || matches != 1 || object.Type != "TextObject" {
|
||||||
|
return fmt.Errorf("invoice slot requires one direct text object")
|
||||||
|
}
|
||||||
|
text := &object.TextObject
|
||||||
|
if text.CTM != "" || text.Clips != nil || text.ReadDirection != 0 || text.CharDirection != 0 || text.Italic || text.Weight > 400 || text.VScale != 0 || text.Decoration != "" || text.DrawParam != "" || text.Stroke != nil && *text.Stroke || !finite(text.HScale) || text.HScale < 0 || len(text.TextCode) == 0 {
|
||||||
|
return fmt.Errorf("invoice slot requires plain horizontal text without transforms")
|
||||||
|
}
|
||||||
|
box, err := creationBox(text.Boundary)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
x, err := strconv.ParseFloat(text.TextCode[0].X, 64)
|
||||||
|
if err != nil || !finite(x) || x < 0 || x > box.W {
|
||||||
|
return fmt.Errorf("invoice slot has an invalid horizontal origin")
|
||||||
|
}
|
||||||
|
y, err := strconv.ParseFloat(text.TextCode[0].Y, 64)
|
||||||
|
if err != nil || !finite(y) || y < 0 || y > box.H || !finite(text.Size) || text.Size <= 0 {
|
||||||
|
return fmt.Errorf("invoice slot has an invalid baseline or size")
|
||||||
|
}
|
||||||
|
for _, code := range text.TextCode {
|
||||||
|
if code.Index != "" || code.DeltaY != "" || code.Y != "" && code.Y != text.TextCode[0].Y {
|
||||||
|
return fmt.Errorf("invoice slot has unsupported glyph indexes or multiple baselines")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
old := text.Text()
|
||||||
|
if strings.ContainsAny(old, "\r\n\t") {
|
||||||
|
return fmt.Errorf("invoice slot contains multiple text lines")
|
||||||
|
}
|
||||||
|
original.WriteString(old)
|
||||||
|
if font == "" && len(fontData) == 0 {
|
||||||
|
if err := validateTextGlyphs(*text, 1<<16); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if mappings[text.Font] == nil {
|
||||||
|
mappings[text.Font] = make(map[rune]uint16)
|
||||||
|
}
|
||||||
|
runes := []rune(old)
|
||||||
|
for _, transform := range text.CGTransform {
|
||||||
|
glyphs := strings.Fields(transform.Glyphs)
|
||||||
|
if transform.CodeCount != transform.GlyphCount || len(glyphs) != transform.CodeCount {
|
||||||
|
return fmt.Errorf("invoice combined glyph layout requires explicit Font or FontData")
|
||||||
|
}
|
||||||
|
for j, encoded := range glyphs {
|
||||||
|
glyph, err := strconv.ParseUint(encoded, 10, 16)
|
||||||
|
at := transform.CodePosition + j
|
||||||
|
if err != nil || glyph == 0 || at < 0 || at >= len(runes) {
|
||||||
|
return fmt.Errorf("invalid invoice glyph mapping")
|
||||||
|
}
|
||||||
|
if previous, exists := mappings[text.Font][runes[at]]; exists && previous != uint16(glyph) {
|
||||||
|
return fmt.Errorf("ambiguous invoice glyph mapping requires explicit Font or FontData")
|
||||||
|
}
|
||||||
|
mappings[text.Font][runes[at]] = uint16(glyph)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slots[i] = slot{binding: binding, object: object, box: box, x: x, y: y, right: box.W}
|
||||||
|
if i == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
previous := &slots[i-1]
|
||||||
|
step := math.Max(previous.object.TextObject.Size, text.Size)
|
||||||
|
if math.Abs(box.Y+y-previous.box.Y-previous.y) <= step*.1 {
|
||||||
|
start := box.X + x
|
||||||
|
if start <= previous.box.X+previous.x || start-(previous.box.X+previous.box.W) > step {
|
||||||
|
return fmt.Errorf("invoice references cross columns or reverse reading order")
|
||||||
|
}
|
||||||
|
previous.right = math.Min(previous.right, start-previous.box.X)
|
||||||
|
} else if math.Abs(box.X+x-slots[0].box.X-slots[0].x) > step*.25 || box.Y < previous.box.Y+previous.box.H-1e-6 || box.Y-(previous.box.Y+previous.box.H) > step*.5 {
|
||||||
|
return fmt.Errorf("invoice references do not form adjacent aligned text rows")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if original.String() != before {
|
||||||
|
return fmt.Errorf("invoice slot text changed")
|
||||||
|
}
|
||||||
|
reader, err := e.ReaderContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
renderer := e.newRenderer(reader)
|
||||||
|
selected := make(map[string]int, len(slots))
|
||||||
|
for i, current := range slots {
|
||||||
|
selected[editorResourceID(current.binding.ObjectID)] = i
|
||||||
|
}
|
||||||
|
var obstacles []Box
|
||||||
|
page, err := e.page(bindings[0].Page)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, layer := range page.Content.Layer {
|
||||||
|
for _, object := range layer.Objects {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if object.Type != "TextObject" && object.Type != "ImageObject" && object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
box, err := renderer.ObjectBounds(object, layer.DrawParam)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot verify invoice slot neighbours: %w", err)
|
||||||
|
}
|
||||||
|
if index, ok := selected[editorResourceID(editorObjectID(object))]; ok {
|
||||||
|
slots[index].visible = box
|
||||||
|
} else if box.W > 0 && box.H > 0 {
|
||||||
|
obstacles = append(obstacles, box)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(fontData) != 0 {
|
||||||
|
var err error
|
||||||
|
font, err = e.AddFont(FontFile{Data: fontData}, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runes, offset := []rune(value), 0
|
||||||
|
boundaries := richTextBoundaries(value)
|
||||||
|
var missing error
|
||||||
|
for i := range slots {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
current := &slots[i]
|
||||||
|
text := ¤t.object.TextObject
|
||||||
|
if font != "" {
|
||||||
|
text.Font = font
|
||||||
|
}
|
||||||
|
metrics, err := e.editorFont(text.Font)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
end := len(runes)
|
||||||
|
if i == 0 && (text.Text() == "¥" || text.Text() == "¥") && offset < end && string(runes[offset]) == text.Text() {
|
||||||
|
end = offset + 1
|
||||||
|
}
|
||||||
|
count, glyphErr, err := invoiceSlotText(ctx, text, runes[offset:end], offset, boundaries, current.x, current.y, current.right, current.box, current.visible, obstacles, metrics, mappings[text.Font])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
missing = nil
|
||||||
|
}
|
||||||
|
if glyphErr != nil {
|
||||||
|
missing = glyphErr
|
||||||
|
}
|
||||||
|
offset += count
|
||||||
|
}
|
||||||
|
if offset != len(runes) {
|
||||||
|
if missing != nil {
|
||||||
|
return fmt.Errorf("invoice slot requires Font or FontData with the missing characters: %w", missing)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("invoice text exceeds original reference slot capacity")
|
||||||
|
}
|
||||||
|
layers := copyEditorPage(e.pages[bindings[0].Page]).Content.Layer
|
||||||
|
for _, current := range slots {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := validateEditorGeometry(current.object); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if current.object.TextObject.Text() != "" {
|
||||||
|
checked := cloneEditorData(current.object.TextObject)
|
||||||
|
if err := e.prepareText(&checked); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range layers {
|
||||||
|
for j := range layers[i].Objects {
|
||||||
|
if sameResourceID(editorObjectID(layers[i].Objects[j]), current.binding.ObjectID) {
|
||||||
|
layers[i].Objects[j] = current.object
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.replaceLayers(bindings[0].Page, layers)
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// invoiceSlotText 用字体步进及轮廓填充单一原基线槽,保留可搜索文字和显式字形映射
|
||||||
|
// 入参: ctx 取消上下文, text 目标对象, value 剩余文字, offset 原文偏移, boundaries 字素边界, x和y 原点, right 右界, box 原框, visible 原可见范围, obstacles 外部障碍, metrics 字体, mapping 原映射
|
||||||
|
// 返回: int 已容纳码点数, error 可递延缺字, error 度量错误
|
||||||
|
func invoiceSlotText(ctx context.Context, text *TextObject, value []rune, offset int, boundaries map[int]bool, x, y, right float64, box, visible Box, obstacles []Box, metrics FontMetrics, mapping map[rune]uint16) (int, error, error) {
|
||||||
|
if metrics.UnitsPerEm() == 0 {
|
||||||
|
return 0, nil, fmt.Errorf("invalid invoice font units")
|
||||||
|
}
|
||||||
|
unit, cursor := text.Size/float64(metrics.UnitsPerEm()), x
|
||||||
|
scale := text.HScale
|
||||||
|
if scale == 0 {
|
||||||
|
scale = 1
|
||||||
|
}
|
||||||
|
count := 0
|
||||||
|
var missing error
|
||||||
|
var advances, glyphs []string
|
||||||
|
for i, char := range value {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
glyph := mapping[char]
|
||||||
|
if glyph == 0 {
|
||||||
|
glyph = metrics.GlyphIndex(char)
|
||||||
|
}
|
||||||
|
if glyph == 0 {
|
||||||
|
missing = missingGlyphError(text.Font, []rune{char})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var bounds Box
|
||||||
|
var err error
|
||||||
|
if provider, ok := metrics.(FontGlyphBounds); ok {
|
||||||
|
bounds, err = provider.GlyphBounds(glyph, text.Size)
|
||||||
|
} else if outlines, ok := metrics.(FontOutlines); ok {
|
||||||
|
var outline GeometryPath
|
||||||
|
outline, err = outlines.GlyphOutline(glyph, text.Size)
|
||||||
|
if err == nil {
|
||||||
|
bounds, err = outline.Bounds()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 0, nil, fmt.Errorf("invoice slot requires glyph outline metrics")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
advance := float64(metrics.GlyphAdvance(glyph)) * unit * scale
|
||||||
|
bounds.X, bounds.W = bounds.X*scale, bounds.W*scale
|
||||||
|
if !finite(advance) || advance < 0 || !finite(bounds.X) || !finite(bounds.Y) || !finite(bounds.W) || !finite(bounds.H) {
|
||||||
|
return 0, nil, fmt.Errorf("invalid invoice glyph geometry")
|
||||||
|
}
|
||||||
|
if cursor+advance > right+1e-6 || cursor+bounds.X < -1e-6 || cursor+bounds.X+bounds.W > right+1e-6 || y+bounds.Y < -1e-6 || y+bounds.Y+bounds.H > box.H+1e-6 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
blocked := false
|
||||||
|
if bounds.W > 0 && bounds.H > 0 {
|
||||||
|
for _, obstacle := range obstacles {
|
||||||
|
left := math.Max(box.X+cursor+bounds.X, obstacle.X)
|
||||||
|
top := math.Max(box.Y+y+bounds.Y, obstacle.Y)
|
||||||
|
right := math.Min(box.X+cursor+bounds.X+bounds.W, obstacle.X+obstacle.W)
|
||||||
|
bottom := math.Min(box.Y+y+bounds.Y+bounds.H, obstacle.Y+obstacle.H)
|
||||||
|
if right > left+1e-6 && bottom > top+1e-6 && (visible.W <= 0 || visible.H <= 0 || left < visible.X-1e-6 || top < visible.Y-1e-6 || right > visible.X+visible.W+1e-6 || bottom > visible.Y+visible.H+1e-6) {
|
||||||
|
blocked = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if blocked {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
advances = append(advances, ofdNumber(advance))
|
||||||
|
glyphs = append(glyphs, strconv.Itoa(int(glyph)))
|
||||||
|
cursor += advance
|
||||||
|
if boundaries[offset+i+1] {
|
||||||
|
count = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text.TextCode = []TextCode{{X: text.TextCode[0].X, Y: text.TextCode[0].Y, Value: escapeOFDText(string(value[:count]))}}
|
||||||
|
text.CGTransform, text.layout = nil, nil
|
||||||
|
if count > 0 {
|
||||||
|
text.TextCode[0].DeltaX = strings.Join(advances[:count-1], " ")
|
||||||
|
text.CGTransform = []CGTransform{{CodePosition: 0, CodeCount: count, GlyphCount: count, Glyphs: strings.Join(glyphs[:count], " ")}}
|
||||||
|
}
|
||||||
|
return count, missing, nil
|
||||||
|
}
|
||||||
|
|
||||||
// invoiceCheckReflowReferences 检查当前文档快照中指向待重排页的标准导航及已识别标引
|
// invoiceCheckReflowReferences 检查当前文档快照中指向待重排页的标准导航及已识别标引
|
||||||
// 排除本次发票自身的标引,不推测未知扩展中的引用,也不读取其他文档或历史入口
|
// 排除本次发票自身的标引,不推测未知扩展中的引用,也不读取其他文档或历史入口
|
||||||
// 入参: ctx 取消上下文, e 当前编辑器, ownedPageIDs 待替换页面标识, ownStructureIndex 自有标引索引,-1表示无
|
// 入参: ctx 取消上下文, e 当前编辑器, ownedPageIDs 待替换页面标识, ownStructureIndex 自有标引索引,-1表示无
|
||||||
|
|||||||
+15
-5
@@ -161,7 +161,7 @@ func (r *Reader) readInvoiceXML(ctx context.Context, data []byte, source Invoice
|
|||||||
value := strings.TrimSpace(fact.Value)
|
value := strings.TrimSpace(fact.Value)
|
||||||
if numeric {
|
if numeric {
|
||||||
value = strings.TrimSpace(strings.TrimLeft(value, "¥¥"))
|
value = strings.TrimSpace(strings.TrimLeft(value, "¥¥"))
|
||||||
if invoiceDecimal(value) == nil {
|
if value != "" && invoiceDecimal(value) == nil {
|
||||||
report.Issues = append(report.Issues, InvoiceIssue{Code: "invalid-decimal", Message: "invalid invoice amount", Source: factSource, Field: nodePath})
|
report.Issues = append(report.Issues, InvoiceIssue{Code: "invalid-decimal", Message: "invalid invoice amount", Source: factSource, Field: nodePath})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -656,6 +656,7 @@ func (e *Editor) editInvoiceReferences(ctx context.Context, source InvoiceSource
|
|||||||
var selected []InvoiceBinding
|
var selected []InvoiceBinding
|
||||||
var singles []InvoiceBinding
|
var singles []InvoiceBinding
|
||||||
joint := make(map[int][]InvoiceBinding)
|
joint := make(map[int][]InvoiceBinding)
|
||||||
|
flow := make(map[int]InvoiceFieldChange)
|
||||||
editable := invoiceReferenceEditability(invoice)
|
editable := invoiceReferenceEditability(invoice)
|
||||||
seen := make(map[int]bool)
|
seen := make(map[int]bool)
|
||||||
for _, change := range changes {
|
for _, change := range changes {
|
||||||
@@ -667,7 +668,7 @@ func (e *Editor) editInvoiceReferences(ctx context.Context, source InvoiceSource
|
|||||||
return fmt.Errorf("object reference edit requires a unique fact and matching expected text")
|
return fmt.Errorf("object reference edit requires a unique fact and matching expected text")
|
||||||
}
|
}
|
||||||
seen[index] = true
|
seen[index] = true
|
||||||
if change.Value == invoice.Facts[index].Value {
|
if change.Value == invoice.Facts[index].Value && change.Font == "" && len(change.FontData) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !editable[index] {
|
if !editable[index] {
|
||||||
@@ -678,13 +679,13 @@ func (e *Editor) editInvoiceReferences(ctx context.Context, source InvoiceSource
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if binding >= 0 {
|
if binding >= 0 && change.Font == "" && len(change.FontData) == 0 && value != "" {
|
||||||
selected = append(selected, bindings[binding])
|
selected = append(selected, bindings[binding])
|
||||||
singles = append(singles, bindings[binding])
|
singles = append(singles, bindings[binding])
|
||||||
} else {
|
} else {
|
||||||
fact := invoice.Facts[index]
|
fact := invoice.Facts[index]
|
||||||
if utf8.RuneCountInString(value) != utf8.RuneCountInString(fact.Value) || value == "" {
|
if utf8.RuneCountInString(value) != utf8.RuneCountInString(fact.Value) || value == "" || change.Font != "" || len(change.FontData) != 0 {
|
||||||
return fmt.Errorf("multiple invoice references require equal-length positioned replacement")
|
flow[index] = change
|
||||||
}
|
}
|
||||||
for _, ref := range fact.References {
|
for _, ref := range fact.References {
|
||||||
page := slices.IndexFunc(next.pages, func(page PageContent) bool { return sameResourceID(page.ID, ref.PageID) })
|
page := slices.IndexFunc(next.pages, func(page PageContent) bool { return sameResourceID(page.ID, ref.PageID) })
|
||||||
@@ -742,6 +743,12 @@ func (e *Editor) editInvoiceReferences(ctx context.Context, source InvoiceSource
|
|||||||
if len(joint[i]) == 0 {
|
if len(joint[i]) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if change, ok := flow[i]; ok {
|
||||||
|
if err := next.layoutInvoiceReferenceSlots(ctx, joint[i], fact.Value, invoice.Facts[i].Value, change.Font, change.FontData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
value := []rune(invoice.Facts[i].Value)
|
value := []rune(invoice.Facts[i].Value)
|
||||||
offset := 0
|
offset := 0
|
||||||
var original strings.Builder
|
var original strings.Builder
|
||||||
@@ -813,6 +820,9 @@ func (e *Editor) editInvoiceReferences(ctx context.Context, source InvoiceSource
|
|||||||
// 入参: invoice 原发票, fact 原事实, value 新业务值
|
// 入参: invoice 原发票, fact 原事实, value 新业务值
|
||||||
// 返回: string 显示文字, error 值格式错误
|
// 返回: string 显示文字, error 值格式错误
|
||||||
func invoiceReferenceDisplay(invoice Invoice, fact InvoiceFact, value string) (string, error) {
|
func invoiceReferenceDisplay(invoice Invoice, fact InvoiceFact, value string) (string, error) {
|
||||||
|
if value == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
binding, ok := invoiceEDRMFactBinding(invoice, fact)
|
binding, ok := invoiceEDRMFactBinding(invoice, fact)
|
||||||
if !ok {
|
if !ok {
|
||||||
return value, nil
|
return value, nil
|
||||||
|
|||||||
@@ -58,9 +58,12 @@ type InvoiceFieldDescriptor struct {
|
|||||||
|
|
||||||
// InvoiceFieldChange 修改已有叶节点,Revision为源XML的SHA256十六进制摘要
|
// InvoiceFieldChange 修改已有叶节点,Revision为源XML的SHA256十六进制摘要
|
||||||
// 未提供Revision时必须提供ExpectedValue,ExpectedNil缺省表示原节点非空值
|
// 未提供Revision时必须提供ExpectedValue,ExpectedNil缺省表示原节点非空值
|
||||||
|
// Font和FontData仅用于图元引用字段的票面字体替换,FontData优先,不改变字号
|
||||||
type InvoiceFieldChange struct {
|
type InvoiceFieldChange struct {
|
||||||
Source InvoiceSource `json:"source"`
|
Source InvoiceSource `json:"source"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
|
Font string `json:"font,omitempty"`
|
||||||
|
FontData []byte `json:"fontData,omitempty"`
|
||||||
Nil bool `json:"nil,omitempty"`
|
Nil bool `json:"nil,omitempty"`
|
||||||
Delete bool `json:"delete,omitempty"`
|
Delete bool `json:"delete,omitempty"`
|
||||||
Revision string `json:"revision,omitempty"`
|
Revision string `json:"revision,omitempty"`
|
||||||
@@ -984,6 +987,9 @@ func PatchInvoice(ctx context.Context, data []byte, changes []InvoiceFieldChange
|
|||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
var patches []invoiceXMLPatch
|
var patches []invoiceXMLPatch
|
||||||
for _, change := range changes {
|
for _, change := range changes {
|
||||||
|
if change.Font != "" || len(change.FontData) != 0 {
|
||||||
|
return nil, fmt.Errorf("invoice font replacement requires object references")
|
||||||
|
}
|
||||||
path := change.Source.NodePath
|
path := change.Source.NodePath
|
||||||
s, ok := spans[path]
|
s, ok := spans[path]
|
||||||
if !ok || !allowed[path] || len(s.node.children) > 0 || seen[path] {
|
if !ok || !allowed[path] || len(s.node.children) > 0 || seen[path] {
|
||||||
|
|||||||
Reference in new issue
Block a user