From b1c851b7b0be665d01815c1d44ce0b96f298fe6c Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Fri, 30 Aug 2019 08:50:30 +0000 Subject: [PATCH] Becoded action support (#161) * initial commit to add action support * Add support for all actions as defined in the specs * Implement filespec for actions * Add url filespec test * update file test * Implement remarks + add tests --- creator/creator.go | 3 +- creator/text_chunk.go | 7 +- model/action.go | 1019 +++++++++++++++++++++++++++++++++++++++++ model/action_test.go | 569 +++++++++++++++++++++++ model/annotations.go | 53 ++- model/file.go | 178 +++++++ model/file_test.go | 98 ++++ 7 files changed, 1921 insertions(+), 6 deletions(-) create mode 100644 model/action.go create mode 100644 model/action_test.go create mode 100644 model/file.go create mode 100644 model/file_test.go diff --git a/creator/creator.go b/creator/creator.go index a5f4d8cf..06e8a4a0 100644 --- a/creator/creator.go +++ b/creator/creator.go @@ -291,10 +291,11 @@ func (c *Creator) initContext() { } // NewPage adds a new Page to the Creator and sets as the active Page. -func (c *Creator) NewPage() { +func (c *Creator) NewPage() *model.PdfPage { page := c.newPage() c.pages = append(c.pages, page) c.context.Page++ + return page } // AddPage adds the specified page to the creator. diff --git a/creator/text_chunk.go b/creator/text_chunk.go index f0b88bf5..f88634d7 100644 --- a/creator/text_chunk.go +++ b/creator/text_chunk.go @@ -44,10 +44,9 @@ func newExternalLinkAnnotation(url string) *model.PdfAnnotation { annotation.BS = bs.ToPdfObject() // Set link destination. - action := core.MakeDict() - action.Set(core.PdfObjectName("S"), core.MakeName("URI")) - action.Set(core.PdfObjectName("URI"), core.MakeString(url)) - annotation.A = action + action := model.NewPdfActionURI() + action.URI = core.MakeString(url) + annotation.SetAction(action.PdfAction) return annotation.PdfAnnotation } diff --git a/model/action.go b/model/action.go new file mode 100644 index 00000000..82fd25b5 --- /dev/null +++ b/model/action.go @@ -0,0 +1,1019 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package model + +import ( + "fmt" + "github.com/unidoc/unipdf/v3/common" + "github.com/unidoc/unipdf/v3/core" +) + +// PdfActionType represents an action type in PDF (section 12.6.4 p. 417). +type PdfActionType string + +// (Section 12.6.4 p. 417). +// See Table 198 - Action types +const ( + ActionTypeGoTo PdfActionType = "GoTo" // Go to a destination in the current document + ActionTypeGoTo3DView PdfActionType = "GoTo3DView" // Set the current view of a 3D annotation + ActionTypeGoToE PdfActionType = "GoToE" // Go to embedded, PDF 1.6, Got to a destination in an embedded file + ActionTypeGoToR PdfActionType = "GoToR" // Go to remote, Go to a destination in another document + ActionTypeHide PdfActionType = "Hide" // Set an annotation's Hidden flag + ActionTypeImportData PdfActionType = "ImportData" // Import field values from a file + ActionTypeJavaScript PdfActionType = "JavaScript" // Execute a JavaScript script + ActionTypeLaunch PdfActionType = "Launch" // Launch an application, usually to open a file + ActionTypeMovie PdfActionType = "Movie" // Play a movie + ActionTypeNamed PdfActionType = "Named" // Execute an action predefined by the conforming reader + ActionTypeRendition PdfActionType = "Rendition" // Controls the playing of multimedia content + ActionTypeResetForm PdfActionType = "ResetForm" // Set fields to their default values + ActionTypeSetOCGState PdfActionType = "SetOCGState" // Set the states of optional content groups + ActionTypeSound PdfActionType = "Sound" // Play a sound + ActionTypeSubmitForm PdfActionType = "SubmitForm" // Send data to a uniform resource locator + ActionTypeThread PdfActionType = "Thread" // Begin reading an article thread + ActionTypeTrans PdfActionType = "Trans" // Updates the display of a document, using a transition dictionary + ActionTypeURI PdfActionType = "URI" // Resolves a uniform resource identifier +) + +// PdfAction represents an action in PDF (section 12.6 p. 412). +type PdfAction struct { + context PdfModel + + Type core.PdfObject + S core.PdfObject + Next core.PdfObject + + container *core.PdfIndirectObject +} + +// GetContext returns the action context which contains the specific type-dependent context. +// The context represents the subaction. +func (a *PdfAction) GetContext() PdfModel { + if a == nil { + return nil + } + return a.context +} + +// SetContext sets the sub action (context). +func (a *PdfAction) SetContext(ctx PdfModel) { + a.context = ctx +} + +// GetContainingPdfObject implements interface PdfModel. +func (a *PdfAction) GetContainingPdfObject() core.PdfObject { + return a.container +} + +// ToPdfObject implements interface PdfModel. +func (a *PdfAction) ToPdfObject() core.PdfObject { + container := a.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.Clear() + + d.Set("Type", core.MakeName("Action")) + d.SetIfNotNil("S", a.S) + d.SetIfNotNil("Next", a.Next) + + return container +} + +// String implements interface PdfObject. +func (a *PdfAction) String() string { + obj, ok := a.ToPdfObject().(*core.PdfIndirectObject) + if ok { + return fmt.Sprintf("%T: %s", a.context, obj.PdfObject.String()) + } + + return "" +} + +// PdfActionGoTo represents a GoTo action. +type PdfActionGoTo struct { + *PdfAction + D core.PdfObject // name, byte string or array +} + +// PdfActionGoToR represents a GoToR action. +type PdfActionGoToR struct { + *PdfAction + F *PdfFilespec + D core.PdfObject // name, byte string or array + NewWindow core.PdfObject +} + +// PdfActionGoToE represents a GoToE action. +type PdfActionGoToE struct { + *PdfAction + F *PdfFilespec + D core.PdfObject // name, byte string or array + NewWindow core.PdfObject + T core.PdfObject +} + +// PdfActionLaunch represents a launch action. +type PdfActionLaunch struct { + *PdfAction + F *PdfFilespec + Win core.PdfObject + Mac core.PdfObject + Unix core.PdfObject + NewWindow core.PdfObject +} + +// PdfActionThread represents a thread action. +type PdfActionThread struct { + *PdfAction + F *PdfFilespec + D core.PdfObject + B core.PdfObject +} + +// PdfActionURI represents an URI action. +type PdfActionURI struct { + *PdfAction + URI core.PdfObject + IsMap core.PdfObject +} + +// PdfActionSound represents a sound action. +type PdfActionSound struct { + *PdfAction + Sound core.PdfObject + Volume core.PdfObject + Synchronous core.PdfObject + Repeat core.PdfObject + Mix core.PdfObject +} + +// PdfActionMovie represents a movie action. +type PdfActionMovie struct { + *PdfAction + Annotation core.PdfObject + T core.PdfObject + Operation core.PdfObject +} + +// PdfActionHide represents a hide action. +type PdfActionHide struct { + *PdfAction + T core.PdfObject + H core.PdfObject +} + +// PdfActionNamed represents a named action. +type PdfActionNamed struct { + *PdfAction + N core.PdfObject +} + +// PdfActionSubmitForm represents a submitForm action. +type PdfActionSubmitForm struct { + *PdfAction + F *PdfFilespec + Fields core.PdfObject + Flags core.PdfObject +} + +// PdfActionResetForm represents a resetForm action. +type PdfActionResetForm struct { + *PdfAction + Fields core.PdfObject + Flags core.PdfObject +} + +// PdfActionImportData represents a importData action. +type PdfActionImportData struct { + *PdfAction + F *PdfFilespec +} + +// PdfActionSetOCGState represents a SetOCGState action. +type PdfActionSetOCGState struct { + *PdfAction + State core.PdfObject + PreserveRB core.PdfObject +} + +// PdfActionRendition represents a Rendition action. +type PdfActionRendition struct { + *PdfAction + R core.PdfObject + AN core.PdfObject + OP core.PdfObject + JS core.PdfObject +} + +// PdfActionTrans represents a trans action. +type PdfActionTrans struct { + *PdfAction + Trans core.PdfObject +} + +// PdfActionGoTo3DView represents a GoTo3DView action. +type PdfActionGoTo3DView struct { + *PdfAction + TA core.PdfObject + V core.PdfObject +} + +// PdfActionJavaScript represents a javaScript action. +type PdfActionJavaScript struct { + *PdfAction + JS core.PdfObject +} + +// NewPdfAction returns an initialized generic PDF action model. +func NewPdfAction() *PdfAction { + action := &PdfAction{} + action.container = core.MakeIndirectObject(core.MakeDict()) + return action +} + +// NewPdfActionGoTo returns a new "go to" action. +func NewPdfActionGoTo() *PdfActionGoTo { + action := NewPdfAction() + goToAction := &PdfActionGoTo{} + goToAction.PdfAction = action + action.SetContext(goToAction) + return goToAction +} + +// NewPdfActionGoToR returns a new "go to remote" action. +func NewPdfActionGoToR() *PdfActionGoToR { + action := NewPdfAction() + goToRAction := &PdfActionGoToR{} + goToRAction.PdfAction = action + action.SetContext(goToRAction) + return goToRAction +} + +// NewPdfActionGoToE returns a new "go to embedded" action. +func NewPdfActionGoToE() *PdfActionGoToE { + action := NewPdfAction() + goToEAction := &PdfActionGoToE{} + goToEAction.PdfAction = action + action.SetContext(goToEAction) + return goToEAction +} + +// NewPdfActionLaunch returns a new "launch" action. +func NewPdfActionLaunch() *PdfActionLaunch { + action := NewPdfAction() + launchAction := &PdfActionLaunch{} + launchAction.PdfAction = action + action.SetContext(launchAction) + return launchAction +} + +// NewPdfActionThread returns a new "thread" action. +func NewPdfActionThread() *PdfActionThread { + action := NewPdfAction() + threadAction := &PdfActionThread{} + threadAction.PdfAction = action + action.SetContext(threadAction) + return threadAction +} + +// NewPdfActionURI returns a new "Uri" action. +func NewPdfActionURI() *PdfActionURI { + action := NewPdfAction() + uriAction := &PdfActionURI{} + uriAction.PdfAction = action + action.SetContext(uriAction) + return uriAction +} + +// NewPdfActionSound returns a new "sound" action. +func NewPdfActionSound() *PdfActionSound { + action := NewPdfAction() + soundAction := &PdfActionSound{} + soundAction.PdfAction = action + action.SetContext(soundAction) + return soundAction +} + +// NewPdfActionMovie returns a new "movie" action. +func NewPdfActionMovie() *PdfActionMovie { + action := NewPdfAction() + movieAction := &PdfActionMovie{} + movieAction.PdfAction = action + action.SetContext(movieAction) + return movieAction +} + +// NewPdfActionHide returns a new "hide" action. +func NewPdfActionHide() *PdfActionHide { + action := NewPdfAction() + hideAction := &PdfActionHide{} + hideAction.PdfAction = action + action.SetContext(hideAction) + return hideAction +} + +// NewPdfActionNamed returns a new "named" action. +func NewPdfActionNamed() *PdfActionNamed { + action := NewPdfAction() + namedAction := &PdfActionNamed{} + namedAction.PdfAction = action + action.SetContext(namedAction) + return namedAction +} + +// NewPdfActionSubmitForm returns a new "submit form" action. +func NewPdfActionSubmitForm() *PdfActionSubmitForm { + action := NewPdfAction() + submitFormAction := &PdfActionSubmitForm{} + submitFormAction.PdfAction = action + action.SetContext(submitFormAction) + return submitFormAction +} + +// NewPdfActionResetForm returns a new "reset form" action. +func NewPdfActionResetForm() *PdfActionResetForm { + action := NewPdfAction() + resetFormAction := &PdfActionResetForm{} + resetFormAction.PdfAction = action + action.SetContext(resetFormAction) + return resetFormAction +} + +// NewPdfActionImportData returns a new "import data" action. +func NewPdfActionImportData() *PdfActionImportData { + action := NewPdfAction() + importDataAction := &PdfActionImportData{} + importDataAction.PdfAction = action + action.SetContext(importDataAction) + return importDataAction +} + +// NewPdfActionSetOCGState returns a new "named" action. +func NewPdfActionSetOCGState() *PdfActionSetOCGState { + action := NewPdfAction() + setOCGStateAction := &PdfActionSetOCGState{} + setOCGStateAction.PdfAction = action + action.SetContext(setOCGStateAction) + return setOCGStateAction +} + +// NewPdfActionRendition returns a new "rendition" action. +func NewPdfActionRendition() *PdfActionRendition { + action := NewPdfAction() + renditionAction := &PdfActionRendition{} + renditionAction.PdfAction = action + action.SetContext(renditionAction) + return renditionAction +} + +// NewPdfActionTrans returns a new "trans" action. +func NewPdfActionTrans() *PdfActionTrans { + action := NewPdfAction() + transAction := &PdfActionTrans{} + transAction.PdfAction = action + action.SetContext(transAction) + return transAction +} + +// NewPdfActionGoTo3DView returns a new "goTo3DView" action. +func NewPdfActionGoTo3DView() *PdfActionGoTo3DView { + action := NewPdfAction() + goTo3DViewAction := &PdfActionGoTo3DView{} + goTo3DViewAction.PdfAction = action + action.SetContext(goTo3DViewAction) + return goTo3DViewAction +} + +// NewPdfActionJavaScript returns a new "javaScript" action. +func NewPdfActionJavaScript() *PdfActionJavaScript { + action := NewPdfAction() + javaScriptAction := &PdfActionJavaScript{} + javaScriptAction.PdfAction = action + action.SetContext(javaScriptAction) + return javaScriptAction +} + +// ToPdfObject implements interface PdfModel. +func (gotoAct *PdfActionGoTo) ToPdfObject() core.PdfObject { + gotoAct.PdfAction.ToPdfObject() + container := gotoAct.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("S", core.MakeName(string(ActionTypeGoTo))) + d.SetIfNotNil("D", gotoAct.D) + return container +} + +// ToPdfObject implements interface PdfModel. +func (gotoRAct *PdfActionGoToR) ToPdfObject() core.PdfObject { + gotoRAct.PdfAction.ToPdfObject() + container := gotoRAct.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("S", core.MakeName(string(ActionTypeGoToR))) + + if gotoRAct.F != nil { + d.Set("F", gotoRAct.F.ToPdfObject()) + } + + d.SetIfNotNil("D", gotoRAct.D) + d.SetIfNotNil("NewWindow", gotoRAct.NewWindow) + return container +} + +// ToPdfObject implements interface PdfModel. +func (gotoEAct *PdfActionGoToE) ToPdfObject() core.PdfObject { + gotoEAct.PdfAction.ToPdfObject() + container := gotoEAct.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("S", core.MakeName(string(ActionTypeGoToE))) + + if gotoEAct.F != nil { + d.Set("F", gotoEAct.F.ToPdfObject()) + } + + d.SetIfNotNil("D", gotoEAct.D) + d.SetIfNotNil("NewWindow", gotoEAct.NewWindow) + d.SetIfNotNil("T", gotoEAct.T) + return container +} + +// ToPdfObject implements interface PdfModel. +func (launchAct *PdfActionLaunch) ToPdfObject() core.PdfObject { + launchAct.PdfAction.ToPdfObject() + container := launchAct.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("S", core.MakeName(string(ActionTypeLaunch))) + + if launchAct.F != nil { + d.Set("F", launchAct.F.ToPdfObject()) + } + + d.SetIfNotNil("Win", launchAct.Win) + d.SetIfNotNil("Mac", launchAct.Mac) + d.SetIfNotNil("Unix", launchAct.Unix) + d.SetIfNotNil("NewWindow", launchAct.NewWindow) + return container +} + +// ToPdfObject implements interface PdfModel. +func (threadAct *PdfActionThread) ToPdfObject() core.PdfObject { + threadAct.PdfAction.ToPdfObject() + container := threadAct.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("S", core.MakeName(string(ActionTypeThread))) + + if threadAct.F != nil { + d.Set("F", threadAct.F.ToPdfObject()) + } + + d.SetIfNotNil("D", threadAct.D) + d.SetIfNotNil("B", threadAct.B) + return container +} + +// ToPdfObject implements interface PdfModel. +func (uriAct *PdfActionURI) ToPdfObject() core.PdfObject { + uriAct.PdfAction.ToPdfObject() + container := uriAct.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("S", core.MakeName(string(ActionTypeURI))) + d.SetIfNotNil("URI", uriAct.URI) + d.SetIfNotNil("IsMap", uriAct.IsMap) + return container +} + +// ToPdfObject implements interface PdfModel. +func (soundAct *PdfActionSound) ToPdfObject() core.PdfObject { + soundAct.PdfAction.ToPdfObject() + container := soundAct.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("S", core.MakeName(string(ActionTypeSound))) + d.SetIfNotNil("Sound", soundAct.Sound) + d.SetIfNotNil("Volume", soundAct.Volume) + d.SetIfNotNil("Synchronous", soundAct.Synchronous) + d.SetIfNotNil("Repeat", soundAct.Repeat) + d.SetIfNotNil("Mix", soundAct.Mix) + return container +} + +// ToPdfObject implements interface PdfModel. +func (movieAct *PdfActionMovie) ToPdfObject() core.PdfObject { + movieAct.PdfAction.ToPdfObject() + container := movieAct.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("S", core.MakeName(string(ActionTypeMovie))) + d.SetIfNotNil("Annotation", movieAct.Annotation) + d.SetIfNotNil("T", movieAct.T) + d.SetIfNotNil("Operation", movieAct.Operation) + return container +} + +// ToPdfObject implements interface PdfModel. +func (hideAct *PdfActionHide) ToPdfObject() core.PdfObject { + hideAct.PdfAction.ToPdfObject() + container := hideAct.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("S", core.MakeName(string(ActionTypeHide))) + d.SetIfNotNil("T", hideAct.T) + d.SetIfNotNil("H", hideAct.H) + return container +} + +// ToPdfObject implements interface PdfModel. +func (namedAct *PdfActionNamed) ToPdfObject() core.PdfObject { + namedAct.PdfAction.ToPdfObject() + container := namedAct.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("S", core.MakeName(string(ActionTypeNamed))) + d.SetIfNotNil("N", namedAct.N) + return container +} + +// ToPdfObject implements interface PdfModel. +func (submitFormAct *PdfActionSubmitForm) ToPdfObject() core.PdfObject { + submitFormAct.PdfAction.ToPdfObject() + container := submitFormAct.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("S", core.MakeName(string(ActionTypeSubmitForm))) + + if submitFormAct.F != nil { + d.Set("F", submitFormAct.F.ToPdfObject()) + } + + d.SetIfNotNil("Fields", submitFormAct.Fields) + d.SetIfNotNil("Flags", submitFormAct.Flags) + return container +} + +// ToPdfObject implements interface PdfModel. +func (resetFormAct *PdfActionResetForm) ToPdfObject() core.PdfObject { + resetFormAct.PdfAction.ToPdfObject() + container := resetFormAct.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("S", core.MakeName(string(ActionTypeResetForm))) + d.SetIfNotNil("Fields", resetFormAct.Fields) + d.SetIfNotNil("Flags", resetFormAct.Flags) + return container +} + +// ToPdfObject implements interface PdfModel. +func (importDataAct *PdfActionImportData) ToPdfObject() core.PdfObject { + importDataAct.PdfAction.ToPdfObject() + container := importDataAct.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("S", core.MakeName(string(ActionTypeImportData))) + + if importDataAct.F != nil { + d.Set("F", importDataAct.F.ToPdfObject()) + } + + return container +} + +// ToPdfObject implements interface PdfModel. +func (setOCGStateAct *PdfActionSetOCGState) ToPdfObject() core.PdfObject { + setOCGStateAct.PdfAction.ToPdfObject() + container := setOCGStateAct.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("S", core.MakeName(string(ActionTypeSetOCGState))) + d.SetIfNotNil("State", setOCGStateAct.State) + d.SetIfNotNil("PreserveRB", setOCGStateAct.PreserveRB) + return container +} + +// ToPdfObject implements interface PdfModel. +func (renditionAct *PdfActionRendition) ToPdfObject() core.PdfObject { + renditionAct.PdfAction.ToPdfObject() + container := renditionAct.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("S", core.MakeName(string(ActionTypeRendition))) + d.SetIfNotNil("R", renditionAct.R) + d.SetIfNotNil("AN", renditionAct.AN) + d.SetIfNotNil("OP", renditionAct.OP) + d.SetIfNotNil("JS", renditionAct.JS) + return container +} + +// ToPdfObject implements interface PdfModel. +func (transAct *PdfActionTrans) ToPdfObject() core.PdfObject { + transAct.PdfAction.ToPdfObject() + container := transAct.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("S", core.MakeName(string(ActionTypeTrans))) + d.SetIfNotNil("Trans", transAct.Trans) + return container +} + +// ToPdfObject implements interface PdfModel. +func (goTo3DViewAct *PdfActionGoTo3DView) ToPdfObject() core.PdfObject { + goTo3DViewAct.PdfAction.ToPdfObject() + container := goTo3DViewAct.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("S", core.MakeName(string(ActionTypeGoTo3DView))) + d.SetIfNotNil("TA", goTo3DViewAct.TA) + d.SetIfNotNil("V", goTo3DViewAct.V) + return container +} + +// ToPdfObject implements interface PdfModel. +func (javaScriptAct *PdfActionJavaScript) ToPdfObject() core.PdfObject { + javaScriptAct.PdfAction.ToPdfObject() + container := javaScriptAct.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("S", core.MakeName(string(ActionTypeJavaScript))) + d.SetIfNotNil("JS", javaScriptAct.JS) + return container +} + +// Used for PDF parsing. Loads a PDF action model from a PDF dictionary. +// Loads the common PDF action dictionary, and anything needed for the action subtype. +func (r *PdfReader) newPdfActionFromIndirectObject(container *core.PdfIndirectObject) (*PdfAction, error) { + d, isDict := container.PdfObject.(*core.PdfObjectDictionary) + if !isDict { + return nil, fmt.Errorf("action indirect object not containing a dictionary") + } + + // Check if cached, return cached model if exists. + if model := r.modelManager.GetModelFromPrimitive(d); model != nil { + action, ok := model.(*PdfAction) + if !ok { + return nil, fmt.Errorf("cached model not a PDF action") + } + return action, nil + } + + action := &PdfAction{} + action.container = container + r.modelManager.Register(d, action) + + if obj := d.Get("Type"); obj != nil { + str, ok := obj.(*core.PdfObjectName) + if !ok { + common.Log.Trace("Incompatibility! Invalid type of Type (%T) - should be Name", obj) + } else { + if *str != "Action" { + // Log a debug message. + // Not returning an error on this. + common.Log.Trace("Unsuspected Type != Action (%s)", *str) + } + action.Type = str + } + } + + if obj := d.Get("Next"); obj != nil { + action.Next = obj + } + + if obj := d.Get("S"); obj != nil { + action.S = obj + } + + actionName, ok := action.S.(*core.PdfObjectName) + if !ok { + common.Log.Debug("ERROR: Invalid S object type != name (%T)", action.S) + return nil, fmt.Errorf("invalid S object type != name (%T)", action.S) + } + + actionType := PdfActionType(actionName.String()) + switch actionType { + case ActionTypeGoTo: + ctx, err := r.newPdfActionGotoFromDict(d) + if err != nil { + return nil, err + } + ctx.PdfAction = action + action.context = ctx + return action, nil + case ActionTypeGoToR: + ctx, err := r.newPdfActionGotoRFromDict(d) + if err != nil { + return nil, err + } + ctx.PdfAction = action + action.context = ctx + return action, nil + case ActionTypeGoToE: + ctx, err := r.newPdfActionGotoEFromDict(d) + if err != nil { + return nil, err + } + ctx.PdfAction = action + action.context = ctx + return action, nil + case ActionTypeLaunch: + ctx, err := r.newPdfActionLaunchFromDict(d) + if err != nil { + return nil, err + } + ctx.PdfAction = action + action.context = ctx + return action, nil + case ActionTypeThread: + ctx, err := r.newPdfActionThreadFromDict(d) + if err != nil { + return nil, err + } + ctx.PdfAction = action + action.context = ctx + return action, nil + case ActionTypeURI: + ctx, err := r.newPdfActionURIFromDict(d) + if err != nil { + return nil, err + } + ctx.PdfAction = action + action.context = ctx + return action, nil + case ActionTypeSound: + ctx, err := r.newPdfActionSoundFromDict(d) + if err != nil { + return nil, err + } + ctx.PdfAction = action + action.context = ctx + return action, nil + case ActionTypeMovie: + ctx, err := r.newPdfActionMovieFromDict(d) + if err != nil { + return nil, err + } + ctx.PdfAction = action + action.context = ctx + return action, nil + case ActionTypeHide: + ctx, err := r.newPdfActionHideFromDict(d) + if err != nil { + return nil, err + } + ctx.PdfAction = action + action.context = ctx + return action, nil + case ActionTypeNamed: + ctx, err := r.newPdfActionNamedFromDict(d) + if err != nil { + return nil, err + } + ctx.PdfAction = action + action.context = ctx + return action, nil + case ActionTypeSubmitForm: + ctx, err := r.newPdfActionSubmitFormFromDict(d) + if err != nil { + return nil, err + } + ctx.PdfAction = action + action.context = ctx + return action, nil + case ActionTypeResetForm: + ctx, err := r.newPdfActionResetFormFromDict(d) + if err != nil { + return nil, err + } + ctx.PdfAction = action + action.context = ctx + return action, nil + case ActionTypeImportData: + ctx, err := r.newPdfActionImportDataFromDict(d) + if err != nil { + return nil, err + } + ctx.PdfAction = action + action.context = ctx + return action, nil + case ActionTypeSetOCGState: + ctx, err := r.newPdfActionSetOCGStateFromDict(d) + if err != nil { + return nil, err + } + ctx.PdfAction = action + action.context = ctx + return action, nil + case ActionTypeRendition: + ctx, err := r.newPdfActionRenditionFromDict(d) + if err != nil { + return nil, err + } + ctx.PdfAction = action + action.context = ctx + return action, nil + case ActionTypeTrans: + ctx, err := r.newPdfActionTransFromDict(d) + if err != nil { + return nil, err + } + ctx.PdfAction = action + action.context = ctx + return action, nil + case ActionTypeGoTo3DView: + ctx, err := r.newPdfActionGoTo3DViewFromDict(d) + if err != nil { + return nil, err + } + ctx.PdfAction = action + action.context = ctx + return action, nil + case ActionTypeJavaScript: + ctx, err := r.newPdfActionJavaScriptFromDict(d) + if err != nil { + return nil, err + } + ctx.PdfAction = action + action.context = ctx + return action, nil + } + + common.Log.Debug("ERROR: Ignoring unknown action: %s", actionType) + return nil, nil +} + +func tryLoadFilespec(obj core.PdfObject) (*PdfFilespec, error) { + if obj == nil { + return nil, nil + } + + return NewPdfFilespecFromObj(obj) +} + +func (r *PdfReader) newPdfActionGotoFromDict(d *core.PdfObjectDictionary) (*PdfActionGoTo, error) { + return &PdfActionGoTo{ + D: d.Get("D"), + }, nil +} + +func (r *PdfReader) newPdfActionGotoRFromDict(d *core.PdfObjectDictionary) (*PdfActionGoToR, error) { + filespec, err := tryLoadFilespec(d.Get("F")) + if err != nil { + return nil, err + } + + return &PdfActionGoToR{ + D: d.Get("D"), + NewWindow: d.Get("NewWindow"), + F: filespec, + }, nil +} + +func (r *PdfReader) newPdfActionGotoEFromDict(d *core.PdfObjectDictionary) (*PdfActionGoToE, error) { + filespec, err := tryLoadFilespec(d.Get("F")) + if err != nil { + return nil, err + } + + return &PdfActionGoToE{ + D: d.Get("D"), + NewWindow: d.Get("NewWindow"), + T: d.Get("T"), + F: filespec, + }, nil +} + +func (r *PdfReader) newPdfActionLaunchFromDict(d *core.PdfObjectDictionary) (*PdfActionLaunch, error) { + filespec, err := tryLoadFilespec(d.Get("F")) + if err != nil { + return nil, err + } + + return &PdfActionLaunch{ + Win: d.Get("Win"), + Mac: d.Get("Mac"), + Unix: d.Get("Unix"), + NewWindow: d.Get("NewWindow"), + F: filespec, + }, nil +} + +func (r *PdfReader) newPdfActionThreadFromDict(d *core.PdfObjectDictionary) (*PdfActionThread, error) { + filespec, err := tryLoadFilespec(d.Get("F")) + if err != nil { + return nil, err + } + + return &PdfActionThread{ + D: d.Get("D"), + B: d.Get("B"), + F: filespec, + }, nil +} + +func (r *PdfReader) newPdfActionURIFromDict(d *core.PdfObjectDictionary) (*PdfActionURI, error) { + return &PdfActionURI{ + URI: d.Get("URI"), + IsMap: d.Get("IsMap"), + }, nil +} + +func (r *PdfReader) newPdfActionSoundFromDict(d *core.PdfObjectDictionary) (*PdfActionSound, error) { + return &PdfActionSound{ + Sound: d.Get("Sound"), + Volume: d.Get("Volume"), + Synchronous: d.Get("Synchronous"), + Repeat: d.Get("Repeat"), + Mix: d.Get("Mix"), + }, nil +} + +func (r *PdfReader) newPdfActionMovieFromDict(d *core.PdfObjectDictionary) (*PdfActionMovie, error) { + return &PdfActionMovie{ + Annotation: d.Get("Annotation"), + T: d.Get("T"), + Operation: d.Get("Operation"), + }, nil +} + +func (r *PdfReader) newPdfActionHideFromDict(d *core.PdfObjectDictionary) (*PdfActionHide, error) { + return &PdfActionHide{ + T: d.Get("T"), + H: d.Get("H"), + }, nil +} + +func (r *PdfReader) newPdfActionNamedFromDict(d *core.PdfObjectDictionary) (*PdfActionNamed, error) { + return &PdfActionNamed{ + N: d.Get("N"), + }, nil +} + +func (r *PdfReader) newPdfActionSubmitFormFromDict(d *core.PdfObjectDictionary) (*PdfActionSubmitForm, error) { + filespec, err := tryLoadFilespec(d.Get("F")) + if err != nil { + return nil, err + } + + return &PdfActionSubmitForm{ + F: filespec, + Fields: d.Get("Fields"), + Flags: d.Get("Flags"), + }, nil +} + +func (r *PdfReader) newPdfActionResetFormFromDict(d *core.PdfObjectDictionary) (*PdfActionResetForm, error) { + return &PdfActionResetForm{ + Fields: d.Get("Fields"), + Flags: d.Get("Flags"), + }, nil +} + +func (r *PdfReader) newPdfActionImportDataFromDict(d *core.PdfObjectDictionary) (*PdfActionImportData, error) { + filespec, err := tryLoadFilespec(d.Get("F")) + if err != nil { + return nil, err + } + + return &PdfActionImportData{ + F: filespec, + }, nil +} + +func (r *PdfReader) newPdfActionSetOCGStateFromDict(d *core.PdfObjectDictionary) (*PdfActionSetOCGState, error) { + return &PdfActionSetOCGState{ + State: d.Get("State"), + PreserveRB: d.Get("PreserveRB"), + }, nil +} + +func (r *PdfReader) newPdfActionRenditionFromDict(d *core.PdfObjectDictionary) (*PdfActionRendition, error) { + return &PdfActionRendition{ + R: d.Get("R"), + AN: d.Get("AN"), + OP: d.Get("OP"), + JS: d.Get("JS"), + }, nil +} + +func (r *PdfReader) newPdfActionTransFromDict(d *core.PdfObjectDictionary) (*PdfActionTrans, error) { + return &PdfActionTrans{ + Trans: d.Get("Trans"), + }, nil +} + +func (r *PdfReader) newPdfActionGoTo3DViewFromDict(d *core.PdfObjectDictionary) (*PdfActionGoTo3DView, error) { + return &PdfActionGoTo3DView{ + TA: d.Get("TA"), + V: d.Get("V"), + }, nil +} + +func (r *PdfReader) newPdfActionJavaScriptFromDict(d *core.PdfObjectDictionary) (*PdfActionJavaScript, error) { + return &PdfActionJavaScript{ + JS: d.Get("JS"), + }, nil +} diff --git a/model/action_test.go b/model/action_test.go new file mode 100644 index 00000000..325f42fe --- /dev/null +++ b/model/action_test.go @@ -0,0 +1,569 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package model + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/unidoc/unipdf/v3/core" +) + +// testAction loads an action object from object number 1 loaded from `rawText` PDF content and checks that +// it matches the `actionType`. Then it applies testFunc() on the action which does action-specific checks. +// Lastly the serialized output is checked against the input PDF object. +func testAction(t *testing.T, rawText string, actionType PdfActionType, testFunc func(t *testing.T, action *PdfAction)) { + // Read raw text + r := NewReaderForText(rawText) + + err := r.ParseIndObjSeries() + require.NoError(t, err) + + // Load the field from object number 1 as all actions in these tests are defined in object 1 + obj, err := r.parser.LookupByNumber(1) + require.NoError(t, err) + + ind, ok := obj.(*core.PdfIndirectObject) + require.True(t, ok) + + // Parse action + action, err := r.newPdfActionFromIndirectObject(ind) + require.NoError(t, err) + + // Check if raw text can be parsed to the expected action objects + + // The object should be of type action + the actionType should match the expected action + require.Equal(t, "Action", action.Type.String()) + require.Equal(t, string(actionType), action.S.String()) + + // Verify some action specific fields + testFunc(t, action) + + // Check if object can be serialized to the expected text + outDict, ok := core.GetDict(action.context.ToPdfObject()) + if !ok { + t.Fatalf("error") + } + + require.Containsf( + t, + strings.Replace(rawText, "\n", "", -1), + outDict.WriteString(), + "generated output doesn't match the expected output - %s", + outDict.WriteString()) +} + +func TestPdfActionGoTo(t *testing.T) { + rawText := ` +1 0 obj +<> +endobj +` + testAction(t, rawText, ActionTypeGoTo, func(t *testing.T, action *PdfAction) { + contextAction, ok := action.context.(*PdfActionGoTo) + require.True(t, ok) + require.Equal(t, "name", contextAction.D.String()) + }) +} + +func TestPdfActionGoToR(t *testing.T) { + rawText := ` +1 0 obj +<> +/D (name) +/NewWindow true +>> +endobj +` + + testAction(t, rawText, ActionTypeGoToR, func(t *testing.T, action *PdfAction) { + contextAction, ok := action.context.(*PdfActionGoToR) + require.True(t, ok) + require.Equal(t, "name", contextAction.D.String()) + require.Equal(t, "true", contextAction.NewWindow.String()) + require.IsType(t, &PdfFilespec{}, contextAction.F) + }) +} + +func TestPdfActionGoToE(t *testing.T) { + rawText := ` +1 0 obj +<> +/D (name) +/NewWindow true +/T <> +>> +endobj +` + + testAction(t, rawText, ActionTypeGoToE, func(t *testing.T, action *PdfAction) { + contextAction, ok := action.context.(*PdfActionGoToE) + require.True(t, ok) + require.Equal(t, "name", contextAction.D.String()) + require.Equal(t, "true", contextAction.NewWindow.String()) + require.IsType(t, &PdfFilespec{}, contextAction.F) + }) +} + +func TestPdfActionLaunch(t *testing.T) { + rawText := ` +1 0 obj +<> +/NewWindow true +>> +endobj +` + + testAction(t, rawText, ActionTypeLaunch, func(t *testing.T, action *PdfAction) { + contextAction, ok := action.context.(*PdfActionLaunch) + require.True(t, ok) + require.Equal(t, "true", contextAction.NewWindow.String()) + require.IsType(t, &PdfFilespec{}, contextAction.F) + }) +} + +func TestPdfActionThread(t *testing.T) { + rawText := ` +1 0 obj +<> +endobj +` + + testAction(t, rawText, ActionTypeThread, func(t *testing.T, action *PdfAction) { + contextAction, ok := action.context.(*PdfActionThread) + require.True(t, ok) + require.Equal(t, "4", contextAction.D.String()) + require.Equal(t, "5", contextAction.B.String()) + }) +} + +func TestPdfActionURI(t *testing.T) { + rawText := ` +1 0 obj +<> +endobj +` + + testAction(t, rawText, ActionTypeURI, func(t *testing.T, action *PdfAction) { + contextAction, ok := action.context.(*PdfActionURI) + require.True(t, ok) + require.Equal(t, "https://unidoc.io/", contextAction.URI.String()) + require.Equal(t, "true", contextAction.IsMap.String()) + }) +} + +func TestPdfActionSound(t *testing.T) { + rawText := ` +1 0 obj +<> +endobj + +2 0 obj +<< +/B 16 +/C 2 +/E /Signed +/Filter /FlateDecode +/Length 12 +/R 44100 +/Type /Sound +>> +stream +abcdefghijkl +endstream +endobj +` + + testAction(t, rawText, ActionTypeSound, func(t *testing.T, action *PdfAction) { + contextAction, ok := action.context.(*PdfActionSound) + require.True(t, ok) + require.Equal(t, "Object stream 2: Dict(\"B\": 16, \"C\": 2, \"E\": Signed, \"Filter\": FlateDecode, \"Length\": 12, \"R\": 44100, \"Type\": Sound, )", contextAction.Sound.String()) + require.Equal(t, "0.500000", contextAction.Volume.String()) + require.Equal(t, "true", contextAction.Synchronous.String()) + require.Equal(t, "true", contextAction.Repeat.String()) + require.Equal(t, "true", contextAction.Mix.String()) + }) +} + +func TestPdfActionMovie(t *testing.T) { + rawText := ` +1 0 obj +<> +/T (Title of the movie) +/Operation /Stop +>> +endobj +` + + testAction(t, rawText, ActionTypeMovie, func(t *testing.T, action *PdfAction) { + contextAction, ok := action.context.(*PdfActionMovie) + require.True(t, ok) + require.Equal(t, "Dict(\"Foo\": bar, )", contextAction.Annotation.String()) + require.Equal(t, "Title of the movie", contextAction.T.String()) + require.Equal(t, "Stop", contextAction.Operation.String()) + }) +} + +func TestPdfActionHide(t *testing.T) { + rawText := ` +1 0 obj +<> +endobj +` + + testAction(t, rawText, ActionTypeHide, func(t *testing.T, action *PdfAction) { + contextAction, ok := action.context.(*PdfActionHide) + require.True(t, ok) + require.Equal(t, "Field", contextAction.T.String()) + require.Equal(t, "false", contextAction.H.String()) + }) +} + +func TestPdfActionNamed(t *testing.T) { + rawText := ` +1 0 obj +<> +endobj +` + + testAction(t, rawText, ActionTypeNamed, func(t *testing.T, action *PdfAction) { + contextAction, ok := action.context.(*PdfActionNamed) + require.True(t, ok) + require.Equal(t, "NextPage", contextAction.N.String()) + }) +} + +func TestPdfActionSubmitForm(t *testing.T) { + rawText := ` +1 0 obj +<> +/Fields [(Address) (By) (Date) (Email) (TelNum) (Title)] +/Flags 2 +>> +endobj +` + + testAction(t, rawText, ActionTypeSubmitForm, func(t *testing.T, action *PdfAction) { + contextAction, ok := action.context.(*PdfActionSubmitForm) + require.True(t, ok) + require.Equal(t, "[Address, By, Date, Email, TelNum, Title]", contextAction.Fields.String()) + require.Equal(t, "2", contextAction.Flags.String()) + }) +} + +func TestPdfActionResetForm(t *testing.T) { + rawText := ` +1 0 obj +<> +endobj +` + + testAction(t, rawText, ActionTypeResetForm, func(t *testing.T, action *PdfAction) { + contextAction, ok := action.context.(*PdfActionResetForm) + require.True(t, ok) + require.Equal(t, "[Address, By, Date, Email, TelNum, Title]", contextAction.Fields.String()) + require.Equal(t, "2", contextAction.Flags.String()) + }) +} + +func TestPdfActionImportData(t *testing.T) { + rawText := ` +1 0 obj +<> +>> +endobj +` + + testAction(t, rawText, ActionTypeImportData, func(t *testing.T, action *PdfAction) { + contextAction, ok := action.context.(*PdfActionImportData) + require.True(t, ok) + require.IsType(t, &PdfFilespec{}, contextAction.F) + }) +} + +func TestPdfActionSetOCGState(t *testing.T) { + rawText := ` +1 0 obj +<> /Toggle <> /ON <>] +/PreserveRB false +>> +endobj +` + + testAction(t, rawText, ActionTypeSetOCGState, func(t *testing.T, action *PdfAction) { + contextAction, ok := action.context.(*PdfActionSetOCGState) + require.True(t, ok) + require.Equal(t, "[Off, Dict(\"OffFoo\": Bar, ), Toggle, Dict(\"ToggleFoo\": Bar, ), ON, Dict(\"OnFoo\": Bar, )]", contextAction.State.String()) + require.Equal(t, "false", contextAction.PreserveRB.String()) + }) +} + +func TestPdfActionRendition(t *testing.T) { + rawText := ` +1 0 obj +<> +/AN <> +/OP 4 +/JS (javascript) +>> +endobj +` + + testAction(t, rawText, ActionTypeRendition, func(t *testing.T, action *PdfAction) { + contextAction, ok := action.context.(*PdfActionRendition) + require.True(t, ok) + require.Equal(t, "Dict(\"R\": 1, )", contextAction.R.String()) + require.Equal(t, "Dict(\"AN\": 2, )", contextAction.AN.String()) + require.Equal(t, "4", contextAction.OP.String()) + require.Equal(t, "javascript", contextAction.JS.String()) + }) +} + +func TestPdfActionTrans(t *testing.T) { + rawText := ` +1 0 obj +<> +>> +endobj +` + + testAction(t, rawText, ActionTypeTrans, func(t *testing.T, action *PdfAction) { + contextAction, ok := action.context.(*PdfActionTrans) + require.True(t, ok) + require.Equal(t, "Dict(\"X\": 123, \"Y\": 456, )", contextAction.Trans.String()) + }) +} + +func TestPdfActionGoto3DView(t *testing.T) { + rawText := ` +1 0 obj +<> +/V <> +>> +endobj +` + + testAction(t, rawText, ActionTypeGoTo3DView, func(t *testing.T, action *PdfAction) { + contextAction, ok := action.context.(*PdfActionGoTo3DView) + require.True(t, ok) + require.Equal(t, "Dict(\"X\": 123, \"Y\": 456, )", contextAction.TA.String()) + require.Equal(t, "Dict(\"Name\": fake, )", contextAction.V.String()) + }) +} + +func TestPdfActionJavaScript(t *testing.T) { + rawText := ` +1 0 obj +<> +endobj +` + + testAction(t, rawText, ActionTypeJavaScript, func(t *testing.T, action *PdfAction) { + contextAction, ok := action.context.(*PdfActionJavaScript) + require.True(t, ok) + require.Equal(t, "alert(\"test\")", contextAction.JS.String()) + }) +} + +func TestNewPdfAction(t *testing.T) { + action := NewPdfAction() + require.IsType(t, &PdfAction{}, action) + require.IsType(t, &core.PdfIndirectObject{}, action.container) +} + +func TestNewPdfActionGoTo(t *testing.T) { + action := NewPdfActionGoTo() + require.IsType(t, &PdfActionGoTo{}, action) + require.IsType(t, &PdfAction{}, action.PdfAction) + require.Equal(t, action, action.PdfAction.context) +} + +func TestNewPdfActionGoToR(t *testing.T) { + action := NewPdfActionGoToR() + require.IsType(t, &PdfActionGoToR{}, action) + require.IsType(t, &PdfAction{}, action.PdfAction) + require.Equal(t, action, action.PdfAction.context) +} + +func TestNewPdfActionGoToE(t *testing.T) { + action := NewPdfActionGoToE() + require.IsType(t, &PdfActionGoToE{}, action) + require.IsType(t, &PdfAction{}, action.PdfAction) + require.Equal(t, action, action.PdfAction.context) +} + +func TestNewPdfActionLaunch(t *testing.T) { + action := NewPdfActionLaunch() + require.IsType(t, &PdfActionLaunch{}, action) + require.IsType(t, &PdfAction{}, action.PdfAction) + require.Equal(t, action, action.PdfAction.context) +} + +func TestNewPdfActionThread(t *testing.T) { + action := NewPdfActionThread() + require.IsType(t, &PdfActionThread{}, action) + require.IsType(t, &PdfAction{}, action.PdfAction) + require.Equal(t, action, action.PdfAction.context) +} + +func TestNewPdfActionURI(t *testing.T) { + action := NewPdfActionURI() + require.IsType(t, &PdfActionURI{}, action) + require.IsType(t, &PdfAction{}, action.PdfAction) + require.Equal(t, action, action.PdfAction.context) +} + +func TestNewPdfActionSound(t *testing.T) { + action := NewPdfActionSound() + require.IsType(t, &PdfActionSound{}, action) + require.IsType(t, &PdfAction{}, action.PdfAction) + require.Equal(t, action, action.PdfAction.context) +} + +func TestNewPdfActionMovie(t *testing.T) { + action := NewPdfActionMovie() + require.IsType(t, &PdfActionMovie{}, action) + require.IsType(t, &PdfAction{}, action.PdfAction) + require.Equal(t, action, action.PdfAction.context) +} + +func TestNewPdfActionHide(t *testing.T) { + action := NewPdfActionHide() + require.IsType(t, &PdfActionHide{}, action) + require.IsType(t, &PdfAction{}, action.PdfAction) + require.Equal(t, action, action.PdfAction.context) +} + +func TestNewPdfActionNamed(t *testing.T) { + action := NewPdfActionNamed() + require.IsType(t, &PdfActionNamed{}, action) + require.IsType(t, &PdfAction{}, action.PdfAction) + require.Equal(t, action, action.PdfAction.context) +} + +func TestNewPdfActionSubmitForm(t *testing.T) { + action := NewPdfActionSubmitForm() + require.IsType(t, &PdfActionSubmitForm{}, action) + require.IsType(t, &PdfAction{}, action.PdfAction) + require.Equal(t, action, action.PdfAction.context) +} + +func TestNewPdfActionResetForm(t *testing.T) { + action := NewPdfActionResetForm() + require.IsType(t, &PdfActionResetForm{}, action) + require.IsType(t, &PdfAction{}, action.PdfAction) + require.Equal(t, action, action.PdfAction.context) +} + +func TestNewPdfActionImportData(t *testing.T) { + action := NewPdfActionImportData() + require.IsType(t, &PdfActionImportData{}, action) + require.IsType(t, &PdfAction{}, action.PdfAction) + require.Equal(t, action, action.PdfAction.context) +} + +func TestNewPdfActionSetOCGState(t *testing.T) { + action := NewPdfActionSetOCGState() + require.IsType(t, &PdfActionSetOCGState{}, action) + require.IsType(t, &PdfAction{}, action.PdfAction) + require.Equal(t, action, action.PdfAction.context) +} + +func TestNewPdfActionRendition(t *testing.T) { + action := NewPdfActionRendition() + require.IsType(t, &PdfActionRendition{}, action) + require.IsType(t, &PdfAction{}, action.PdfAction) + require.Equal(t, action, action.PdfAction.context) +} + +func TestNewPdfActionTrans(t *testing.T) { + action := NewPdfActionTrans() + require.IsType(t, &PdfActionTrans{}, action) + require.IsType(t, &PdfAction{}, action.PdfAction) + require.Equal(t, action, action.PdfAction.context) +} + +func TestNewPdfActionGoTo3DView(t *testing.T) { + action := NewPdfActionGoTo3DView() + require.IsType(t, &PdfActionGoTo3DView{}, action) + require.IsType(t, &PdfAction{}, action.PdfAction) + require.Equal(t, action, action.PdfAction.context) +} + +func TestNewPdfActionJavaScript(t *testing.T) { + action := NewPdfActionJavaScript() + require.IsType(t, &PdfActionJavaScript{}, action) + require.IsType(t, &PdfAction{}, action.PdfAction) + require.Equal(t, action, action.PdfAction.context) +} diff --git a/model/annotations.go b/model/annotations.go index e0ac6ef6..4a0fd6bb 100644 --- a/model/annotations.go +++ b/model/annotations.go @@ -97,6 +97,38 @@ type PdfAnnotationLink struct { PA core.PdfObject QuadPoints core.PdfObject BS core.PdfObject + + action *PdfAction + reader *PdfReader +} + +// GetAction returns the PDF action for the annotation link. +func (a *PdfAnnotationLink) GetAction() (*PdfAction, error) { + if a.action != nil { + return a.action, nil + } + if a.A == nil { + return nil, nil + } + if a.reader == nil { + return nil, nil + } + + action, err := a.reader.loadAction(a.A) + if err != nil { + return nil, err + } + a.action = action + + return a.action, nil +} + +// SetAction sets the PDF action for the annotation link. +func (a *PdfAnnotationLink) SetAction(action *PdfAction) { + a.action = action + if action == nil { + a.A = nil + } } // PdfAnnotationFreeText represents FreeText annotations. @@ -1054,6 +1086,19 @@ func (r *PdfReader) newPdfAnnotationLinkFromDict(d *core.PdfObjectDictionary) (* return &annot, nil } +func (r *PdfReader) loadAction(obj core.PdfObject) (*PdfAction, error) { + if indObj, isIndirect := core.GetIndirect(obj); isIndirect { + actionObj, err := r.newPdfActionFromIndirectObject(indObj) + if err != nil { + return nil, err + } + return actionObj, nil + } else if !core.IsNullObject(obj) { + return nil, errors.New("action should point to an indirect object") + } + return nil, nil +} + func (r *PdfReader) newPdfAnnotationFreeTextFromDict(d *core.PdfObjectDictionary) (*PdfAnnotationFreeText, error) { annot := PdfAnnotationFreeText{} @@ -1497,7 +1542,13 @@ func (link *PdfAnnotationLink) ToPdfObject() core.PdfObject { d := container.PdfObject.(*core.PdfObjectDictionary) d.SetIfNotNil("Subtype", core.MakeName("Link")) - d.SetIfNotNil("A", link.A) + + if link.action != nil && link.action.context != nil { + d.Set("A", link.action.context.ToPdfObject()) + } else if link.A != nil { + d.Set("A", link.A) + } + d.SetIfNotNil("Dest", link.Dest) d.SetIfNotNil("H", link.H) d.SetIfNotNil("PA", link.PA) diff --git a/model/file.go b/model/file.go new file mode 100644 index 00000000..229fc06b --- /dev/null +++ b/model/file.go @@ -0,0 +1,178 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package model + +import ( + "errors" + + "github.com/unidoc/unipdf/v3/common" + "github.com/unidoc/unipdf/v3/core" +) + +// (Section 7.11.3 p. 102). +// See Table 44 - Entries in a file specification dictionary + +/* + * A PDF file can refer to the contents of another file by using a file specification (PDF 1.1), which shall take either + * of two forms: + * • A simple file specification shall give just the name of the target file in a standard format, independent of the + * naming conventions of any particular file system. It shall take the form of either a string or a dictionary + * • A full file specification shall include information related to one or more specific file systems. It shall only be + * represented as a dictionary. + * + * A file specification shall refer to a file external to the PDF file or to a file embedded within the referring PDF file, + * allowing its contents to be stored or transmitted along with the PDF file. The file shall be considered to be + * external to the PDF file in either case. + * A file specification could describe a URL-based file system and will follow the rules of Internet RFC 1808, Relative Uniform Resource Locators + */ + +// PdfFilespec represents a file specification which can either refer to an external or embedded file. +type PdfFilespec struct { + Type core.PdfObject + FS core.PdfObject + F core.PdfObject // A file specification string + UF core.PdfObject // A Unicode text string that provides file specification + DOS core.PdfObject // A file specification string representing a DOS file name. OBSOLETE + Mac core.PdfObject // A file specification string representing a Mac OS file name. OBSOLETE + Unix core.PdfObject // A file specification string representing a UNIX file name. OBSOLETE + ID core.PdfObject // An array of two byte strings constituting a file identifier + V core.PdfObject // A flag indicating whether the file referenced by the file specification is volatile (changes frequently with time). + EF core.PdfObject // A dictionary containing a subset of the keys F, UF, DOS, Mac, and Unix, corresponding to the entries by those names in the file specification dictionary + RF core.PdfObject + Desc core.PdfObject // Descriptive text associated with the file specification + CI core.PdfObject // A collection item dictionary, which shall be used to create the user interface for portable collections + + container core.PdfObject +} + +// GetContainingPdfObject implements interface PdfModel. +func (f *PdfFilespec) GetContainingPdfObject() core.PdfObject { + return f.container +} + +func (f *PdfFilespec) getDict() *core.PdfObjectDictionary { + if indObj, is := f.container.(*core.PdfIndirectObject); is { + dict, ok := indObj.PdfObject.(*core.PdfObjectDictionary) + if !ok { + return nil + } + return dict + } else if dictObj, isDict := f.container.(*core.PdfObjectDictionary); isDict { + return dictObj + } else { + common.Log.Debug("Trying to access Filespec dictionary of invalid object type (%T)", f.container) + return nil + } +} + +// ToPdfObject implements interface PdfModel. +func (f *PdfFilespec) ToPdfObject() core.PdfObject { + d := f.getDict() + + d.Clear() + + d.Set("Type", core.MakeName("Filespec")) + d.SetIfNotNil("FS", f.FS) + d.SetIfNotNil("F", f.F) + d.SetIfNotNil("UF", f.UF) + d.SetIfNotNil("DOS", f.DOS) + d.SetIfNotNil("Mac", f.Mac) + d.SetIfNotNil("Unix", f.Unix) + d.SetIfNotNil("ID", f.ID) + d.SetIfNotNil("V", f.V) + d.SetIfNotNil("EF", f.EF) + d.SetIfNotNil("RF", f.RF) + d.SetIfNotNil("Desc", f.Desc) + d.SetIfNotNil("CI", f.CI) + + return f.container +} + +// NewPdfFilespecFromObj creates and returns a new PdfFilespec object. +func NewPdfFilespecFromObj(obj core.PdfObject) (*PdfFilespec, error) { + fs := &PdfFilespec{} + + var dict *core.PdfObjectDictionary + + if indObj, isInd := core.GetIndirect(obj); isInd { + fs.container = indObj + + d, ok := core.GetDict(indObj.PdfObject) + if !ok { + common.Log.Debug("Object not a dictionary type") + return nil, core.ErrTypeError + } + dict = d + } else if d, isDict := core.GetDict(obj); isDict { + fs.container = d + dict = d + } else { + common.Log.Debug("Object type unexpected (%T)", obj) + return nil, core.ErrTypeError + } + + if dict == nil { + common.Log.Debug("Dictionary missing") + return nil, errors.New("dict missing") + } + + if obj := dict.Get("Type"); obj != nil { + str, ok := obj.(*core.PdfObjectName) + if !ok { + common.Log.Trace("Incompatibility! Invalid type of Type (%T) - should be Name", obj) + } else { + if *str != "Filespec" { + // Log a debug message. + // Not returning an error on this. + common.Log.Trace("Unsuspected Type != Filespec (%s)", *str) + } + } + } + if obj := dict.Get("FS"); obj != nil { + fs.FS = obj + } + if obj := dict.Get("F"); obj != nil { + fs.F = obj + } + if obj := dict.Get("UF"); obj != nil { + fs.UF = obj + } + if obj := dict.Get("DOS"); obj != nil { + fs.DOS = obj + } + if obj := dict.Get("Mac"); obj != nil { + fs.Mac = obj + } + if obj := dict.Get("Unix"); obj != nil { + fs.Unix = obj + } + if obj := dict.Get("ID"); obj != nil { + fs.ID = obj + } + if obj := dict.Get("V"); obj != nil { + fs.V = obj + } + if obj := dict.Get("EF"); obj != nil { + fs.EF = obj + } + if obj := dict.Get("RF"); obj != nil { + fs.RF = obj + } + if obj := dict.Get("Desc"); obj != nil { + fs.Desc = obj + } + if obj := dict.Get("CI"); obj != nil { + fs.CI = obj + } + return fs, nil +} + +// NewPdfFilespec returns an initialized generic PDF filespec model. +func NewPdfFilespec() *PdfFilespec { + action := &PdfFilespec{} + action.container = core.MakeIndirectObject(core.MakeDict()) + return action +} diff --git a/model/file_test.go b/model/file_test.go new file mode 100644 index 00000000..1387c253 --- /dev/null +++ b/model/file_test.go @@ -0,0 +1,98 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package model + +import ( + "github.com/stretchr/testify/require" + "github.com/unidoc/unipdf/v3/core" + "strings" + "testing" +) + +func TestUrlFileSpec(t *testing.T) { + rawText := ` +1 0 obj +<> +endobj +` + + r := NewReaderForText(rawText) + + err := r.ParseIndObjSeries() + require.NoError(t, err) + + // Load the field from object number 1. + obj, err := r.parser.LookupByNumber(1) + require.NoError(t, err) + + ind, ok := obj.(*core.PdfIndirectObject) + require.True(t, ok) + + fileSpec, err := NewPdfFilespecFromObj(ind) + require.NoError(t, err) + + require.Equal(t, "URL", fileSpec.FS.String()) + require.Equal(t, "ftp://www.beatles.com/Movies/AbbeyRoad.mov", fileSpec.F.String()) + + outDict, ok := core.GetDict(fileSpec.ToPdfObject()) + if !ok { + t.Fatalf("error") + } + + contains := strings.Contains( + strings.Replace(rawText, "\n", "", -1), + outDict.WriteString()) + require.True(t, contains, "generated output doesn't match the expected output") +} + +func TestPdfFileSpec(t *testing.T) { + rawText := ` +1 0 obj +<> +endobj +` + + r := NewReaderForText(rawText) + + err := r.ParseIndObjSeries() + require.NoError(t, err) + + // Load the field from object number 1. + obj, err := r.parser.LookupByNumber(1) + require.NoError(t, err) + + ind, ok := obj.(*core.PdfIndirectObject) + require.True(t, ok) + + fileSpec, err := NewPdfFilespecFromObj(ind) + require.NoError(t, err) + + require.Equal(t, "VideoIssue1.mov", fileSpec.F.String()) + require.Equal(t, "VideoIssue2.mov", fileSpec.UF.String()) + require.Equal(t, "VIDEOISSUE.MOV", fileSpec.DOS.String()) + require.Equal(t, "VideoIssue3.mov", fileSpec.Mac.String()) + require.Equal(t, "VideoIssue4.mov", fileSpec.Unix.String()) + + outDict, ok := core.GetDict(fileSpec.ToPdfObject()) + if !ok { + t.Fatalf("error") + } + + contains := strings.Contains( + strings.Replace(rawText, "\n", "", -1), + outDict.WriteString()) + + require.True(t, contains, "generated output doesn't match the expected output") +}