From bbc8d59bdbca71d8712cae1d51445914253ea902 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Fri, 25 Sep 2026 16:10:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=BD=AC=E6=8D=A2=E8=83=BD=E5=8A=9B):=20?= =?UTF-8?q?=E8=A1=A5=E5=85=A8PDF=E5=AD=97=E4=BD=93=E3=80=81=E6=B8=90?= =?UTF-8?q?=E5=8F=98=E4=B8=8E=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/ofdgo.js | 22 +++++++++++++-- go.mod | 2 +- go.sum | 4 +-- internal/webui/wasm.go | 3 +- ofdgo_action.go | 31 ++++++++++++++++----- ofdgo_editor_link.go | 6 ++++ ofdgo_pdf.go | 27 +++++++++++++++--- ofdgo_pdf_group.go | 4 +-- ofdgo_pdf_link.go | 62 +++++++++++++++++++++++++++++++++++++++--- ofdgo_pdf_object.go | 23 ++++++++-------- ofdgo_writer.go | 20 ++++++++++---- 11 files changed, 165 insertions(+), 39 deletions(-) diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index aa94c9c..d10d623 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -4996,6 +4996,13 @@ 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 = state.doc.pages.findIndex(page => page.id === dest.pageID); @@ -5008,11 +5015,22 @@ 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], shell = pageShell(state.pageIndex)?.getBoundingClientRect(); + if (current && shell) { + const viewer = el.viewerPanel.getBoundingClientRect(); + retained = sourcePoint(current, + (viewer.left + pageSpace() - shell.left) / (MM_TO_PX * scale), + (viewer.top + pageBlockSpace() - shell.top) / (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.left, top = dest.top; + let left = dest.omitLeft && retained ? retained.x : dest.left; + let top = dest.omitTop && retained ? retained.y : dest.top; if (dest.type === "Fit") { fitHeight(false); scrollToPage(index); @@ -5028,7 +5046,7 @@ async function navigateDestination(dest) { if (dest.right <= dest.left || dest.bottom <= dest.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.zoom > 0 ? dest.zoom : scale, false); + setScale(!dest.omitZoom && dest.zoom > 0 ? dest.zoom : scale, false); } const point = destinationPoint(page, left, top); if (dest.type === "FitR") { diff --git a/go.mod b/go.mod index 8503b09..2b5bd6a 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/xiaoqidun/jbig2 v1.1.1 - github.com/xiaoqidun/pdfgo v0.0.0-20260924162208-698614ef984e + github.com/xiaoqidun/pdfgo v0.0.0-20260925061819-c89bfccf349d golang.org/x/crypto v0.57.0 golang.org/x/image v0.46.0 golang.org/x/text v0.42.0 diff --git a/go.sum b/go.sum index 7b48bd2..c16a954 100644 --- a/go.sum +++ b/go.sum @@ -60,8 +60,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI= github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= -github.com/xiaoqidun/pdfgo v0.0.0-20260924162208-698614ef984e h1:17a5JOpYM6gwXZnhapgrXA6H7LhtGVrF0rpeiEORYUA= -github.com/xiaoqidun/pdfgo v0.0.0-20260924162208-698614ef984e/go.mod h1:/D0XHgfZ7xNhix95swpNaThpuqoJjVR5wIadogtR5pI= +github.com/xiaoqidun/pdfgo v0.0.0-20260925061819-c89bfccf349d h1:hjdo01d4EsiNe9l0JNyc+Bqfrvdj9XGG786EYxAx3yw= +github.com/xiaoqidun/pdfgo v0.0.0-20260925061819-c89bfccf349d/go.mod h1:/D0XHgfZ7xNhix95swpNaThpuqoJjVR5wIadogtR5pI= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index 118a25b..b52b86c 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -529,7 +529,8 @@ func renderPage(args []js.Value) (any, error) { } if dest := link.Dest; dest != nil { item["dest"] = map[string]any{"type": dest.Type, "pageID": dest.PageID, "left": dest.Left, "top": dest.Top, - "right": dest.Right, "bottom": dest.Bottom, "zoom": dest.Zoom} + "right": dest.Right, "bottom": dest.Bottom, "zoom": dest.Zoom, + "omitLeft": dest.OmitLeft, "omitTop": dest.OmitTop, "omitZoom": dest.OmitZoom} } links[i] = item } diff --git a/ofdgo_action.go b/ofdgo_action.go index 994b3df..3f9d604 100644 --- a/ofdgo_action.go +++ b/ofdgo_action.go @@ -39,13 +39,16 @@ type Goto struct { // Dest 文档内跳转目标 type Dest struct { - Type string `xml:"Type,attr"` - PageID string `xml:"PageID,attr"` - Left float64 `xml:"Left"` - Right float64 `xml:"Right"` - Top float64 `xml:"Top"` - Bottom float64 `xml:"Bottom"` - Zoom float64 `xml:"Zoom"` + Type string `xml:"Type,attr"` + PageID string `xml:"PageID,attr"` + Left float64 `xml:"Left"` + Right float64 `xml:"Right"` + Top float64 `xml:"Top"` + Bottom float64 `xml:"Bottom"` + Zoom float64 `xml:"Zoom"` + OmitLeft bool `xml:"-"` + OmitTop bool `xml:"-"` + OmitZoom bool `xml:"-"` } // GotoBookmark 书签跳转目标 @@ -151,6 +154,17 @@ func (dest *Dest) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { } dest.Type = attrValue(start, "Type") dest.PageID = attrValue(start, "PageID") + dest.OmitLeft, dest.OmitTop, dest.OmitZoom = true, true, true + for _, attr := range start.Attr { + switch attr.Name.Local { + case "Left": + dest.OmitLeft = false + case "Top": + dest.OmitTop = false + case "Zoom": + dest.OmitZoom = false + } + } dest.Left = actionFloatAttr(start, "Left") dest.Right = actionFloatAttr(start, "Right") dest.Top = actionFloatAttr(start, "Top") @@ -161,18 +175,21 @@ func (dest *Dest) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { } if value.Left != nil { dest.Left = *value.Left + dest.OmitLeft = false } if value.Right != nil { dest.Right = *value.Right } if value.Top != nil { dest.Top = *value.Top + dest.OmitTop = false } if value.Bottom != nil { dest.Bottom = *value.Bottom } if value.Zoom != nil { dest.Zoom = *value.Zoom + dest.OmitZoom = false } return nil } diff --git a/ofdgo_editor_link.go b/ofdgo_editor_link.go index 4cb4a05..802d56e 100644 --- a/ofdgo_editor_link.go +++ b/ofdgo_editor_link.go @@ -372,6 +372,12 @@ func annotationLinkXML(target AnnotationLink) ([]byte, error) { 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)) } diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index 6c2d17b..a12c6a4 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -378,8 +378,8 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error { margin := mark.Style.LineWidth * scale / 2 * math.Max(1, mark.Style.MiterLimit) box = Box{box.X - margin, box.Y - margin, box.W + 2*margin, box.H + 2*margin} } - strokeColor := StrokeColor(*p.pathColor(mark.Style.Stroke, box)) - object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: p.pathData(mark.Path, box), Fill: &mark.Fill, Stroke: &mark.Stroke, FillColor: p.pathColor(mark.Style.Fill, box), StrokeColor: &strokeColor, LineWidth: mark.Style.LineWidth * scale, Cap: []string{"Butt", "Round", "Square"}[mark.Style.Cap], Join: []string{"Miter", "Round", "Bevel"}[mark.Style.Join], MiterLimit: mark.Style.MiterLimit, Clips: p.clips(mark.Style.Clips, box)} + strokeColor := StrokeColor(*p.paintColor(mark.Style.Stroke, box)) + object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: p.pathData(mark.Path, box), Fill: &mark.Fill, Stroke: &mark.Stroke, FillColor: p.paintColor(mark.Style.Fill, box), StrokeColor: &strokeColor, LineWidth: mark.Style.LineWidth * scale, Cap: []string{"Butt", "Round", "Square"}[mark.Style.Cap], Join: []string{"Miter", "Round", "Bevel"}[mark.Style.Join], MiterLimit: mark.Style.MiterLimit, Clips: p.clips(mark.Style.Clips, box)} object.LineWidthSet = object.LineWidth == 0 if mark.Path.EvenOdd { object.Rule = "Even-Odd" @@ -398,11 +398,30 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error { return nil } -// pathColor 将页面渐变轴转换为对象局部坐标 +// paintColor 将页面渐变转换为对象局部坐标 // 入参: paint PDF画刷, box 对象边界 // 返回: *FillColor OFD颜色或渐变 -func (p *pdfImporter) pathColor(paint pdfgo.Paint, box Box) *FillColor { +func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) *FillColor { color := p.color(paint) + if paint.Radial != nil { + gradient := paint.Radial + start, end := p.matrix.Apply(gradient.Start), p.matrix.Apply(gradient.End) + scale := math.Hypot(p.matrix[0], p.matrix[1]) + extend := 0 + if gradient.Extend[0] { + extend |= 1 + } + if gradient.Extend[1] { + extend |= 2 + } + shading := &RadialShd{StartPoint: pdfNumbers(start.X-box.X, start.Y-box.Y), EndPoint: pdfNumbers(end.X-box.X, end.Y-box.Y), StartRadius: gradient.StartRadius * scale, EndRadius: gradient.EndRadius * scale, Extend: strconv.Itoa(extend)} + for _, stop := range gradient.Stops { + shading.Segment = append(shading.Segment, ShdSegment{Position: stop.Position, Color: ShdColor{Value: pdfNumbers(math.Round(stop.RGB[0]*255), math.Round(stop.RGB[1]*255), math.Round(stop.RGB[2]*255))}}) + } + color.Value = "" + color.RadialShd = shading + return color + } if paint.Axial == nil { return color } diff --git a/ofdgo_pdf_group.go b/ofdgo_pdf_group.go index 84a5a82..3084ea1 100644 --- a/ofdgo_pdf_group.go +++ b/ofdgo_pdf_group.go @@ -72,7 +72,7 @@ func (p *pdfImporter) group(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error if stroke { color, overprint = path.Style.Stroke, path.Style.StrokeOverprint } - if color.Alpha != 1 || color.Axial != nil || overprint && pdfOverprintNeedsSeparation(color) { + if color.Alpha != 1 || color.Axial != nil || color.Radial != nil || overprint && pdfOverprintNeedsSeparation(color) { return &pdfgo.UnsupportedError{Feature: "transparent or overprinted group content"} } if paint == nil { @@ -230,7 +230,7 @@ func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) { } visitor.Path = func(mark pdfgo.PathMark) error { paint := mark.Style.Fill - if !mark.Fill || mark.Stroke || paint.Alpha != 1 || paint.CMYK != nil || paint.Axial != nil || paint.RGB[0] != paint.RGB[1] || paint.RGB[1] != paint.RGB[2] || mark.Style.SoftMask != nil { + if !mark.Fill || mark.Stroke || paint.Alpha != 1 || paint.CMYK != nil || paint.Axial != nil || paint.Radial != nil || paint.RGB[0] != paint.RGB[1] || paint.RGB[1] != paint.RGB[2] || mark.Style.SoftMask != nil { return &pdfgo.UnsupportedError{Feature: "nonbinary mask graphic"} } visible, err := opacity(paint.RGB[0]) diff --git a/ofdgo_pdf_link.go b/ofdgo_pdf_link.go index ca9449d..b94197f 100644 --- a/ofdgo_pdf_link.go +++ b/ofdgo_pdf_link.go @@ -41,7 +41,7 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict if annotation.Subtype != "Link" { return &pdfgo.UnsupportedError{Feature: "annotation " + string(annotation.Subtype)} } - for _, key := range []pdfgo.Name{"AP", "AA", "BS", "QuadPoints", "OC"} { + for _, key := range []pdfgo.Name{"AP", "AA", "BS", "OC"} { if dict[key] != nil { return &pdfgo.UnsupportedError{Feature: fmt.Sprintf("link annotation field %q", key)} } @@ -127,6 +127,7 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict dest := &Dest{Type: string(destination.Mode), PageID: p.pageIDs[destination.Page]} if destination.Mode == "XYZ" { values := [3]float64{} + retained := [3]bool{} for n, v := range destination.Parameters { switch v := v.(type) { case pdfgo.Integer: @@ -134,13 +135,16 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict case pdfgo.Real: values[n] = float64(v) case nil: - if n < 2 { - return &pdfgo.UnsupportedError{Feature: "retained destination coordinates"} - } + retained[n] = true } } point := matrix.Apply(pdfgo.Point{X: values[0], Y: values[1]}) dest.Left, dest.Top, dest.Zoom = point.X, point.Y, values[2] + dest.OmitLeft, dest.OmitTop = retained[0], retained[1] + if targetPage.Rotation == 90 || targetPage.Rotation == 270 { + dest.OmitLeft, dest.OmitTop = retained[1], retained[0] + } + dest.OmitZoom = retained[2] || values[2] == 0 } else if destination.Mode != "Fit" { return &pdfgo.UnsupportedError{Feature: "destination mode " + string(destination.Mode)} } @@ -154,9 +158,59 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict } no := false object.Fill, object.Stroke = &no, &no + action.Region, err = p.linkRegion(annotation, box) + if err != nil { + return err + } object.Actions = []Action{action} p.objects = append(p.objects, GraphicObject{Type: "PathObject", PathObject: object}) p.report.Links++ } return nil } + +// linkRegion 将PDF链接四边形映射为OFD动作区域,越出注解矩形时使用矩形 +// 入参: annotation PDF链接注解, box OFD对象边界 +// 返回: *Region 点击区域, error 解析错误 +func (p *pdfImporter) linkRegion(annotation pdfgo.Annotation, box Box) (*Region, error) { + value, err := p.reader.Resolve(annotation.Dictionary["QuadPoints"]) + if err != nil || value == nil { + return nil, err + } + array, ok := value.(pdfgo.Array) + if !ok || len(array) == 0 || len(array)%8 != 0 { + return nil, nil + } + region := &Region{Area: make([]RegionArea, 0, len(array)/8)} + for start := 0; start < len(array); start += 8 { + var points [4]pdfgo.Point + for index := range points { + coordinates := [2]float64{} + for axis := range coordinates { + resolved, err := p.reader.Resolve(array[start+index*2+axis]) + if err != nil { + return nil, err + } + switch number := resolved.(type) { + case pdfgo.Integer: + coordinates[axis] = float64(number) + case pdfgo.Real: + coordinates[axis] = float64(number) + default: + return nil, nil + } + } + x, y := coordinates[0], coordinates[1] + if !finite(x) || !finite(y) || x < annotation.Rect.XMin || x > annotation.Rect.XMax || y < annotation.Rect.YMin || y > annotation.Rect.YMax { + return nil, nil + } + points[index] = p.matrix.Apply(pdfgo.Point{X: x, Y: y}) + } + area := RegionArea{Start: pdfNumbers(points[0].X-box.X, points[0].Y-box.Y)} + for _, point := range points[1:] { + area.Command = append(area.Command, RegionCommand{Type: "Line", Point1: pdfNumbers(point.X-box.X, point.Y-box.Y)}) + } + region.Area = append(region.Area, area) + } + return region, nil +} diff --git a/ofdgo_pdf_object.go b/ofdgo_pdf_object.go index 80652f0..27fb815 100644 --- a/ofdgo_pdf_object.go +++ b/ofdgo_pdf_object.go @@ -39,7 +39,7 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error { if err := p.flushPath(); err != nil { return err } - if mark.Image.ImageMask && mark.Style.Fill.Axial != nil { + if mark.Image.ImageMask && (mark.Style.Fill.Axial != nil || mark.Style.Fill.Radial != nil) { return &pdfgo.UnsupportedError{Feature: "gradient stencil image"} } var err error @@ -147,9 +147,6 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { if err := p.flushPath(); err != nil { return err } - if mark.Style.Fill.Axial != nil || mark.Style.Stroke.Axial != nil { - return &pdfgo.UnsupportedError{Feature: "gradient text paint"} - } var err error mark.Style, err = p.maskStyle(mark.Style) if err != nil { @@ -167,7 +164,8 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { if id == "" { var err error if embedded { - program, err := pdfFontProgram(font) + var program []byte + program, err = pdfFontProgram(font) if err != nil { return err } @@ -225,7 +223,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { } } if embedded && gid >= metrics.NumGlyphs() { - return fmt.Errorf("PDF glyph index outside embedded font") + return fmt.Errorf("PDF glyph index %d outside embedded font %s (%d glyphs)", gid, font.Name, metrics.NumGlyphs()) } if !embedded && glyph.Text == "" { return &pdfgo.UnsupportedError{Feature: "unmapped external font character"} @@ -263,8 +261,6 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { } margin := mark.Style.LineWidth * unit / 2 * math.Max(1, mark.Style.MiterLimit) box = Box{box.X - margin, box.Y - margin, box.W + 2*margin, box.H + 2*margin} - color := StrokeColor(*p.color(mark.Style.Stroke)) - object.StrokeColor = &color object.LineWidth = mark.Style.LineWidth * unit / scale object.LineWidthSet = object.LineWidth == 0 object.Join = []string{"Miter", "Round", "Bevel"}[mark.Style.Join] @@ -274,6 +270,10 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { } } object.Boundary = pdfBoundary(box) + if stroke { + color := StrokeColor(*p.paintColor(mark.Style.Stroke, box)) + object.StrokeColor = &color + } if box.X >= p.pageWidth || box.Y >= p.pageHeight || box.X+box.W <= 0 || box.Y+box.H <= 0 { visible = false } @@ -281,7 +281,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { object.Fill = &fill object.Stroke = &stroke object.Visible = &visible - object.FillColor = p.color(mark.Style.Fill) + object.FillColor = p.paintColor(mark.Style.Fill, box) object.Clips = p.clips(mark.Style.Clips, box) p.objects = append(p.objects, GraphicObject{Type: "TextObject", TextObject: object}) p.report.TextObjects++ @@ -293,7 +293,8 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { // 返回: []byte 封装后的字体数据, error 错误信息 func pdfFontProgram(source *pdfgo.Font) ([]byte, error) { program := source.Program - if source.ProgramType == "Type1C" || source.ProgramType == "CIDFontType0C" { + bareCFF := len(program) >= 4 && program[0] == 1 && program[1] == 0 && program[2] >= 4 && program[3] >= 1 && program[3] <= 4 + if source.ProgramType == "Type1C" || source.ProgramType == "CIDFontType0C" || bareCFF { var err error program, _, err = wrapCFFToOTF(source.Program) if err != nil { @@ -377,7 +378,7 @@ func pdfFontProgram(source *pdfgo.Font) ([]byte, error) { tables["OS/2"] = buildOS2TableWithMetrics(int16(binary.BigEndian.Uint16(hhea[4:6])), int16(binary.BigEndian.Uint16(hhea[6:8]))) changed = true } - if !changed { + if !changed && len(program)%4 == 0 { return program, nil } return serializeOTF(tables) diff --git a/ofdgo_writer.go b/ofdgo_writer.go index 34e3003..f9ef74c 100644 --- a/ofdgo_writer.go +++ b/ofdgo_writer.go @@ -961,13 +961,23 @@ func (x *ofdXML) actions(actions []Action) { attrs := ofdAttrs{{Name: xml.Name{Local: "Type"}, Value: dest.Type}, {Name: xml.Name{Local: "PageID"}, Value: dest.PageID}} switch dest.Type { case "XYZ": - attrs.add("Left", ofdNumber(dest.Left)) - attrs.add("Top", ofdNumber(dest.Top)) - attrs.add("Zoom", ofdNumber(dest.Zoom)) + 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": - attrs.add("Top", ofdNumber(dest.Top)) + if !dest.OmitTop { + attrs.add("Top", ofdNumber(dest.Top)) + } case "FitV": - attrs.add("Left", ofdNumber(dest.Left)) + if !dest.OmitLeft { + attrs.add("Left", ofdNumber(dest.Left)) + } case "FitR": attrs.add("Left", ofdNumber(dest.Left)) attrs.add("Right", ofdNumber(dest.Right))