From 2c96b4e490d72388766ce67c3b3a91906c15a08e Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Fri, 9 Oct 2026 18:28:32 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D):=20?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=AD=97=E5=BD=A2=E6=B8=B2=E6=9F=93=E4=B8=8E?= =?UTF-8?q?=E4=B9=A6=E7=AD=BE=E8=B7=B3=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/ofdgo.js | 25 ++------ ofdgo_action.go | 48 +++++++++------ ofdgo_action_destination.go | 109 ++++++++++++++++++++++++++++++++++ ofdgo_editor_import.go | 5 +- ofdgo_editor_link.go | 24 ++------ ofdgo_pdf_link.go | 14 +++++ ofdgo_render_text.go | 6 -- ofdgo_renderer_text_canvas.go | 18 +----- ofdgo_text.go | 18 ------ ofdgo_writer.go | 29 +-------- 10 files changed, 164 insertions(+), 132 deletions(-) create mode 100644 ofdgo_action_destination.go diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 47c9d97..3987474 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -6209,13 +6209,6 @@ function destinationPoint(page, x, y) { return { x, y }; } -function sourcePoint(page, x, y) { - if (state.rotation === 90) return { x: y, y: page.height - x }; - if (state.rotation === 180) return { x: page.width - x, y: page.height - y }; - if (state.rotation === 270) return { x: page.width - y, y: x }; - return { x, y }; -} - async function navigateDestination(dest) { if (document.body.hasAttribute("aria-busy")) return; const index = pageIndexByID(state.doc.pages, dest.pageID); @@ -6228,22 +6221,12 @@ async function navigateDestination(dest) { return; } const seq = state.openSeq, scale = state.scale; - let retained = null; - if (dest.omitLeft || dest.omitTop) { - const current = state.doc.pages[state.pageIndex], node = pageShell(state.pageIndex); - if (current && node) { - const shell = node.getBoundingClientRect(), viewer = el.viewerPanel.getBoundingClientRect(); - retained = sourcePoint(current, - (viewer.left + (el.viewerPanel.clientLeft || 0) - shell.left - (node.clientLeft || 0)) / (MM_TO_PX * scale), - (viewer.top + (el.viewerPanel.clientTop || 0) - shell.top - (node.clientTop || 0)) / (MM_TO_PX * scale)); - } - } await renderPage(index, { fit: false, scroll: false }); if (seq !== state.openSeq || state.pageIndex !== index) return; const page = state.doc.pages[index], size = pageViewSize(page), space = pageSpace(); const width = el.viewerPanel.clientWidth - space * 2, height = el.viewerPanel.clientHeight - space * 2; - let left = dest.omitLeft && retained ? retained.x : dest.left; - let top = dest.omitTop && retained ? retained.y : dest.top; + let left = dest.omitLeft ? 0 : (dest.left ?? 0); + let top = dest.omitTop ? 0 : (dest.top ?? 0); if (dest.type === "Fit") { fitHeight(false); scrollToPage(index); @@ -6255,8 +6238,8 @@ async function navigateDestination(dest) { setScale(height / (size.height * MM_TO_PX), false); top = 0; } else if (dest.type === "FitR") { - const a = destinationPoint(page, dest.left, dest.top), b = destinationPoint(page, dest.right, dest.bottom); - if (dest.right <= dest.left || dest.bottom <= dest.top) return; + const a = destinationPoint(page, left, top), b = destinationPoint(page, dest.right, dest.bottom); + if (dest.right <= left || dest.bottom <= top) return; setScale(Math.min(width / Math.abs(b.x - a.x), height / Math.abs(b.y - a.y)) / MM_TO_PX, false); } else { setScale(!dest.omitZoom && dest.zoom > 0 ? dest.zoom : scale, false); diff --git a/ofdgo_action.go b/ofdgo_action.go index c85bb58..82a124c 100644 --- a/ofdgo_action.go +++ b/ofdgo_action.go @@ -16,6 +16,7 @@ package ofdgo import ( "encoding/xml" + "fmt" "net/url" "strconv" ) @@ -37,7 +38,8 @@ type Goto struct { Bookmark *GotoBookmark `xml:"Bookmark"` } -// Dest 文档内跳转目标 +// Dest 文档内跳转目标,坐标单位为毫米 +// Omit字段保留属性省略状态;Left和Top省略时取0,Zoom省略或为0时保持当前缩放 type Dest struct { Type string `xml:"Type,attr"` PageID string `xml:"PageID,attr"` @@ -144,24 +146,35 @@ func (dest *Dest) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { Bottom *float64 `xml:"Bottom"` Zoom *float64 `xml:"Zoom"` } - dest.Type = attrValue(start, "Type") - dest.PageID = attrValue(start, "PageID") - dest.OmitLeft, dest.OmitTop, dest.OmitZoom = true, true, true + *dest = Dest{Type: attrValue(start, "Type"), PageID: attrValue(start, "PageID"), OmitLeft: true, OmitTop: true, OmitZoom: true} for _, attr := range start.Attr { + if attr.Name.Space != "" { + continue + } + var target *float64 switch attr.Name.Local { case "Left": dest.OmitLeft = false + target = &dest.Left case "Top": dest.OmitTop = false + target = &dest.Top case "Zoom": dest.OmitZoom = false + target = &dest.Zoom + case "Right": + target = &dest.Right + case "Bottom": + target = &dest.Bottom + } + if target != nil { + v, err := strconv.ParseFloat(attr.Value, 64) + if err != nil || !finite(v) { + return fmt.Errorf("invalid destination %s", attr.Name.Local) + } + *target = v } } - dest.Left = actionFloatAttr(start, "Left") - dest.Right = actionFloatAttr(start, "Right") - dest.Top = actionFloatAttr(start, "Top") - dest.Bottom = actionFloatAttr(start, "Bottom") - dest.Zoom = actionFloatAttr(start, "Zoom") if err := d.DecodeElement(&value, &start); err != nil { return err } @@ -183,6 +196,11 @@ func (dest *Dest) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { dest.Zoom = *value.Zoom dest.OmitZoom = false } + for _, number := range []float64{dest.Left, dest.Right, dest.Top, dest.Bottom, dest.Zoom} { + if !finite(number) { + return fmt.Errorf("invalid destination coordinate") + } + } return nil } @@ -239,24 +257,18 @@ func (a *RegionArea) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error // 返回: *Dest 跳转目标 func gotoDest(action *Goto, bookmarks map[string]Dest) *Dest { if action.Dest != nil { - return action.Dest + dest := action.Dest.effective() + return &dest } if action.Bookmark != nil { if dest, ok := bookmarks[action.Bookmark.Name]; ok { + dest = dest.effective() return &dest } } return nil } -// actionFloatAttr 获取动作浮点属性 -// 入参: start 起始节点, name 属性名 -// 返回: float64 属性值 -func actionFloatAttr(start xml.StartElement, name string) float64 { - value, _ := strconv.ParseFloat(attrValue(start, name), 64) - return value -} - // pageActionSources 获取页面动作来源 // 入参: page 页面内容, box 页面区域 // 返回: []actionSource 动作来源 diff --git a/ofdgo_action_destination.go b/ofdgo_action_destination.go new file mode 100644 index 0000000..d3cfdab --- /dev/null +++ b/ofdgo_action_destination.go @@ -0,0 +1,109 @@ +// Copyright 2025-2026 肖其顿 (XIAO QI DUN) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ofdgo + +import ( + "encoding/xml" + "fmt" +) + +// MarshalXML 按标准属性写入目标,书签和动作采用相同格式 +// 入参: encoder XML编码器, start 起始节点 +// 返回: error 参数或编码错误 +func (dest Dest) MarshalXML(encoder *xml.Encoder, start xml.StartElement) error { + attrs, err := dest.attributes() + if err != nil { + return err + } + start.Attr = attrs + if err := encoder.EncodeToken(start); err != nil { + return err + } + return encoder.EncodeToken(start.End()) +} + +// effective 应用省略属性的默认值,不改变原始声明 +// 返回: Dest 生效的目标参数 +func (dest Dest) effective() Dest { + if dest.OmitLeft { + dest.Left = 0 + } + if dest.OmitTop { + dest.Top = 0 + } + if dest.OmitZoom { + dest.Zoom = 0 + } + return dest +} + +// attributes 校验并生成目标属性,缩放为0表示沿用当前比例 +// 返回: ofdAttrs 标准属性, error 模式或参数错误 +func (dest Dest) attributes() (ofdAttrs, error) { + dest = dest.effective() + attrs := ofdAttrs{{Name: xml.Name{Local: "Type"}, Value: dest.Type}, {Name: xml.Name{Local: "PageID"}, Value: dest.PageID}} + if dest.PageID == "" { + return nil, fmt.Errorf("missing destination page") + } + add := func(name string, value float64, omit bool) error { + if !finite(value) { + return fmt.Errorf("invalid destination %s", name) + } + if !omit { + attrs.add(name, ofdNumber(value)) + } + return nil + } + switch dest.Type { + case "XYZ": + if dest.Zoom != 0 && (dest.Zoom < 0.1 || dest.Zoom > 64) { + return nil, fmt.Errorf("destination zoom must be zero or between 0.1 and 64") + } + if err := add("Zoom", dest.Zoom, dest.OmitZoom); err != nil { + return nil, err + } + fallthrough + case "FitR": + if err := add("Left", dest.Left, dest.OmitLeft); err != nil { + return nil, err + } + if err := add("Top", dest.Top, dest.OmitTop); err != nil { + return nil, err + } + if dest.Type == "FitR" { + if dest.Right <= dest.Left || dest.Bottom <= dest.Top { + return nil, fmt.Errorf("empty destination rectangle") + } + if err := add("Right", dest.Right, false); err != nil { + return nil, err + } + if err := add("Bottom", dest.Bottom, false); err != nil { + return nil, err + } + } + case "FitH": + if err := add("Top", dest.Top, dest.OmitTop); err != nil { + return nil, err + } + case "FitV": + if err := add("Left", dest.Left, dest.OmitLeft); err != nil { + return nil, err + } + case "Fit": + default: + return nil, fmt.Errorf("invalid destination type %q", dest.Type) + } + return attrs, nil +} diff --git a/ofdgo_editor_import.go b/ofdgo_editor_import.go index 43d1234..19580bd 100644 --- a/ofdgo_editor_import.go +++ b/ofdgo_editor_import.go @@ -1019,10 +1019,7 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b continue } dest := bookmark.Dest - encoded, err := xml.Marshal(struct { - XMLName xml.Name `xml:"Dest"` - Dest - }{Dest: dest}) + encoded, err := xml.Marshal(dest) if err != nil { return nil, err } diff --git a/ofdgo_editor_link.go b/ofdgo_editor_link.go index 8e6b802..62e2b4f 100644 --- a/ofdgo_editor_link.go +++ b/ofdgo_editor_link.go @@ -391,26 +391,10 @@ func annotationLinkXML(target AnnotationLink) ([]byte, error) { data, err = editorXMLContainer("URI", attrs, nil) } else { dest := target.Dest - if !slices.Contains([]string{"XYZ", "Fit", "FitH", "FitV", "FitR"}, dest.Type) { - return nil, fmt.Errorf("invalid destination type %q", dest.Type) - } - attrs := ofdAttrs{{Name: xml.Name{Local: "Type"}, Value: dest.Type}, {Name: xml.Name{Local: "PageID"}, Value: dest.PageID}} - for _, value := range []struct { - name string - value float64 - }{{"Left", dest.Left}, {"Top", dest.Top}, {"Right", dest.Right}, {"Bottom", dest.Bottom}, {"Zoom", dest.Zoom}} { - if !finite(value.value) { - return nil, fmt.Errorf("destination coordinates must be finite") - } - omit := dest.Type == "XYZ" && (value.name == "Left" && dest.OmitLeft || value.name == "Top" && dest.OmitTop || value.name == "Zoom" && dest.OmitZoom) - omit = omit || dest.Type == "FitH" && value.name == "Top" && dest.OmitTop - omit = omit || dest.Type == "FitV" && value.name == "Left" && dest.OmitLeft - if omit { - continue - } - if dest.Type == "XYZ" && slices.Contains([]string{"Left", "Top", "Zoom"}, value.name) || dest.Type == "FitH" && value.name == "Top" || dest.Type == "FitV" && value.name == "Left" || dest.Type == "FitR" && value.name != "Zoom" { - attrs.add(value.name, ofdNumber(value.value)) - } + var attrs ofdAttrs + attrs, err = dest.attributes() + if err != nil { + return nil, err } data, err = editorXMLContainer("Dest", attrs, nil) if err == nil { diff --git a/ofdgo_pdf_link.go b/ofdgo_pdf_link.go index 1f48f30..58e3e8b 100644 --- a/ofdgo_pdf_link.go +++ b/ofdgo_pdf_link.go @@ -488,6 +488,20 @@ func (p *pdfImporter) linkAction(annotation pdfgo.Annotation, strict bool, curre target.KeepLeft, target.KeepTop = false, false } dest := &Dest{Type: string(target.Mode), PageID: p.pageIDs[destination.Page], Left: target.Left, Top: target.Top, Right: target.Right, Bottom: target.Bottom, Zoom: target.Zoom, OmitLeft: target.KeepLeft, OmitTop: target.KeepTop, OmitZoom: target.KeepZoom} + if target.KeepLeft || target.KeepTop { + if strict { + return nil, &pdfgo.UnsupportedError{Feature: "destination retained coordinates cannot be represented in OFD"} + } + p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF destination retained coordinates use OFD default zero"}) + } + if dest.Type == "XYZ" && !dest.OmitZoom && dest.Zoom != 0 && (dest.Zoom < 0.1 || dest.Zoom > 64) { + if strict { + return nil, &pdfgo.UnsupportedError{Feature: "destination zoom outside OFD range"} + } + dest.OmitZoom = true + p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF destination zoom outside OFD range; current zoom retained"}) + } + *dest = dest.effective() action.Goto = &Goto{Dest: dest} *currentPage = p.pageIndexes[destination.Page] } diff --git a/ofdgo_render_text.go b/ofdgo_render_text.go index a525612..6090c37 100644 --- a/ofdgo_render_text.go +++ b/ofdgo_render_text.go @@ -181,12 +181,6 @@ func (r *Renderer) PositionText(object TextObject, state RenderState) (*Position } origin := positioner.Next(advance) scaleX := horizontal - if object.CharDirection == 0 { - limit := textGlyphAdvanceLimit(positioner.dxs, positioner.dys, positioner.xs, i, len(glyphs), origin.X) - if limit > 0 && width*scaleX > limit { - scaleX = limit / width - } - } if bold && len(path) > 0 { outline, err := geometry.Stroke(path, StrokeOptions{Width: size * 0.04, Join: "Round", Cap: "Round"}) if err != nil { diff --git a/ofdgo_renderer_text_canvas.go b/ofdgo_renderer_text_canvas.go index 551310a..5795bbf 100644 --- a/ofdgo_renderer_text_canvas.go +++ b/ofdgo_renderer_text_canvas.go @@ -224,7 +224,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 spans = textGlyphSpans(runes, glyphTransforms, codePos) } positioner := NewTextPositioner(tc, obj.ReadDirection) - dxs, dys, xs, ys := positioner.dxs, positioner.dys, positioner.xs, positioner.ys + xs, ys := positioner.xs, positioner.ys drawAsPath := embeddedFont || face.FauxBold > 0 || textCodePositioned(tc, xs, ys) || fillClip != nil || shadedFill || fillPattern != nil || shouldStroke for i, glyph := range glyphs { str := glyph.Text @@ -260,19 +260,12 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 canvasX, canvasY = tx+bx, pageH-(ty+by) } textWidth := glyphWidth * hScale - advanceLimit := 0.0 - if obj.CharDirection == 0 { - advanceLimit = textGlyphAdvanceLimit(dxs, dys, xs, i, len(glyphs), cx) - } if textRun != nil { if i < len(spans) { if glyphPath == nil { glyphPath, _ = r.cachedTextGlyphPath(face, glyph) } scaleX := hScale - if drawAsGlyphPath && advanceLimit > 0 && glyphWidth*scaleX > advanceLimit { - scaleX = advanceLimit / glyphWidth - } m := canvas.Identity.Translate(canvasX, canvasY) if useTextMatrix { m = m.Mul(glyphMatrix) @@ -294,9 +287,6 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 } if shouldStroke { scaleX := hScale - if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit { - scaleX = advanceLimit / glyphWidth - } transform := canvas.Identity.Translate(canvasX, canvasY) if useTextMatrix { transform = transform.Mul(glyphMatrix) @@ -359,9 +349,6 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 } if glyphClip != nil || shadedFill || fillPattern != nil { scaleX := hScale - if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit { - scaleX = advanceLimit / glyphWidth - } textWidth = glyphWidth * scaleX textTransform := canvas.Identity.Translate(canvasX, canvasY) if useTextMatrix { @@ -394,9 +381,6 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 drawGlyph := func(x, y float64) { if drawAsGlyphPath { scaleX := hScale - if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit { - scaleX = advanceLimit / glyphWidth - } textWidth = glyphWidth * scaleX if face.FauxBold != 0 { if renderer, ok := ctx.Renderer.(*pdfRenderer); ok { diff --git a/ofdgo_text.go b/ofdgo_text.go index af82090..302c8b6 100644 --- a/ofdgo_text.go +++ b/ofdgo_text.go @@ -265,24 +265,6 @@ func (r *Renderer) textIndexRune(fontID string, glyphID int) rune { return rune(glyphID) } -// textGlyphAdvanceLimit 获取显式字形推进宽度 -// 入参: dxs X方向偏移, dys Y方向偏移, xs X坐标列表, index 字形索引, count 字形数量, currentX 当前X坐标 -// 返回: float64 推进宽度 -func textGlyphAdvanceLimit(dxs, dys, xs []float64, index int, count int, currentX float64) float64 { - if index+1 >= count || len(dys) > 0 { - return 0 - } - if index+1 < len(xs) { - if advance := xs[index+1] - currentX; advance > 0 { - return advance - } - } - if advance, ok := textDelta(dxs, index); ok && advance > 0 { - return advance - } - return 0 -} - // textCodePositioned 判断文本编码是否带显式定位 // 入参: textCode 文本编码, xs X坐标列表, ys Y坐标列表 // 返回: bool 是否带显式定位 diff --git a/ofdgo_writer.go b/ofdgo_writer.go index b6445bc..c218308 100644 --- a/ofdgo_writer.go +++ b/ofdgo_writer.go @@ -1193,34 +1193,7 @@ func (x *ofdXML) actions(actions []Action) { if action.Goto != nil { x.start("Goto", nil) if dest := action.Goto.Dest; dest != nil { - attrs := ofdAttrs{{Name: xml.Name{Local: "Type"}, Value: dest.Type}, {Name: xml.Name{Local: "PageID"}, Value: dest.PageID}} - switch dest.Type { - case "XYZ": - if !dest.OmitLeft { - attrs.add("Left", ofdNumber(dest.Left)) - } - if !dest.OmitTop { - attrs.add("Top", ofdNumber(dest.Top)) - } - if !dest.OmitZoom { - attrs.add("Zoom", ofdNumber(dest.Zoom)) - } - case "FitH": - if !dest.OmitTop { - attrs.add("Top", ofdNumber(dest.Top)) - } - case "FitV": - if !dest.OmitLeft { - attrs.add("Left", ofdNumber(dest.Left)) - } - case "FitR": - attrs.add("Left", ofdNumber(dest.Left)) - attrs.add("Right", ofdNumber(dest.Right)) - attrs.add("Top", ofdNumber(dest.Top)) - attrs.add("Bottom", ofdNumber(dest.Bottom)) - } - x.start("Dest", attrs) - x.end("Dest") + x.element("Dest", dest) } if action.Goto.Bookmark != nil { x.element("Bookmark", action.Goto.Bookmark)