diff --git a/ofdgo_action.go b/ofdgo_action.go new file mode 100644 index 0000000..f857769 --- /dev/null +++ b/ofdgo_action.go @@ -0,0 +1,198 @@ +// 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" + "strconv" +) + +// Action 动作 +type Action struct { + Event string `xml:"Event,attr"` + Region *Region `xml:"Region"` + Goto *Goto `xml:"Goto"` + URI *URI `xml:"URI"` + GotoA *GotoA `xml:"GotoA"` + Sound *Sound `xml:"Sound"` + Movie *Movie `xml:"Movie"` +} + +// Goto 文档内跳转动作 +type Goto struct { + Dest *Dest `xml:"Dest"` + Bookmark *GotoBookmark `xml:"Bookmark"` +} + +// 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"` +} + +// GotoBookmark 书签跳转目标 +type GotoBookmark struct { + Name string `xml:"Name,attr"` +} + +// URI URI动作 +type URI struct { + URI string `xml:"URI,attr"` + Base string `xml:"Base,attr"` +} + +// GotoA 附件动作 +type GotoA struct { + AttachID string `xml:"AttachID,attr"` + NewWindow *bool `xml:"NewWindow,attr"` +} + +// Sound 音频动作 +type Sound struct { + ResourceID string `xml:"ResourceID,attr"` + Volume *int `xml:"Volume,attr"` + Repeat bool `xml:"Repeat,attr"` + Synchronous bool `xml:"Synchronous,attr"` +} + +// Movie 视频动作 +type Movie struct { + ResourceID string `xml:"ResourceID,attr"` + Operator string `xml:"Operator,attr"` +} + +// Region 动作区域 +type Region struct { + Area []RegionArea `xml:"Area"` +} + +// RegionArea 动作区域分路径 +type RegionArea struct { + Start string `xml:"Start,attr"` + Command []RegionCommand `xml:"-"` +} + +// RegionCommand 动作区域绘制指令 +type RegionCommand struct { + Type string + Point1 string + Point2 string + Point3 string + EllipseSize string + RotationAngle string + LargeArc string + SweepDirection string + EndPoint string +} + +// UnmarshalXML 解析跳转目标 +// 入参: d XML解码器, start 起始节点 +// 返回: error 错误信息 +func (dest *Dest) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var value struct { + Left *float64 `xml:"Left"` + Right *float64 `xml:"Right"` + Top *float64 `xml:"Top"` + Bottom *float64 `xml:"Bottom"` + Zoom *float64 `xml:"Zoom"` + } + dest.Type = attrValue(start, "Type") + dest.PageID = attrValue(start, "PageID") + 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 + } + if value.Left != nil { + dest.Left = *value.Left + } + if value.Right != nil { + dest.Right = *value.Right + } + if value.Top != nil { + dest.Top = *value.Top + } + if value.Bottom != nil { + dest.Bottom = *value.Bottom + } + if value.Zoom != nil { + dest.Zoom = *value.Zoom + } + return nil +} + +// UnmarshalXML 解析视频动作并应用默认值 +// 入参: d XML解码器, start 起始节点 +// 返回: error 错误信息 +func (m *Movie) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + type movie Movie + value := movie{Operator: "Play"} + if err := d.DecodeElement(&value, &start); err != nil { + return err + } + *m = Movie(value) + return nil +} + +// UnmarshalXML 解析动作区域分路径并保留指令顺序 +// 入参: d XML解码器, start 起始节点 +// 返回: error 错误信息 +func (a *RegionArea) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + *a = RegionArea{Start: attrValue(start, "Start")} + for { + tok, err := d.Token() + if err != nil { + return err + } + switch node := tok.(type) { + case xml.StartElement: + command := RegionCommand{ + Type: node.Name.Local, + Point1: attrValue(node, "Point1"), + Point2: attrValue(node, "Point2"), + Point3: attrValue(node, "Point3"), + EllipseSize: attrValue(node, "EllipseSize"), + RotationAngle: attrValue(node, "RotationAngle"), + LargeArc: attrValue(node, "LargeArc"), + SweepDirection: attrValue(node, "SweepDirection"), + EndPoint: attrValue(node, "EndPoint"), + } + a.Command = append(a.Command, command) + if err := d.Skip(); err != nil { + return err + } + case xml.EndElement: + if node.Name.Local == start.Name.Local { + return nil + } + } + } +} + +// actionFloatAttr 获取动作浮点属性 +// 入参: start 起始节点, name 属性名 +// 返回: float64 属性值 +func actionFloatAttr(start xml.StartElement, name string) float64 { + value, _ := strconv.ParseFloat(attrValue(start, name), 64) + return value +} diff --git a/ofdgo_doc.go b/ofdgo_doc.go index 372163c..f607fee 100644 --- a/ofdgo_doc.go +++ b/ofdgo_doc.go @@ -23,26 +23,32 @@ type Document struct { Pages Pages `xml:"Pages"` Outlines Outlines `xml:"Outlines"` Permissions Permissions `xml:"Permissions"` + Actions []Action `xml:"Actions>Action"` + Bookmarks Bookmarks `xml:"Bookmarks"` Annotations string `xml:"Annotations"` Signatures string `xml:"Signatures"` Attachments Attachments `xml:"Attachments"` + CustomTags CustomTags `xml:"CustomTags"` Extensions Extensions `xml:"Extensions"` } // Extensions 扩展集合 type Extensions struct { + XMLName xml.Name `xml:"Extensions"` + Path string `xml:",chardata"` Extension []Extension `xml:"Extension"` } // Extension 扩展信息 type Extension struct { - AppName string `xml:"AppName,attr"` - Company string `xml:"Company,attr"` - AppVersion string `xml:"AppVersion,attr"` - Date string `xml:"Date,attr"` - RefID string `xml:"RefId,attr"` - Property []Property `xml:"Property"` - Data string `xml:"Data"` + AppName string `xml:"AppName,attr"` + Company string `xml:"Company,attr"` + AppVersion string `xml:"AppVersion,attr"` + Date string `xml:"Date,attr"` + RefID string `xml:"RefId,attr"` + Property []Property `xml:"Property"` + ExtendData []string `xml:"ExtendData"` + Data []ExtensionData `xml:"Data"` } // Property 扩展属性 @@ -52,16 +58,45 @@ type Property struct { Type string `xml:"Type,attr"` } +// ExtensionData 扩展数据 +type ExtensionData struct { + Attr []xml.Attr `xml:",any,attr"` + Content string `xml:",innerxml"` +} + // Attachments 附件集合 type Attachments struct { + XMLName xml.Name `xml:"Attachments"` + Path string `xml:",chardata"` Attachment []Attachment `xml:"Attachment"` } // Attachment 附件信息 type Attachment struct { - Name string `xml:"Name,attr"` - File string `xml:"File,attr"` - ID string `xml:"ID,attr"` + ID string `xml:"ID,attr"` + Name string `xml:"Name,attr"` + Format string `xml:"Format,attr"` + CreationDate string `xml:"CreationDate,attr"` + ModDate string `xml:"ModDate,attr"` + Size *float64 `xml:"Size,attr"` + Visible bool `xml:"Visible,attr"` + Usage string `xml:"Usage,attr"` + FileLoc string `xml:"FileLoc"` +} + +// CustomTags 自定义标引集合 +type CustomTags struct { + XMLName xml.Name `xml:"CustomTags"` + Path string `xml:",chardata"` + CustomTag []CustomTag `xml:"CustomTag"` +} + +// CustomTag 自定义标引 +type CustomTag struct { + TypeID string `xml:"TypeID,attr"` + NameSpace string `xml:"NameSpace,attr"` + SchemaLoc string `xml:"SchemaLoc"` + FileLoc string `xml:"FileLoc"` } // CommonData 文档公共数据 @@ -101,6 +136,17 @@ type TemplatePage struct { ZOrder string `xml:"ZOrder,attr"` } +// Bookmarks 书签集合 +type Bookmarks struct { + Bookmark []Bookmark `xml:"Bookmark"` +} + +// Bookmark 书签 +type Bookmark struct { + Name string `xml:"Name,attr"` + Dest Dest `xml:"Dest"` +} + // Outlines 大纲集合 type Outlines struct { OutlineElem []OutlineElem `xml:"OutlineElem"` @@ -108,15 +154,148 @@ type Outlines struct { // OutlineElem 大纲节点 type OutlineElem struct { - Title string `xml:"Title,attr"` - Count int `xml:"Count,attr"` - Actions string `xml:"Actions"` + Title string `xml:"Title,attr"` + Count int `xml:"Count,attr"` + Expanded bool `xml:"Expanded,attr"` + Actions []Action `xml:"Actions>Action"` + OutlineElem []OutlineElem `xml:"OutlineElem"` } // Permissions 权限声明 type Permissions struct { - Edit bool `xml:"Edit"` - Print bool `xml:"Print"` - Export bool `xml:"Export"` - Copy bool `xml:"Copy"` + Edit bool `xml:"Edit"` + Annot bool `xml:"Annot"` + Export bool `xml:"Export"` + Signature bool `xml:"Signature"` + Watermark bool `xml:"Watermark"` + PrintScreen bool `xml:"PrintScreen"` + Print bool `xml:"-"` + Copies int `xml:"-"` + Copy bool `xml:"CopyText"` + ContentRegist bool `xml:"ContentRegist"` + ValidPeriod *ValidPeriod `xml:"ValidPeriod"` +} + +// ValidPeriod 文档访问有效期 +type ValidPeriod struct { + StartDate string `xml:"StartDate,attr"` + EndDate string `xml:"EndDate,attr"` +} + +// UnmarshalXML 解析文档并应用权限默认值 +// 入参: d XML解码器, start 起始节点 +// 返回: error 错误信息 +func (doc *Document) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + type document Document + value := document{Permissions: defaultPermissions()} + if err := d.DecodeElement(&value, &start); err != nil { + return err + } + *doc = Document(value) + return nil +} + +// UnmarshalXML 解析附件并应用默认值 +// 入参: d XML解码器, start 起始节点 +// 返回: error 错误信息 +func (a *Attachment) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + type attachment Attachment + value := attachment{Visible: true, Usage: "none"} + if err := d.DecodeElement(&value, &start); err != nil { + return err + } + *a = Attachment(value) + return nil +} + +// UnmarshalXML 解析大纲节点并应用默认值 +// 入参: d XML解码器, start 起始节点 +// 返回: error 错误信息 +func (o *OutlineElem) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + type outlineElem OutlineElem + value := outlineElem{Expanded: true} + if err := d.DecodeElement(&value, &start); err != nil { + return err + } + *o = OutlineElem(value) + return nil +} + +// UnmarshalXML 解析文档权限并应用默认值 +// 入参: d XML解码器, start 起始节点 +// 返回: error 错误信息 +func (p *Permissions) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var value struct { + Edit *bool `xml:"Edit"` + Annot *bool `xml:"Annot"` + Export *bool `xml:"Export"` + Signature *bool `xml:"Signature"` + Watermark *bool `xml:"Watermark"` + PrintScreen *bool `xml:"PrintScreen"` + Copy *bool `xml:"CopyText"` + ContentRegist *bool `xml:"ContentRegist"` + Print *print `xml:"Print"` + ValidPeriod *ValidPeriod `xml:"ValidPeriod"` + } + if err := d.DecodeElement(&value, &start); err != nil { + return err + } + *p = defaultPermissions() + if value.Edit != nil { + p.Edit = *value.Edit + } + if value.Annot != nil { + p.Annot = *value.Annot + } + if value.Export != nil { + p.Export = *value.Export + } + if value.Signature != nil { + p.Signature = *value.Signature + } + if value.Watermark != nil { + p.Watermark = *value.Watermark + } + if value.PrintScreen != nil { + p.PrintScreen = *value.PrintScreen + } + if value.Copy != nil { + p.Copy = *value.Copy + } + if value.ContentRegist != nil { + p.ContentRegist = *value.ContentRegist + } + if value.Print != nil { + if value.Print.Printable != nil { + p.Print = *value.Print.Printable + } + if value.Print.Copies != nil { + p.Copies = *value.Print.Copies + } + } + p.ValidPeriod = value.ValidPeriod + return nil +} + +// print 打印权限节点 +type print struct { + Printable *bool `xml:"Printable,attr"` + Copies *int `xml:"Copies,attr"` +} + +// defaultPermissions 获取默认文档权限 +// 返回: Permissions 文档权限 +func defaultPermissions() Permissions { + return Permissions{ + Edit: true, + Annot: true, + Export: true, + Signature: true, + Watermark: true, + PrintScreen: true, + Print: true, + Copies: -1, + Copy: true, + ContentRegist: true, + } } diff --git a/ofdgo_image.go b/ofdgo_image.go index d188a50..46f3d38 100644 --- a/ofdgo_image.go +++ b/ofdgo_image.go @@ -20,9 +20,13 @@ import ( "fmt" "image" "image/color" + + // 注册GIF图片解码器 _ "image/gif" canvasimage "github.com/tdewolff/canvas/image" + + // 注册扩展图片解码器 _ "github.com/xiaoqidun/jbig2" _ "golang.org/x/image/bmp" _ "golang.org/x/image/tiff" diff --git a/ofdgo_object.go b/ofdgo_object.go index b0aa2c5..eb97cd9 100644 --- a/ofdgo_object.go +++ b/ofdgo_object.go @@ -19,6 +19,76 @@ import ( "strconv" ) +// graphicObjectTarget 图形对象集合 +type graphicObjectTarget struct { + objects *[]GraphicObject + text *[]TextObject + path *[]PathObject + image *[]ImageObject + composite *[]CompositeGraphicUnit +} + +// decodeGraphicObject 解析图形对象 +// 入参: d XML解码器, start 起始节点, target 图形对象集合 +// 返回: bool 是否为图形对象, error 错误信息 +func decodeGraphicObject(d *xml.Decoder, start xml.StartElement, target graphicObjectTarget) (bool, error) { + switch start.Name.Local { + case "TextObject": + var obj TextObject + if err := d.DecodeElement(&obj, &start); err != nil { + return true, err + } + *target.text = append(*target.text, obj) + *target.objects = append(*target.objects, GraphicObject{Type: start.Name.Local, TextObject: obj}) + case "PathObject": + var obj PathObject + if err := d.DecodeElement(&obj, &start); err != nil { + return true, err + } + *target.path = append(*target.path, obj) + *target.objects = append(*target.objects, GraphicObject{Type: start.Name.Local, PathObject: obj}) + case "ImageObject": + var obj ImageObject + if err := d.DecodeElement(&obj, &start); err != nil { + return true, err + } + *target.image = append(*target.image, obj) + *target.objects = append(*target.objects, GraphicObject{Type: start.Name.Local, ImageObject: obj}) + case "CompositeGraphicUnit", "CompositeObject": + var obj CompositeGraphicUnit + if err := d.DecodeElement(&obj, &start); err != nil { + return true, err + } + *target.composite = append(*target.composite, obj) + *target.objects = append(*target.objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj}) + default: + return false, nil + } + return true, nil +} + +// decodeObjectContainer 解析图形对象容器 +// 入参: d XML解码器, start 起始节点, decode 对象解码函数 +// 返回: error 错误信息 +func decodeObjectContainer(d *xml.Decoder, start xml.StartElement, decode func(*xml.Decoder, xml.StartElement) error) error { + for { + tok, err := d.Token() + if err != nil { + return err + } + switch node := tok.(type) { + case xml.StartElement: + if err := decode(d, node); err != nil { + return err + } + case xml.EndElement: + if node.Name.Local == start.Name.Local { + return nil + } + } + } +} + // UnmarshalXML 解析图层并保留对象顺序 // 入参: d XML解码器, start 起始节点 // 返回: error 错误信息 @@ -26,85 +96,27 @@ func (l *Layer) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { *l = Layer{} l.ID = attrValue(start, "ID") l.DrawParam = attrValue(start, "DrawParam") - for { - tok, err := d.Token() - if err != nil { - return err - } - switch node := tok.(type) { - case xml.StartElement: - if err := l.decodeObject(d, node); err != nil { - return err - } - case xml.EndElement: - if node.Name.Local == start.Name.Local { - return nil - } - } - } + return decodeObjectContainer(d, start, l.decodeObject) } // decodeObject 解析图层子对象 // 入参: d XML解码器, start 起始节点 // 返回: error 错误信息 func (l *Layer) decodeObject(d *xml.Decoder, start xml.StartElement) error { - switch start.Name.Local { - case "TextObject": - var obj TextObject - if err := d.DecodeElement(&obj, &start); err != nil { - return err - } - l.TextObject = append(l.TextObject, obj) - l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj}) - case "PathObject": - var obj PathObject - if err := d.DecodeElement(&obj, &start); err != nil { - return err - } - l.PathObject = append(l.PathObject, obj) - l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj}) - case "ImageObject": - var obj ImageObject - if err := d.DecodeElement(&obj, &start); err != nil { - return err - } - l.ImageObject = append(l.ImageObject, obj) - l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj}) - case "CompositeGraphicUnit", "CompositeObject": - var obj CompositeGraphicUnit - if err := d.DecodeElement(&obj, &start); err != nil { - return err - } - l.CompositeGraphicUnit = append(l.CompositeGraphicUnit, obj) - l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj}) - case "PageBlock": - return l.decodePageBlock(d, start) - default: - return d.Skip() + target := graphicObjectTarget{ + objects: &l.Objects, + text: &l.TextObject, + path: &l.PathObject, + image: &l.ImageObject, + composite: &l.CompositeGraphicUnit, } - return nil -} - -// decodePageBlock 解析页块子对象 -// 入参: d XML解码器, start 起始节点 -// 返回: error 错误信息 -func (l *Layer) decodePageBlock(d *xml.Decoder, start xml.StartElement) error { - for { - tok, err := d.Token() - if err != nil { - return err - } - switch node := tok.(type) { - case xml.StartElement: - if err := l.decodeObject(d, node); err != nil { - return err - } - case xml.EndElement: - if node.Name.Local == start.Name.Local { - return nil - } - } + if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil { + return err } + if start.Name.Local == "PageBlock" { + return decodeObjectContainer(d, start, l.decodeObject) + } + return d.Skip() } // UnmarshalXML 解析复合图元并保留对象顺序 @@ -128,178 +140,73 @@ func (c *CompositeGraphicUnit) UnmarshalXML(d *xml.Decoder, start xml.StartEleme c.Visible = &visible } } - for { - tok, err := d.Token() - if err != nil { - return err - } - switch node := tok.(type) { - case xml.StartElement: - if err := c.decodeObject(d, node); err != nil { - return err - } - case xml.EndElement: - if node.Name.Local == start.Name.Local { - return nil - } - } - } + return decodeObjectContainer(d, start, c.decodeObject) } // decodeObject 解析复合图元子对象 // 入参: d XML解码器, start 起始节点 // 返回: error 错误信息 func (c *CompositeGraphicUnit) decodeObject(d *xml.Decoder, start xml.StartElement) error { + target := graphicObjectTarget{ + objects: &c.Objects, + text: &c.TextObject, + path: &c.PathObject, + image: &c.ImageObject, + composite: &c.CompositeGraphicUnit, + } + if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil { + return err + } switch start.Name.Local { - case "TextObject": - var obj TextObject - if err := d.DecodeElement(&obj, &start); err != nil { - return err - } - c.TextObject = append(c.TextObject, obj) - c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj}) - case "PathObject": - var obj PathObject - if err := d.DecodeElement(&obj, &start); err != nil { - return err - } - c.PathObject = append(c.PathObject, obj) - c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj}) - case "ImageObject": - var obj ImageObject - if err := d.DecodeElement(&obj, &start); err != nil { - return err - } - c.ImageObject = append(c.ImageObject, obj) - c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj}) - case "CompositeGraphicUnit", "CompositeObject": - var obj CompositeGraphicUnit - if err := d.DecodeElement(&obj, &start); err != nil { - return err - } - c.CompositeGraphicUnit = append(c.CompositeGraphicUnit, obj) - c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj}) case "Clips": var clips Clips if err := d.DecodeElement(&clips, &start); err != nil { return err } c.Clips = &clips + case "Actions": + var actions struct { + Action []Action `xml:"Action"` + } + if err := d.DecodeElement(&actions, &start); err != nil { + return err + } + c.Actions = actions.Action case "Content", "PageBlock": - return c.decodePageBlock(d, start) + return decodeObjectContainer(d, start, c.decodeObject) default: return d.Skip() } return nil } -// decodePageBlock 解析页块子对象 -// 入参: d XML解码器, start 起始节点 -// 返回: error 错误信息 -func (c *CompositeGraphicUnit) decodePageBlock(d *xml.Decoder, start xml.StartElement) error { - for { - tok, err := d.Token() - if err != nil { - return err - } - switch node := tok.(type) { - case xml.StartElement: - if err := c.decodeObject(d, node); err != nil { - return err - } - case xml.EndElement: - if node.Name.Local == start.Name.Local { - return nil - } - } - } -} - // UnmarshalXML 解析注释外观并保留对象顺序 // 入参: d XML解码器, start 起始节点 // 返回: error 错误信息 func (a *Appearance) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { *a = Appearance{} a.Boundary = attrValue(start, "Boundary") - for { - tok, err := d.Token() - if err != nil { - return err - } - switch node := tok.(type) { - case xml.StartElement: - if err := a.decodeObject(d, node); err != nil { - return err - } - case xml.EndElement: - if node.Name.Local == start.Name.Local { - return nil - } - } - } + return decodeObjectContainer(d, start, a.decodeObject) } // decodeObject 解析注释外观子对象 // 入参: d XML解码器, start 起始节点 // 返回: error 错误信息 func (a *Appearance) decodeObject(d *xml.Decoder, start xml.StartElement) error { - switch start.Name.Local { - case "TextObject": - var obj TextObject - if err := d.DecodeElement(&obj, &start); err != nil { - return err - } - a.TextObject = append(a.TextObject, obj) - a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj}) - case "PathObject": - var obj PathObject - if err := d.DecodeElement(&obj, &start); err != nil { - return err - } - a.PathObject = append(a.PathObject, obj) - a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj}) - case "ImageObject": - var obj ImageObject - if err := d.DecodeElement(&obj, &start); err != nil { - return err - } - a.ImageObject = append(a.ImageObject, obj) - a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj}) - case "CompositeGraphicUnit", "CompositeObject": - var obj CompositeGraphicUnit - if err := d.DecodeElement(&obj, &start); err != nil { - return err - } - a.CompositeGraphicUnit = append(a.CompositeGraphicUnit, obj) - a.Objects = append(a.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj}) - case "PageBlock": - return a.decodePageBlock(d, start) - default: - return d.Skip() + target := graphicObjectTarget{ + objects: &a.Objects, + text: &a.TextObject, + path: &a.PathObject, + image: &a.ImageObject, + composite: &a.CompositeGraphicUnit, } - return nil -} - -// decodePageBlock 解析注释外观页块子对象 -// 入参: d XML解码器, start 起始节点 -// 返回: error 错误信息 -func (a *Appearance) decodePageBlock(d *xml.Decoder, start xml.StartElement) error { - for { - tok, err := d.Token() - if err != nil { - return err - } - switch node := tok.(type) { - case xml.StartElement: - if err := a.decodeObject(d, node); err != nil { - return err - } - case xml.EndElement: - if node.Name.Local == start.Name.Local { - return nil - } - } + if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil { + return err } + if start.Name.Local == "PageBlock" { + return decodeObjectContainer(d, start, a.decodeObject) + } + return d.Skip() } // UnmarshalXML 解析图案单元内容并保留对象顺序 @@ -307,85 +214,27 @@ func (a *Appearance) decodePageBlock(d *xml.Decoder, start xml.StartElement) err // 返回: error 错误信息 func (p *PatternContent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { *p = PatternContent{} - for { - tok, err := d.Token() - if err != nil { - return err - } - switch node := tok.(type) { - case xml.StartElement: - if err := p.decodeObject(d, node); err != nil { - return err - } - case xml.EndElement: - if node.Name.Local == start.Name.Local { - return nil - } - } - } + return decodeObjectContainer(d, start, p.decodeObject) } // decodeObject 解析图案单元内容子对象 // 入参: d XML解码器, start 起始节点 // 返回: error 错误信息 func (p *PatternContent) decodeObject(d *xml.Decoder, start xml.StartElement) error { - switch start.Name.Local { - case "TextObject": - var obj TextObject - if err := d.DecodeElement(&obj, &start); err != nil { - return err - } - p.TextObject = append(p.TextObject, obj) - p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj}) - case "PathObject": - var obj PathObject - if err := d.DecodeElement(&obj, &start); err != nil { - return err - } - p.PathObject = append(p.PathObject, obj) - p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj}) - case "ImageObject": - var obj ImageObject - if err := d.DecodeElement(&obj, &start); err != nil { - return err - } - p.ImageObject = append(p.ImageObject, obj) - p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj}) - case "CompositeGraphicUnit", "CompositeObject": - var obj CompositeGraphicUnit - if err := d.DecodeElement(&obj, &start); err != nil { - return err - } - p.CompositeGraphicUnit = append(p.CompositeGraphicUnit, obj) - p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj}) - case "PageBlock": - return p.decodePageBlock(d, start) - default: - return d.Skip() + target := graphicObjectTarget{ + objects: &p.Objects, + text: &p.TextObject, + path: &p.PathObject, + image: &p.ImageObject, + composite: &p.CompositeGraphicUnit, } - return nil -} - -// decodePageBlock 解析图案单元内容页块子对象 -// 入参: d XML解码器, start 起始节点 -// 返回: error 错误信息 -func (p *PatternContent) decodePageBlock(d *xml.Decoder, start xml.StartElement) error { - for { - tok, err := d.Token() - if err != nil { - return err - } - switch node := tok.(type) { - case xml.StartElement: - if err := p.decodeObject(d, node); err != nil { - return err - } - case xml.EndElement: - if node.Name.Local == start.Name.Local { - return nil - } - } + if decoded, err := decodeGraphicObject(d, start, target); decoded || err != nil { + return err } + if start.Name.Local == "PageBlock" { + return decodeObjectContainer(d, start, p.decodeObject) + } + return d.Skip() } // attrValue 获取XML属性值 diff --git a/ofdgo_page.go b/ofdgo_page.go index 43ba002..dc37b6b 100644 --- a/ofdgo_page.go +++ b/ofdgo_page.go @@ -23,6 +23,7 @@ type PageContent struct { Area PageArea `xml:"Area"` Template []Template `xml:"Template"` Content Content `xml:"Content"` + Actions []Action `xml:"Actions>Action"` } // Template 页面模板引用 @@ -97,6 +98,7 @@ type TextObject struct { CGTransform []CGTransform `xml:"CGTransform"` TextCode []TextCode `xml:"TextCode"` Clips *Clips `xml:"Clips"` + Actions []Action `xml:"Actions>Action"` } // FillColor 填充颜色 @@ -165,6 +167,7 @@ type PathObject struct { FillColor *FillColor `xml:"FillColor"` AbbreviatedData string `xml:"AbbreviatedData"` Clips *Clips `xml:"Clips"` + Actions []Action `xml:"Actions>Action"` } // StrokeColor 勾边颜色 @@ -207,12 +210,13 @@ type ShdColor struct { // ImageObject 图片对象 type ImageObject struct { - ID string `xml:"ID,attr"` - Boundary string `xml:"Boundary,attr"` - ResourceID string `xml:"ResourceID,attr"` - ImageMask string `xml:"ImageMask,attr"` - CTM string `xml:"CTM,attr"` - Alpha *int `xml:"Alpha,attr"` - Visible *bool `xml:"Visible,attr"` - Clips *Clips `xml:"Clips"` + ID string `xml:"ID,attr"` + Boundary string `xml:"Boundary,attr"` + ResourceID string `xml:"ResourceID,attr"` + ImageMask string `xml:"ImageMask,attr"` + CTM string `xml:"CTM,attr"` + Alpha *int `xml:"Alpha,attr"` + Visible *bool `xml:"Visible,attr"` + Clips *Clips `xml:"Clips"` + Actions []Action `xml:"Actions>Action"` } diff --git a/ofdgo_paint.go b/ofdgo_paint.go index 0cf40ab..b8cb5b0 100644 --- a/ofdgo_paint.go +++ b/ofdgo_paint.go @@ -142,10 +142,10 @@ func parseFillColor(fillColor *FillColor) color.Color { return nil } if fillColor.AxialShd != nil { - return parseAxialShdColor(fillColor.AxialShd, fillColor.Alpha) + return parseShdColor(fillColor.AxialShd.Segment, fillColor.Alpha) } if fillColor.RadialShd != nil { - return parseRadialShdColor(fillColor.RadialShd, fillColor.Alpha) + return parseShdColor(fillColor.RadialShd.Segment, fillColor.Alpha) } if strings.TrimSpace(fillColor.Value) != "" { return parseColorWithAlpha(fillColor.Value, fillColor.Alpha) @@ -170,10 +170,10 @@ func parseFillPaint(fillColor *FillColor, x, y, pageH, originX, originY float64) return gradient } if fillColor.AxialShd != nil { - return parseAxialShdColor(fillColor.AxialShd, fillColor.Alpha) + return parseShdColor(fillColor.AxialShd.Segment, fillColor.Alpha) } if fillColor.RadialShd != nil { - return parseRadialShdColor(fillColor.RadialShd, fillColor.Alpha) + return parseShdColor(fillColor.RadialShd.Segment, fillColor.Alpha) } if strings.TrimSpace(fillColor.Value) != "" { return parseColorWithAlpha(fillColor.Value, fillColor.Alpha) @@ -189,10 +189,10 @@ func parseStrokeColor(strokeColor *StrokeColor) color.Color { return nil } if strokeColor.AxialShd != nil { - return parseAxialShdColor(strokeColor.AxialShd, strokeColor.Alpha) + return parseShdColor(strokeColor.AxialShd.Segment, strokeColor.Alpha) } if strokeColor.RadialShd != nil { - return parseRadialShdColor(strokeColor.RadialShd, strokeColor.Alpha) + return parseShdColor(strokeColor.RadialShd.Segment, strokeColor.Alpha) } if strings.TrimSpace(strokeColor.Value) != "" { return parseColorWithAlpha(strokeColor.Value, strokeColor.Alpha) @@ -214,10 +214,10 @@ func parseStrokePaint(strokeColor *StrokeColor, x, y, pageH, originX, originY fl return gradient } if strokeColor.AxialShd != nil { - return parseAxialShdColor(strokeColor.AxialShd, strokeColor.Alpha) + return parseShdColor(strokeColor.AxialShd.Segment, strokeColor.Alpha) } if strokeColor.RadialShd != nil { - return parseRadialShdColor(strokeColor.RadialShd, strokeColor.Alpha) + return parseShdColor(strokeColor.RadialShd.Segment, strokeColor.Alpha) } if strings.TrimSpace(strokeColor.Value) != "" { return parseColorWithAlpha(strokeColor.Value, strokeColor.Alpha) @@ -235,24 +235,44 @@ func patternColor(fillColor *FillColor) color.Color { return parseColorWithAlpha(fillColor.Value, fillColor.Alpha) } -// parseAxialShdColor 解析轴向渐变颜色 -// 入参: axialShd 轴向渐变节点, alpha 透明度 +// parseShdColor 解析渐变颜色 +// 入参: segments 渐变分段, alpha 透明度 // 返回: color.Color 颜色对象 -func parseAxialShdColor(axialShd *AxialShd, alpha *int) color.Color { - if axialShd != nil { - for _, segment := range axialShd.Segment { - if strings.TrimSpace(segment.Color.Value) == "" { - continue - } - if alpha == nil { - alpha = segment.Color.Alpha - } - return parseColorWithAlpha(segment.Color.Value, alpha) +func parseShdColor(segments []ShdSegment, alpha *int) color.Color { + for _, segment := range segments { + if strings.TrimSpace(segment.Color.Value) == "" { + continue } + segmentAlpha := alpha + if segmentAlpha == nil { + segmentAlpha = segment.Color.Alpha + } + return parseColorWithAlpha(segment.Color.Value, segmentAlpha) } return color.Black } +// parseShdSegments 解析渐变分段 +// 入参: segments 渐变分段, alpha 透明度 +// 返回: canvas.Grad 渐变分段 +func parseShdSegments(segments []ShdSegment, alpha *int) canvas.Grad { + gradient := canvas.NewGradient() + for _, segment := range segments { + if strings.TrimSpace(segment.Color.Value) == "" { + continue + } + segmentAlpha := alpha + if segmentAlpha == nil { + segmentAlpha = segment.Color.Alpha + } + gradient.Add(segment.Position, colorToRGBA(parseColorWithAlpha(segment.Color.Value, segmentAlpha))) + } + if len(gradient) == 0 { + return nil + } + return gradient +} + // parseAxialShdGradient 解析轴向渐变 // 入参: axialShd 轴向渐变节点, alpha 透明度, x X坐标, y Y坐标, pageH 页面高度, originX 原点X坐标, originY 原点Y坐标 // 返回: canvas.Gradient 渐变对象 @@ -265,18 +285,8 @@ func parseAxialShdGradient(axialShd *AxialShd, alpha *int, x, y, pageH, originX, if len(start) < 2 || len(end) < 2 { return nil } - grad := canvas.NewGradient() - for _, segment := range axialShd.Segment { - if strings.TrimSpace(segment.Color.Value) == "" { - continue - } - segmentAlpha := alpha - if segmentAlpha == nil { - segmentAlpha = segment.Color.Alpha - } - grad.Add(segment.Position, colorToRGBA(parseColorWithAlpha(segment.Color.Value, segmentAlpha))) - } - if len(grad) == 0 { + gradient := parseShdSegments(axialShd.Segment, alpha) + if gradient == nil { return nil } startPoint := canvas.Point{X: x + start[0] - originX, Y: pageH - (y + start[1]) - originY} @@ -284,25 +294,7 @@ func parseAxialShdGradient(axialShd *AxialShd, alpha *int, x, y, pageH, originX, if startPoint.Equals(endPoint) { return nil } - return grad.ToLinear(startPoint, endPoint) -} - -// parseRadialShdColor 解析径向渐变颜色 -// 入参: radialShd 径向渐变节点, alpha 透明度 -// 返回: color.Color 颜色对象 -func parseRadialShdColor(radialShd *RadialShd, alpha *int) color.Color { - if radialShd != nil { - for _, segment := range radialShd.Segment { - if strings.TrimSpace(segment.Color.Value) == "" { - continue - } - if alpha == nil { - alpha = segment.Color.Alpha - } - return parseColorWithAlpha(segment.Color.Value, alpha) - } - } - return color.Black + return gradient.ToLinear(startPoint, endPoint) } // parseRadialShdGradient 解析径向渐变 @@ -317,23 +309,13 @@ func parseRadialShdGradient(radialShd *RadialShd, alpha *int, x, y, pageH, origi if len(start) < 2 || len(end) < 2 { return nil } - grad := canvas.NewGradient() - for _, segment := range radialShd.Segment { - if strings.TrimSpace(segment.Color.Value) == "" { - continue - } - segmentAlpha := alpha - if segmentAlpha == nil { - segmentAlpha = segment.Color.Alpha - } - grad.Add(segment.Position, colorToRGBA(parseColorWithAlpha(segment.Color.Value, segmentAlpha))) - } - if len(grad) == 0 { + gradient := parseShdSegments(radialShd.Segment, alpha) + if gradient == nil { return nil } startPoint := canvas.Point{X: x + start[0] - originX, Y: pageH - (y + start[1]) - originY} endPoint := canvas.Point{X: x + end[0] - originX, Y: pageH - (y + end[1]) - originY} - return grad.ToRadial(startPoint, radialShd.StartRadius, endPoint, radialShd.EndRadius) + return gradient.ToRadial(startPoint, radialShd.StartRadius, endPoint, radialShd.EndRadius) } // colorToRGBA 转换颜色对象 diff --git a/ofdgo_reader.go b/ofdgo_reader.go index 284212d..0f5014f 100644 --- a/ofdgo_reader.go +++ b/ofdgo_reader.go @@ -279,6 +279,21 @@ func (r *Reader) ResData(resLink string) ([]byte, error) { return r.readFile(fullPath) } +// readDocumentPart 读取文档关联文件 +// 入参: loc 文件位置, value 文档结构 +// 返回: string 文件路径, error 错误信息 +func (r *Reader) readDocumentPart(loc string, value any) (string, error) { + fullPath := r.ResPath(strings.TrimSpace(loc)) + data, err := r.readFile(fullPath) + if err != nil { + return "", err + } + if err := xml.Unmarshal(data, value); err != nil { + return "", fmt.Errorf("failed to unmarshal %s: %w", path.Base(fullPath), err) + } + return fullPath, nil +} + // Version 获取OFD版本号 // 返回: string 版本号 func (r *Reader) Version() string { @@ -326,6 +341,26 @@ func (r *Reader) Outlines() ([]OutlineElem, error) { return doc.Outlines.OutlineElem, nil } +// Actions 获取文档动作 +// 返回: []Action 动作列表, error 错误信息 +func (r *Reader) Actions() ([]Action, error) { + doc, err := r.Doc() + if err != nil { + return nil, err + } + return doc.Actions, nil +} + +// Bookmarks 获取文档书签 +// 返回: []Bookmark 书签列表, error 错误信息 +func (r *Reader) Bookmarks() ([]Bookmark, error) { + doc, err := r.Doc() + if err != nil { + return nil, err + } + return doc.Bookmarks.Bookmark, nil +} + // Attachments 获取附件列表 // 返回: []Attachment 附件列表, error 错误信息 func (r *Reader) Attachments() ([]Attachment, error) { @@ -333,9 +368,50 @@ func (r *Reader) Attachments() ([]Attachment, error) { if err != nil { return nil, err } + if strings.TrimSpace(doc.Attachments.Path) != "" && doc.Attachments.Attachment == nil { + var attachments Attachments + partPath, err := r.readDocumentPart(doc.Attachments.Path, &attachments) + if err != nil { + return nil, err + } + for i := range attachments.Attachment { + attachment := &attachments.Attachment[i] + attachment.FileLoc = resolveResourcePath(partPath, "", attachment.FileLoc) + } + if attachments.Attachment == nil { + attachments.Attachment = []Attachment{} + } + doc.Attachments.Attachment = attachments.Attachment + } return doc.Attachments.Attachment, nil } +// CustomTags 获取自定义标引 +// 返回: []CustomTag 自定义标引列表, error 错误信息 +func (r *Reader) CustomTags() ([]CustomTag, error) { + doc, err := r.Doc() + if err != nil { + return nil, err + } + if strings.TrimSpace(doc.CustomTags.Path) != "" && doc.CustomTags.CustomTag == nil { + var customTags CustomTags + partPath, err := r.readDocumentPart(doc.CustomTags.Path, &customTags) + if err != nil { + return nil, err + } + for i := range customTags.CustomTag { + tag := &customTags.CustomTag[i] + tag.SchemaLoc = resolveResourcePath(partPath, "", tag.SchemaLoc) + tag.FileLoc = resolveResourcePath(partPath, "", tag.FileLoc) + } + if customTags.CustomTag == nil { + customTags.CustomTag = []CustomTag{} + } + doc.CustomTags.CustomTag = customTags.CustomTag + } + return doc.CustomTags.CustomTag, nil +} + // CustomDatas 获取自定义数据 // 返回: []CustomData 自定义数据列表, error 错误信息 func (r *Reader) CustomDatas() ([]CustomData, error) { @@ -349,12 +425,29 @@ func (r *Reader) CustomDatas() ([]CustomData, error) { return info.CustomDatas.CustomData, nil } -// Extensions 获取分列扩展项 +// Extensions 获取扩展项 // 返回: []Extension 扩展项列表, error 错误信息 func (r *Reader) Extensions() ([]Extension, error) { doc, err := r.Doc() if err != nil { return nil, err } + if strings.TrimSpace(doc.Extensions.Path) != "" && doc.Extensions.Extension == nil { + var extensions Extensions + partPath, err := r.readDocumentPart(doc.Extensions.Path, &extensions) + if err != nil { + return nil, err + } + for i := range extensions.Extension { + extension := &extensions.Extension[i] + for j := range extension.ExtendData { + extension.ExtendData[j] = resolveResourcePath(partPath, "", extension.ExtendData[j]) + } + } + if extensions.Extension == nil { + extensions.Extension = []Extension{} + } + doc.Extensions.Extension = extensions.Extension + } return doc.Extensions.Extension, nil } diff --git a/ofdgo_renderer_output.go b/ofdgo_renderer_output.go index 031e78f..5fc1f11 100644 --- a/ofdgo_renderer_output.go +++ b/ofdgo_renderer_output.go @@ -71,9 +71,16 @@ func (r *Renderer) RenderToPDF(page *PageContent, writer io.Writer) error { if err != nil { return err } + box, err := r.GetPageBox(page) + if err != nil { + return err + } + pages := []pdfPage{{Content: page, Box: box}} + navigation := newPDFNavigation(r, r.Reader.doc, pages) var buf bytes.Buffer p := pdf.New(&buf, c.W, c.H, nil) p.SetInfo("", "", "", "", "xiaoqidun/ofdgo") + navigation.apply(p, 0) c.RenderTo(p) if err := p.Close(); err != nil { return err @@ -104,28 +111,33 @@ func (r *Renderer) RenderToMultiPagePDF(writer io.Writer) error { if len(doc.Pages.Page) == 0 { return fmt.Errorf("no pages found") } + pages := make([]pdfPage, len(doc.Pages.Page)) + for i, pageRef := range doc.Pages.Page { + page, err := r.Reader.PageContent(pageRef) + if err != nil { + return fmt.Errorf("failed to read page %d: %w", i+1, err) + } + box, err := r.GetPageBox(page) + if err != nil { + return fmt.Errorf("failed to read page %d area: %w", i+1, err) + } + pages[i] = pdfPage{Content: page, Box: box} + } + navigation := newPDFNavigation(r, doc, pages) var buf bytes.Buffer - var p *pdf.PDF - for _, pgRef := range doc.Pages.Page { - page, err := r.Reader.PageContent(pgRef) + p := pdf.New(&buf, pages[0].Box.W, pages[0].Box.H, nil) + p.SetInfo("", "", "", "", "xiaoqidun/ofdgo") + for i, page := range pages { + c, err := r.renderPage(page.Content) if err != nil { - continue + return fmt.Errorf("failed to render page %d: %w", i+1, err) } - c, err := r.renderPage(page) - if err != nil { - continue - } - if p == nil { - p = pdf.New(&buf, c.W, c.H, nil) - p.SetInfo("", "", "", "", "xiaoqidun/ofdgo") - } else { + if i > 0 { p.NewPage(c.W, c.H) } + navigation.apply(p, i) c.RenderTo(p) } - if p == nil { - return fmt.Errorf("failed to render any page") - } if err := p.Close(); err != nil { return err } diff --git a/ofdgo_renderer_pdf.go b/ofdgo_renderer_pdf.go new file mode 100644 index 0000000..80566bd --- /dev/null +++ b/ofdgo_renderer_pdf.go @@ -0,0 +1,322 @@ +// 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 ( + "fmt" + "net/url" + + "github.com/tdewolff/canvas" + "github.com/tdewolff/canvas/renderers/pdf" +) + +// pdfPage PDF页面数据 +type pdfPage struct { + Content *PageContent + Box Box +} + +// pdfNavigation PDF导航信息 +type pdfNavigation struct { + Anchor map[int][]pdfAnchor + Link map[int][]pdfLink + Outline map[int][]pdfOutline + nextID int +} + +// pdfAnchor PDF跳转目标 +type pdfAnchor struct { + Name string + Rect canvas.Rect +} + +// pdfLink PDF链接 +type pdfLink struct { + URI string + Rect canvas.Rect +} + +// pdfOutline PDF大纲 +type pdfOutline struct { + Name string + Level int + Y float64 +} + +// pdfActionSource PDF动作来源 +type pdfActionSource struct { + Box Box + Actions []Action +} + +// newPDFNavigation 创建PDF导航信息 +// 入参: renderer 渲染器, doc 文档结构, pages 页面数据 +// 返回: *pdfNavigation PDF导航信息 +func newPDFNavigation(renderer *Renderer, doc *Document, pages []pdfPage) *pdfNavigation { + navigation := &pdfNavigation{ + Anchor: make(map[int][]pdfAnchor), + Link: make(map[int][]pdfLink), + Outline: make(map[int][]pdfOutline), + } + pageIndex := make(map[string]int, len(pages)) + for i, page := range pages { + pageIndex[page.Content.ID] = i + } + bookmarks := make(map[string]Dest) + if doc != nil { + for _, bookmark := range doc.Bookmarks.Bookmark { + bookmarks[bookmark.Name] = bookmark.Dest + } + } + for i, page := range pages { + sources := pageActionSources(page) + if renderer.RenderAnnotations { + sources = append(sources, annotationActionSources(renderer.Reader.Annots[page.Content.ID])...) + } + for _, source := range sources { + rect := pdfSourceRect(source.Box, page.Box.H) + for _, action := range source.Actions { + if action.Event != "CLICK" { + continue + } + navigation.addAction(i, rect, action, bookmarks, pageIndex, pages) + } + } + } + if doc != nil { + navigation.addOutlines(doc.Outlines.OutlineElem, 0, bookmarks, pageIndex, pages) + } + return navigation +} + +// addAction 添加PDF动作 +// 入参: page 页面索引, rect 动作区域, action 动作, bookmarks 书签, pageIndex 页面索引表, pages 页面数据 +func (n *pdfNavigation) addAction(page int, rect canvas.Rect, action Action, bookmarks map[string]Dest, pageIndex map[string]int, pages []pdfPage) { + if action.Goto != nil { + dest := gotoDest(action.Goto, bookmarks) + if dest == nil { + return + } + target, ok := pageIndex[dest.PageID] + if !ok { + return + } + targetRect, ok := pdfDestRect(*dest, pages[target].Box.H) + if !ok { + return + } + name := fmt.Sprintf("ofdgo-dest-%d", n.nextID) + n.nextID++ + n.Anchor[target] = append(n.Anchor[target], pdfAnchor{Name: name, Rect: targetRect}) + n.Link[page] = append(n.Link[page], pdfLink{URI: "#" + name, Rect: rect}) + return + } + if action.URI != nil && action.URI.URI != "" { + n.Link[page] = append(n.Link[page], pdfLink{URI: resolveActionURI(*action.URI), Rect: rect}) + } +} + +// addOutlines 添加PDF大纲 +// 入参: outlines 大纲节点, level 节点层级, bookmarks 书签, pageIndex 页面索引表, pages 页面数据 +func (n *pdfNavigation) addOutlines(outlines []OutlineElem, level int, bookmarks map[string]Dest, pageIndex map[string]int, pages []pdfPage) { + for _, outline := range outlines { + if dest := outlineDest(outline, bookmarks); dest != nil { + if page, ok := pageIndex[dest.PageID]; ok { + n.Outline[page] = append(n.Outline[page], pdfOutline{ + Name: outline.Title, + Level: level, + Y: pdfDestY(*dest, pages[page].Box.H), + }) + } + } + n.addOutlines(outline.OutlineElem, level+1, bookmarks, pageIndex, pages) + } +} + +// apply 应用PDF导航信息 +// 入参: renderer PDF渲染器, page 页面索引 +func (n *pdfNavigation) apply(renderer *pdf.PDF, page int) { + for _, anchor := range n.Anchor[page] { + renderer.AddAnchor(anchor.Name, anchor.Rect) + } + for _, link := range n.Link[page] { + renderer.AddLink(link.URI, link.Rect) + } + for _, outline := range n.Outline[page] { + renderer.AddOutline(outline.Name, outline.Level, outline.Y) + } +} + +// pageActionSources 获取页面动作来源 +// 入参: page 页面数据 +// 返回: []pdfActionSource 动作来源 +func pageActionSources(page pdfPage) []pdfActionSource { + sources := make([]pdfActionSource, 0) + if len(page.Content.Actions) > 0 { + sources = append(sources, pdfActionSource{ + Box: Box{W: page.Box.W, H: page.Box.H}, + Actions: page.Content.Actions, + }) + } + for _, layer := range page.Content.Content.Layer { + for _, object := range layer.Objects { + sources = appendGraphicActionSources(sources, object, nil) + } + } + return sources +} + +// annotationActionSources 获取注释动作来源 +// 入参: annotations 页面注释 +// 返回: []pdfActionSource 动作来源 +func annotationActionSources(annotations []Annotation) []pdfActionSource { + sources := make([]pdfActionSource, 0) + for _, annotation := range annotations { + box, err := ParseBox(annotation.Appearance.Boundary) + if err != nil { + continue + } + for _, object := range annotation.Appearance.Objects { + sources = appendGraphicActionSources(sources, object, &box) + } + } + return sources +} + +// appendGraphicActionSources 添加图形对象动作来源 +// 入参: sources 动作来源, object 图形对象, box 指定动作区域 +// 返回: []pdfActionSource 动作来源 +func appendGraphicActionSources(sources []pdfActionSource, object GraphicObject, box *Box) []pdfActionSource { + var boundary string + var actions []Action + var children []GraphicObject + switch object.Type { + case "TextObject": + boundary = object.TextObject.Boundary + actions = object.TextObject.Actions + case "PathObject": + boundary = object.PathObject.Boundary + actions = object.PathObject.Actions + case "ImageObject": + boundary = object.ImageObject.Boundary + actions = object.ImageObject.Actions + case "CompositeGraphicUnit", "CompositeObject": + boundary = object.CompositeGraphicUnit.Boundary + actions = object.CompositeGraphicUnit.Actions + children = object.CompositeGraphicUnit.Objects + } + sourceBox := box + if sourceBox == nil && boundary != "" { + if value, err := ParseBox(boundary); err == nil { + sourceBox = &value + } + } + if sourceBox != nil && len(actions) > 0 { + sources = append(sources, pdfActionSource{Box: *sourceBox, Actions: actions}) + } + for _, child := range children { + sources = appendGraphicActionSources(sources, child, box) + } + return sources +} + +// gotoDest 获取文档内跳转目标 +// 入参: action 跳转动作, bookmarks 书签 +// 返回: *Dest 跳转目标 +func gotoDest(action *Goto, bookmarks map[string]Dest) *Dest { + if action.Dest != nil { + return action.Dest + } + if action.Bookmark != nil { + if dest, ok := bookmarks[action.Bookmark.Name]; ok { + return &dest + } + } + return nil +} + +// outlineDest 获取大纲跳转目标 +// 入参: outline 大纲节点, bookmarks 书签 +// 返回: *Dest 跳转目标 +func outlineDest(outline OutlineElem, bookmarks map[string]Dest) *Dest { + for _, action := range outline.Actions { + if action.Goto != nil { + if dest := gotoDest(action.Goto, bookmarks); dest != nil { + return dest + } + } + } + for _, child := range outline.OutlineElem { + if dest := outlineDest(child, bookmarks); dest != nil { + return dest + } + } + return nil +} + +// resolveActionURI 解析URI动作地址 +// 入参: action URI动作 +// 返回: string URI地址 +func resolveActionURI(action URI) string { + if action.Base == "" { + return action.URI + } + base, err := url.Parse(action.Base) + if err != nil { + return action.URI + } + target, err := url.Parse(action.URI) + if err != nil { + return action.URI + } + return base.ResolveReference(target).String() +} + +// pdfSourceRect 转换PDF动作区域 +// 入参: box OFD区域, pageH 页面高度 +// 返回: canvas.Rect PDF区域 +func pdfSourceRect(box Box, pageH float64) canvas.Rect { + return canvas.RectFromSize(box.X, pageH-box.Y-box.H, box.W, box.H) +} + +// pdfDestRect 转换PDF跳转目标 +// 入参: dest OFD跳转目标, pageH 页面高度 +// 返回: canvas.Rect PDF目标区域, bool 是否支持 +func pdfDestRect(dest Dest, pageH float64) (canvas.Rect, bool) { + switch dest.Type { + case "XYZ": + return canvas.Rect{X0: dest.Left, Y0: pageH - dest.Top, X1: dest.Left, Y1: pageH - dest.Top}, true + case "Fit": + return canvas.Rect{}, true + case "FitH": + return canvas.Rect{Y0: pageH - dest.Top, Y1: pageH - dest.Top}, true + case "FitV": + return canvas.Rect{X0: dest.Left, X1: dest.Left}, true + case "FitR": + return canvas.Rect{X0: dest.Left, Y0: pageH - dest.Bottom, X1: dest.Right, Y1: pageH - dest.Top}, true + } + return canvas.Rect{}, false +} + +// pdfDestY 获取PDF大纲目标位置 +// 入参: dest OFD跳转目标, pageH 页面高度 +// 返回: float64 PDF纵坐标 +func pdfDestY(dest Dest, pageH float64) float64 { + switch dest.Type { + case "XYZ", "FitH", "FitR": + return pageH - dest.Top + } + return 0 +} diff --git a/ofdgo_res.go b/ofdgo_res.go index 982e70a..5e03948 100644 --- a/ofdgo_res.go +++ b/ofdgo_res.go @@ -104,4 +104,5 @@ type CompositeGraphicUnit struct { ImageObject []ImageObject `xml:"ImageObject"` CompositeGraphicUnit []CompositeGraphicUnit `xml:"CompositeGraphicUnit"` Clips *Clips `xml:"Clips"` + Actions []Action `xml:"Actions>Action"` } diff --git a/ofdgo_sign.go b/ofdgo_sign.go index 3bbff4f..7f94d4b 100644 --- a/ofdgo_sign.go +++ b/ofdgo_sign.go @@ -36,7 +36,7 @@ const ( // Signatures 签名列表 type Signatures struct { XMLName xml.Name `xml:"Signatures"` - MaxSignId string `xml:"MaxSignId"` + MaxSignID string `xml:"MaxSignId"` List []Signature `xml:"Signature"` } diff --git a/ofdgo_sign_sm2.go b/ofdgo_sign_sm2.go index 3b2df15..d55ce83 100644 --- a/ofdgo_sign_sm2.go +++ b/ofdgo_sign_sm2.go @@ -15,7 +15,6 @@ package ofdgo import ( - "crypto/elliptic" "encoding/asn1" "encoding/binary" "math/big" @@ -31,17 +30,32 @@ type sm2PublicKey struct { Y *big.Int } +// sm2Curve SM2椭圆曲线 +type sm2Curve struct { + P *big.Int + N *big.Int + B *big.Int + Gx *big.Int + Gy *big.Int +} + +// sm2Point SM2雅可比坐标点 +type sm2Point struct { + X *big.Int + Y *big.Int + Z *big.Int +} + // newSM2P256 创建SM2椭圆曲线 -// 返回: elliptic.Curve SM2椭圆曲线 -func newSM2P256() elliptic.Curve { - c := &elliptic.CurveParams{Name: "SM2-P-256"} - c.P = sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF") - c.N = sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123") - c.B = sm2Big("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93") - c.Gx = sm2Big("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7") - c.Gy = sm2Big("BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0") - c.BitSize = 256 - return c +// 返回: *sm2Curve SM2椭圆曲线 +func newSM2P256() *sm2Curve { + return &sm2Curve{ + P: sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF"), + N: sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123"), + B: sm2Big("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93"), + Gx: sm2Big("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7"), + Gy: sm2Big("BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0"), + } } // sm2Big 解析SM2大整数常量 @@ -85,11 +99,11 @@ func parseSM2Signature(sig []byte) (*big.Int, *big.Int, bool) { // 入参: pub 公钥, userID 用户标识, msg 原文, r R值, s S值 // 返回: bool 是否验证通过 func sm2Verify(pub sm2PublicKey, userID, msg []byte, r, s *big.Int) bool { - n := sm2P256.Params().N + n := sm2P256.N if r.Sign() <= 0 || s.Sign() <= 0 || r.Cmp(n) >= 0 || s.Cmp(n) >= 0 { return false } - if pub.X == nil || pub.Y == nil || !sm2P256.IsOnCurve(pub.X, pub.Y) { + if pub.X == nil || pub.Y == nil || !sm2P256.isOnCurve(pub.X, pub.Y) { return false } e := new(big.Int).SetBytes(sm2MessageDigest(pub, userID, msg)) @@ -98,10 +112,8 @@ func sm2Verify(pub sm2PublicKey, userID, msg []byte, r, s *big.Int) bool { if t.Sign() == 0 { return false } - x1, y1 := sm2P256.ScalarBaseMult(s.Bytes()) - x2, y2 := sm2P256.ScalarMult(pub.X, pub.Y, t.Bytes()) - x, _ := sm2P256.Add(x1, y1, x2, y2) - if x == nil { + x, ok := sm2P256.combinedMult(pub.X, pub.Y, s, t) + if !ok { return false } v := new(big.Int).Add(e, x) @@ -132,9 +144,9 @@ func sm2ZA(pub sm2PublicKey, userID []byte) []byte { h.Write(entl[:]) h.Write(userID) h.Write(sm2Fixed(sm2A())) - h.Write(sm2Fixed(sm2P256.Params().B)) - h.Write(sm2Fixed(sm2P256.Params().Gx)) - h.Write(sm2Fixed(sm2P256.Params().Gy)) + h.Write(sm2Fixed(sm2P256.B)) + h.Write(sm2Fixed(sm2P256.Gx)) + h.Write(sm2Fixed(sm2P256.Gy)) h.Write(sm2Fixed(pub.X)) h.Write(sm2Fixed(pub.Y)) return h.Sum(nil) @@ -143,7 +155,158 @@ func sm2ZA(pub sm2PublicKey, userID []byte) []byte { // sm2A 获取SM2曲线A参数 // 返回: *big.Int 曲线A参数 func sm2A() *big.Int { - return new(big.Int).Sub(sm2P256.Params().P, big.NewInt(3)) + return new(big.Int).Sub(sm2P256.P, big.NewInt(3)) +} + +// isOnCurve 判断点是否位于SM2曲线 +// 入参: x X坐标, y Y坐标 +// 返回: bool 是否位于曲线 +func (c *sm2Curve) isOnCurve(x, y *big.Int) bool { + if x.Sign() < 0 || y.Sign() < 0 || x.Cmp(c.P) >= 0 || y.Cmp(c.P) >= 0 { + return false + } + left := c.fieldSquare(y) + right := c.fieldAdd(c.fieldSub(c.fieldMul(c.fieldSquare(x), x), c.fieldScale(x, 3)), c.B) + return left.Cmp(right) == 0 +} + +// combinedMult 计算sG+tP +// 入参: x 公钥X坐标, y 公钥Y坐标, s 标量S, t 标量T +// 返回: *big.Int 结果X坐标, bool 是否计算成功 +func (c *sm2Curve) combinedMult(x, y, s, t *big.Int) (*big.Int, bool) { + base := c.scalarMult(c.Gx, c.Gy, s.Bytes()) + public := c.scalarMult(x, y, t.Bytes()) + result := c.add(base, public) + affineX, _, ok := c.affine(result) + return affineX, ok +} + +// scalarMult 计算椭圆曲线标量乘法 +// 入参: x 点X坐标, y 点Y坐标, scalar 标量 +// 返回: sm2Point 雅可比坐标点 +func (c *sm2Curve) scalarMult(x, y *big.Int, scalar []byte) sm2Point { + result := c.infinity() + point := sm2Point{X: new(big.Int).Set(x), Y: new(big.Int).Set(y), Z: big.NewInt(1)} + for _, value := range scalar { + for bit := 7; bit >= 0; bit-- { + result = c.double(result) + if value&(1<