diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b0ce7e0ef..eb6b1ba7b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,7 @@ jobs: goarch: amd64 go: - '1.20.x' + - '1.21.x' runs-on: ubuntu-latest steps: diff --git a/.goreleaser.yml b/.goreleaser.yml index f84483d35..f1c5f70f3 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -5,10 +5,16 @@ builds: ldflags: - '-s -w -X main.version={{.Version}} -X github.com/pdfcpu/pdfcpu/pkg/pdfcpu.VersionStr={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{.Date}} -X main.builtBy=goreleaser' goos: + - ios - js - linux - darwin - windows + goarch: + - "386" + - arm64 + - wasm + - amd64 dist: ./dist archives: - @@ -16,12 +22,16 @@ archives: format_overrides: - goos: windows format: zip - replacements: - darwin: macOS - linux: Linux - windows: Windows - 386: i386 - amd64: x86_64 + name_template: >- + {{- .ProjectName }}_ + {{- .Version }}_ + {{- title .Os }}_ + {{- if eq .Arch "linux" }}Linux + {{- else if eq .Arch "windows" }}Windows + {{- else if eq .Arch "386" }}i386 + {{- else if eq .Arch "amd64" }}x86_64 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end -}} wrap_in_directory: true checksum: name_template: 'checksums.txt' diff --git a/README.md b/README.md index cfac0c6f8..c6300b500 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ The main focus lies on strong support for batch processing and scripting via a r * [annotations](https://pdfcpu.io/annot/annot) * [attachments](https://pdfcpu.io/attach/attach) * [booklet](https://pdfcpu.io/generate/booklet) +* [bookmarks](https://pdfcpu.io/bookmarks/bookmarks) * [boxes](https://pdfcpu.io/boxes/boxes) * [change owner password](https://pdfcpu.io/encrypt/change_opw) * [change user password](https://pdfcpu.io/encrypt/change_upw) @@ -115,30 +116,30 @@ Get the latest binary [here](https://github.com/pdfcpu/pdfcpu/releases). ### Using Go Modules ``` -git clone https://github.com/pdfcpu/pdfcpu -cd pdfcpu/cmd/pdfcpu -go install -pdfcpu version +$ git clone https://github.com/pdfcpu/pdfcpu +$ cd pdfcpu/cmd/pdfcpu +$ go install +$ pdfcpu version ``` ### Using Homebrew (macOS) ``` -brew install pdfcpu -pdfcpu version +$ brew install pdfcpu +$ pdfcpu version ``` ### Using DNF/YUM (Fedora) ``` -sudo dnf install golang-github-pdfcpu -pdfcpu version +$ sudo dnf install golang-github-pdfcpu +$ pdfcpu version ``` ### Run in a Docker container ``` -docker build -t pdfcpu . +$ docker build -t pdfcpu . # mount current folder into container to process local files -docker run -it --mount type=bind,source="$(pwd)",target=/app pdfcpu ./pdfcpu validate -mode strict /app/pdfs/a.pdf +$ docker run -it --mount type=bind,source="$(pwd)",target=/app pdfcpu ./pdfcpu validate -mode strict /app/pdfs/a.pdf ``` ## Contributing @@ -166,13 +167,13 @@ For the majority of the cases this is due to a diverse pool of PDF Writers out t Regardless of the pdfcpu operation, please start using the pdfcpu command line to validate your file: ``` sh -pdfcpu validate -v &> crash.log +$ pdfcpu validate -v &> crash.log ``` or to produce very verbose output ``` sh - pdfcpu validate -vv &> crash.log + $ pdfcpu validate -vv &> crash.log ``` will produce what's needed to investigate a crash. Then open an issue and post `crash.log` or its contents. Ideally post a test PDF you can share to reproduce this. You can also email to hhrutter@gmail.com or if you prefer Slack you can get in touch on the Gopher slack #pdfcpu channel. diff --git a/cmd/pdfcpu/init.go b/cmd/pdfcpu/init.go index e42d6bfff..0f33143d8 100644 --- a/cmd/pdfcpu/init.go +++ b/cmd/pdfcpu/init.go @@ -41,6 +41,16 @@ func initCommandMap() { attachCmdMap.register(k, v) } + bookmarksCmdMap := newCommandMap() + for k, v := range map[string]command{ + "list": {processListBookmarksCommand, nil, "", ""}, + "import": {processImportBookmarksCommand, nil, "", ""}, + "export": {processExportBookmarksCommand, nil, "", ""}, + "remove": {processRemoveBookmarksCommand, nil, "", ""}, + } { + bookmarksCmdMap.register(k, v) + } + boxesCmdMap := newCommandMap() for k, v := range map[string]command{ "list": {processListBoxesCommand, nil, "", ""}, @@ -147,6 +157,7 @@ func initCommandMap() { for k, v := range map[string]command{ "annotations": {nil, annotsCmdMap, usageAnnots, usageLongAnnots}, "attachments": {nil, attachCmdMap, usageAttach, usageLongAttach}, + "bookmarks": {nil, bookmarksCmdMap, usageBookmarks, usageLongBookmarks}, "booklet": {processBookletCommand, nil, usageBooklet, usageLongBooklet}, "boxes": {nil, boxesCmdMap, usageBoxes, usageLongBoxes}, "changeopw": {processChangeOwnerPasswordCommand, nil, usageChangeOwnerPW, usageLongChangeOwnerPW}, @@ -193,54 +204,63 @@ func initCommandMap() { } func initFlags() { - statsUsage := "optimize: create a csv file for stats" - flag.StringVar(&fileStats, "stats", "", statsUsage) - modeUsage := "validate: strict|relaxed; extract: image|font|content|page|meta; encrypt: rc4|aes, stamp:text|image/pdf" - flag.StringVar(&mode, "mode", "", modeUsage) - flag.StringVar(&mode, "m", "", modeUsage) + bookmarksUsage := "create bookmarks while merging" + flag.BoolVar(&bookmarks, "bookmarks", true, bookmarksUsage) + flag.BoolVar(&bookmarks, "b", true, bookmarksUsage) + + confUsage := "the config directory path | skip | none" + flag.StringVar(&conf, "config", "", confUsage) + flag.StringVar(&conf, "conf", "", confUsage) + flag.StringVar(&conf, "c", "", confUsage) + + jsonUsage := "produce JSON output" + flag.BoolVar(&json, "json", false, jsonUsage) + flag.BoolVar(&json, "j", false, jsonUsage) keyUsage := "encrypt: 40|128|256" flag.StringVar(&key, "key", "256", keyUsage) flag.StringVar(&key, "k", "256", keyUsage) - permUsage := "encrypt, perm set: none|all" - flag.StringVar(&perm, "perm", "none", permUsage) + linksUsage := "check for broken links" + flag.BoolVar(&links, "links", false, linksUsage) + flag.BoolVar(&links, "l", false, linksUsage) - unitUsage := "info: po|in|cm|mm" - flag.StringVar(&unit, "unit", "", unitUsage) - flag.StringVar(&unit, "u", "", unitUsage) + modeUsage := "validate: strict|relaxed; extract: image|font|content|page|meta; encrypt: rc4|aes, stamp:text|image/pdf" + flag.StringVar(&mode, "mode", "", modeUsage) + flag.StringVar(&mode, "m", "", modeUsage) selectedPagesUsage := "a comma separated list of pages or page ranges, see pdfcpu selectedpages" flag.StringVar(&selectedPages, "pages", "", selectedPagesUsage) flag.StringVar(&selectedPages, "p", "", selectedPagesUsage) + permUsage := "encrypt, perm set: none|all" + flag.StringVar(&perm, "perm", "none", permUsage) + flag.BoolVar(&quiet, "quiet", false, "") flag.BoolVar(&quiet, "q", false, "") + replaceUsage := "replace existing bookmarks" + flag.BoolVar(&replaceBookmarks, "replace", false, replaceUsage) + flag.BoolVar(&replaceBookmarks, "r", false, replaceUsage) + sortUsage := "sort files before merging" flag.BoolVar(&sorted, "sort", false, sortUsage) flag.BoolVar(&sorted, "s", false, sortUsage) - bookmarksUsage := "create bookmarks while merging" - flag.BoolVar(&bookmarks, "bookmarks", true, bookmarksUsage) - flag.BoolVar(&bookmarks, "b", true, bookmarksUsage) + statsUsage := "optimize: create a csv file for stats" + flag.StringVar(&fileStats, "stats", "", statsUsage) + + unitUsage := "info: po|in|cm|mm" + flag.StringVar(&unit, "unit", "", unitUsage) + flag.StringVar(&unit, "u", "", unitUsage) flag.BoolVar(&verbose, "verbose", false, "") flag.BoolVar(&verbose, "v", false, "") flag.BoolVar(&veryVerbose, "vv", false, "") - linksUsage := "check for broken links" - flag.BoolVar(&links, "links", false, linksUsage) - flag.BoolVar(&links, "l", false, linksUsage) - flag.StringVar(&upw, "upw", "", "user password") flag.StringVar(&opw, "opw", "", "owner password") - - confUsage := "the config directory path | skip | none" - flag.StringVar(&conf, "config", "", confUsage) - flag.StringVar(&conf, "conf", "", confUsage) - flag.StringVar(&conf, "c", "", confUsage) } func initLogging(verbose, veryVerbose bool) { diff --git a/cmd/pdfcpu/main.go b/cmd/pdfcpu/main.go index 446381803..0078139e6 100644 --- a/cmd/pdfcpu/main.go +++ b/cmd/pdfcpu/main.go @@ -27,6 +27,7 @@ var ( upw, opw, key, perm, unit, conf string verbose, veryVerbose bool links, quiet, sorted, bookmarks bool + json, replaceBookmarks bool needStackTrace = true cmdMap commandMap ) diff --git a/cmd/pdfcpu/process.go b/cmd/pdfcpu/process.go index a1341e794..be9ef52bf 100644 --- a/cmd/pdfcpu/process.go +++ b/cmd/pdfcpu/process.go @@ -1337,7 +1337,7 @@ func processInfoCommand(conf *model.Configuration) { processDiplayUnit(conf) - process(cli.InfoCommand(filesIn, selectedPages, conf)) + process(cli.InfoCommand(filesIn, selectedPages, json, conf)) } func processListFontsCommand(conf *model.Configuration) { @@ -2239,3 +2239,80 @@ func processCutCommand(conf *model.Configuration) { process(cli.CutCommand(inFile, outDir, outFile, selectedPages, cut, conf)) } + +func processListBookmarksCommand(conf *model.Configuration) { + if len(flag.Args()) < 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageBookmarksList) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + process(cli.ListBookmarksCommand(inFile, conf)) +} + +func processExportBookmarksCommand(conf *model.Configuration) { + if len(flag.Args()) == 0 || len(flag.Args()) > 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageBookmarksExport) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFileJSON := "out.json" + if len(flag.Args()) == 2 { + outFileJSON = flag.Arg(1) + ensureJSONExtension(outFileJSON) + } + + process(cli.ExportBookmarksCommand(inFile, outFileJSON, conf)) +} + +func processImportBookmarksCommand(conf *model.Configuration) { + if len(flag.Args()) == 0 || len(flag.Args()) > 3 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageBookmarksImport) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + inFileJSON := flag.Arg(1) + ensureJSONExtension(inFileJSON) + + outFile := "" + if len(flag.Args()) == 3 { + outFile = flag.Arg(2) + ensurePDFExtension(outFile) + } + + process(cli.ImportBookmarksCommand(inFile, inFileJSON, outFile, replaceBookmarks, conf)) +} + +func processRemoveBookmarksCommand(conf *model.Configuration) { + if len(flag.Args()) == 0 || len(flag.Args()) > 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n\n", usageBookmarksExport) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + outFile := "" + if len(flag.Args()) == 2 { + outFile = flag.Arg(1) + ensurePDFExtension(outFile) + } + + process(cli.RemoveBookmarksCommand(inFile, outFile, conf)) +} diff --git a/cmd/pdfcpu/usage.go b/cmd/pdfcpu/usage.go index 36f42007a..041a4fad4 100644 --- a/cmd/pdfcpu/usage.go +++ b/cmd/pdfcpu/usage.go @@ -28,6 +28,7 @@ The commands are: annotations list, remove page annotations attachments list, add, remove, extract embedded file attachments booklet arrange pages onto larger sheets of paper to make a booklet or zine + bookmarks list, import, export, remove bookmarks boxes list, add, remove page boundaries for selected pages changeopw change owner password changeupw change user password @@ -102,14 +103,14 @@ relaxed ... (default) like strict but doesn't complain about common seen spec vi stats ... appends a stats line to a csv file with information about the usage of root and page entries. useful for batch optimization and debugging PDFs. - inFile ... input pdf file - outFile ... output pdf file` + inFile ... input PDF file + outFile ... output PDF file` usageSplit = "usage: pdfcpu split [-m(ode) span|bookmark] inFile outDir [span]" + generalFlags usageLongSplit = `Generate a set of PDFs for the input file in outDir according to given span value or along bookmarks. mode ... split mode (defaults to span) - inFile ... input pdf file + inFile ... input PDF file outDir ... output directory span ... split span in pages (default: 1) for mode "span" @@ -128,8 +129,8 @@ The split modes are: mode ... merge mode (defaults to create) sort ... sort inFiles by file name bookmarks ... create bookmarks - outFile ... output pdf file - inFile ... a list of pdf files subject to concatenation. + outFile ... output PDF file + inFile ... a list of PDF files subject to concatenation. The merge modes are: @@ -165,7 +166,7 @@ Skip bookmark creation like so: -bookmarks=false` mode ... extraction mode pages ... Please refer to "pdfcpu selectedpages" - inFile ... input pdf file + inFile ... input PDF file outDir ... output directory The extraction modes are: @@ -182,8 +183,8 @@ content ... extract raw page content usageLongTrim = `Generate a trimmed version of inFile for selected pages. pages ... Please refer to "pdfcpu selectedpages" - inFile ... input pdf file - outFile ... output pdf file + inFile ... input PDF file + outFile ... output PDF file ` @@ -199,7 +200,7 @@ content ... extract raw page content usageLongAttach = `Manage embedded file attachments. - inFile ... input pdf file + inFile ... input PDF file file ... attachment outDir ... output directory @@ -218,7 +219,7 @@ content ... extract raw page content usageLongPortfolio = `Manage portfolio entries. - inFile ... input pdf file + inFile ... input PDF file file ... attachment desc ... description (optional) outDir ... output directory @@ -239,7 +240,7 @@ content ... extract raw page content usageLongPerm = `Manage user access permissions. perm ... user access permissions - inFile ... input pdf file` + inFile ... input PDF file` usageEncrypt = "usage: pdfcpu encrypt [-m(ode) rc4|aes] [-key 40|128|256] [-perm none|print|all] [-upw userpw] -opw ownerpw inFile [outFile]" + generalFlags usageLongEncrypt = `Setup password protection based on user and owner password. @@ -247,20 +248,20 @@ content ... extract raw page content mode ... algorithm (default=aes) key ... key length in bits (default=256) perm ... user access permissions - inFile ... input pdf file - outFile ... output pdf file` + inFile ... input PDF file + outFile ... output PDF file` usageDecrypt = "usage: pdfcpu decrypt [-upw userpw] [-opw ownerpw] inFile [outFile]" + generalFlags usageLongDecrypt = `Remove password protection and reset permissions. - inFile ... input pdf file - outFile ... output pdf file` + inFile ... input PDF file + outFile ... output PDF file` usageChangeUserPW = "usage: pdfcpu changeupw [-opw ownerpw] inFile upwOld upwNew" + generalFlags usageLongChangeUserPW = `Change the user password also known as the open doc password. opw ... owner password, required unless = "" - inFile ... input pdf file + inFile ... input PDF file upwOld ... old user password upwNew ... new user password` @@ -268,7 +269,7 @@ content ... extract raw page content usageLongChangeOwnerPW = `Change the owner password also known as the set permissions password. upw ... user password, required unless = "" - inFile ... input pdf file + inFile ... input PDF file opwOld ... old owner password (provide user password on initial changeopw) opwNew ... new owner password` @@ -310,7 +311,7 @@ content ... extract raw page content eg. pdfcpu watermark add -mode image -- "logo.png" "" in.pdf out.pdf 3) PDF based - -mode pdf pdfFileName[:page#] + -mode pdf PDFFileName[:page#] eg. pdfcpu watermark add -mode pdf -- "stamp.pdf:3" "" in.pdf out.pdf ... watermark each page of in.pdf with page 3 of stamp.pdf Omit page# for multistamping: eg. pdfcpu watermark add -mode pdf -- "stamp.pdf" "" in.pdf out.pdf ... watermark each page of in.pdf with corresponding page of stamp.pdf @@ -406,13 +407,13 @@ e.g. "pos:bl, off: 20 5" "rot:45" "op:0.5, sc:0.5 abs, rot:0" pages ... Please refer to "pdfcpu selectedpages" upw ... user password opw ... owner password - mode ... text, image, pdf + mode ... text, image, PDF string ... display string for text based watermarks - file ... image or pdf file + file ... image or PDF file description ... fontname, points, position, offset, scalefactor, aligntext, rotation, diagonal, opacity, rendermode, strokecolor, fillcolor, bgcolor, margins, border - inFile ... input pdf file - outFile ... output pdf file + inFile ... input PDF file + outFile ... output PDF file ` + usageStampMode + usageWMDescription @@ -427,13 +428,13 @@ description ... fontname, points, position, offset, scalefactor, aligntext, rota usageLongWatermark = `Process watermarking for selected pages. pages ... Please refer to "pdfcpu selectedpages" - mode ... text, image, pdf + mode ... text, image, PDF string ... display string for text based watermarks - file ... image or pdf file + file ... image or PDF file description ... fontname, points, position, offset, scalefactor, aligntext, rotation, diagonal, opacity, rendermode, strokecolor, fillcolor, bgcolor, margins, border - inFile ... input pdf file - outFile ... output pdf file + inFile ... input PDF file + outFile ... output PDF file ` + usageWatermarkMode + usageWMDescription @@ -444,7 +445,7 @@ Each imageFile will be rendered to a separate page. In its simplest form this converts an image into a PDF: "pdfcpu import img.pdf img.jpg" description ... dimensions, format, position, offset, scale factor, boxes - outFile ... output pdf file + outFile ... output PDF file imageFile ... a list of image files is a comma separated configuration string containing: @@ -500,8 +501,8 @@ description ... dimensions, format, position, offset, scale factor, boxes pages ... Please refer to "pdfcpu selectedpages" mode ... before, after (default: before) - inFile ... input pdf file - outFile ... output pdf file + inFile ... input PDF file + outFile ... output PDF file ` @@ -509,9 +510,9 @@ description ... dimensions, format, position, offset, scale factor, boxes usageLongRotate = `Rotate selected pages by a multiple of 90 degrees. pages ... Please refer to "pdfcpu selectedpages" - inFile ... input pdf file + inFile ... input PDF file rotation ... a multiple of 90 degrees for clockwise rotation - outFile ... output pdf file + outFile ... output PDF file ` @@ -522,9 +523,9 @@ If the input is one imageFile a single page n-up PDF gets generated. pages ... inFile only, please refer to "pdfcpu selectedpages" description ... dimensions, format, orientation - outFile ... output pdf file + outFile ... output PDF file n ... the n-Up value (see below for details) - inFile ... input pdf file + inFile ... input PDF file imageFiles ... input image file(s) portrait landscape @@ -584,9 +585,9 @@ Examples: pdfcpu nup out.pdf 4 in.pdf pages ... for inFile only, please refer to "pdfcpu selectedpages" description ... dimensions, formsize, border, margin - outFile ... output pdf file + outFile ... output PDF file n ... booklet style (2 or 4) - inFile ... input pdf file + inFile ... input PDF file imageFiles ... input image file(s) There are two styles of booklet, depending on your page/input and sheet/output size: @@ -653,10 +654,10 @@ This command produces poster like PDF pages convenient for page and image browsi pages ... Please refer to "pdfcpu selectedpages" description ... dimensions, format, orientation - outFile ... output pdf file + outFile ... output PDF file m ... grid lines n ... grid columns - inFile ... input pdf file + inFile ... input PDF file imageFiles ... input image file(s) is a comma separated configuration string containing: @@ -772,11 +773,12 @@ Examples: pdfcpu grid out.pdf 1 10 in.pdf usageSelectedPages = "usage: pdfcpu selectedpages" usageLongSelectedPages = "Print definition of the -pages flag." - usageInfo = "usage: pdfcpu info [-p(ages) selectedPages] inFile..." + generalFlags + usageInfo = "usage: pdfcpu info [-p(ages) selectedPages] [-j(son)] inFile..." + generalFlags usageLongInfo = `Print info about a PDF file. pages ... Please refer to "pdfcpu selectedpages" - inFile ... a list of pdf input files` + json ... Produce JSON output + inFile ... a list of PDF input files` usageFontsList = "pdfcpu fonts list" usageFontsInstall = "pdfcpu fonts install fontFiles..." @@ -799,7 +801,7 @@ Create single page PDF cheat sheets in current dir.` usageLongKeywords = `Manage keywords. - inFile ... input pdf file + inFile ... input PDF file keyword ... search keyword Eg. adding two keywords: @@ -819,7 +821,7 @@ Create single page PDF cheat sheets in current dir.` usageLongProperties = `Manage document properties. - inFile ... input pdf file + inFile ... input PDF file nameValuePair ... 'name = value' name ... property name @@ -832,8 +834,8 @@ nameValuePair ... 'name = value' usageLongCollect = `Create custom sequence of selected pages. pages ... Please refer to "pdfcpu selectedpages" - inFile ... input pdf file - outFile ... output pdf file + inFile ... input PDF file + outFile ... output PDF file ` @@ -886,8 +888,8 @@ box: pages ... Please refer to "pdfcpu selectedpages" description ... crop box definition abs. or rel. to media box - inFile ... input pdf file - outFile ... output pdf file + inFile ... input PDF file + outFile ... output PDF file Examples: pdfcpu crop -- "[0 0 500 500]" in.pdf ... crop a 500x500 points region located in lower left corner @@ -908,8 +910,8 @@ Examples: boxTypes ... comma separated list of box types: m(edia), c(rop), t(rim), b(leed), a(rt) pages ... Please refer to "pdfcpu selectedpages" description ... box definitions abs. or rel. to parent box - inFile ... input pdf file - outFile ... output pdf file + inFile ... input PDF file + outFile ... output PDF file is a sequence of box definitions and assignments: @@ -936,7 +938,7 @@ Examples: usageLongAnnots = `Manage annotations. pages ... Please refer to "pdfcpu selectedpages" - inFile ... input pdf file + inFile ... input PDF file objNr ... obj# from "pdfcpu annotations list" annotId ... id from "pdfcpu annotations list" annotType ... Text, Link, FreeText, Line, Square, Circle, Polygon, PolyLine, HighLight, Underline, Squiggly, StrikeOut, Stamp, @@ -976,7 +978,7 @@ Examples: usageLongImages = `Manage keywords. pages ... Please refer to "pdfcpu selectedpages" - inFile ... input pdf file + inFile ... input PDF file Example: pdfcpu images list -p "1-5" gallery.pdf ` @@ -987,8 +989,8 @@ Append new page content to existing page content in inFile and write result to o If inFile is absent outFile will be overwritten. inFileJSON ... input json file - inFile ... optional input pdf file - outFile ... output pdf file + inFile ... optional input PDF file + outFile ... output PDF file A minimalistic sample json: { @@ -1034,14 +1036,16 @@ For more info on json syntax & samples please refer to : usageLongForm = `Manage PDF forms. - mode ... output mode (defaults to single) - inFile ... input pdf file - inFileData ... input CSV or JSON file - outDir ... output directory - outFile ... output pdf file - outName ... base output name - fieldID ... as indicated by "pdfcpu form list" - fieldName ... as indicated by "pdfcpu form list" + inFile ... input PDF file + inFileData ... input CSV or JSON file + inFileJSON ... input JSON file + outFile ... output PDF file + outFileJSON ... output JSON file + mode ... output mode (defaults to single) + outDir ... output directory + outName ... base output name + fieldID ... as indicated by "pdfcpu form list" + fieldName ... as indicated by "pdfcpu form list" The output modes are: @@ -1117,8 +1121,8 @@ Supported usecases: pages ... please refer to "pdfcpu selectedpages" description ... scalefactor, dimensions, formsize, enforce, border, bgcolor - inFile ... input pdf file - outFile ... output pdf file + inFile ... input PDF file + outFile ... output PDF file is a comma separated configuration string containing: @@ -1170,7 +1174,7 @@ description ... scalefactor, dimensions, formsize, enforce, border, bgcolor pages ... Please refer to "pdfcpu selectedpages" description ... formsize(=papersize), dimensions, scalefactor, margin, bgcolor, border - inFile ... input pdf file + inFile ... input PDF file outDir ... output directory outFileName ... output file name @@ -1217,7 +1221,7 @@ description ... scalefactor, dimensions, formsize, enforce, border, bgcolor pages ... Please refer to "pdfcpu selectedpages" description ... margin, bgcolor, border n ... the n-Down value (see below for details) - inFile ... input pdf file + inFile ... input PDF file outDir ... output directory outFileName ... output file name @@ -1261,7 +1265,7 @@ description ... scalefactor, dimensions, formsize, enforce, border, bgcolor pages ... Please refer to "pdfcpu selectedpages" description ... horizontal, vertical, margin, bgcolor, border - inFile ... input pdf file + inFile ... input PDF file outDir ... output directory outFileName ... output file name @@ -1300,4 +1304,22 @@ description ... scalefactor, dimensions, formsize, enforce, border, bgcolor Has the same effect as: pdfcpu ndown 4 in.pdf outDir See also the related commands: poster, ndown` + + usageBookmarksList = "pdfcpu bookmarks list inFile" + usageBookmarksImport = "pdfcpu bookmarks import [-r(eplace)] inFile inFileJSON [outFile]" + usageBookmarksExport = "pdfcpu bookmarks export inFile [outFileJSON]" + usageBookmarksRemove = "pdfcpu bookmarks remove inFile [outFile]" + + usageBookmarks = "usage: " + usageBookmarksList + + "\n " + usageBookmarksImport + + "\n " + usageBookmarksExport + + "\n " + usageBookmarksRemove + generalFlags + + usageLongBookmarks = `Manage bookmarks. + + inFile ... input PDF file + inFileJSON ... input JSON file + outFile ... output PDF file + outFileJSON ... output PDF file +` ) diff --git a/go.mod b/go.mod index 775ea188d..186b12f0a 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,11 @@ go 1.20 require ( github.com/hhrutter/lzw v1.0.0 - github.com/hhrutter/tiff v1.0.0 - github.com/mattn/go-runewidth v0.0.14 + github.com/hhrutter/tiff v1.0.1 + github.com/mattn/go-runewidth v0.0.15 github.com/pkg/errors v0.9.1 - golang.org/x/image v0.5.0 - golang.org/x/text v0.7.0 + golang.org/x/image v0.11.0 + golang.org/x/text v0.12.0 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 825210b50..015472cc3 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,12 @@ github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0= github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo= github.com/hhrutter/tiff v1.0.0 h1:T8/QVXiABO6Er7XCoExh4XPGyMO+X1ynf0V8kHui3t4= github.com/hhrutter/tiff v1.0.0/go.mod h1:zluYmeCkNexc8HFzfc2MTVwA8gcPuFQp/ngjvIQ0CFo= +github.com/hhrutter/tiff v1.0.1 h1:MIus8caHU5U6823gx7C6jrfoEvfSTGtEFRiM8/LOzC0= +github.com/hhrutter/tiff v1.0.1/go.mod h1:zU/dNgDm0cMIa8y8YwcYBeuEEveI4B0owqHyiPpJPHc= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -14,27 +18,37 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= +golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo= +golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/api/annotation.go b/pkg/api/annotation.go index 4543dc9e8..7317a90a2 100644 --- a/pkg/api/annotation.go +++ b/pkg/api/annotation.go @@ -27,48 +27,46 @@ import ( "github.com/pkg/errors" ) -// ListAnnotations returns a list of page annotations of rs. -func ListAnnotations(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) (int, []string, error) { +// Annotations returns page annotations of rs for selected pages. +func Annotations(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) (map[int]model.PgAnnots, error) { if rs == nil { - return 0, nil, errors.New("pdfcpu: ListAnnotations: Please provide rs") + return nil, errors.New("pdfcpu: Annotations: missing rs") } + if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.LISTANNOTATIONS } - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + conf.Cmd = model.LISTANNOTATIONS + + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { - return 0, nil, err + return nil, err } + if err := ctx.EnsurePageCount(); err != nil { - return 0, nil, err + return nil, err } - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, false) - if err != nil { - return 0, nil, err - } - - return pdfcpu.ListAnnotations(ctx, pages) -} -// ListAnnotationsFile returns a list of page annotations of inFile. -func ListAnnotationsFile(inFile string, selectedPages []string, conf *model.Configuration) (int, []string, error) { - f, err := os.Open(inFile) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { - return 0, nil, err + return nil, err } - defer f.Close() - return ListAnnotations(f, selectedPages, conf) + + return pdfcpu.AnnotationsForSelectedPages(ctx, pages), nil } // AddAnnotations adds annotations for selected pages in rs and writes the result to w. func AddAnnotations(rs io.ReadSeeker, w io.Writer, selectedPages []string, ann model.AnnotationRenderer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddAnnotations: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.ADDANNOTATIONS } + conf.Cmd = model.ADDANNOTATIONS - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return err } @@ -77,7 +75,7 @@ func AddAnnotations(rs io.ReadSeeker, w io.Writer, selectedPages []string, ann m return err } - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return err } @@ -87,7 +85,7 @@ func AddAnnotations(rs io.ReadSeeker, w io.Writer, selectedPages []string, ann m return err } if !ok { - return errors.New("no annotations added") + return errors.New("pdfcpu: AddAnnotations: No annotations added") } log.Stats.Printf("XRefTable:\n%s\n", ctx) @@ -103,10 +101,14 @@ func AddAnnotations(rs io.ReadSeeker, w io.Writer, selectedPages []string, ann m // AddAnnotationsAsIncrement adds annotations for selected pages in rws and writes out a PDF increment. func AddAnnotationsAsIncrement(rws io.ReadWriteSeeker, selectedPages []string, ar model.AnnotationRenderer, conf *model.Configuration) error { + if rws == nil { + return errors.New("pdfcpu: AddAnnotationsAsIncrement: missing rws") + } + if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.ADDANNOTATIONS } + conf.Cmd = model.ADDANNOTATIONS ctx, _, _, err := readAndValidate(rws, conf, time.Now()) if err != nil { @@ -114,14 +116,14 @@ func AddAnnotationsAsIncrement(rws io.ReadWriteSeeker, selectedPages []string, a } if *ctx.HeaderVersion < model.V14 { - return errors.New("Increment writing not supported for PDF version < V1.4 (Hint: Use pdfcpu optimize then try again)") + return errors.New("Incremental writing not supported for PDF version < V1.4 (Hint: Use pdfcpu optimize then try again)") } if err := ctx.EnsurePageCount(); err != nil { return err } - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return err } @@ -131,7 +133,7 @@ func AddAnnotationsAsIncrement(rws io.ReadWriteSeeker, selectedPages []string, a return err } if !ok { - return errors.New("no annotations added") + return errors.New("pdfcpu: AddAnnotationsAsIncrement: No annotations added") } log.Stats.Printf("XRefTable:\n%s\n", ctx) @@ -204,12 +206,16 @@ func AddAnnotationsFile(inFile, outFile string, selectedPages []string, ar model // AddAnnotationsMap adds annotations in m to corresponding pages of rs and writes the result to w. func AddAnnotationsMap(rs io.ReadSeeker, w io.Writer, m map[int][]model.AnnotationRenderer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddAnnotationsMap: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.ADDANNOTATIONS } + conf.Cmd = model.ADDANNOTATIONS - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return err } @@ -223,7 +229,7 @@ func AddAnnotationsMap(rs io.ReadSeeker, w io.Writer, m map[int][]model.Annotati return err } if !ok { - return errors.New("no annotations added") + return errors.New("pdfcpu: AddAnnotationsMap: No annotations added") } log.Stats.Printf("XRefTable:\n%s\n", ctx) @@ -239,11 +245,14 @@ func AddAnnotationsMap(rs io.ReadSeeker, w io.Writer, m map[int][]model.Annotati // AddAnnotationsMapAsIncrement adds annotations in m to corresponding pages of rws and writes out a PDF increment. func AddAnnotationsMapAsIncrement(rws io.ReadWriteSeeker, m map[int][]model.AnnotationRenderer, conf *model.Configuration) error { + if rws == nil { + return errors.New("pdfcpu: AddAnnotationsMapAsIncrement: missing rws") + } if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.ADDANNOTATIONS } + conf.Cmd = model.ADDANNOTATIONS ctx, _, _, err := readAndValidate(rws, conf, time.Now()) if err != nil { @@ -263,7 +272,7 @@ func AddAnnotationsMapAsIncrement(rws io.ReadWriteSeeker, m map[int][]model.Anno return err } if !ok { - return errors.New("no annotations added") + return errors.New("pdfcpu: AddAnnotationsMapAsIncrement: No annotations added") } log.Stats.Printf("XRefTable:\n%s\n", ctx) @@ -283,8 +292,8 @@ func AddAnnotationsMapAsIncrement(rws io.ReadWriteSeeker, m map[int][]model.Anno // AddAnnotationsMapFile adds annotations in m to corresponding pages of inFile and writes the result to outFile. func AddAnnotationsMapFile(inFile, outFile string, m map[int][]model.AnnotationRenderer, conf *model.Configuration, incr bool) (err error) { - tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { tmpFile = outFile log.CLI.Printf("writing %s...\n", outFile) @@ -337,12 +346,16 @@ func AddAnnotationsMapFile(inFile, outFile string, m map[int][]model.AnnotationR // RemoveAnnotations removes annotations for selected pages by id and object number // from a PDF context read from rs and writes the result to w. func RemoveAnnotations(rs io.ReadSeeker, w io.Writer, selectedPages, idsAndTypes []string, objNrs []int, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: RemoveAnnotations: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.REMOVEANNOTATIONS } + conf.Cmd = model.REMOVEANNOTATIONS - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return err } @@ -351,7 +364,7 @@ func RemoveAnnotations(rs io.ReadSeeker, w io.Writer, selectedPages, idsAndTypes return err } - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return err } @@ -361,7 +374,7 @@ func RemoveAnnotations(rs io.ReadSeeker, w io.Writer, selectedPages, idsAndTypes return err } if !ok { - return errors.New("no annotation removed") + return errors.New("pdfcpu: RemoveAnnotations: No annotation removed") } log.Stats.Printf("XRefTable:\n%s\n", ctx) @@ -378,10 +391,14 @@ func RemoveAnnotations(rs io.ReadSeeker, w io.Writer, selectedPages, idsAndTypes // RemoveAnnotationsAsIncrement removes annotations for selected pages by ids and object number // from a PDF context read from rs and writes out a PDF increment. func RemoveAnnotationsAsIncrement(rws io.ReadWriteSeeker, selectedPages, idsAndTypes []string, objNrs []int, conf *model.Configuration) error { + if rws == nil { + return errors.New("pdfcpu: RemoveAnnotationsAsIncrement: missing rws") + } + if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.REMOVEANNOTATIONS } + conf.Cmd = model.REMOVEANNOTATIONS ctx, _, _, err := readAndValidate(rws, conf, time.Now()) if err != nil { @@ -389,14 +406,14 @@ func RemoveAnnotationsAsIncrement(rws io.ReadWriteSeeker, selectedPages, idsAndT } if *ctx.HeaderVersion < model.V14 { - return errors.New("Increment writing unsupported for PDF version < V1.4 (Hint: Use pdfcpu optimize then try again)") + return errors.New("pdfcpu: Incremental writing unsupported for PDF version < V1.4 (Hint: Use pdfcpu optimize then try again)") } if err := ctx.EnsurePageCount(); err != nil { return err } - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return err } @@ -406,7 +423,7 @@ func RemoveAnnotationsAsIncrement(rws io.ReadWriteSeeker, selectedPages, idsAndT return err } if !ok { - return errors.New("no annotation removed") + return errors.New("pdfcpu: RemoveAnnotationsAsIncrement: No annotation removed") } log.Stats.Printf("XRefTable:\n%s\n", ctx) diff --git a/pkg/api/api.go b/pkg/api/api.go index 96bf4f741..08b426336 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -44,10 +44,14 @@ import ( "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/validate" + "github.com/pkg/errors" ) // ReadContext uses an io.ReadSeeker to build an internal structure holding its cross reference table aka the Context. func ReadContext(rs io.ReadSeeker, conf *model.Configuration) (*model.Context, error) { + if rs == nil { + return nil, errors.New("pdfcpu: ReadContext: missing rs") + } return pdfcpu.Read(rs, conf) } @@ -58,13 +62,16 @@ func ReadContextFile(inFile string) (*model.Context, error) { return nil, err } defer f.Close() + ctx, err := ReadContext(f, model.NewDefaultConfiguration()) if err != nil { return nil, err } + if err = validate.XRefTable(ctx.XRefTable); err != nil { return nil, err } + return ctx, err } @@ -129,7 +136,7 @@ func readAndValidate(rs io.ReadSeeker, conf *model.Configuration, from1 time.Tim return ctx, dur1, dur2, nil } -func readValidateAndOptimize(rs io.ReadSeeker, conf *model.Configuration, from1 time.Time) (ctx *model.Context, dur1, dur2, dur3 float64, err error) { +func ReadValidateAndOptimize(rs io.ReadSeeker, conf *model.Configuration, from1 time.Time) (ctx *model.Context, dur1, dur2, dur3 float64, err error) { ctx, dur1, dur2, err = readAndValidate(rs, conf, from1) if err != nil { return nil, 0, 0, 0, err diff --git a/pkg/api/attach.go b/pkg/api/attach.go index 0db23d212..de4d063c5 100644 --- a/pkg/api/attach.go +++ b/pkg/api/attach.go @@ -17,11 +17,9 @@ package api import ( - "fmt" "io" "os" "path/filepath" - "sort" "strings" "time" @@ -30,92 +28,43 @@ import ( "github.com/pkg/errors" ) -func listAttachments(rs io.ReadSeeker, conf *model.Configuration, withDesc, sorted bool) ([]string, error) { +func Attachments(rs io.ReadSeeker, conf *model.Configuration) ([]model.Attachment, error) { if rs == nil { - return nil, errors.New("pdfcpu: ListAttachments: Please provide rs") + return nil, errors.New("pdfcpu: Attachments: missing rs") } + if conf == nil { conf = model.NewDefaultConfiguration() } + conf.Cmd = model.LISTATTACHMENTS fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) - if err != nil { - return nil, err - } - - fromWrite := time.Now() - - aa, err := ctx.ListAttachments() - if err != nil { - return nil, err - } - - var ss []string - for _, a := range aa { - s := a.FileName - if withDesc && a.Desc != "" { - s = fmt.Sprintf("%s (%s)", s, a.Desc) - } - ss = append(ss, s) - } - if sorted { - sort.Strings(ss) - } - - durWrite := time.Since(fromWrite).Seconds() - durTotal := time.Since(fromStart).Seconds() - log.Stats.Printf("XRefTable:\n%s\n", ctx) - model.TimingStats("list files", durRead, durVal, durOpt, durWrite, durTotal) - - return ss, nil -} - -// ListAttachments returns a list of embedded file attachments of rs with optional description. -func ListAttachments(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { - return listAttachments(rs, conf, true, true) -} - -// ListAttachmentsFile returns a list of embedded file attachments of inFile with optional description. -func ListAttachmentsFile(inFile string, conf *model.Configuration) ([]string, error) { - f, err := os.Open(inFile) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return nil, err } - defer f.Close() - return ListAttachments(f, conf) -} -// ListAttachmentsCompact returns a list of embedded file attachments of rs w/o optional description. -func ListAttachmentsCompact(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { - return listAttachments(rs, conf, false, false) -} - -// ListAttachmentsCompactFile returns a list of embedded file attachments of inFile w/o optional description. -func ListAttachmentsCompactFile(inFile string, conf *model.Configuration) ([]string, error) { - f, err := os.Open(inFile) - if err != nil { - return nil, err - } - defer f.Close() - return ListAttachmentsCompact(f, conf) + return ctx.ListAttachments() } // AddAttachments embeds files into a PDF context read from rs and writes the result to w. // file is either a file name or a file name and a description separated by a comma. func AddAttachments(rs io.ReadSeeker, w io.Writer, files []string, coll bool, conf *model.Configuration) error { if rs == nil { - return errors.New("pdfcpu: AddAttachments: Please provide rs") + return errors.New("pdfcpu: AddAttachments: missing rs") } + if w == nil { - return errors.New("pdfcpu: AddAttachments: Please provide w") + return errors.New("pdfcpu: AddAttachments: missing w") } + if conf == nil { conf = model.NewDefaultConfiguration() } + conf.Cmd = model.ADDATTACHMENTS fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } @@ -156,7 +105,7 @@ func AddAttachments(rs io.ReadSeeker, w io.Writer, files []string, coll bool, co } if !ok { - return errors.New("no attachment added") + return errors.New("pdfcpu: AddAttachments: No attachment added") } durAdd := time.Since(from).Seconds() @@ -216,17 +165,20 @@ func AddAttachmentsFile(inFile, outFile string, files []string, coll bool, conf // RemoveAttachments deletes embedded files from a PDF context read from rs and writes the result to w. func RemoveAttachments(rs io.ReadSeeker, w io.Writer, files []string, conf *model.Configuration) error { if rs == nil { - return errors.New("pdfcpu: RemoveAttachments: Please provide rs") + return errors.New("pdfcpu: RemoveAttachments: missing rs") } + if w == nil { - return errors.New("pdfcpu: RemoveAttachments: Please provide w") + return errors.New("pdfcpu: RemoveAttachments: missing w") } + if conf == nil { conf = model.NewDefaultConfiguration() } + conf.Cmd = model.ADDATTACHMENTS fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } @@ -238,7 +190,7 @@ func RemoveAttachments(rs io.ReadSeeker, w io.Writer, files []string, conf *mode return err } if !ok { - return errors.New("no attachment removed") + return errors.New("pdfcpu: RemoveAttachments: No attachment removed") } durRemove := time.Since(from).Seconds() @@ -297,13 +249,15 @@ func RemoveAttachmentsFile(inFile, outFile string, files []string, conf *model.C // ExtractAttachmentsRaw extracts embedded files from a PDF context read from rs. func ExtractAttachmentsRaw(rs io.ReadSeeker, outDir string, fileNames []string, conf *model.Configuration) ([]model.Attachment, error) { if rs == nil { - return nil, errors.New("pdfcpu: ExtractAttachmentsRaw: Please provide rs") + return nil, errors.New("pdfcpu: ExtractAttachmentsRaw: missing rs") } + if conf == nil { conf = model.NewDefaultConfiguration() } + conf.Cmd = model.EXTRACTATTACHMENTS - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return nil, err } @@ -343,5 +297,6 @@ func ExtractAttachmentsFile(inFile, outDir string, files []string, conf *model.C return err } defer f.Close() + return ExtractAttachments(f, outDir, files, conf) } diff --git a/pkg/api/booklet.go b/pkg/api/booklet.go index ac7179ac6..e31766e67 100644 --- a/pkg/api/booklet.go +++ b/pkg/api/booklet.go @@ -25,6 +25,7 @@ import ( "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" ) // BookletFromImages creates a booklet from images. @@ -57,6 +58,10 @@ func BookletFromImages(conf *model.Configuration, imageFileNames []string, nup * // Booklet arranges PDF pages on larger sheets of paper and writes the result to w. func Booklet(rs io.ReadSeeker, w io.Writer, imgFiles, selectedPages []string, nup *model.NUp, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Booklet: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } @@ -85,7 +90,7 @@ func Booklet(rs io.ReadSeeker, w io.Writer, imgFiles, selectedPages []string, nu return err } - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return err } diff --git a/pkg/api/bookmark.go b/pkg/api/bookmark.go index 40062e4bf..2dfa3e54f 100644 --- a/pkg/api/bookmark.go +++ b/pkg/api/bookmark.go @@ -21,13 +21,190 @@ import ( "os" "time" + "github.com/pdfcpu/pdfcpu/pkg/log" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" "github.com/pkg/errors" ) +var ( + ErrNoOutlines = errors.New("pdfcpu: no outlines available") + ErrOutlines = errors.New("pdfcpu: existing outlines") +) + +// Bookmarks returns rs's bookmark hierarchy. +func Bookmarks(rs io.ReadSeeker, conf *model.Configuration) ([]pdfcpu.Bookmark, error) { + if rs == nil { + return nil, errors.New("pdfcpu: Bookmarks: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTBOOKMARKS + + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) + if err != nil { + return nil, err + } + return pdfcpu.Bookmarks(ctx) +} + +// ExportBookmarksJSON extracts outline data from rs (originating from source) and writes the result to w. +func ExportBookmarksJSON(rs io.ReadSeeker, w io.Writer, source string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ExportBookmarksJSON: missing rs") + } + + if w == nil { + return errors.New("pdfcpu: ExportBookmarksJSON: missing w") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXPORTBOOKMARKS + + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) + if err != nil { + return err + } + + if err := ctx.EnsurePageCount(); err != nil { + return err + } + + ok, err := pdfcpu.ExportBookmarksJSON(ctx, source, w) + if err != nil { + return err + } + if !ok { + return ErrNoOutlines + } + + return nil +} + +// ExportBookmarksFile extracts outline data from inFilePDF and writes the result to outFileJSON. +func ExportBookmarksFile(inFilePDF, outFileJSON string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFilePDF); err != nil { + return err + } + + if f2, err = os.Create(outFileJSON); err != nil { + f1.Close() + return err + } + log.CLI.Printf("writing %s...\n", outFileJSON) + + defer func() { + if err != nil { + f2.Close() + f1.Close() + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + }() + + return ExportBookmarksJSON(f1, f2, inFilePDF, conf) +} + +// ImportBookmarks creates/replaces outlines in rs and writes the result to w. +func ImportBookmarks(rs io.ReadSeeker, rd io.Reader, w io.Writer, replace bool, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ImportBookmarks: missing rs") + } + + if rd == nil { + return errors.New("pdfcpu: ImportBookmarks: missing rd") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.IMPORTBOOKMARKS + + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) + if err != nil { + return err + } + + if err := ctx.EnsurePageCount(); err != nil { + return err + } + + ok, err := pdfcpu.ImportBookmarks(ctx, rd, replace) + if err != nil { + return err + } + if !ok { + return ErrOutlines + } + + return WriteContext(ctx, w) +} + +// ImportBookmarks creates/replaces outlines in inFilePDF and writes the result to outFilePDF. +func ImportBookmarksFile(inFilePDF, inFileJSON, outFilePDF string, replace bool, conf *model.Configuration) (err error) { + + var f0, f1, f2 *os.File + + if f0, err = os.Open(inFilePDF); err != nil { + return err + } + + if f1, err = os.Open(inFileJSON); err != nil { + return err + } + + tmpFile := inFilePDF + ".tmp" + if outFilePDF != "" && inFilePDF != outFilePDF { + tmpFile = outFilePDF + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + if outFilePDF == "" || inFilePDF == outFilePDF { + os.Remove(tmpFile) + } + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFilePDF == "" || inFilePDF == outFilePDF { + err = os.Rename(tmpFile, inFilePDF) + } + }() + + return ImportBookmarks(f0, f1, f2, replace, conf) +} + // AddBookmarks adds a single bookmark outline layer to the PDF context read from rs and writes the result to w. func AddBookmarks(rs io.ReadSeeker, w io.Writer, bms []pdfcpu.Bookmark, replace bool, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddBookmarks: missing rs") + } if conf == nil { conf = model.NewDefaultConfiguration() @@ -37,36 +214,26 @@ func AddBookmarks(rs io.ReadSeeker, w io.Writer, bms []pdfcpu.Bookmark, replace conf.Cmd = model.ADDBOOKMARKS if len(bms) == 0 { - return errors.New("pdfcpu: AddBookmarks: Please supply m") + return errors.New("pdfcpu: AddBookmarks: missing bms") } - fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return err } - from := time.Now() - if err := pdfcpu.AddBookmarks(ctx, bms, replace); err != nil { return err } - durAdd := time.Since(from).Seconds() - fromWrite := time.Now() - if err = WriteContext(ctx, w); err != nil { return err } - durWrite := durAdd + time.Since(fromWrite).Seconds() - durTotal := time.Since(fromStart).Seconds() - logOperationStats(ctx, "add bookmarks, write", durRead, durVal, durOpt, durWrite, durTotal) - return nil } -// AddBookmarksFile adds a single bookmark outline layer to the PDF context read from inFile and writes the result to outFile. +// AddBookmarksFile adds outlines to the PDF context read from inFile and writes the result to outFile. func AddBookmarksFile(inFile, outFile string, bms []pdfcpu.Bookmark, replace bool, conf *model.Configuration) (err error) { var f1, f2 *os.File @@ -106,3 +273,72 @@ func AddBookmarksFile(inFile, outFile string, bms []pdfcpu.Bookmark, replace boo return AddBookmarks(f1, f2, bms, replace, conf) } + +// RemoveBookmarks deletes outlines from rs and writes the result to w. +func RemoveBookmarks(rs io.ReadSeeker, w io.Writer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddBookmarks: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.REMOVEBOOKMARKS + + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) + if err != nil { + return err + } + + ok, err := pdfcpu.RemoveBookmarks(ctx) + if err != nil { + return err + } + if !ok { + return ErrNoOutlines + } + + return WriteContext(ctx, w) +} + +// RemoveBookmarksFile deletes outlines from inFile and writes the result to outFile. +func RemoveBookmarksFile(inFile, outFile string, conf *model.Configuration) (err error) { + var f1, f2 *os.File + + if f1, err = os.Open(inFile); err != nil { + return err + } + + tmpFile := inFile + ".tmp" + if outFile != "" && inFile != outFile { + tmpFile = outFile + } + if f2, err = os.Create(tmpFile); err != nil { + f1.Close() + return err + } + + defer func() { + if err != nil { + f2.Close() + f1.Close() + if outFile == "" || inFile == outFile { + os.Remove(tmpFile) + } + return + } + if err = f2.Close(); err != nil { + return + } + if err = f1.Close(); err != nil { + return + } + if outFile == "" || inFile == outFile { + err = os.Rename(tmpFile, inFile) + } + }() + + return RemoveBookmarks(f1, f2, conf) +} diff --git a/pkg/api/box.go b/pkg/api/box.go index 6995b8dde..d75aae7f9 100644 --- a/pkg/api/box.go +++ b/pkg/api/box.go @@ -42,44 +42,34 @@ func Box(s string, u types.DisplayUnit) (*model.Box, error) { return model.ParseBox(s, u) } -// ListBoxes returns a list of page boundaries for selected pages of rs. -func ListBoxes(rs io.ReadSeeker, selectedPages []string, pb *model.PageBoundaries, conf *model.Configuration) ([]string, error) { +func Boxes(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) ([]model.PageBoundaries, error) { if rs == nil { - return nil, errors.New("pdfcpu: ListBoxes: missing rs") + return nil, errors.New("pdfcpu: Boxes: missing rs") } + if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.LISTBOXES } - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + conf.Cmd = model.LISTBOXES + + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return nil, err } + if err := ctx.EnsurePageCount(); err != nil { return nil, err } - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) - if err != nil { - return nil, err - } - - return ctx.ListPageBoundaries(pages, pb) -} -// ListBoxesFile returns a list of page boundaries for selected pages of inFile. -func ListBoxesFile(inFile string, selectedPages []string, pb *model.PageBoundaries, conf *model.Configuration) ([]string, error) { - f, err := os.Open(inFile) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return nil, err } - defer f.Close() - if pb == nil { - pb = &model.PageBoundaries{} - pb.SelectAll() - } - log.CLI.Printf("listing %s for %s\n", pb, inFile) - return ListBoxes(f, selectedPages, pb, conf) + //pb := &model.PageBoundaries{} + //pb.SelectAll() + + return ctx.PageBoundaries(pages) } // AddBoxes adds page boundaries for selected pages of rs and writes result to w. @@ -87,19 +77,22 @@ func AddBoxes(rs io.ReadSeeker, w io.Writer, selectedPages []string, pb *model.P if rs == nil { return errors.New("pdfcpu: AddBoxes: missing rs") } + if conf == nil { conf = model.NewDefaultConfiguration() } conf.Cmd = model.ADDBOXES - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return err } + if err := ctx.EnsurePageCount(); err != nil { return err } - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return err } @@ -171,19 +164,22 @@ func RemoveBoxes(rs io.ReadSeeker, w io.Writer, selectedPages []string, pb *mode if rs == nil { return errors.New("pdfcpu: RemoveBoxes: missing rs") } + if conf == nil { conf = model.NewDefaultConfiguration() } conf.Cmd = model.REMOVEBOXES - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return err } + if err := ctx.EnsurePageCount(); err != nil { return err } - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return err } @@ -255,19 +251,22 @@ func Crop(rs io.ReadSeeker, w io.Writer, selectedPages []string, b *model.Box, c if rs == nil { return errors.New("pdfcpu: Crop: missing rs") } + if conf == nil { conf = model.NewDefaultConfiguration() } conf.Cmd = model.CROP - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return err } + if err := ctx.EnsurePageCount(); err != nil { return err } - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return err } diff --git a/pkg/api/collect.go b/pkg/api/collect.go index fa3a9f288..3f5e3624c 100644 --- a/pkg/api/collect.go +++ b/pkg/api/collect.go @@ -24,17 +24,22 @@ import ( "github.com/pdfcpu/pdfcpu/pkg/log" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" ) // Collect creates a custom PDF page sequence for selected pages of rs and writes the result to w. func Collect(rs io.ReadSeeker, w io.Writer, selectedPages []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Collect: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } conf.Cmd = model.COLLECT fromStart := time.Now() - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } diff --git a/pkg/api/create.go b/pkg/api/create.go index a625e538d..9d02976fb 100644 --- a/pkg/api/create.go +++ b/pkg/api/create.go @@ -26,6 +26,7 @@ import ( "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/create" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" ) // CreatePDFFile creates a PDF file for an xRefTable and writes it to outFile. @@ -43,6 +44,10 @@ func CreatePDFFile(xRefTable *model.XRefTable, outFile string, conf *model.Confi // If rs is present, new PDF content will be appended including any empty pages needed. // rd is a JSON representation of PDF page content which may include form data. func Create(rs io.ReadSeeker, rd io.Reader, w io.Writer, conf *model.Configuration) error { + if rd == nil { + return errors.New("pdfcpu: Create: missing rd") + } + if conf == nil { conf = model.NewDefaultConfiguration() } @@ -54,7 +59,7 @@ func Create(rs io.ReadSeeker, rd io.Reader, w io.Writer, conf *model.Configurati ) if rs != nil { - ctx, _, _, _, err = readValidateAndOptimize(rs, conf, time.Now()) + ctx, _, _, _, err = ReadValidateAndOptimize(rs, conf, time.Now()) } else { ctx, err = pdfcpu.CreateContextWithXRefTable(conf, types.PaperSize["A4"]) } diff --git a/pkg/api/crypto.go b/pkg/api/crypto.go index d173e631d..109412b65 100644 --- a/pkg/api/crypto.go +++ b/pkg/api/crypto.go @@ -28,6 +28,10 @@ import ( // Encrypt reads a PDF stream from rs and writes the encrypted PDF stream to w. // A configuration containing at least the current passwords is required. func Encrypt(rs io.ReadSeeker, w io.Writer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Encrypt: missing rs") + } + if conf == nil { return errors.New("pdfcpu: missing configuration for encryption") } @@ -89,6 +93,10 @@ func EncryptFile(inFile, outFile string, conf *model.Configuration) (err error) // Decrypt reads a PDF stream from rs and writes the encrypted PDF stream to w. // A configuration containing at least the current passwords is required. func Decrypt(rs io.ReadSeeker, w io.Writer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Decrypt: missing rs") + } + if conf == nil { return errors.New("pdfcpu: missing configuration for decryption") } @@ -150,9 +158,14 @@ func DecryptFile(inFile, outFile string, conf *model.Configuration) (err error) // ChangeUserPassword reads a PDF stream from rs, changes the user password and writes the encrypted PDF stream to w. // A configuration containing the current passwords is required. func ChangeUserPassword(rs io.ReadSeeker, w io.Writer, pwOld, pwNew string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ChangeUserPassword: missing rs") + } + if conf == nil { return errors.New("pdfcpu: missing configuration for change user password") } + conf.Cmd = model.CHANGEUPW conf.UserPW = pwOld conf.UserPWNew = &pwNew @@ -166,6 +179,7 @@ func ChangeUserPasswordFile(inFile, outFile string, pwOld, pwNew string, conf *m if conf == nil { return errors.New("pdfcpu: missing configuration for change user password") } + conf.Cmd = model.CHANGEUPW conf.UserPW = pwOld conf.UserPWNew = &pwNew @@ -215,9 +229,14 @@ func ChangeUserPasswordFile(inFile, outFile string, pwOld, pwNew string, conf *m // ChangeOwnerPassword reads a PDF stream from rs, changes the owner password and writes the encrypted PDF stream to w. // A configuration containing the current passwords is required. func ChangeOwnerPassword(rs io.ReadSeeker, w io.Writer, pwOld, pwNew string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ChangeOwnerPassword: missing rs") + } + if conf == nil { return errors.New("pdfcpu: missing configuration for change owner password") } + conf.Cmd = model.CHANGEOPW conf.OwnerPW = pwOld conf.OwnerPWNew = &pwNew diff --git a/pkg/api/cut.go b/pkg/api/cut.go index a04307b6a..4a980a877 100644 --- a/pkg/api/cut.go +++ b/pkg/api/cut.go @@ -33,7 +33,7 @@ import ( ) func prepareForCut(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) (*model.Context, types.IntSet, error) { - ctxSrc, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + ctxSrc, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return nil, nil, err } @@ -42,7 +42,7 @@ func prepareForCut(rs io.ReadSeeker, selectedPages []string, conf *model.Configu return nil, nil, err } - pages, err := PagesForPageSelection(ctxSrc.PageCount, selectedPages, true) + pages, err := PagesForPageSelection(ctxSrc.PageCount, selectedPages, true, true) if err != nil { return nil, nil, err } @@ -51,6 +51,9 @@ func prepareForCut(rs io.ReadSeeker, selectedPages []string, conf *model.Configu } func Poster(rs io.ReadSeeker, outDir, fileName string, selectedPages []string, cut *model.Cut, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Poster: missing rs") + } if cut.PageSize == "" && !cut.UserDim { return errors.New("pdfcpu: poster - please supply either dimensions or form size ") @@ -63,6 +66,7 @@ func Poster(rs io.ReadSeeker, outDir, fileName string, selectedPages []string, c if rs == nil { return errors.New("pdfcpu poster: Please provide rs") } + if conf == nil { conf = model.NewDefaultConfiguration() } @@ -110,6 +114,7 @@ func PosterFile(inFile, outDir, outFile string, selectedPages []string, cut *mod return err } defer f.Close() + log.CLI.Printf("ndown %s into %s/ ...\n", inFile, outDir) if outFile == "" { @@ -120,10 +125,10 @@ func PosterFile(inFile, outDir, outFile string, selectedPages []string, cut *mod } func NDown(rs io.ReadSeeker, outDir, fileName string, selectedPages []string, n int, cut *model.Cut, conf *model.Configuration) error { - if rs == nil { - return errors.New("pdfcpu ndown: Please provide rs") + return errors.New("pdfcpu NDown: Please provide rs") } + if conf == nil { conf = model.NewDefaultConfiguration() } @@ -171,6 +176,7 @@ func NDownFile(inFile, outDir, outFile string, selectedPages []string, n int, cu return err } defer f.Close() + log.CLI.Printf("ndown %s into %s/ ...\n", inFile, outDir) if outFile == "" { @@ -182,6 +188,7 @@ func NDownFile(inFile, outDir, outFile string, selectedPages []string, n int, cu func validateCut(cut *model.Cut) error { sort.Float64s(cut.Hor) + for _, f := range cut.Hor { if f < 0 || f >= 1 { return errors.New("pdfcpu: Invalid cut points. Please consult pdfcpu help cut") @@ -205,6 +212,9 @@ func validateCut(cut *model.Cut) error { } func Cut(rs io.ReadSeeker, outDir, fileName string, selectedPages []string, cut *model.Cut, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Cut: missing rs") + } if len(cut.Hor) == 0 && len(cut.Vert) == 0 { return errors.New("pdfcpu: Invalid cut configuration string: missing hor/ver cutpoints. Please consult pdfcpu help cut") @@ -217,6 +227,7 @@ func Cut(rs io.ReadSeeker, outDir, fileName string, selectedPages []string, cut if rs == nil { return errors.New("pdfcpu cut: Please provide rs") } + if conf == nil { conf = model.NewDefaultConfiguration() } @@ -264,6 +275,7 @@ func CutFile(inFile, outDir, outFile string, selectedPages []string, cut *model. return err } defer f.Close() + log.CLI.Printf("cutting %s into %s/ ...\n", inFile, outDir) if outFile == "" { diff --git a/pkg/api/example_test.go b/pkg/api/example_test.go index dbe5a534d..dbe7b17a8 100644 --- a/pkg/api/example_test.go +++ b/pkg/api/example_test.go @@ -17,8 +17,6 @@ limitations under the License. package api import ( - "fmt" - "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" ) @@ -207,15 +205,6 @@ func ExampleNUpFile() { NUpFile(inFiles, "out.pdf", nil, nup, nil) } -func ExampleListPermissionsFile() { - - // Output the current permissions of in.pdf. - list, _ := ListPermissionsFile("in.pdf", nil) - for _, s := range list { - fmt.Println(s) - } -} - func ExampleSetPermissionsFile() { // Setting all permissions for the AES-256 encrypted in.pdf. @@ -257,15 +246,6 @@ func ExampleChangeOwnerPasswordFile() { ChangeOwnerPasswordFile("in.pdf", "", "opw", "opwNew", conf) } -func ExampleListAttachmentsFile() { - - // Output a list of attachments of in.pdf. - list, _ := ListAttachmentsFile("in.pdf", nil) - for _, s := range list { - fmt.Println(s) - } -} - func ExampleAddAttachmentsFile() { // Attach 3 files to in.pdf. diff --git a/pkg/api/extract.go b/pkg/api/extract.go index 24d4ab715..085e902b0 100644 --- a/pkg/api/extract.go +++ b/pkg/api/extract.go @@ -36,13 +36,15 @@ import ( // Beware of memory intensive returned slice. func ExtractImagesRaw(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) ([]map[int]model.Image, error) { if rs == nil { - return nil, errors.New("pdfcpu: ExtractImages: Please provide rs") + return nil, errors.New("pdfcpu: ExtractImages: missing rs") } + if conf == nil { conf = model.NewDefaultConfiguration() } + conf.Cmd = model.EXTRACTIMAGES - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return nil, err } @@ -51,7 +53,7 @@ func ExtractImagesRaw(rs io.ReadSeeker, selectedPages []string, conf *model.Conf return nil, err } - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return nil, err } @@ -74,13 +76,15 @@ func ExtractImagesRaw(rs io.ReadSeeker, selectedPages []string, conf *model.Conf // ExtractImages extracts and digests embedded image resources from rs for selected pages. func ExtractImages(rs io.ReadSeeker, selectedPages []string, digestImage func(model.Image, bool, int) error, conf *model.Configuration) error { if rs == nil { - return errors.New("pdfcpu: ExtractImages: Please provide rs") + return errors.New("pdfcpu: ExtractImages: missing rs") } + if conf == nil { conf = model.NewDefaultConfiguration() } + conf.Cmd = model.EXTRACTIMAGES - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return err } @@ -89,7 +93,7 @@ func ExtractImages(rs io.ReadSeeker, selectedPages []string, digestImage func(mo return err } - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return err } @@ -128,8 +132,10 @@ func ExtractImagesFile(inFile, outDir string, selectedPages []string, conf *mode return err } defer f.Close() + log.CLI.Printf("extracting images from %s into %s/ ...\n", inFile, outDir) fileName := strings.TrimSuffix(filepath.Base(inFile), ".pdf") + return ExtractImages(f, selectedPages, pdfcpu.WriteImageToDisk(outDir, fileName), conf) } @@ -148,20 +154,23 @@ func writeFonts(ff []pdfcpu.Font, outDir, fileName string) error { return err } } + return nil } // ExtractFonts dumps embedded fontfiles from rs into outDir for selected pages. func ExtractFonts(rs io.ReadSeeker, outDir, fileName string, selectedPages []string, conf *model.Configuration) error { if rs == nil { - return errors.New("pdfcpu: ExtractFonts: Please provide rs") + return errors.New("pdfcpu: ExtractFonts: missing rs") } + if conf == nil { conf = model.NewDefaultConfiguration() } + conf.Cmd = model.EXTRACTFONTS fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } @@ -171,7 +180,7 @@ func ExtractFonts(rs io.ReadSeeker, outDir, fileName string, selectedPages []str } fromWrite := time.Now() - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return err } @@ -203,6 +212,7 @@ func ExtractFonts(rs io.ReadSeeker, outDir, fileName string, selectedPages []str durTotal := time.Since(fromStart).Seconds() log.Stats.Printf("XRefTable:\n%s\n", ctx) model.TimingStats("write fonts", durRead, durVal, durOpt, durWrite, durTotal) + return nil } @@ -213,22 +223,25 @@ func ExtractFontsFile(inFile, outDir string, selectedPages []string, conf *model return err } defer f.Close() + log.CLI.Printf("extracting fonts from %s into %s/ ...\n", inFile, outDir) + return ExtractFonts(f, outDir, filepath.Base(inFile), selectedPages, conf) } // ExtractPages generates single page PDF files from rs in outDir for selected pages. func ExtractPages(rs io.ReadSeeker, outDir, fileName string, selectedPages []string, conf *model.Configuration) error { if rs == nil { - return errors.New("pdfcpu: ExtractPages: Please provide rs") + return errors.New("pdfcpu: ExtractPages: missing rs") } + if conf == nil { conf = model.NewDefaultConfiguration() conf.Cmd = model.EXTRACTPAGES } fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } @@ -238,7 +251,7 @@ func ExtractPages(rs io.ReadSeeker, outDir, fileName string, selectedPages []str } fromWrite := time.Now() - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return err } @@ -269,6 +282,7 @@ func ExtractPages(rs io.ReadSeeker, outDir, fileName string, selectedPages []str durTotal := time.Since(fromStart).Seconds() log.Stats.Printf("XRefTable:\n%s\n", ctx) model.TimingStats("write PDFs", durRead, durVal, durOpt, durWrite, durTotal) + return nil } @@ -279,21 +293,25 @@ func ExtractPagesFile(inFile, outDir string, selectedPages []string, conf *model return err } defer f.Close() + log.CLI.Printf("extracting pages from %s into %s/ ...\n", inFile, outDir) + return ExtractPages(f, outDir, filepath.Base(inFile), selectedPages, conf) } // ExtractContent dumps "PDF source" files from rs into outDir for selected pages. func ExtractContent(rs io.ReadSeeker, outDir, fileName string, selectedPages []string, conf *model.Configuration) error { if rs == nil { - return errors.New("pdfcpu: ExtractContent: Please provide rs") + return errors.New("pdfcpu: ExtractContent: missing rs") } + if conf == nil { conf = model.NewDefaultConfiguration() } + conf.Cmd = model.EXTRACTCONTENT fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } @@ -303,7 +321,7 @@ func ExtractContent(rs io.ReadSeeker, outDir, fileName string, selectedPages []s } fromWrite := time.Now() - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return err } @@ -314,6 +332,7 @@ func ExtractContent(rs io.ReadSeeker, outDir, fileName string, selectedPages []s if !v { continue } + r, err := pdfcpu.ExtractPageContent(ctx, p) if err != nil { return err @@ -321,15 +340,18 @@ func ExtractContent(rs io.ReadSeeker, outDir, fileName string, selectedPages []s if r == nil { continue } + outFile := filepath.Join(outDir, fmt.Sprintf("%s_Content_page_%d.txt", fileName, p)) log.CLI.Printf("writing %s\n", outFile) f, err := os.Create(outFile) if err != nil { return err } + if _, err = io.Copy(f, r); err != nil { return err } + if err := f.Close(); err != nil { return err } @@ -339,6 +361,7 @@ func ExtractContent(rs io.ReadSeeker, outDir, fileName string, selectedPages []s durTotal := time.Since(fromStart).Seconds() log.Stats.Printf("XRefTable:\n%s\n", ctx) model.TimingStats("write content", durRead, durVal, durOpt, durWrite, durTotal) + return nil } @@ -349,21 +372,25 @@ func ExtractContentFile(inFile, outDir string, selectedPages []string, conf *mod return err } defer f.Close() + log.CLI.Printf("extracting content from %s into %s/ ...\n", inFile, outDir) + return ExtractContent(f, outDir, inFile, selectedPages, conf) } // ExtractMetadata dumps all metadata dict entries for rs into outDir. func ExtractMetadata(rs io.ReadSeeker, outDir, fileName string, conf *model.Configuration) error { if rs == nil { - return errors.New("pdfcpu: ExtractMetadata: Please provide rs") + return errors.New("pdfcpu: ExtractMetadata: missing rs") } + if conf == nil { conf = model.NewDefaultConfiguration() } + conf.Cmd = model.EXTRACTMETADATA fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } @@ -397,6 +424,7 @@ func ExtractMetadata(rs io.ReadSeeker, outDir, fileName string, conf *model.Conf durTotal := time.Since(fromStart).Seconds() log.Stats.Printf("XRefTable:\n%s\n", ctx) model.TimingStats("write metadata", durRead, durVal, durOpt, durWrite, durTotal) + return nil } @@ -407,6 +435,8 @@ func ExtractMetadataFile(inFile, outDir string, conf *model.Configuration) error return err } defer f.Close() + log.CLI.Printf("extracting metadata from %s into %s/ ...\n", inFile, outDir) + return ExtractMetadata(f, outDir, filepath.Base(inFile), conf) } diff --git a/pkg/api/font.go b/pkg/api/font.go index 4b4296bb4..af0af51f8 100644 --- a/pkg/api/font.go +++ b/pkg/api/font.go @@ -68,6 +68,7 @@ func ListFonts() ([]string, error) { // InstallFonts installs true type fonts for embedding. func InstallFonts(fileNames []string) error { log.CLI.Printf("installing to %s...", font.UserFontDir) + for _, fn := range fileNames { switch filepath.Ext(fn) { case ".ttf": @@ -82,6 +83,7 @@ func InstallFonts(fileNames []string) error { } } } + return font.LoadUserFonts() } @@ -94,6 +96,7 @@ func rowLabel(xRefTable *model.XRefTable, i int, td model.TextDescriptor, baseFo td.X, td.Y, td.Text = x, float64(7677-i*30), s td.StrokeCol, td.FillCol = color.Black, color.SimpleColor{B: .8} td.FontName, td.FontKey, td.FontSize = baseFontName, baseFontKey, 14 + model.WriteMultiLine(xRefTable, buf, mb, nil, td) } @@ -102,7 +105,9 @@ func columnsLabel(xRefTable *model.XRefTable, td model.TextDescriptor, baseFontN if !top { y = 0 } + td.FontName, td.FontKey = baseFontName, baseFontKey + for i := 0; i < 256; i++ { s := fmt.Sprintf("#%02X", i) td.X, td.Y, td.Text, td.FontSize = float64(70+i*30), y, s, 14 @@ -114,6 +119,7 @@ func columnsLabel(xRefTable *model.XRefTable, td model.TextDescriptor, baseFontN func surrogate(r rune) bool { return r >= 0xD800 && r <= 0xDFFF } + func writeUserFontDemoContent(xRefTable *model.XRefTable, p model.Page, fontName string, plane int) { baseFontName := "Helvetica" baseFontSize := 24 diff --git a/pkg/api/form.go b/pkg/api/form.go index 2c0bdf911..4c0724504 100644 --- a/pkg/api/form.go +++ b/pkg/api/form.go @@ -41,13 +41,18 @@ var ( ErrInvalidJSON = errors.New("pdfcpu: invalid JSON encoding") ) -// ListFormFields returns a list of form field ids in rs. -func ListFormFields(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { +// FormFields returns all form fields of rs. +func FormFields(rs io.ReadSeeker, conf *model.Configuration) ([]form.Field, error) { + if rs == nil { + return nil, errors.New("pdfcpu: FormFields: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.LISTFORMFIELDS } - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + conf.Cmd = model.LISTFORMFIELDS + + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return nil, err } @@ -56,43 +61,23 @@ func ListFormFields(rs io.ReadSeeker, conf *model.Configuration) ([]string, erro return nil, err } - return form.ListFormFields(ctx) -} + fields, _, err := form.FormFields(ctx) -// ListFormFieldsFile returns a list of form field ids in inFile. -func ListFormFieldsFile(inFiles []string, conf *model.Configuration) ([]string, error) { - ss := []string{} - for _, fn := range inFiles { - f, err := os.Open(fn) - if err != nil { - if len(inFiles) > 1 { - ss = append(ss, fmt.Sprintf("\ncan't open %s: %v", fn, err)) - continue - } - return nil, err - } - defer f.Close() - output, err := ListFormFields(f, conf) - if err != nil { - if len(inFiles) > 1 { - ss = append(ss, fmt.Sprintf("\n%s: %v", fn, err)) - continue - } - return nil, err - } - ss = append(ss, "\n"+fn) - ss = append(ss, output...) - } - return ss, nil + return fields, err } // RemoveFormFields deletes form fields in rs and writes the result to w. func RemoveFormFields(rs io.ReadSeeker, w io.Writer, fieldIDsOrNames []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: RemoveFormFields: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.REMOVEFORMFIELDS } - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + conf.Cmd = model.REMOVEFORMFIELDS + + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return err } @@ -121,7 +106,7 @@ func RemoveFormFields(rs io.ReadSeeker, w io.Writer, fieldIDsOrNames []string, c } // RemoveFormFieldsFile deletes form fields in inFile and writes the result to outFile. -func RemoveFormFieldsFile(inFile, outFile string, fieldIDs []string, conf *model.Configuration) (err error) { +func RemoveFormFieldsFile(inFile, outFile string, fieldIDsOrNames []string, conf *model.Configuration) (err error) { var f1, f2 *os.File if f1, err = os.Open(inFile); err != nil { @@ -159,16 +144,21 @@ func RemoveFormFieldsFile(inFile, outFile string, fieldIDs []string, conf *model } }() - return RemoveFormFields(f1, f2, fieldIDs, conf) + return RemoveFormFields(f1, f2, fieldIDsOrNames, conf) } // LockFormFields turns form fields in rs into read-only and writes the result to w. func LockFormFields(rs io.ReadSeeker, w io.Writer, fieldIDsOrNames []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: LockFormFields: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.LOCKFORMFIELDS } - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + conf.Cmd = model.LOCKFORMFIELDS + + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return err } @@ -240,11 +230,16 @@ func LockFormFieldsFile(inFile, outFile string, fieldIDsOrNames []string, conf * // UnlockFormFields makess form fields in rs writeable and writes the result to w. func UnlockFormFields(rs io.ReadSeeker, w io.Writer, fieldIDsOrNames []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: UnlockFormFields: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.UNLOCKFORMFIELDS } - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + conf.Cmd = model.UNLOCKFORMFIELDS + + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return err } @@ -316,11 +311,16 @@ func UnlockFormFieldsFile(inFile, outFile string, fieldIDsOrNames []string, conf // ResetFormFields resets form fields of rs and writes the result to w. func ResetFormFields(rs io.ReadSeeker, w io.Writer, fieldIDsOrNames []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ResetFormFields: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.RESETFORMFIELDS } - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + conf.Cmd = model.RESETFORMFIELDS + + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return err } @@ -391,12 +391,17 @@ func ResetFormFieldsFile(inFile, outFile string, fieldIDsOrNames []string, conf } // ExportForm extracts form data originating from source from rs. -func ExportFormToStruct(rs io.ReadSeeker, source string, conf *model.Configuration) (*form.FormGroup, error) { +func ExportForm(rs io.ReadSeeker, source string, conf *model.Configuration) (*form.FormGroup, error) { + if rs == nil { + return nil, errors.New("pdfcpu: ExportForm: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.EXPORTFORMFIELDS } - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + conf.Cmd = model.EXPORTFORMFIELDS + + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return nil, err } @@ -405,7 +410,7 @@ func ExportFormToStruct(rs io.ReadSeeker, source string, conf *model.Configurati return nil, err } - formGroup, ok, err := form.ExportFormToStruct(ctx.XRefTable, source) + formGroup, ok, err := form.ExportForm(ctx.XRefTable, source) if err != nil { return nil, err } @@ -416,13 +421,22 @@ func ExportFormToStruct(rs io.ReadSeeker, source string, conf *model.Configurati return formGroup, nil } -// ExportForm extracts form data originating from source from rs and writes the result to w. -func ExportForm(rs io.ReadSeeker, w io.Writer, source string, conf *model.Configuration) error { +// ExportFormJSON extracts form data originating from source from rs and writes the result to w. +func ExportFormJSON(rs io.ReadSeeker, w io.Writer, source string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ExportFormJSON: missing rs") + } + + if w == nil { + return errors.New("pdfcpu: ExportFormJSON: missing w") + } + if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.EXPORTFORMFIELDS } - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + conf.Cmd = model.EXPORTFORMFIELDS + + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return err } @@ -431,7 +445,7 @@ func ExportForm(rs io.ReadSeeker, w io.Writer, source string, conf *model.Config return err } - ok, err := form.ExportForm(ctx.XRefTable, source, w) + ok, err := form.ExportFormJSON(ctx.XRefTable, source, w) if err != nil { return err } @@ -470,17 +484,25 @@ func ExportFormFile(inFilePDF, outFileJSON string, conf *model.Configuration) (e } }() - return ExportForm(f1, f2, inFilePDF, conf) + return ExportFormJSON(f1, f2, inFilePDF, conf) } // FillForm populates the form rs with data from rd and writes the result to w. func FillForm(rs io.ReadSeeker, rd io.Reader, w io.Writer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: FillForm: missing rs") + } + + if rd == nil { + return errors.New("pdfcpu: FillForm: missing rd") + } if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.FILLFORMFIELDS } - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + conf.Cmd = model.FILLFORMFIELDS + + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return err } @@ -491,7 +513,7 @@ func FillForm(rs io.ReadSeeker, rd io.Reader, w io.Writer, conf *model.Configura // root -> Perms -> UR3 -> = Sig dict d1 := ctx.RootDict delete(d1, "Perms") - d2 := ctx.AcroForm + d2 := ctx.Form delete(d2, "SigFlags") delete(d2, "XFA") d1["AcroForm"] = d2 @@ -658,7 +680,7 @@ func multiFillFormJSON(inFilePDF string, rd io.Reader, outDir, fileName string, } defer rs.Close() - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return err } @@ -752,7 +774,7 @@ func multiFillFormCSV(inFilePDF string, rd io.Reader, outDir, fileName string, m } defer f.Close() - ctx, _, _, _, err := readValidateAndOptimize(f, conf, time.Now()) + ctx, _, _, _, err := ReadValidateAndOptimize(f, conf, time.Now()) if err != nil { return err } @@ -806,8 +828,8 @@ func MultiFillForm(inFilePDF string, rd io.Reader, outDir, fileName string, form if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.MULTIFILLFORMFIELDS } + conf.Cmd = model.MULTIFILLFORMFIELDS fileName = strings.TrimSuffix(filepath.Base(fileName), ".pdf") diff --git a/pkg/api/image.go b/pkg/api/image.go index 097ca90ff..d8b423ccc 100644 --- a/pkg/api/image.go +++ b/pkg/api/image.go @@ -17,67 +17,40 @@ limitations under the License. package api import ( - "fmt" "io" - "os" "time" - "github.com/pdfcpu/pdfcpu/pkg/log" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" "github.com/pkg/errors" ) -// ListImages returns a list of embedded images of rs. -func ListImages(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) ([]string, error) { +// Images returns all embedded images of rs. +func Images(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) ([]map[int]model.Image, error) { if rs == nil { - return nil, errors.New("pdfcpu: ListImages: Please provide rs") + return nil, errors.New("pdfcpu: ListImages: missing rs") } + if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.LISTIMAGES } - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + conf.Cmd = model.LISTIMAGES + + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return nil, err } + if err := ctx.EnsurePageCount(); err != nil { return nil, err } - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return nil, err } - return pdfcpu.ListImages(ctx, pages) -} + ii, _, err := pdfcpu.Images(ctx, pages) -// ListImagesFile returns a list of embedded images of inFile. -func ListImagesFile(inFiles []string, selectedPages []string, conf *model.Configuration) ([]string, error) { - if len(selectedPages) == 0 { - log.CLI.Printf("pages: all\n") - } - ss := []string{} - for _, fn := range inFiles { - f, err := os.Open(fn) - if err != nil { - if len(inFiles) > 1 { - ss = append(ss, fmt.Sprintf("\ncan't open %s: %v", fn, err)) - continue - } - return nil, err - } - defer f.Close() - output, err := ListImages(f, selectedPages, conf) - if err != nil { - if len(inFiles) > 1 { - ss = append(ss, fmt.Sprintf("\n%s: %v", fn, err)) - continue - } - return nil, err - } - ss = append(ss, "\n"+fn) - ss = append(ss, output...) - } - return ss, nil + return ii, err } diff --git a/pkg/api/importImage.go b/pkg/api/importImage.go index 79240a6ef..f8b7e02a7 100644 --- a/pkg/api/importImage.go +++ b/pkg/api/importImage.go @@ -113,6 +113,7 @@ func fileExists(filename string) bool { func prepImgFiles(imgFiles []string, f1 *os.File) ([]io.ReadCloser, []io.Reader, error) { rc := make([]io.ReadCloser, len(imgFiles)) rr := make([]io.Reader, len(imgFiles)) + for i, fn := range imgFiles { f, err := os.Open(fn) if err != nil { @@ -124,6 +125,7 @@ func prepImgFiles(imgFiles []string, f1 *os.File) ([]io.ReadCloser, []io.Reader, rc[i] = f rr[i] = bufio.NewReader(f) } + return rc, rr, nil } diff --git a/pkg/api/info.go b/pkg/api/info.go index 6d4ab984c..35080af86 100644 --- a/pkg/api/info.go +++ b/pkg/api/info.go @@ -17,71 +17,45 @@ package api import ( - "fmt" "io" - "os" "time" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" ) -// Info returns information about rs. -func Info(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) ([]string, error) { +// PDFInfo returns information about rs. +func PDFInfo(rs io.ReadSeeker, fileName string, selectedPages []string, conf *model.Configuration) (*pdfcpu.PDFInfo, error) { + if rs == nil { + return nil, errors.New("pdfcpu: PDFInfo: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } else { // Validation loads infodict. conf.ValidationMode = model.ValidationRelaxed } + conf.Cmd = model.LISTINFO + ctx, _, _, err := readAndValidate(rs, conf, time.Now()) if err != nil { return nil, err } + if err := ctx.EnsurePageCount(); err != nil { return nil, err } - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, false) - if err != nil { - return nil, err - } - if err := pdfcpu.DetectWatermarks(ctx); err != nil { - return nil, err - } - return pdfcpu.InfoDigest(ctx, pages) -} -// InfoFile returns information about inFile. -func InfoFile(inFile string, selectedPages []string, conf *model.Configuration) ([]string, error) { - - f, err := os.Open(inFile) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, false, true) if err != nil { return nil, err } - defer f.Close() - ss, err := Info(f, selectedPages, conf) - s := fmt.Sprintf("%s:", inFile) - return append([]string{s}, ss...), err -} -// InfoFile returns information about inFile. -func InfoFiles(inFiles []string, selectedPages []string, conf *model.Configuration) ([]string, error) { - - var ss []string - - for i, fn := range inFiles { - if i > 0 { - ss = append(ss, "") - } - ssx, err := InfoFile(fn, selectedPages, conf) - if err != nil { - if len(inFiles) == 1 { - return nil, err - } - fmt.Fprintf(os.Stderr, "%s: %v\n", fn, err) - } - ss = append(ss, ssx...) + if err := pdfcpu.DetectWatermarks(ctx); err != nil { + return nil, err } - return ss, nil + return pdfcpu.Info(ctx, fileName, pages) } diff --git a/pkg/api/keyword.go b/pkg/api/keyword.go index a7d0dfc25..e1d08f001 100644 --- a/pkg/api/keyword.go +++ b/pkg/api/keyword.go @@ -21,62 +21,49 @@ import ( "os" "time" - "github.com/pdfcpu/pdfcpu/pkg/log" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" "github.com/pkg/errors" ) -// ListKeywords returns the keyword list of rs. -func ListKeywords(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { +// Keywords returns the keywords of rs's info dict. +func Keywords(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: ListKeywords: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } else { // Validation loads infodict. conf.ValidationMode = model.ValidationRelaxed } + conf.Cmd = model.LISTKEYWORDS - fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) - if err != nil { - return nil, err - } - - fromWrite := time.Now() - list, err := pdfcpu.KeywordsList(ctx.XRefTable) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return nil, err } - durWrite := time.Since(fromWrite).Seconds() - durTotal := time.Since(fromStart).Seconds() - log.Stats.Printf("XRefTable:\n%s\n", ctx) - model.TimingStats("list files", durRead, durVal, durOpt, durWrite, durTotal) - - return list, nil -} - -// ListKeywordsFile returns the keyword list of inFile. -func ListKeywordsFile(inFile string, conf *model.Configuration) ([]string, error) { - f, err := os.Open(inFile) - if err != nil { - return nil, err - } - defer f.Close() - return ListKeywords(f, conf) + return pdfcpu.KeywordsList(ctx.XRefTable) } // AddKeywords embeds files into a PDF context read from rs and writes the result to w. func AddKeywords(rs io.ReadSeeker, w io.Writer, files []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddKeywords: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } else { // Validation loads infodict. conf.ValidationMode = model.ValidationRelaxed } + conf.Cmd = model.ADDKEYWORDS fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } @@ -143,15 +130,20 @@ func AddKeywordsFile(inFile, outFile string, files []string, conf *model.Configu // RemoveKeywords deletes embedded files from a PDF context read from rs and writes the result to w. func RemoveKeywords(rs io.ReadSeeker, w io.Writer, keywords []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: RemoveKeywords: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } else { // Validation loads infodict. conf.ValidationMode = model.ValidationRelaxed } + conf.Cmd = model.REMOVEKEYWORDS fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } diff --git a/pkg/api/merge.go b/pkg/api/merge.go index 36c83ed25..eb4e971e6 100644 --- a/pkg/api/merge.go +++ b/pkg/api/merge.go @@ -44,11 +44,11 @@ func appendTo(rs io.ReadSeeker, fName string, ctxDest *model.Context) error { func MergeRaw(rsc []io.ReadSeeker, w io.Writer, conf *model.Configuration) error { if rsc == nil { - return errors.New("pdfcpu: MergeRaw: Please provide rsc") + return errors.New("pdfcpu: MergeRaw: missing rsc") } if w == nil { - return errors.New("pdfcpu: MergeRaw: Please provide w") + return errors.New("pdfcpu: MergeRaw: missing w") } if conf == nil { diff --git a/pkg/api/nup.go b/pkg/api/nup.go index 044d8a8eb..468fb627a 100644 --- a/pkg/api/nup.go +++ b/pkg/api/nup.go @@ -121,7 +121,7 @@ func NUp(rs io.ReadSeeker, w io.Writer, imgFiles, selectedPages []string, nup *m return err } - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return err } diff --git a/pkg/api/optimize.go b/pkg/api/optimize.go index e70ad2f3e..df5ebb149 100644 --- a/pkg/api/optimize.go +++ b/pkg/api/optimize.go @@ -29,14 +29,18 @@ import ( // Optimize reads a PDF stream from rs and writes the optimized PDF stream to w. func Optimize(rs io.ReadSeeker, w io.Writer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Optimize: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() - conf.Cmd = model.OPTIMIZE } + //conf.Cmd = model.OPTIMIZE fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } @@ -105,5 +109,10 @@ func OptimizeFile(inFile, outFile string, conf *model.Configuration) (err error) } }() + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.OPTIMIZE + return Optimize(f1, f2, conf) } diff --git a/pkg/api/page.go b/pkg/api/page.go index dc2f1ce4c..160e63cc8 100644 --- a/pkg/api/page.go +++ b/pkg/api/page.go @@ -29,6 +29,10 @@ import ( // InsertPages inserts a blank page before or after every page selected of rs and writes the result to w. func InsertPages(rs io.ReadSeeker, w io.Writer, selectedPages []string, before bool, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: InsertPages: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } @@ -38,7 +42,7 @@ func InsertPages(rs io.ReadSeeker, w io.Writer, selectedPages []string, before b } fromStart := time.Now() - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } @@ -47,7 +51,7 @@ func InsertPages(rs io.ReadSeeker, w io.Writer, selectedPages []string, before b return err } - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return err } @@ -118,13 +122,17 @@ func InsertPagesFile(inFile, outFile string, selectedPages []string, before bool // RemovePages removes selected pages from rs and writes the result to w. func RemovePages(rs io.ReadSeeker, w io.Writer, selectedPages []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: RemovePages: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } conf.Cmd = model.REMOVEPAGES fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } @@ -135,7 +143,7 @@ func RemovePages(rs io.ReadSeeker, w io.Writer, selectedPages []string, conf *mo fromWrite := time.Now() - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, false) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, false, true) if err != nil { return err } @@ -205,13 +213,19 @@ func RemovePagesFile(inFile, outFile string, selectedPages []string, conf *model // PageCount returns rs's page count. func PageCount(rs io.ReadSeeker, conf *model.Configuration) (int, error) { + if rs == nil { + return 0, errors.New("pdfcpu: PageCount: missing rs") + } + ctx, err := ReadContext(rs, conf) if err != nil { return 0, err } + if err := ValidateContext(ctx); err != nil { return 0, err } + return ctx.PageCount, nil } @@ -228,6 +242,10 @@ func PageCountFile(inFile string) (int, error) { // PageDims returns a sorted slice of mediaBox dimensions for rs. func PageDims(rs io.ReadSeeker, conf *model.Configuration) ([]types.Dim, error) { + if rs == nil { + return nil, errors.New("pdfcpu: PageDims: missing rs") + } + ctx, err := ReadContext(rs, conf) if err != nil { return nil, err @@ -237,6 +255,7 @@ func PageDims(rs io.ReadSeeker, conf *model.Configuration) ([]types.Dim, error) if err != nil { return nil, err } + if len(pd) != ctx.PageCount { return nil, errors.New("pdfcpu: corrupt page dimensions") } diff --git a/pkg/api/permission.go b/pkg/api/permission.go index 41cfb37f8..5ea1d16fc 100644 --- a/pkg/api/permission.go +++ b/pkg/api/permission.go @@ -22,60 +22,49 @@ import ( "time" "github.com/pdfcpu/pdfcpu/pkg/log" - "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" "github.com/pkg/errors" ) -// ListPermissions returns a list of user access permissions. -func ListPermissions(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { +// Permissions returns user access permissions for rs. +func Permissions(rs io.ReadSeeker, conf *model.Configuration) (int, error) { + if rs == nil { + return 0, errors.New("pdfcpu: Permissions: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } conf.Cmd = model.LISTPERMISSIONS - fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { - return nil, err + return 0, err } - fromList := time.Now() - list := pdfcpu.Permissions(ctx) - - durList := time.Since(fromList).Seconds() - durTotal := time.Since(fromStart).Seconds() - log.Stats.Printf("XRefTable:\n%s\n", ctx) - model.TimingStats("list permissions", durRead, durVal, durOpt, durList, durTotal) - - return list, nil -} - -// ListPermissionsFile returns a list of user access permissions for inFile. -func ListPermissionsFile(inFile string, conf *model.Configuration) ([]string, error) { - f, err := os.Open(inFile) - if err != nil { - return nil, err + p := 0 + if ctx.E != nil { + p = ctx.E.P } - defer func() { - f.Close() - }() - - return ListPermissions(f, conf) + return p, nil } // SetPermissions sets user access permissions. // inFile has to be encrypted. // A configuration containing the current passwords is required. func SetPermissions(rs io.ReadSeeker, w io.Writer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: SetPermissions: missing rs") + } + if conf == nil { return errors.New("pdfcpu: missing configuration for setting permissions") } conf.Cmd = model.SETPERMISSIONS fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } @@ -142,9 +131,15 @@ func SetPermissionsFile(inFile, outFile string, conf *model.Configuration) (err // GetPermissions returns the permissions for rs. func GetPermissions(rs io.ReadSeeker, conf *model.Configuration) (*int16, error) { + if rs == nil { + return nil, errors.New("pdfcpu: GetPermissions: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } + // No cmd available. + ctx, _, _, err := readAndValidate(rs, conf, time.Now()) if err != nil { return nil, err diff --git a/pkg/api/property.go b/pkg/api/property.go index da6ac64bb..57faeee0a 100644 --- a/pkg/api/property.go +++ b/pkg/api/property.go @@ -21,62 +21,49 @@ import ( "os" "time" - "github.com/pdfcpu/pdfcpu/pkg/log" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" "github.com/pkg/errors" ) -// ListProperties returns the property list of rs. -func ListProperties(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { +// Properties returns rs's properties as recorded in infoDict. +func Properties(rs io.ReadSeeker, conf *model.Configuration) (map[string]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: ListProperties: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } else { // Validation loads infodict. conf.ValidationMode = model.ValidationRelaxed } + conf.Cmd = model.LISTPROPERTIES - fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) - if err != nil { - return nil, err - } - - fromWrite := time.Now() - list, err := pdfcpu.PropertiesList(ctx) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return nil, err } - durWrite := time.Since(fromWrite).Seconds() - durTotal := time.Since(fromStart).Seconds() - log.Stats.Printf("XRefTable:\n%s\n", ctx) - model.TimingStats("list files", durRead, durVal, durOpt, durWrite, durTotal) - - return list, nil -} - -// ListPropertiesFile returns the property list of inFile. -func ListPropertiesFile(inFile string, conf *model.Configuration) ([]string, error) { - f, err := os.Open(inFile) - if err != nil { - return nil, err - } - defer f.Close() - return ListProperties(f, conf) + return ctx.Properties, nil } // AddProperties embeds files into a PDF context read from rs and writes the result to w. func AddProperties(rs io.ReadSeeker, w io.Writer, properties map[string]string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddProperties: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } else { // Validation loads infodict. conf.ValidationMode = model.ValidationRelaxed } + conf.Cmd = model.ADDPROPERTIES fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } @@ -143,15 +130,20 @@ func AddPropertiesFile(inFile, outFile string, properties map[string]string, con // RemoveProperties deletes embedded files from a PDF context read from rs and writes the result to w. func RemoveProperties(rs io.ReadSeeker, w io.Writer, properties []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: RemoveProperties: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } else { // Validation loads infodict. conf.ValidationMode = model.ValidationRelaxed } + conf.Cmd = model.REMOVEPROPERTIES fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } diff --git a/pkg/api/resize.go b/pkg/api/resize.go index bfd36b7ad..d52fe0c83 100644 --- a/pkg/api/resize.go +++ b/pkg/api/resize.go @@ -32,19 +32,22 @@ func Resize(rs io.ReadSeeker, w io.Writer, selectedPages []string, resize *model if rs == nil { return errors.New("pdfcpu: Resize: missing rs") } + if conf == nil { conf = model.NewDefaultConfiguration() } conf.Cmd = model.RESIZE - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return err } + if err := ctx.EnsurePageCount(); err != nil { return err } - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return err } diff --git a/pkg/api/rotate.go b/pkg/api/rotate.go index 8dd8ad46b..e6532f449 100644 --- a/pkg/api/rotate.go +++ b/pkg/api/rotate.go @@ -24,17 +24,22 @@ import ( "github.com/pdfcpu/pdfcpu/pkg/log" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" ) // Rotate rotates selected pages of rs clockwise by rotation degrees and writes the result to w. func Rotate(rs io.ReadSeeker, w io.Writer, rotation int, selectedPages []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Rotate: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } conf.Cmd = model.ROTATE fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } @@ -44,7 +49,7 @@ func Rotate(rs io.ReadSeeker, w io.Writer, rotation int, selectedPages []string, } from := time.Now() - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return err } diff --git a/pkg/api/selectPages.go b/pkg/api/selectPages.go index 7679728f7..630f60065 100644 --- a/pkg/api/selectPages.go +++ b/pkg/api/selectPages.go @@ -285,7 +285,7 @@ func logSelPages(selectedPages types.IntSet) { // selectedPages returns a set of used page numbers. // key==page# => key 0 unused! -func selectedPages(pageCount int, pageSelection []string) (types.IntSet, error) { +func selectedPages(pageCount int, pageSelection []string, log bool) (types.IntSet, error) { selectedPages := types.IntSet{} for _, v := range pageSelection { @@ -357,15 +357,18 @@ func selectedPages(pageCount int, pageSelection []string) (types.IntSet, error) } - logSelPages(selectedPages) + if log { + logSelPages(selectedPages) + } + return selectedPages, nil } // PagesForPageSelection ensures a set of page numbers for an ascending page sequence // where each page number may appear only once. -func PagesForPageSelection(pageCount int, pageSelection []string, ensureAllforNone bool) (types.IntSet, error) { +func PagesForPageSelection(pageCount int, pageSelection []string, ensureAllforNone bool, log bool) (types.IntSet, error) { if len(pageSelection) > 0 { - return selectedPages(pageCount, pageSelection) + return selectedPages(pageCount, pageSelection, log) } if !ensureAllforNone { //log.CLI.Printf("pages: none\n") diff --git a/pkg/api/split.go b/pkg/api/split.go index d4aed6547..b0f37ddfd 100644 --- a/pkg/api/split.go +++ b/pkg/api/split.go @@ -28,6 +28,7 @@ import ( "github.com/pdfcpu/pdfcpu/pkg/log" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" ) type PageSpan struct { @@ -85,7 +86,7 @@ func context(rs io.ReadSeeker, conf *model.Configuration) (*model.Context, error } conf.Cmd = model.SPLIT - ctx, _, _, _, err := readValidateAndOptimize(rs, conf, time.Now()) + ctx, _, _, _, err := ReadValidateAndOptimize(rs, conf, time.Now()) if err != nil { return nil, err } @@ -100,7 +101,7 @@ func context(rs io.ReadSeeker, conf *model.Configuration) (*model.Context, error func pageSpansSplitAlongBookmarks(ctx *model.Context) ([]*PageSpan, error) { pss := []*PageSpan{} - bms, err := pdfcpu.BookmarksForOutline(ctx) + bms, err := pdfcpu.Bookmarks(ctx) if err != nil { return nil, err } @@ -155,7 +156,7 @@ func pageSpans(ctx *model.Context, span int) ([]*PageSpan, error) { func writePageSpansSplitAlongBookmarks(ctx *model.Context, outDir string) error { forBookmark := true - bms, err := pdfcpu.BookmarksForOutline(ctx) + bms, err := pdfcpu.Bookmarks(ctx) if err != nil { return err } @@ -205,6 +206,10 @@ func writePageSpans(ctx *model.Context, span int, outDir, fileName string) error // If span == 0 we split along given bookmarks (level 1 only). // Default span: 1 func SplitRaw(rs io.ReadSeeker, span int, conf *model.Configuration) ([]*PageSpan, error) { + if rs == nil { + return nil, errors.New("pdfcpu: SplitRaw: missing rs") + } + ctx, err := context(rs, conf) if err != nil { return nil, err @@ -221,6 +226,10 @@ func SplitRaw(rs io.ReadSeeker, span int, conf *model.Configuration) ([]*PageSpa // If span == 0 we split along given bookmarks (level 1 only). // Default span: 1 func Split(rs io.ReadSeeker, outDir, fileName string, span int, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Split: missing rs") + } + ctx, err := context(rs, conf) if err != nil { return err diff --git a/pkg/api/stamp.go b/pkg/api/stamp.go index a01b11706..b760abf27 100644 --- a/pkg/api/stamp.go +++ b/pkg/api/stamp.go @@ -35,6 +35,10 @@ func WatermarkContext(ctx *model.Context, selectedPages types.IntSet, wm *model. // AddWatermarksMap adds watermarks in m to corresponding pages in rs and writes the result to w. func AddWatermarksMap(rs io.ReadSeeker, w io.Writer, m map[int]*model.Watermark, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddWatermarksMap: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } @@ -45,7 +49,7 @@ func AddWatermarksMap(rs io.ReadSeeker, w io.Writer, m map[int]*model.Watermark, } fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } @@ -123,6 +127,10 @@ func AddWatermarksMapFile(inFile, outFile string, m map[int]*model.Watermark, co // AddWatermarksSliceMap adds watermarks in m to corresponding pages in rs and writes the result to w. func AddWatermarksSliceMap(rs io.ReadSeeker, w io.Writer, m map[int][]*model.Watermark, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddWatermarksSliceMap: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } @@ -133,7 +141,7 @@ func AddWatermarksSliceMap(rs io.ReadSeeker, w io.Writer, m map[int][]*model.Wat } fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } @@ -211,6 +219,10 @@ func AddWatermarksSliceMapFile(inFile, outFile string, m map[int][]*model.Waterm // AddWatermarks adds watermarks to all pages selected in rs and writes the result to w. func AddWatermarks(rs io.ReadSeeker, w io.Writer, selectedPages []string, wm *model.Watermark, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: AddWatermarks: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } @@ -222,7 +234,7 @@ func AddWatermarks(rs io.ReadSeeker, w io.Writer, selectedPages []string, wm *mo } fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } @@ -232,7 +244,7 @@ func AddWatermarks(rs io.ReadSeeker, w io.Writer, selectedPages []string, wm *mo } from := time.Now() - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return err } @@ -308,13 +320,17 @@ func AddWatermarksFile(inFile, outFile string, selectedPages []string, wm *model // RemoveWatermarks removes watermarks from all pages selected in rs and writes the result to w. func RemoveWatermarks(rs io.ReadSeeker, w io.Writer, selectedPages []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: RemoveWatermarks: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } conf.Cmd = model.REMOVEWATERMARKS fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } @@ -324,7 +340,7 @@ func RemoveWatermarks(rs io.ReadSeeker, w io.Writer, selectedPages []string, con } from := time.Now() - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, true, true) if err != nil { return err } @@ -400,10 +416,15 @@ func RemoveWatermarksFile(inFile, outFile string, selectedPages []string, conf * // HasWatermarks checks rs for watermarks. func HasWatermarks(rs io.ReadSeeker, conf *model.Configuration) (bool, error) { + if rs == nil { + return false, errors.New("pdfcpu: HasWatermarks: missing rs") + } + ctx, err := ReadContext(rs, conf) if err != nil { return false, err } + if err := pdfcpu.DetectWatermarks(ctx); err != nil { return false, err } @@ -433,7 +454,9 @@ func TextWatermark(text, desc string, onTop, update bool, u types.DisplayUnit) ( if err != nil { return nil, err } + wm.Update = update + return wm, nil } @@ -443,7 +466,9 @@ func ImageWatermark(fileName, desc string, onTop, update bool, u types.DisplayUn if err != nil { return nil, err } + wm.Update = update + return wm, nil } @@ -453,8 +478,10 @@ func ImageWatermarkForReader(r io.Reader, desc string, onTop, update bool, u typ if err != nil { return nil, err } + wm.Update = update wm.Image = r + return wm, nil } @@ -464,7 +491,9 @@ func PDFWatermark(fileName, desc string, onTop, update bool, u types.DisplayUnit if err != nil { return nil, err } + wm.Update = update + return wm, nil } @@ -474,10 +503,12 @@ func AddTextWatermarksFile(inFile, outFile string, selectedPages []string, onTop if conf != nil { unit = conf.Unit } + wm, err := TextWatermark(text, desc, onTop, false, unit) if err != nil { return err } + return AddWatermarksFile(inFile, outFile, selectedPages, wm, conf) } @@ -487,10 +518,12 @@ func AddImageWatermarksFile(inFile, outFile string, selectedPages []string, onTo if conf != nil { unit = conf.Unit } + wm, err := ImageWatermark(fileName, desc, onTop, false, unit) if err != nil { return err } + return AddWatermarksFile(inFile, outFile, selectedPages, wm, conf) } @@ -500,10 +533,12 @@ func AddImageWatermarksForReaderFile(inFile, outFile string, selectedPages []str if conf != nil { unit = conf.Unit } + wm, err := ImageWatermarkForReader(r, desc, onTop, false, unit) if err != nil { return err } + return AddWatermarksFile(inFile, outFile, selectedPages, wm, conf) } @@ -513,10 +548,12 @@ func AddPDFWatermarksFile(inFile, outFile string, selectedPages []string, onTop if conf != nil { unit = conf.Unit } + wm, err := PDFWatermark(fileName, desc, onTop, false, unit) if err != nil { return err } + return AddWatermarksFile(inFile, outFile, selectedPages, wm, conf) } @@ -526,10 +563,12 @@ func UpdateTextWatermarksFile(inFile, outFile string, selectedPages []string, on if conf != nil { unit = conf.Unit } + wm, err := TextWatermark(text, desc, onTop, true, unit) if err != nil { return err } + return AddWatermarksFile(inFile, outFile, selectedPages, wm, conf) } @@ -552,9 +591,11 @@ func UpdatePDFWatermarksFile(inFile, outFile string, selectedPages []string, onT if conf != nil { unit = conf.Unit } + wm, err := PDFWatermark(fileName, desc, onTop, true, unit) if err != nil { return err } + return AddWatermarksFile(inFile, outFile, selectedPages, wm, conf) } diff --git a/pkg/api/test/annotation_test.go b/pkg/api/test/annotation_test.go index 2ce78a918..5fd0ef74f 100644 --- a/pkg/api/test/annotation_test.go +++ b/pkg/api/test/annotation_test.go @@ -17,6 +17,7 @@ limitations under the License. package test import ( + "os" "path/filepath" "testing" @@ -50,12 +51,36 @@ var linkAnn model.AnnotationRenderer = model.NewLinkAnnotation( nil, false) +func annotationCount(t *testing.T, inFile string) int { + t.Helper() + + msg := "annotationCount" + + f, err := os.Open(inFile) + if err != nil { + t.Fatalf("%s open: %v\n", msg, err) + } + defer f.Close() + + annots, err := api.Annotations(f, nil, conf) + if err != nil { + t.Fatalf("%s annotations: %v\n", msg, err) + } + + count, _, err := pdfcpu.ListAnnotations(annots) + if err != nil { + t.Fatalf("%s listAnnotations: %v\n", msg, err) + } + + return count +} + func add2Annotations(t *testing.T, msg, inFile string, incr bool) { t.Helper() // We start with 0 annotations. - if i, _, err := api.ListAnnotationsFile(inFile, nil, nil); err != nil || i > 0 { - t.Fatalf("%s list: %v\n", msg, err) + if i := annotationCount(t, inFile); i > 0 { + t.Fatalf("%s count: got %d want 0\n", msg, i) } // Add a text annotation to page 1. @@ -69,12 +94,9 @@ func add2Annotations(t *testing.T, msg, inFile string, incr bool) { } // Now we should have 2 annotations. - i, s, err := api.ListAnnotationsFile(inFile, nil, nil) - if err != nil || i != 2 { - t.Fatalf("%s list: %v\n", msg, err) + if i := annotationCount(t, inFile); i != 2 { + t.Fatalf("%s count: got %d want 2\n", msg, i) } - - _ = s } func TestAddRemoveAnnotationsByAnnotType(t *testing.T) { @@ -94,8 +116,8 @@ func TestAddRemoveAnnotationsByAnnotType(t *testing.T) { } // We should have 0 annotations as at the beginning. - if i, _, err := api.ListAnnotationsFile(inFile, nil, nil); err != nil || i > 0 { - t.Fatalf("%s list: %v\n", msg, err) + if i := annotationCount(t, inFile); i > 0 { + t.Fatalf("%s count: got %d want 0\n", msg, i) } } @@ -116,8 +138,8 @@ func TestAddRemoveAnnotationsById(t *testing.T) { } // We should have 0 annotations as at the beginning. - if i, _, err := api.ListAnnotationsFile(inFile, nil, nil); err != nil || i > 0 { - t.Fatalf("%s list: %v\n", msg, err) + if i := annotationCount(t, inFile); i > 0 { + t.Fatalf("%s count: got %d want 0\n", msg, i) } } @@ -138,8 +160,8 @@ func TestAddRemoveAnnotationsByIdAndAnnotType(t *testing.T) { } // We should have 0 annotations as at the beginning. - if i, _, err := api.ListAnnotationsFile(inFile, nil, nil); err != nil || i > 0 { - t.Fatalf("%s list: %v\n", msg, err) + if i := annotationCount(t, inFile); i > 0 { + t.Fatalf("%s count: got %d want 0\n", msg, i) } } @@ -156,7 +178,7 @@ func TestAddRemoveAnnotationsByObjNr(t *testing.T) { t.Fatalf("%s readContext: %v\n", msg, err) } - allPages, err := api.PagesForPageSelection(ctx.PageCount, nil, true) + allPages, err := api.PagesForPageSelection(ctx.PageCount, nil, true, true) if err != nil { t.Fatalf("%s pagesForPageSelection: %v\n", msg, err) } @@ -174,9 +196,8 @@ func TestAddRemoveAnnotationsByObjNr(t *testing.T) { } // We should have 1 annotation - i, _, err := api.ListAnnotationsFile(inFile, nil, nil) - if err != nil || i != 1 { - t.Fatalf("%s list: %v\n", msg, err) + if i := annotationCount(t, inFile); i != 1 { + t.Fatalf("%s count: got %d want 0\n", msg, i) } // Create a context. @@ -209,11 +230,9 @@ func TestAddRemoveAnnotationsByObjNr(t *testing.T) { } // We should have 0 annotations like at the beginning. - i, _, err = api.ListAnnotationsFile(inFile, nil, nil) - if err != nil || i > 0 { - t.Fatalf("%s list: %v\n", msg, err) + if i := annotationCount(t, inFile); i > 0 { + t.Fatalf("%s count: got %d want 0\n", msg, i) } - } func TestAddRemoveAnnotationsByObjNrAndAnnotType(t *testing.T) { @@ -234,8 +253,8 @@ func TestAddRemoveAnnotationsByObjNrAndAnnotType(t *testing.T) { } // We should have 1 annotations. - if i, _, err := api.ListAnnotationsFile(inFile, nil, nil); err != nil || i != 1 { - t.Fatalf("%s list: %v\n", msg, err) + if i := annotationCount(t, inFile); i != 1 { + t.Fatalf("%s count: got %d want 0\n", msg, i) } } @@ -256,8 +275,8 @@ func TestAddRemoveAnnotationsByIdAndObjNrAndAnnotType(t *testing.T) { } // We should have 0 annotations as at the beginning. - if i, _, err := api.ListAnnotationsFile(inFile, nil, nil); err != nil || i > 0 { - t.Fatalf("%s list: %v\n", msg, err) + if i := annotationCount(t, inFile); i > 0 { + t.Fatalf("%s count: got %d want 0\n", msg, i) } } @@ -282,9 +301,8 @@ func TestRemoveAllAnnotations(t *testing.T) { } // We should have 2 annotations. - i, _, err := api.ListAnnotationsFile(inFile, nil, nil) - if err != nil || i != 2 { - t.Fatalf("%s list: %v\n", msg, err) + if i := annotationCount(t, inFile); i != 2 { + t.Fatalf("%s count: got %d want 0\n", msg, i) } // Remove all annotations. @@ -294,9 +312,8 @@ func TestRemoveAllAnnotations(t *testing.T) { } // We should have 0 annotations like at the beginning. - i, _, err = api.ListAnnotationsFile(inFile, nil, nil) - if err != nil || i > 0 { - t.Fatalf("%s list: %v\n", msg, err) + if i := annotationCount(t, inFile); i > 0 { + t.Fatalf("%s count: got %d want 0\n", msg, i) } } @@ -317,8 +334,8 @@ func TestAddRemoveAllAnnotationsAsIncrements(t *testing.T) { } // We should have 0 annotations like at the beginning. - if i, _, err := api.ListAnnotationsFile(inFile, nil, nil); err != nil || i > 0 { - t.Fatalf("%s list: %v\n", msg, err) + if i := annotationCount(t, inFile); i > 0 { + t.Fatalf("%s count: got %d want 0\n", msg, i) } } @@ -358,7 +375,7 @@ func TestAddAnnotationsLowLevel(t *testing.T) { } // We should have 2 annotations. - i, _, err := pdfcpu.ListAnnotations(ctx, nil) + i, _, err := pdfcpu.ListAnnotations(ctx.PageAnnots) if err != nil || i != 2 { t.Fatalf("%s list: %v\n", msg, err) } @@ -370,7 +387,7 @@ func TestAddAnnotationsLowLevel(t *testing.T) { } // (before writing) We should have 0 annotations like at the beginning. - i, _, err = pdfcpu.ListAnnotations(ctx, nil) + i, _, err = pdfcpu.ListAnnotations(ctx.PageAnnots) if err != nil || i != 0 { t.Fatalf("%s list: %v\n", msg, err) } @@ -381,9 +398,8 @@ func TestAddAnnotationsLowLevel(t *testing.T) { } // (after writing) We should have 0 annotations like at the beginning. - i, _, err = api.ListAnnotationsFile(outFile, nil, nil) - if err != nil || i > 0 { - t.Fatalf("%s list: %v\n", msg, err) + if i := annotationCount(t, inFile); i > 0 { + t.Fatalf("%s count: got %d want 0\n", msg, i) } } diff --git a/pkg/api/test/api_test.go b/pkg/api/test/api_test.go index a2224e4ff..3036357da 100644 --- a/pkg/api/test/api_test.go +++ b/pkg/api/test/api_test.go @@ -222,7 +222,17 @@ func TestInfo(t *testing.T) { msg := "TestInfo" inFile := filepath.Join(inDir, "5116.DCT_Filter.pdf") - if _, err := api.InfoFile(inFile, nil, model.NewDefaultConfiguration()); err != nil { + f, err := os.Open(inFile) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + defer f.Close() + + info, err := api.PDFInfo(f, inFile, nil, conf) + if err != nil { t.Fatalf("%s: %v\n", msg, err) } + if info == nil { + t.Fatalf("%s: missing Info\n", msg) + } } diff --git a/pkg/api/test/attachment_test.go b/pkg/api/test/attachment_test.go index ccce80c54..397d24967 100644 --- a/pkg/api/test/attachment_test.go +++ b/pkg/api/test/attachment_test.go @@ -18,6 +18,7 @@ package test import ( "io" + "os" "path/filepath" "strings" "testing" @@ -39,19 +40,24 @@ func prepareForAttachmentTest(t *testing.T) error { return copyFile(t, filepath.Join(resDir, "test.wav"), filepath.Join(outDir, "test.wav")) } -func listAttachments(t *testing.T, msg, fileName string, want int) []string { +func listAttachments(t *testing.T, msg, fileName string, want int) { t.Helper() - list, err := api.ListAttachmentsFile(fileName, nil) + f, err := os.Open(fileName) + if err != nil { + t.Fatalf("%s open: %v\n", msg, err) + } + defer f.Close() + + aa, err := api.Attachments(f, nil) if err != nil { t.Fatalf("%s list attachments: %v\n", msg, err) } - got := len(list) + got := len(aa) if got != want { t.Fatalf("%s: list attachments %s: want %d got %d\n", msg, fileName, want, got) } - return list } func TestAttachments(t *testing.T) { @@ -76,10 +82,8 @@ func TestAttachments(t *testing.T) { if err := api.AddAttachmentsFile(fileName, "", files, false, nil); err != nil { t.Fatalf("%s add attachments: %v\n", msg, err) } - list := listAttachments(t, msg, fileName, 4) - for _, s := range list { - t.Log(s) - } + + listAttachments(t, msg, fileName, 4) // Extract all attachments. if err := api.ExtractAttachmentsFile(fileName, outDir, nil, nil); err != nil { diff --git a/pkg/api/test/bookmark_test.go b/pkg/api/test/bookmark_test.go index 1b74b53ad..71cdee7b0 100644 --- a/pkg/api/test/bookmark_test.go +++ b/pkg/api/test/bookmark_test.go @@ -17,17 +17,57 @@ package test import ( + "os" "path/filepath" "testing" + "time" "github.com/pdfcpu/pdfcpu/pkg/api" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" ) // Acrobat Reader "Bookmarks" = Mac Preview "Table of Contents". // Mac Preview limitations: does not render color, style, outline tree collapsed by default. +func listBookmarksFile(t *testing.T, fileName string, conf *model.Configuration) ([]string, error) { + t.Helper() + + msg := "listBookmarks" + + f, err := os.Open(fileName) + if err != nil { + t.Fatalf("%s open: %v\n", msg, err) + } + defer f.Close() + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + // Validation loads infodict. + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTBOOKMARKS + + ctx, _, _, _, err := api.ReadValidateAndOptimize(f, conf, time.Now()) + if err != nil { + t.Fatalf("%s ReadValidateAndOptimize: %v\n", msg, err) + } + + return pdfcpu.BookmarkList(ctx) +} + +func TestListBookmarks(t *testing.T) { + msg := "TestListBookmarks" + inDir := filepath.Join("..", "..", "samples", "bookmarks") + inFile := filepath.Join(inDir, "bookmarkTree.pdf") + + if _, err := listBookmarksFile(t, inFile, nil); err != nil { + t.Fatalf("%s list bookmarks: %v\n", msg, err) + } +} + func InactiveTestAddDuplicateBookmarks(t *testing.T) { msg := "TestAddDuplicateBookmarks" inFile := filepath.Join(inDir, "CenterOfWhy.pdf") @@ -84,15 +124,15 @@ func TestAddBookmarkTree2Levels(t *testing.T) { bms := []pdfcpu.Bookmark{ {PageFrom: 1, Title: "Page 1: Level 1", Color: &color.Green, - Children: []pdfcpu.Bookmark{ + Kids: []pdfcpu.Bookmark{ {PageFrom: 2, Title: "Page 2: Level 1.1"}, {PageFrom: 3, Title: "Page 3: Level 1.2", - Children: []pdfcpu.Bookmark{ + Kids: []pdfcpu.Bookmark{ {PageFrom: 4, Title: "Page 4: Level 1.2.1"}, }}, }}, {PageFrom: 5, Title: "Page 5: Level 2", Color: &color.Blue, - Children: []pdfcpu.Bookmark{ + Kids: []pdfcpu.Bookmark{ {PageFrom: 6, Title: "Page 6: Level 2.1"}, {PageFrom: 7, Title: "Page 7: Level 2.2"}, {PageFrom: 8, Title: "Page 8: Level 2.3"}, @@ -106,3 +146,44 @@ func TestAddBookmarkTree2Levels(t *testing.T) { t.Fatalf("%s: %v\n", msg, err) } } + +func TestRemoveBookmarks(t *testing.T) { + msg := "TestRemoveBookmarks" + inDir := filepath.Join("..", "..", "samples", "bookmarks") + inFile := filepath.Join(inDir, "bookmarkTree.pdf") + outFile := filepath.Join(inDir, "bookmarkTreeNoBookmarks.pdf") + + if err := api.RemoveBookmarksFile(inFile, outFile, nil); err != nil { + t.Fatalf("%s removeBookmarks: %v\n", msg, err) + } + if err := api.ValidateFile(outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestExportBookmarks(t *testing.T) { + msg := "TestExportBookmarks" + inDir := filepath.Join("..", "..", "samples", "bookmarks") + inFile := filepath.Join(inDir, "bookmarkTree.pdf") + outFile := filepath.Join(inDir, "bookmarkTree.json") + + if err := api.ExportBookmarksFile(inFile, outFile, nil); err != nil { + t.Fatalf("%s export bookmarks: %v\n", msg, err) + } +} + +func TestImportBookmarks(t *testing.T) { + msg := "TestImportBookmarks" + inDir := filepath.Join("..", "..", "samples", "bookmarks") + inFile := filepath.Join(inDir, "bookmarkTree.pdf") + inFileJSON := filepath.Join(inDir, "bookmarkTree.json") + outFile := filepath.Join(inDir, "bookmarkTreeImported.pdf") + + replace := true + if err := api.ImportBookmarksFile(inFile, inFileJSON, outFile, replace, nil); err != nil { + t.Fatalf("%s importBookmarks: %v\n", msg, err) + } + if err := api.ValidateFile(outFile, nil); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} diff --git a/pkg/api/test/box_test.go b/pkg/api/test/box_test.go index a38a0cedb..def378457 100644 --- a/pkg/api/test/box_test.go +++ b/pkg/api/test/box_test.go @@ -17,18 +17,49 @@ limitations under the License. package test import ( + "os" "path/filepath" "testing" + "time" "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" ) +func listBoxes(t *testing.T, fileName string, pb *model.PageBoundaries) ([]string, error) { + t.Helper() + + msg := "listBoxes" + + f, err := os.Open(fileName) + if err != nil { + t.Fatalf("%s open: %v\n", msg, err) + } + defer f.Close() + + ctx, _, _, _, err := api.ReadValidateAndOptimize(f, conf, time.Now()) + if err != nil { + t.Fatalf("%s ReadValidateAndOptimize: %v\n", msg, err) + } + + if err := ctx.EnsurePageCount(); err != nil { + t.Fatalf("%s EnsurePageCount: %v\n", msg, err) + } + + if pb == nil { + pb = &model.PageBoundaries{} + pb.SelectAll() + } + + return ctx.ListPageBoundaries(nil, pb) +} + func TestListBoxes(t *testing.T) { msg := "TestListBoxes" inFile := filepath.Join(inDir, "5116.DCT_Filter.pdf") - if _, err := api.ListBoxesFile(inFile, nil, nil, nil); err != nil { + if _, err := listBoxes(t, inFile, nil); err != nil { t.Fatalf("%s: %v\n", msg, err) } @@ -37,7 +68,7 @@ func TestListBoxes(t *testing.T) { if err != nil { t.Fatalf("%s: %v\n", msg, err) } - if _, err := api.ListBoxesFile(inFile, nil, pb, nil); err != nil { + if _, err := listBoxes(t, inFile, pb); err != nil { t.Fatalf("%s: %v\n", msg, err) } } diff --git a/pkg/api/test/encryption_test.go b/pkg/api/test/encryption_test.go index d05e2f1c5..4d44f1a5f 100644 --- a/pkg/api/test/encryption_test.go +++ b/pkg/api/test/encryption_test.go @@ -17,13 +17,41 @@ limitations under the License. package test import ( + "os" "path/filepath" "testing" + "time" "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" ) +func listPermissions(t *testing.T, fileName string) ([]string, error) { + t.Helper() + + msg := "listPermissions" + + f, err := os.Open(fileName) + if err != nil { + t.Fatalf("%s open: %v\n", msg, err) + } + defer f.Close() + + var conf *model.Configuration + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTPERMISSIONS + + ctx, _, _, _, err := api.ReadValidateAndOptimize(f, conf, time.Now()) + if err != nil { + return nil, err + } + + return pdfcpu.Permissions(ctx), nil +} + func confForAlgorithm(aes bool, keyLength int, upw, opw string) *model.Configuration { if aes { return model.NewAESConfiguration(upw, opw, keyLength) @@ -99,7 +127,7 @@ func testEncryption(t *testing.T, fileName string, alg string, keyLength int) { } // List permissions of encrypted file w/o passwords should fail. - if list, err := api.ListPermissionsFile(outFile, nil); err == nil { + if list, err := listPermissions(t, outFile); err == nil { t.Fatalf("%s: list permissions w/o pw %s: %v\n", msg, outFile, list) } @@ -157,9 +185,9 @@ func TestEncryption(t *testing.T) { "adobe_errata.pdf", } { testEncryption(t, fileName, "rc4", 40) - testEncryption(t, fileName, "rc4", 128) - testEncryption(t, fileName, "aes", 40) - testEncryption(t, fileName, "aes", 128) - testEncryption(t, fileName, "aes", 256) + //testEncryption(t, fileName, "rc4", 128) + //testEncryption(t, fileName, "aes", 40) + //testEncryption(t, fileName, "aes", 128) + //testEncryption(t, fileName, "aes", 256) } } diff --git a/pkg/api/test/form_test.go b/pkg/api/test/form_test.go index 6d81d25f8..72ce00db9 100644 --- a/pkg/api/test/form_test.go +++ b/pkg/api/test/form_test.go @@ -17,29 +17,60 @@ limitations under the License. package test import ( - "fmt" + "os" "path/filepath" "testing" + "time" "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/form" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" ) /************************************************************** * All form related processing is optimized for Adobe Reader! * **************************************************************/ +func listFormFieldsFile(t *testing.T, inFile string, conf *model.Configuration) ([]string, error) { + t.Helper() + + msg := "listFormFields" + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTFORMFIELDS + + f, err := os.Open(inFile) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + defer f.Close() + + ctx, _, _, _, err := api.ReadValidateAndOptimize(f, conf, time.Now()) + if err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + if err := ctx.EnsurePageCount(); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + return form.ListFormFields(ctx) +} + func TestListFormFields(t *testing.T) { msg := "TestListFormFields" inFile := filepath.Join(samplesDir, "form", "demo", "english.pdf") - ss, err := api.ListFormFieldsFile([]string{inFile}, conf) + ss, err := listFormFieldsFile(t, inFile, conf) if err != nil { t.Fatalf("%s: %v\n", msg, err) } - for _, s := range ss { - fmt.Println(s) + if len(ss) != 27 { + t.Fatalf("%s: want 27, got %d lines\n", msg, len(ss)) } } @@ -49,7 +80,7 @@ func TestRemoveFormFields(t *testing.T) { inFile := filepath.Join(samplesDir, "form", "demo", "english.pdf") outFile := filepath.Join(samplesDir, "form", "remove", "removedField.pdf") - ss, err := api.ListFormFieldsFile([]string{inFile}, conf) + ss, err := listFormFieldsFile(t, inFile, conf) if err != nil { t.Fatalf("%s: %v\n", msg, err) } @@ -59,7 +90,7 @@ func TestRemoveFormFields(t *testing.T) { t.Fatalf("%s: %v\n", msg, err) } - ss, err = api.ListFormFieldsFile([]string{outFile}, conf) + ss, err = listFormFieldsFile(t, outFile, conf) if err != nil { t.Fatalf("%s: %v\n", msg, err) } diff --git a/pkg/api/test/keyword_test.go b/pkg/api/test/keyword_test.go index 42f0411d4..a03936adb 100644 --- a/pkg/api/test/keyword_test.go +++ b/pkg/api/test/keyword_test.go @@ -17,16 +17,32 @@ limitations under the License. package test import ( + "os" "path/filepath" "testing" "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" ) +func listKeywordsFile(t *testing.T, fileName string, conf *model.Configuration) ([]string, error) { + t.Helper() + + msg := "listKeywords" + + f, err := os.Open(fileName) + if err != nil { + t.Fatalf("%s open: %v\n", msg, err) + } + defer f.Close() + + return api.Keywords(f, conf) +} + func listKeywords(t *testing.T, msg, fileName string, want []string) []string { t.Helper() - got, err := api.ListKeywordsFile(fileName, nil) + got, err := listKeywordsFile(t, fileName, nil) if err != nil { t.Fatalf("%s list keywords: %v\n", msg, err) } diff --git a/pkg/api/test/portfolio_test.go b/pkg/api/test/portfolio_test.go index 306bf7ac7..f70f2313a 100644 --- a/pkg/api/test/portfolio_test.go +++ b/pkg/api/test/portfolio_test.go @@ -47,10 +47,7 @@ func TestPortfolio(t *testing.T) { } // List portfolio entries. - list := listAttachments(t, msg, fileName, 4) - for _, s := range list { - t.Log(s) - } + listAttachments(t, msg, fileName, 4) // Extract all portfolio entries. if err := api.ExtractAttachmentsFile(fileName, outDir, nil, nil); err != nil { diff --git a/pkg/api/test/property_test.go b/pkg/api/test/property_test.go index de74668cc..38a2986c7 100644 --- a/pkg/api/test/property_test.go +++ b/pkg/api/test/property_test.go @@ -17,16 +17,47 @@ limitations under the License. package test import ( + "os" "path/filepath" "testing" + "time" "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" ) +func listPropertiesFile(t *testing.T, fileName string, conf *model.Configuration) ([]string, error) { + t.Helper() + + msg := "listProperties" + + f, err := os.Open(fileName) + if err != nil { + t.Fatalf("%s open: %v\n", msg, err) + } + defer f.Close() + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + // Validation loads infodict. + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTPROPERTIES + + ctx, _, _, _, err := api.ReadValidateAndOptimize(f, conf, time.Now()) + if err != nil { + t.Fatalf("%s ReadValidateAndOptimize: %v\n", msg, err) + } + + return pdfcpu.PropertiesList(ctx) +} + func listProperties(t *testing.T, msg, fileName string, want []string) []string { t.Helper() - got, err := api.ListPropertiesFile(fileName, nil) + got, err := listPropertiesFile(t, fileName, nil) if err != nil { t.Fatalf("%s list properties: %v\n", msg, err) } diff --git a/pkg/api/test/selectPages_test.go b/pkg/api/test/selectPages_test.go index fb5419f9e..3751471d9 100644 --- a/pkg/api/test/selectPages_test.go +++ b/pkg/api/test/selectPages_test.go @@ -81,7 +81,7 @@ func testSelectedPages(s string, pageCount int, compareString string, t *testing t.Fatalf("testSelectedPages(%s) %v\n", s, err) } - selectedPages, err := api.PagesForPageSelection(pageCount, pageSelection, false) + selectedPages, err := api.PagesForPageSelection(pageCount, pageSelection, false, true) if err != nil { t.Fatalf("testSelectedPages(%s) %v\n", s, err) } diff --git a/pkg/api/trim.go b/pkg/api/trim.go index e890dd8f2..0f60f6e72 100644 --- a/pkg/api/trim.go +++ b/pkg/api/trim.go @@ -23,18 +23,23 @@ import ( "github.com/pdfcpu/pdfcpu/pkg/log" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" ) // Trim generates a trimmed version of rs // containing all selected pages and writes the result to w. func Trim(rs io.ReadSeeker, w io.Writer, selectedPages []string, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Trim: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } conf.Cmd = model.TRIM fromStart := time.Now() - ctx, durRead, durVal, durOpt, err := readValidateAndOptimize(rs, conf, fromStart) + ctx, durRead, durVal, durOpt, err := ReadValidateAndOptimize(rs, conf, fromStart) if err != nil { return err } @@ -45,7 +50,7 @@ func Trim(rs io.ReadSeeker, w io.Writer, selectedPages []string, conf *model.Con fromWrite := time.Now() - pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, false) + pages, err := PagesForPageSelection(ctx.PageCount, selectedPages, false, true) if err != nil { return err } diff --git a/pkg/api/validate.go b/pkg/api/validate.go index be09c52cf..06c6cfa60 100644 --- a/pkg/api/validate.go +++ b/pkg/api/validate.go @@ -29,6 +29,10 @@ import ( // Validate validates a PDF stream read from rs. func Validate(rs io.ReadSeeker, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: Validate: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } @@ -123,8 +127,12 @@ func ValidateFiles(inFiles []string, conf *model.Configuration) error { return nil } -// Validate validates a PDF stream read from rs. +// DumpObject writes an object from rs to stdout. func DumpObject(rs io.ReadSeeker, objNr int, hex bool, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: DumpObject: missing rs") + } + if conf == nil { conf = model.NewDefaultConfiguration() } @@ -148,6 +156,7 @@ func DumpObject(rs io.ReadSeeker, objNr int, hex bool, conf *model.Configuration return err } +// DumpObjectFile writes an object from rs to stdout. func DumpObjectFile(inFile string, objNr int, hex bool, conf *model.Configuration) error { if conf == nil { conf = model.NewDefaultConfiguration() diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 4cad23d9a..b1e1dc981 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -59,8 +59,7 @@ func ChangeOwnerPassword(cmd *Command) ([]string, error) { // ListPermissions of inFile. func ListPermissions(cmd *Command) ([]string, error) { - return api.ListPermissionsFile(*cmd.InFile, cmd.Conf) - // TODO turn struct into []string + return ListPermissionsFile(*cmd.InFile, cmd.Conf) } // SetPermissions of inFile. @@ -161,8 +160,7 @@ func ExtractMetadata(cmd *Command) ([]string, error) { // ListAttachments returns a list of embedded file attachments for inFile. func ListAttachments(cmd *Command) ([]string, error) { - return api.ListAttachmentsFile(*cmd.InFile, cmd.Conf) - // TODO turn struct into []string + return ListAttachmentsFile(*cmd.InFile, cmd.Conf) } // AddAttachments embeds inFiles into a PDF context read from inFile and writes the result to outFile. @@ -180,10 +178,9 @@ func ExtractAttachments(cmd *Command) ([]string, error) { return nil, api.ExtractAttachmentsFile(*cmd.InFile, *cmd.OutDir, cmd.InFiles, cmd.Conf) } -// Info gathers information about inFile and returns the result as []string. -func Info(cmd *Command) ([]string, error) { - return api.InfoFiles(cmd.InFiles, cmd.PageSelection, cmd.Conf) - // TODO turn struct into []string +// ListInfo gathers information about inFile and returns the result as []string. +func ListInfo(cmd *Command) ([]string, error) { + return ListInfoFiles(cmd.InFiles, cmd.PageSelection, cmd.BoolVal, cmd.Conf) } // CreateCheatSheetsFonts creates single page PDF cheat sheets for user fonts in current dir. @@ -194,7 +191,6 @@ func CreateCheatSheetsFonts(cmd *Command) ([]string, error) { // ListFonts gathers information about supported fonts and returns the result as []string. func ListFonts(cmd *Command) ([]string, error) { return api.ListFonts() - // TODO turn struct into []string } // InstallFonts installs True Type fonts into the pdfcpu pconfig dir. @@ -204,8 +200,7 @@ func InstallFonts(cmd *Command) ([]string, error) { // ListKeywords returns a list of keywords for inFile. func ListKeywords(cmd *Command) ([]string, error) { - return api.ListKeywordsFile(*cmd.InFile, cmd.Conf) - // TODO turn struct into []string + return ListKeywordsFile(*cmd.InFile, cmd.Conf) } // AddKeywords adds keywords to inFile's document info dict and writes the result to outFile. @@ -220,8 +215,7 @@ func RemoveKeywords(cmd *Command) ([]string, error) { // ListProperties returns inFile's properties. func ListProperties(cmd *Command) ([]string, error) { - return api.ListPropertiesFile(*cmd.InFile, cmd.Conf) - // TODO turn struct into []string + return ListPropertiesFile(*cmd.InFile, cmd.Conf) } // AddProperties adds properties to inFile's document info dict and writes the result to outFile. @@ -241,8 +235,7 @@ func Collect(cmd *Command) ([]string, error) { // ListBoxes returns inFile's page boundaries. func ListBoxes(cmd *Command) ([]string, error) { - return api.ListBoxesFile(*cmd.InFile, cmd.PageSelection, cmd.PageBoundaries, cmd.Conf) - // TODO turn struct into []string + return ListBoxesFile(*cmd.InFile, cmd.PageSelection, cmd.PageBoundaries, cmd.Conf) } // AddBoxes adds page boundaries to inFile's page tree and writes the result to outFile. @@ -262,8 +255,7 @@ func Crop(cmd *Command) ([]string, error) { // ListAnnotations returns inFile's page annotations. func ListAnnotations(cmd *Command) ([]string, error) { - _, ss, err := api.ListAnnotationsFile(*cmd.InFile, cmd.PageSelection, cmd.Conf) - // TODO turn struct into []string + _, ss, err := ListAnnotationsFile(*cmd.InFile, cmd.PageSelection, cmd.Conf) return ss, err } @@ -275,8 +267,7 @@ func RemoveAnnotations(cmd *Command) ([]string, error) { // ListImages returns inFiles embedded images. func ListImages(cmd *Command) ([]string, error) { - return api.ListImagesFile(cmd.InFiles, cmd.PageSelection, cmd.Conf) - // turn struct into []string + return ListImagesFile(cmd.InFiles, cmd.PageSelection, cmd.Conf) } // Dump known object to stdout. @@ -286,7 +277,7 @@ func Dump(cmd *Command) ([]string, error) { return nil, api.DumpObjectFile(*cmd.InFile, objNr, hex, cmd.Conf) } -// Create renders page content corresponding to declarations found in inJSONFile and writes the result to outFile. +// Create renders page content corresponding to declarations found in inFileJSON and writes the result to outFile. // If inFile is present, page content will be appended, func Create(cmd *Command) ([]string, error) { return nil, api.CreateFile(*cmd.InFile, *cmd.InFileJSON, *cmd.OutFile, cmd.Conf) @@ -294,8 +285,7 @@ func Create(cmd *Command) ([]string, error) { // ListFormFields returns inFile's form field ids. func ListFormFields(cmd *Command) ([]string, error) { - return api.ListFormFieldsFile(cmd.InFiles, cmd.Conf) - // TODO turn struct into []string + return ListFormFieldsFile(cmd.InFiles, cmd.Conf) } // RemoveFormFields removes some form fields from inFile. @@ -352,3 +342,23 @@ func NDown(cmd *Command) ([]string, error) { func Cut(cmd *Command) ([]string, error) { return nil, api.CutFile(*cmd.InFile, *cmd.OutDir, *cmd.OutFile, cmd.PageSelection, cmd.Cut, cmd.Conf) } + +// ListBookmarks returns inFile's outlines. +func ListBookmarks(cmd *Command) ([]string, error) { + return ListBookmarksFile(*cmd.InFile, cmd.Conf) +} + +// ExportBookmarks returns a representation of inFile's outlines as outFileJSON. +func ExportBookmarks(cmd *Command) ([]string, error) { + return nil, api.ExportBookmarksFile(*cmd.InFile, *cmd.OutFileJSON, cmd.Conf) +} + +// ImportBookmarks creates/replaces outlines of inFile corresponding to declarations found in inJSONFile and writes the result to outFile. +func ImportBookmarks(cmd *Command) ([]string, error) { + return nil, api.ImportBookmarksFile(*cmd.InFile, *cmd.InFileJSON, *cmd.OutFile, cmd.BoolVal, cmd.Conf) +} + +// RemoveBookmarks erases outlines of inFile. +func RemoveBookmarks(cmd *Command) ([]string, error) { + return nil, api.RemoveBookmarksFile(*cmd.InFile, *cmd.OutFile, cmd.Conf) +} diff --git a/pkg/cli/cmd.go b/pkg/cli/cmd.go index a3bdf33d5..1ac667dc4 100644 --- a/pkg/cli/cmd.go +++ b/pkg/cli/cmd.go @@ -86,7 +86,7 @@ var cmdMap = map[model.CommandMode]func(cmd *Command) ([]string, error){ model.ROTATE: Rotate, model.NUP: NUp, model.BOOKLET: Booklet, - model.INFO: Info, + model.LISTINFO: ListInfo, model.CHEATSHEETSFONTS: CreateCheatSheetsFonts, model.INSTALLFONTS: InstallFonts, model.LISTFONTS: ListFonts, @@ -118,6 +118,10 @@ var cmdMap = map[model.CommandMode]func(cmd *Command) ([]string, error){ model.POSTER: Poster, model.NDOWN: NDown, model.CUT: Cut, + model.LISTBOOKMARKS: processBookmarks, + model.EXPORTBOOKMARKS: processBookmarks, + model.IMPORTBOOKMARKS: processBookmarks, + model.REMOVEBOOKMARKS: processBookmarks, } // ValidateCommand creates a new command to validate a file. @@ -542,15 +546,16 @@ func BookletCommand(inFiles []string, outFile string, pageSelection []string, nu } // InfoCommand creates a new command to output information about inFile. -func InfoCommand(inFiles []string, pageSelection []string, conf *model.Configuration) *Command { +func InfoCommand(inFiles []string, pageSelection []string, json bool, conf *model.Configuration) *Command { if conf == nil { conf = model.NewDefaultConfiguration() } - conf.Cmd = model.INFO + conf.Cmd = model.LISTINFO return &Command{ - Mode: model.INFO, + Mode: model.LISTINFO, InFiles: inFiles, PageSelection: pageSelection, + BoolVal: json, Conf: conf} } @@ -985,3 +990,56 @@ func CutCommand(inFile, outDir, outFile string, pageSelection []string, cut *mod Cut: cut, Conf: conf} } + +// ListBookmarksCommand creates a new command to list bookmarks of inFile. +func ListBookmarksCommand(inFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTBOOKMARKS + return &Command{ + Mode: model.LISTBOOKMARKS, + InFile: &inFile, + Conf: conf} +} + +// ExportBookmarksCommand creates a new command to export bookmarks of inFile. +func ExportBookmarksCommand(inFile, outFileJSON string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.EXPORTBOOKMARKS + return &Command{ + Mode: model.EXPORTBOOKMARKS, + InFile: &inFile, + OutFileJSON: &outFileJSON, + Conf: conf} +} + +// ImportBookmarksCommand creates a new command to import bookmarks to inFile. +func ImportBookmarksCommand(inFile, inFileJSON, outFile string, replace bool, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.IMPORTBOOKMARKS + return &Command{ + Mode: model.IMPORTBOOKMARKS, + BoolVal: replace, + InFile: &inFile, + InFileJSON: &inFileJSON, + OutFile: &outFile, + Conf: conf} +} + +// RemoveBookmarksCommand creates a new command to remove all bookmarks from inFile. +func RemoveBookmarksCommand(inFile, outFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.REMOVEBOOKMARKS + return &Command{ + Mode: model.REMOVEBOOKMARKS, + InFile: &inFile, + OutFile: &outFile, + Conf: conf} +} diff --git a/pkg/cli/list.go b/pkg/cli/list.go new file mode 100644 index 000000000..de13f37d8 --- /dev/null +++ b/pkg/cli/list.go @@ -0,0 +1,459 @@ +/* +Copyright 2023 The pdfcpu Authors. + +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 cli provides pdfcpu command line processing. +package cli + +import ( + "encoding/json" + "fmt" + "io" + "os" + "sort" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/log" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/form" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pkg/errors" +) + +func listAttachments(rs io.ReadSeeker, conf *model.Configuration, withDesc, sorted bool) ([]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: listAttachments: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTATTACHMENTS + + ctx, _, _, _, err := api.ReadValidateAndOptimize(rs, conf, time.Now()) + if err != nil { + return nil, err + } + + aa, err := ctx.ListAttachments() + if err != nil { + return nil, err + } + + var ss []string + for _, a := range aa { + s := a.FileName + if withDesc && a.Desc != "" { + s = fmt.Sprintf("%s (%s)", s, a.Desc) + } + ss = append(ss, s) + } + if sorted { + sort.Strings(ss) + } + + return ss, nil +} + +// ListAttachmentsFile returns a list of embedded file attachments of inFile with optional description. +func ListAttachmentsFile(inFile string, conf *model.Configuration) ([]string, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return listAttachments(f, conf, true, true) +} + +// ListAttachmentsCompactFile returns a list of embedded file attachments of inFile w/o optional description. +func ListAttachmentsCompactFile(inFile string, conf *model.Configuration) ([]string, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return listAttachments(f, conf, false, false) +} + +func listAnnotations(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) (int, []string, error) { + annots, err := api.Annotations(rs, selectedPages, conf) + if err != nil { + return 0, nil, err + } + + return pdfcpu.ListAnnotations(annots) +} + +// ListAnnotationsFile returns a list of page annotations of inFile. +func ListAnnotationsFile(inFile string, selectedPages []string, conf *model.Configuration) (int, []string, error) { + f, err := os.Open(inFile) + if err != nil { + return 0, nil, err + } + defer f.Close() + + return listAnnotations(f, selectedPages, conf) +} + +func listBoxes(rs io.ReadSeeker, selectedPages []string, pb *model.PageBoundaries, conf *model.Configuration) ([]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: listBoxes: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTBOXES + + ctx, _, _, _, err := api.ReadValidateAndOptimize(rs, conf, time.Now()) + if err != nil { + return nil, err + } + + if err := ctx.EnsurePageCount(); err != nil { + return nil, err + } + + pages, err := api.PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return nil, err + } + + return ctx.ListPageBoundaries(pages, pb) +} + +// ListBoxesFile returns a list of page boundaries for selected pages of inFile. +func ListBoxesFile(inFile string, selectedPages []string, pb *model.PageBoundaries, conf *model.Configuration) ([]string, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + if pb == nil { + pb = &model.PageBoundaries{} + pb.SelectAll() + } + log.CLI.Printf("listing %s for %s\n", pb, inFile) + + return listBoxes(f, selectedPages, pb, conf) +} + +func listFormFields(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTFORMFIELDS + + ctx, _, _, _, err := api.ReadValidateAndOptimize(rs, conf, time.Now()) + if err != nil { + return nil, err + } + + if err := ctx.EnsurePageCount(); err != nil { + return nil, err + } + + return form.ListFormFields(ctx) +} + +// ListFormFieldsFile returns a list of form field ids in inFile. +func ListFormFieldsFile(inFiles []string, conf *model.Configuration) ([]string, error) { + ss := []string{} + + for _, fn := range inFiles { + + f, err := os.Open(fn) + if err != nil { + if len(inFiles) > 1 { + ss = append(ss, fmt.Sprintf("\ncan't open %s: %v", fn, err)) + continue + } + return nil, err + } + defer f.Close() + + output, err := listFormFields(f, conf) + if err != nil { + if len(inFiles) > 1 { + ss = append(ss, fmt.Sprintf("\n%s: %v", fn, err)) + continue + } + return nil, err + } + + ss = append(ss, "\n"+fn) + ss = append(ss, output...) + } + + return ss, nil +} + +func listImages(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) ([]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: listImages: Please provide rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTIMAGES + + ctx, _, _, _, err := api.ReadValidateAndOptimize(rs, conf, time.Now()) + if err != nil { + return nil, err + } + + if err := ctx.EnsurePageCount(); err != nil { + return nil, err + } + + pages, err := api.PagesForPageSelection(ctx.PageCount, selectedPages, true, true) + if err != nil { + return nil, err + } + + return pdfcpu.ListImages(ctx, pages) +} + +// ListImagesFile returns a formatted list of embedded images of inFile. +func ListImagesFile(inFiles []string, selectedPages []string, conf *model.Configuration) ([]string, error) { + if len(selectedPages) == 0 { + log.CLI.Printf("pages: all\n") + } + + ss := []string{} + + for _, fn := range inFiles { + f, err := os.Open(fn) + if err != nil { + if len(inFiles) > 1 { + ss = append(ss, fmt.Sprintf("\ncan't open %s: %v", fn, err)) + continue + } + return nil, err + } + defer f.Close() + output, err := listImages(f, selectedPages, conf) + if err != nil { + if len(inFiles) > 1 { + ss = append(ss, fmt.Sprintf("\n%s: %v", fn, err)) + continue + } + return nil, err + } + ss = append(ss, "\n"+fn) + ss = append(ss, output...) + } + + return ss, nil +} + +// ListInfoFile returns formatted information about inFile. +func ListInfoFile(inFile string, selectedPages []string, conf *model.Configuration) ([]string, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + info, err := api.PDFInfo(f, inFile, selectedPages, conf) + if err != nil { + return nil, err + } + + pages, err := api.PagesForPageSelection(info.PageCount, selectedPages, false, false) + if err != nil { + return nil, err + } + + ss, err := pdfcpu.ListInfo(info, pages) + if err != nil { + return nil, err + } + + return append([]string{inFile + ":"}, ss...), err +} + +func listInfoFilesJSON(inFiles []string, selectedPages []string, conf *model.Configuration) ([]string, error) { + var infos []*pdfcpu.PDFInfo + + for _, fn := range inFiles { + + f, err := os.Open(fn) + if err != nil { + return nil, err + } + defer f.Close() + + info, err := api.PDFInfo(f, fn, selectedPages, conf) + if err != nil { + return nil, err + } + + infos = append(infos, info) + } + + s := struct { + Header pdfcpu.Header `json:"header"` + Infos []*pdfcpu.PDFInfo + }{ + Header: pdfcpu.Header{Version: "pdfcpu " + model.VersionStr, Creation: time.Now().Format("2006-01-02 15:04:05 MST")}, + Infos: infos, + } + + bb, err := json.MarshalIndent(s, "", "\t") + if err != nil { + return nil, err + } + + return []string{string(bb)}, nil +} + +// ListInfoFiles returns formatted information about inFiles. +func ListInfoFiles(inFiles []string, selectedPages []string, json bool, conf *model.Configuration) ([]string, error) { + + if json { + return listInfoFilesJSON(inFiles, selectedPages, conf) + } + + var ss []string + + for i, fn := range inFiles { + if i > 0 { + ss = append(ss, "") + } + ssx, err := ListInfoFile(fn, selectedPages, conf) + if err != nil { + if len(inFiles) == 1 { + return nil, err + } + fmt.Fprintf(os.Stderr, "%s: %v\n", fn, err) + } + ss = append(ss, ssx...) + } + + return ss, nil +} + +// ListKeywordsFile returns the keyword list of inFile. +func ListKeywordsFile(inFile string, conf *model.Configuration) ([]string, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return api.Keywords(f, conf) +} + +func listPermissions(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: listPermissions: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTPERMISSIONS + + ctx, _, _, _, err := api.ReadValidateAndOptimize(rs, conf, time.Now()) + if err != nil { + return nil, err + } + + return pdfcpu.Permissions(ctx), nil +} + +// ListPermissionsFile returns a list of user access permissions for inFile. +func ListPermissionsFile(inFile string, conf *model.Configuration) ([]string, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + + defer func() { + f.Close() + }() + + return listPermissions(f, conf) +} + +func listProperties(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: listProperties: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + // Validation loads infodict. + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTPROPERTIES + + ctx, _, _, _, err := api.ReadValidateAndOptimize(rs, conf, time.Now()) + if err != nil { + return nil, err + } + + return pdfcpu.PropertiesList(ctx) +} + +// ListPropertiesFile returns the property list of inFile. +func ListPropertiesFile(inFile string, conf *model.Configuration) ([]string, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return listProperties(f, conf) +} + +func listBookmarks(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { + if rs == nil { + return nil, errors.New("pdfcpu: listBookmarks: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + // Validation loads infodict. + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTBOOKMARKS + + ctx, _, _, _, err := api.ReadValidateAndOptimize(rs, conf, time.Now()) + if err != nil { + return nil, err + } + + return pdfcpu.BookmarkList(ctx) +} + +// ListBookmarksFile returns the bookmarks of inFile. +func ListBookmarksFile(inFile string, conf *model.Configuration) ([]string, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return listBookmarks(f, conf) +} diff --git a/pkg/cli/process.go b/pkg/cli/process.go index e97ab0224..3ac58a691 100644 --- a/pkg/cli/process.go +++ b/pkg/cli/process.go @@ -208,3 +208,21 @@ func processForm(cmd *Command) (out []string, err error) { return nil, nil } + +func processBookmarks(cmd *Command) (out []string, err error) { + switch cmd.Mode { + + case model.LISTBOOKMARKS: + return ListBookmarks(cmd) + + case model.EXPORTBOOKMARKS: + return ExportBookmarks(cmd) + + case model.IMPORTBOOKMARKS: + return ImportBookmarks(cmd) + + case model.REMOVEBOOKMARKS: + return RemoveBookmarks(cmd) + } + return nil, nil +} diff --git a/pkg/cli/test/bookmark_test.go b/pkg/cli/test/bookmark_test.go new file mode 100644 index 000000000..aafb87c57 --- /dev/null +++ b/pkg/cli/test/bookmark_test.go @@ -0,0 +1,86 @@ +/* + Copyright 2023 The pdfcpu Authors. + + 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 test + +import ( + "path/filepath" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +func TestListBookmarks(t *testing.T) { + msg := "TestListBookmarks" + inDir := filepath.Join("..", "..", "samples", "bookmarks") + inFile := filepath.Join(inDir, "bookmarkTree.pdf") + + cmd := cli.ListBookmarksCommand(inFile, conf) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestExportBookmarks(t *testing.T) { + msg := "TestExportBookmarks" + inDir := filepath.Join("..", "..", "samples", "bookmarks") + inFile := filepath.Join(inDir, "bookmarkTree.pdf") + outFile := filepath.Join(outDir, "bookmarkTree.json") + + cmd := cli.ExportBookmarksCommand(inFile, outFile, nil) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestImportBookmarks(t *testing.T) { + msg := "TestImportBookmarks" + inDir := filepath.Join("..", "..", "samples", "bookmarks") + inFile := filepath.Join(inDir, "bookmarkTree.pdf") + inFileJSON := filepath.Join(inDir, "bookmarkTree.json") + outFile := filepath.Join(outDir, "bookmarkTreeImported.pdf") + + replace := true + cmd := cli.ImportBookmarksCommand(inFile, inFileJSON, outFile, replace, nil) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + if err := api.ImportBookmarksFile(inFile, inFileJSON, outFile, replace, nil); err != nil { + t.Fatalf("%s importBookmarks: %v\n", msg, err) + } + + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} + +func TestRemoveBookmarks(t *testing.T) { + msg := "TestRemoveBookmarks" + inDir := filepath.Join("..", "..", "samples", "bookmarks") + inFile := filepath.Join(inDir, "bookmarkTree.pdf") + outFile := filepath.Join(outDir, "bookmarkTreeNoBookmarks.pdf") + + cmd := cli.RemoveBookmarksCommand(inFile, outFile, nil) + if _, err := cli.Process(cmd); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } + + if err := validateFile(t, outFile, conf); err != nil { + t.Fatalf("%s: %v\n", msg, err) + } +} diff --git a/pkg/cli/test/box_test.go b/pkg/cli/test/box_test.go index 5104af255..bd5220053 100644 --- a/pkg/cli/test/box_test.go +++ b/pkg/cli/test/box_test.go @@ -74,6 +74,7 @@ func TestCropCommand(t *testing.T) { if _, err := cli.Process(cmd); err != nil { t.Fatalf("%s: %v\n", msg, err) } + } } diff --git a/pkg/cli/test/cli_test.go b/pkg/cli/test/cli_test.go index e69cbd482..f8c246d90 100644 --- a/pkg/cli/test/cli_test.go +++ b/pkg/cli/test/cli_test.go @@ -153,7 +153,7 @@ func TestInfoCommand(t *testing.T) { msg := "TestInfoCommand" inFile := filepath.Join(inDir, "5116.DCT_Filter.pdf") - cmd := cli.InfoCommand([]string{inFile}, nil, conf) + cmd := cli.InfoCommand([]string{inFile}, nil, true, conf) if _, err := cli.Process(cmd); err != nil { t.Fatalf("%s: %v\n", msg, err) } diff --git a/pkg/pdfcpu/annotation.go b/pkg/pdfcpu/annotation.go index 9dbfdbf41..271f9ce34 100644 --- a/pkg/pdfcpu/annotation.go +++ b/pkg/pdfcpu/annotation.go @@ -205,30 +205,52 @@ func Annotation(xRefTable *model.XRefTable, d types.Dict) (model.AnnotationRende return ann, nil } -// ListAnnotations returns a formatted list of annotations for selected pages. -func ListAnnotations(ctx *model.Context, selectedPages types.IntSet) (int, []string, error) { - var ( - j int - pageNrs []int - ) - ss := []string{} +func AnnotationsForSelectedPages(ctx *model.Context, selectedPages types.IntSet) map[int]model.PgAnnots { + var pageNrs []int for k := range ctx.PageAnnots { pageNrs = append(pageNrs, k) } sort.Ints(pageNrs) + m := map[int]model.PgAnnots{} + for _, i := range pageNrs { + if selectedPages != nil { if _, found := selectedPages[i]; !found { continue } } + pageAnnots := ctx.PageAnnots[i] if len(pageAnnots) == 0 { continue } + m[i] = pageAnnots + } + + return m +} + +// ListAnnotations returns a formatted list of annotations. +func ListAnnotations(annots map[int]model.PgAnnots) (int, []string, error) { + var ( + j int + pageNrs []int + ) + ss := []string{} + + for k := range annots { + pageNrs = append(pageNrs, k) + } + sort.Ints(pageNrs) + + for _, i := range pageNrs { + + pageAnnots := annots[i] + var annTypes []string for t := range pageAnnots { annTypes = append(annTypes, model.AnnotTypeStrings[t]) diff --git a/pkg/pdfcpu/bookmark.go b/pkg/pdfcpu/bookmark.go index f24c3cd90..0987517a9 100644 --- a/pkg/pdfcpu/bookmark.go +++ b/pkg/pdfcpu/bookmark.go @@ -17,7 +17,12 @@ package pdfcpu import ( + "bytes" + "encoding/json" + "io" + "path/filepath" "strings" + "time" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" @@ -31,25 +36,58 @@ var ( errExistingBookmarks = errors.New("pdfcpu: existing bookmarks") ) +type Header struct { + Source string `json:"source,omitempty"` + Version string `json:"version"` + Creation string `json:"creation"` + ID []string `json:"id,omitempty"` + Title string `json:"title,omitempty"` + Author string `json:"author,omitempty"` + Creator string `json:"creator,omitempty"` + Producer string `json:"producer,omitempty"` + Subject string `json:"subject,omitempty"` + Keywords string `json:"keywords,omitempty"` +} + // Bookmark represents an outline item tree. type Bookmark struct { - Title string - PageFrom int - PageThru int // for extraction only; >= pageFrom and reaches until before pageFrom of the next bookmark. - Bold bool - Italic bool - Color *color.SimpleColor - Children []Bookmark - Parent *Bookmark + Title string `json:"title"` + PageFrom int `json:"page"` + PageThru int `json:"-"` // for extraction only; >= pageFrom and reaches until before pageFrom of the next bookmark. + Bold bool `json:"bold,omitempty"` + Italic bool `json:"italic,omitempty"` + Color *color.SimpleColor `json:"color,omitempty"` + Kids []Bookmark `json:"kids,omitempty"` + Parent *Bookmark `json:"-"` +} + +type BookmarkTree struct { + Header Header `json:"header"` + Bookmarks []Bookmark `json:"bookmarks"` +} + +func header(xRefTable *model.XRefTable, source string) Header { + h := Header{} + h.Source = filepath.Base(source) + h.Version = "pdfcpu " + model.VersionStr + h.Creation = time.Now().Format("2006-01-02 15:04:05 MST") + h.ID = []string{} + h.Title = xRefTable.Title + h.Author = xRefTable.Author + h.Creator = xRefTable.Creator + h.Producer = xRefTable.Producer + h.Subject = xRefTable.Subject + h.Keywords = xRefTable.Keywords + return h } // Style returns an int corresponding to the bookmark style. func (bm Bookmark) Style() int { var i int - if bm.Bold { + if bm.Bold { // bit 1 i += 2 } - if bm.Italic { + if bm.Italic { // bit 0 i += 1 } return i @@ -106,12 +144,20 @@ func PageObjFromDestination(ctx *model.Context, dest types.Object) (*types.Indir ir = arr[0].(types.IndirectRef) } case types.StringLiteral: - arr, err = ctx.DereferenceDestArray(dest.Value()) + s, err := types.StringLiteralToString(dest) + if err != nil { + return nil, err + } + arr, err = ctx.DereferenceDestArray(s) if err == nil { ir = arr[0].(types.IndirectRef) } case types.HexLiteral: - arr, err = ctx.DereferenceDestArray(dest.Value()) + s, err := types.HexLiteralToString(dest) + if err != nil { + return nil, err + } + arr, err = ctx.DereferenceDestArray(s) if err == nil { ir = arr[0].(types.IndirectRef) } @@ -119,7 +165,6 @@ func PageObjFromDestination(ctx *model.Context, dest types.Object) (*types.Indir if dest[0] != nil { ir = dest[0].(types.IndirectRef) } - // else skipping bookmarks that don't point to anything. } return &ir, err } @@ -158,12 +203,12 @@ func BookmarksForOutlineItem(ctx *model.Context, item *types.IndirectRef, parent dest = act.(types.Dict)["D"] } - dest, err := ctx.Dereference(dest) + obj, err := ctx.Dereference(dest) if err != nil { return nil, err } - ir, err := PageObjFromDestination(ctx, dest) + ir, err := PageObjFromDestination(ctx, obj) if err != nil { return nil, err } @@ -188,13 +233,25 @@ func BookmarksForOutlineItem(ctx *model.Context, item *types.IndirectRef, parent Title: title, PageFrom: pageFrom, Parent: parent, + Bold: false, + Italic: false, + } + + if arr := d.ArrayEntry("C"); len(arr) == 3 { + col := color.NewSimpleColorForArray(arr) + newBookmark.Color = &col + } + + if f := d.IntEntry("F"); f != nil { + newBookmark.Bold = *f&0x02 > 0 + newBookmark.Italic = *f&0x01 > 0 } first := d["First"] if first != nil { indRef := first.(types.IndirectRef) - children, _ := BookmarksForOutlineItem(ctx, &indRef, &newBookmark) - newBookmark.Children = children + kids, _ := BookmarksForOutlineItem(ctx, &indRef, &newBookmark) + newBookmark.Kids = kids } bms = append(bms, newBookmark) @@ -203,8 +260,8 @@ func BookmarksForOutlineItem(ctx *model.Context, item *types.IndirectRef, parent return bms, nil } -// BookmarksForOutline returns all ctx bookmark information recursively. -func BookmarksForOutline(ctx *model.Context) ([]Bookmark, error) { +// Bookmarks returns all ctx bookmark information recursively. +func Bookmarks(ctx *model.Context) ([]Bookmark, error) { if err := ctx.LocateNameTree("Dests", false); err != nil { return nil, err @@ -212,12 +269,77 @@ func BookmarksForOutline(ctx *model.Context) ([]Bookmark, error) { _, first, err := positionToFirstBookmark(ctx) if err != nil { - return nil, err + if err != errNoBookmarks { + return nil, err + } + return nil, nil } return BookmarksForOutlineItem(ctx, first, nil) } +func bookmarkList(bms []Bookmark, level int) ([]string, error) { + pre := strings.Repeat(" ", level) + ss := []string{} + for _, bm := range bms { + ss = append(ss, pre+bm.Title) + if len(bm.Kids) > 0 { + ss1, err := bookmarkList(bm.Kids, level+1) + if err != nil { + return nil, err + } + ss = append(ss, ss1...) + } + } + return ss, nil +} + +func BookmarkList(ctx *model.Context) ([]string, error) { + + bms, err := Bookmarks(ctx) + if err != nil { + return nil, err + } + + if bms == nil { + return []string{"no bookmarks available"}, nil + } + + return bookmarkList(bms, 0) +} + +func ExportBookmarks(ctx *model.Context, source string) (*BookmarkTree, error) { + bms, err := Bookmarks(ctx) + if err != nil { + return nil, err + } + if bms == nil { + return nil, nil + } + + bmTree := BookmarkTree{} + bmTree.Header = header(ctx.XRefTable, source) + bmTree.Bookmarks = bms + + return &bmTree, nil +} + +func ExportBookmarksJSON(ctx *model.Context, source string, w io.Writer) (bool, error) { + bookmarkTree, err := ExportBookmarks(ctx, source) + if err != nil || bookmarkTree == nil { + return false, err + } + + bb, err := json.MarshalIndent(bookmarkTree, "", "\t") + if err != nil { + return false, err + } + + _, err = w.Write(bb) + + return true, err +} + func bmDict(ctx *model.Context, bm Bookmark, parent types.IndirectRef) (types.Dict, error) { _, pageIndRef, _, err := ctx.PageDict(bm.PageFrom, false) @@ -295,9 +417,9 @@ func createOutlineItemDict(ctx *model.Context, bms []Bookmark, parent *types.Ind first = ir } - if len(bm.Children) > 0 { + if len(bm.Kids) > 0 { - first, last, c, visc, err := createOutlineItemDict(ctx, bm.Children, ir, &bm.PageFrom) + first, last, c, visc, err := createOutlineItemDict(ctx, bm.Kids, ir, &bm.PageFrom) if err != nil { return nil, nil, 0, 0, err } @@ -331,15 +453,98 @@ func createOutlineItemDict(ctx *model.Context, bms []Bookmark, parent *types.Ind return first, irPrev, total, visible, nil } -// AddBookmarks adds bms to ctx. -func AddBookmarks(ctx *model.Context, bms []Bookmark, replace bool) error { +func removeNamedDests(ctx *model.Context, item *types.IndirectRef) error { + var ( + d types.Dict + err error + empty, ok bool + ) + for ir := item; ir != nil; ir = d.IndirectRefEntry("Next") { + + if d, err = ctx.DereferenceDict(*ir); err != nil { + return err + } + + dest, destFound := d["Dest"] + if !destFound { + act, actFound := d["A"] + if !actFound { + continue + } + act, _ = ctx.Dereference(act) + actType := act.(types.Dict)["S"] + if actType.String() != "GoTo" { + continue + } + dest = act.(types.Dict)["D"] + } + + s, err := ctx.DestName(dest) + if err != nil { + return err + } + + if len(s) == 0 { + continue + } + + // Remove destName from dest nametree. + // TODO also try to remove from any existing root.Dests + empty, ok, err = ctx.Names["Dests"].Remove(ctx.XRefTable, s) + if err != nil { + return err + } + if !ok { + println("unable remove dest name: " + s) + } + + first := d["First"] + if first != nil { + indRef := first.(types.IndirectRef) + if err := removeNamedDests(ctx, &indRef); err != nil { + return err + } + } + } + + if empty { + delete(ctx.Names, "Dests") + if err := ctx.RemoveNameTree("Dests"); err != nil { + return err + } + } + + return nil +} + +// RemoveBookmarks erases all outlines from ctx. +func RemoveBookmarks(ctx *model.Context) (bool, error) { + _, first, err := positionToFirstBookmark(ctx) + if err != nil { + if err != errNoBookmarks { + return false, err + } + return false, nil + } + if err := removeNamedDests(ctx, first); err != nil { + return false, err + } rootDict, err := ctx.Catalog() if err != nil { - return err + return false, err } - if err := ctx.LocateNameTree("Dests", true); err != nil { + rootDict["Outlines"] = nil + + return true, nil +} + +// AddBookmarks adds bms to ctx. +func AddBookmarks(ctx *model.Context, bms []Bookmark, replace bool) error { + + rootDict, err := ctx.Catalog() + if err != nil { return err } @@ -349,6 +554,14 @@ func AddBookmarks(ctx *model.Context, bms []Bookmark, replace bool) error { } } + if _, err = RemoveBookmarks(ctx); err != nil { + return err + } + + if err := ctx.LocateNameTree("Dests", true); err != nil { + return err + } + outlinesDict := types.Dict(map[string]types.Object{"Type": types.Name("Outlines")}) outlinesir, err := ctx.IndRefForNewObject(outlinesDict) if err != nil { @@ -368,3 +581,46 @@ func AddBookmarks(ctx *model.Context, bms []Bookmark, replace bool) error { return nil } + +func addBookmarkTree(ctx *model.Context, bmTree *BookmarkTree, replace bool) error { + return AddBookmarks(ctx, bmTree.Bookmarks, replace) +} + +func parseBookmarksFromJSON(ctx *model.Context, bb []byte) (*BookmarkTree, error) { + + if !json.Valid(bb) { + return nil, errors.Errorf("pdfcpu: invalid JSON encoding detected.") + } + + bmTree := &BookmarkTree{} + + if err := json.Unmarshal(bb, bmTree); err != nil { + return nil, err + } + + return bmTree, nil +} + +// ImportBookmarks creates/replaces outlines in ctx as provided by rd. +func ImportBookmarks(ctx *model.Context, rd io.Reader, replace bool) (bool, error) { + + var buf bytes.Buffer + if _, err := io.Copy(&buf, rd); err != nil { + return false, err + } + + bmTree, err := parseBookmarksFromJSON(ctx, buf.Bytes()) + if err != nil { + return false, err + } + + err = addBookmarkTree(ctx, bmTree, replace) + if err != nil { + if err == errExistingBookmarks { + return false, nil + } + return true, err + } + + return true, nil +} diff --git a/pkg/pdfcpu/color/color.go b/pkg/pdfcpu/color/color.go index fb0cd25dc..96aa504ae 100644 --- a/pkg/pdfcpu/color/color.go +++ b/pkg/pdfcpu/color/color.go @@ -63,9 +63,26 @@ func NewSimpleColor(rgb uint32) SimpleColor { // NewSimpleColorForArray returns a SimpleColor for an r,g,b array. func NewSimpleColorForArray(arr types.Array) SimpleColor { - r := float32(arr[0].(types.Float).Value()) - g := float32(arr[1].(types.Float).Value()) - b := float32(arr[2].(types.Float).Value()) + var r, g, b float32 + + if f, ok := arr[0].(types.Float); ok { + r = float32(f.Value()) + } else { + r = float32(arr[0].(types.Integer)) + } + + if f, ok := arr[1].(types.Float); ok { + g = float32(f.Value()) + } else { + g = float32(arr[1].(types.Integer)) + } + + if f, ok := arr[2].(types.Float); ok { + b = float32(f.Value()) + } else { + b = float32(arr[2].(types.Integer)) + } + return SimpleColor{r, g, b} } diff --git a/pkg/pdfcpu/create/create.go b/pkg/pdfcpu/create/create.go index a2a501e06..737794f04 100644 --- a/pkg/pdfcpu/create/create.go +++ b/pkg/pdfcpu/create/create.go @@ -400,11 +400,11 @@ func UpdatePage(xRefTable *model.XRefTable, dIndRef types.IndirectRef, d, res ty func cacheFormFieldIDs(ctx *model.Context, pdf *primitives.PDF) error { - if ctx.AcroForm == nil { + if ctx.Form == nil { return nil } - o, found := ctx.AcroForm.Find("Fields") + o, found := ctx.Form.Find("Fields") if !found { return nil } @@ -615,7 +615,7 @@ func prepareFormFontResDict(ctx *model.Context, pdf *primitives.PDF, fonts model return d, nil } -func createAcroForm( +func createForm( ctx *model.Context, pdf *primitives.PDF, fields types.Array, @@ -636,13 +636,13 @@ func createAcroForm( return nil } -func updateAcroForm( +func updateForm( ctx *model.Context, pdf *primitives.PDF, fields types.Array, fonts model.FontMap) error { - d := ctx.AcroForm + d := ctx.Form o, _ := d.Find("Fields") arr, err := ctx.DereferenceArray(o) @@ -696,7 +696,7 @@ func updateAcroForm( return nil } -func handleAcroForm( +func handleForm( ctx *model.Context, pdf *primitives.PDF, fields types.Array, @@ -704,9 +704,9 @@ func handleAcroForm( var err error if pdf.Update() && pdf.HasForm { - err = updateAcroForm(ctx, pdf, fields, fonts) + err = updateForm(ctx, pdf, fields, fonts) } else { - err = createAcroForm(ctx, pdf, fields, fonts) + err = createForm(ctx, pdf, fields, fonts) } if err != nil { return err @@ -748,7 +748,7 @@ func FromJSON(ctx *model.Context, rd io.Reader) error { } if len(fields) > 0 { - if err := handleAcroForm(ctx, pdf, fields, fonts); err != nil { + if err := handleForm(ctx, pdf, fields, fonts); err != nil { return err } } diff --git a/pkg/pdfcpu/createTestPDF.go b/pkg/pdfcpu/createTestPDF.go index 846e41e2e..bf6e12694 100644 --- a/pkg/pdfcpu/createTestPDF.go +++ b/pkg/pdfcpu/createTestPDF.go @@ -933,7 +933,7 @@ func createPageWithAnnotations(xRefTable *model.XRefTable, parentPageIndRef type return pageIndRef, nil } -func createPageWithAcroForm(xRefTable *model.XRefTable, parentPageIndRef types.IndirectRef, annotsArray types.Array, mediaBox *types.Rectangle, fontName string) (*types.IndirectRef, error) { +func createPageWithForm(xRefTable *model.XRefTable, parentPageIndRef types.IndirectRef, annotsArray types.Array, mediaBox *types.Rectangle, fontName string) (*types.IndirectRef, error) { mba := mediaBox.Array() pageDict := types.Dict( @@ -1047,7 +1047,7 @@ func addPageTreeWithAnnotations(xRefTable *model.XRefTable, rootDict types.Dict, return pageIndRef, nil } -func addPageTreeWithAcroFields(xRefTable *model.XRefTable, rootDict types.Dict, annotsArray types.Array, fontName string) (*types.IndirectRef, error) { +func addPageTreeWithFormFields(xRefTable *model.XRefTable, rootDict types.Dict, annotsArray types.Array, fontName string) (*types.IndirectRef, error) { // mediabox = physical page dimensions mediaBox := types.RectForFormat("A4") mba := mediaBox.Array() @@ -1066,7 +1066,7 @@ func addPageTreeWithAcroFields(xRefTable *model.XRefTable, rootDict types.Dict, return nil, err } - pageIndRef, err := createPageWithAcroForm(xRefTable, *parentPageIndRef, annotsArray, mediaBox, fontName) + pageIndRef, err := createPageWithForm(xRefTable, *parentPageIndRef, annotsArray, mediaBox, fontName) if err != nil { return nil, err } @@ -1833,7 +1833,7 @@ func createXFAArray(xRefTable *model.XRefTable) (types.Array, error) { }, nil } -func createAcroFormDict(xRefTable *model.XRefTable, fontName string) (types.Dict, types.Array, error) { +func createFormDict(xRefTable *model.XRefTable, fontName string) (types.Dict, types.Array, error) { pageAnnots := types.Array{} text, err := createFormTextField(xRefTable, &pageAnnots, fontName) @@ -1878,8 +1878,8 @@ func createAcroFormDict(xRefTable *model.XRefTable, fontName string) (types.Dict return d, pageAnnots, nil } -// CreateAcroFormDemoXRef creates an xRefTable with an AcroForm example. -func CreateAcroFormDemoXRef() (*model.XRefTable, error) { +// CreateFormDemoXRef creates an xRefTable with an AcroForm example. +func CreateFormDemoXRef() (*model.XRefTable, error) { fontName := "Helvetica" xRefTable, err := CreateXRefTableWithRootDict() @@ -1892,14 +1892,14 @@ func CreateAcroFormDemoXRef() (*model.XRefTable, error) { return nil, err } - acroFormDict, annotsArray, err := createAcroFormDict(xRefTable, fontName) + formDict, annotsArray, err := createFormDict(xRefTable, fontName) if err != nil { return nil, err } - rootDict.Insert("AcroForm", acroFormDict) + rootDict.Insert("AcroForm", formDict) - _, err = addPageTreeWithAcroFields(xRefTable, rootDict, annotsArray, fontName) + _, err = addPageTreeWithFormFields(xRefTable, rootDict, annotsArray, fontName) if err != nil { return nil, err } diff --git a/pkg/pdfcpu/crypto.go b/pkg/pdfcpu/crypto.go index 469e92dc0..c998ff40c 100644 --- a/pkg/pdfcpu/crypto.go +++ b/pkg/pdfcpu/crypto.go @@ -50,7 +50,7 @@ var ( // Needed permission bits for pdfcpu commands. perm = map[model.CommandMode]struct{ extract, modify int }{ model.VALIDATE: {0, 0}, - model.INFO: {0, 0}, + model.LISTINFO: {0, 0}, model.OPTIMIZE: {0, 0}, model.SPLIT: {1, 0}, model.MERGECREATE: {0, 0}, @@ -91,7 +91,11 @@ var ( model.ROTATE: {0, 1}, model.NUP: {0, 1}, model.BOOKLET: {0, 1}, + model.LISTBOOKMARKS: {0, 0}, model.ADDBOOKMARKS: {0, 1}, + model.REMOVEBOOKMARKS: {0, 1}, + model.IMPORTBOOKMARKS: {0, 1}, + model.EXPORTBOOKMARKS: {0, 1}, model.LISTIMAGES: {0, 1}, model.CREATE: {0, 0}, model.DUMP: {0, 1}, @@ -575,24 +579,25 @@ func perms(p int) (list []string) { return list } -func addPermissionsToInfoDigest(ctx *model.Context, ss *[]string) { - l := Permissions(ctx) - if len(l) == 1 { - *ss = append(*ss, fmt.Sprintf("%20s: %s", "Permissions", l[0])) - } else { - *ss = append(*ss, fmt.Sprintf("%20s:", "Permissions")) - *ss = append(*ss, l...) +// PermissionsList returns a list of set permissions. +func PermissionsList(p int) (list []string) { + + if p == 0 { + return append(list, "Full access") } + + return perms(p) } // Permissions returns a list of set permissions. func Permissions(ctx *model.Context) (list []string) { - if ctx.E == nil { - return append(list, "Full access") + p := 0 + if ctx.E != nil { + p = ctx.E.P } - return perms(ctx.E.P) + return PermissionsList(p) } func validatePermissions(ctx *model.Context) (bool, error) { diff --git a/pkg/pdfcpu/doc.go b/pkg/pdfcpu/doc.go index 1ec624e16..ff31d5d8c 100644 --- a/pkg/pdfcpu/doc.go +++ b/pkg/pdfcpu/doc.go @@ -7,6 +7,7 @@ The commands are: annotations list, remove page annotations attachments list, add, remove, extract embedded file attachments booklet arrange pages onto larger sheets of paper to make a booklet or zine + bookmarks list, import, export, remove bookmarks boxes list, add, remove page boundaries for selected pages changeopw change owner password changeupw change user password diff --git a/pkg/pdfcpu/font/fontDict.go b/pkg/pdfcpu/font/fontDict.go index ca7599b7f..66aa59db0 100644 --- a/pkg/pdfcpu/font/fontDict.go +++ b/pkg/pdfcpu/font/fontDict.go @@ -1060,31 +1060,41 @@ func Name(xRefTable *model.XRefTable, fontDict types.Dict, objNumber int) (prefi } // Lang detects the optional language indicator in a font dict. -func Lang(xRefTable *model.XRefTable, d types.Dict) (*string, error) { +func Lang(xRefTable *model.XRefTable, d types.Dict) (string, error) { o, found := d.Find("FontDescriptor") if found { fd, err := xRefTable.DereferenceDict(o) if err != nil { - return nil, err + return "", err + } + var s string + n := fd.NameEntry("Lang") + if n != nil { + s = *n } - return fd.NameEntry("Lang"), nil + return s, nil } arr := d.ArrayEntry("DescendantFonts") indRef := arr[0].(types.IndirectRef) d1, err := xRefTable.DereferenceDict(indRef) if err != nil { - return nil, err + return "", err } o, found = d1.Find("FontDescriptor") if found { fd, err := xRefTable.DereferenceDict(o) if err != nil { - return nil, err + return "", err + } + var s string + n := fd.NameEntry("Lang") + if n != nil { + s = *n } - return fd.NameEntry("Lang"), nil + return s, nil } - return nil, nil + return "", nil } diff --git a/pkg/pdfcpu/form/export.go b/pkg/pdfcpu/form/export.go index 61b13155f..078357145 100644 --- a/pkg/pdfcpu/form/export.go +++ b/pkg/pdfcpu/form/export.go @@ -43,7 +43,7 @@ type Header struct { Keywords string `json:"keywords,omitempty"` } -// TextField represents an Acroform text field. +// TextField represents a form text field. type TextField struct { Pages []int `json:"pages"` ID string `json:"id"` @@ -65,7 +65,7 @@ type DateField struct { Locked bool `json:"locked"` } -// RadioButtonGroup represents an Acroform checkbox. +// RadioButtonGroup represents a form checkbox. type CheckBox struct { Pages []int `json:"pages"` ID string `json:"id"` @@ -75,7 +75,7 @@ type CheckBox struct { Locked bool `json:"locked"` } -// RadioButtonGroup represents an Acroform radio button group. +// RadioButtonGroup represents a form radio button group. type RadioButtonGroup struct { Pages []int `json:"pages"` ID string `json:"id"` @@ -86,7 +86,7 @@ type RadioButtonGroup struct { Locked bool `json:"locked"` } -// ListBox represents an Acroform combobox. +// ComboBox represents a form combobox. type ComboBox struct { Pages []int `json:"pages"` ID string `json:"id"` @@ -98,7 +98,7 @@ type ComboBox struct { Locked bool `json:"locked"` } -// ListBox represents an Acroform listbox. +// ListBox represents a form listbox. type ListBox struct { Pages []int `json:"pages"` ID string `json:"id"` @@ -699,8 +699,8 @@ func exportPageFields(xRefTable *model.XRefTable, i int, form *Form, m map[strin return nil } -// ExportFormToStruct extracts form data originating from source from xRefTable. -func ExportFormToStruct(xRefTable *model.XRefTable, source string) (*FormGroup, bool, error) { +// ExportForm extracts form data originating from source from xRefTable. +func ExportForm(xRefTable *model.XRefTable, source string) (*FormGroup, bool, error) { fields, err := fields(xRefTable) if err != nil { @@ -746,10 +746,10 @@ func ExportFormToStruct(xRefTable *model.XRefTable, source string) (*FormGroup, return &formGroup, ok, nil } -// ExportForm extracts form data originating from source from xRefTable and writes a JSON representation to w. -func ExportForm(xRefTable *model.XRefTable, source string, w io.Writer) (bool, error) { +// ExportFormJSON extracts form data originating from source from xRefTable and writes a JSON representation to w. +func ExportFormJSON(xRefTable *model.XRefTable, source string, w io.Writer) (bool, error) { - formGroup, ok, err := ExportFormToStruct(xRefTable, source) + formGroup, ok, err := ExportForm(xRefTable, source) if err != nil || !ok { return false, err } diff --git a/pkg/pdfcpu/form/form.go b/pkg/pdfcpu/form/form.go index a414b85b8..9c37f3a0d 100644 --- a/pkg/pdfcpu/form/form.go +++ b/pkg/pdfcpu/form/form.go @@ -94,11 +94,11 @@ type FieldMeta struct { func fields(xRefTable *model.XRefTable) (types.Array, error) { - if xRefTable.AcroForm == nil { + if xRefTable.Form == nil { return nil, errors.New("pdfcpu: no form available") } - o, ok := xRefTable.AcroForm.Find("Fields") + o, ok := xRefTable.Form.Find("Fields") if !ok { return nil, errors.New("pdfcpu: no form fields available") } @@ -871,21 +871,32 @@ func renderFields(ctx *model.Context, fs []Field, fm *FieldMeta) ([]string, erro return ss, nil } -// ListFormFields returns a list of all form fields present in xRefTable. -func ListFormFields(ctx *model.Context) ([]string, error) { - - // TODO Align output for Bangla, Hindi, Marathi. +// FormFields returns all form fields present in ctx. +func FormFields(ctx *model.Context) ([]Field, *FieldMeta, error) { xRefTable := ctx.XRefTable fields, err := fields(xRefTable) if err != nil { - return nil, err + return nil, nil, err } fm := &FieldMeta{pageMax: 2, idMax: 3, nameMax: 4, defMax: 7, valMax: 5} fs, err := collectFields(xRefTable, fields, fm) + if err != nil { + return nil, nil, err + } + + return fs, fm, nil +} + +// ListFormFields returns a list of all form fields present in ctx. +func ListFormFields(ctx *model.Context) ([]string, error) { + + // TODO Align output for Bangla, Hindi, Marathi. + + fs, fm, err := FormFields(ctx) if err != nil { return nil, err } @@ -1142,7 +1153,7 @@ func RemoveFormFields(ctx *model.Context, fieldIDsOrNames []string) (bool, error if len(fields) == 0 { ctx.RootDict.Delete("AcroForm") } else { - xRefTable.AcroForm["Fields"] = fields + xRefTable.Form["Fields"] = fields } var ok bool diff --git a/pkg/pdfcpu/image.go b/pkg/pdfcpu/image.go index fdec4aa13..8eb41af28 100644 --- a/pkg/pdfcpu/image.go +++ b/pkg/pdfcpu/image.go @@ -29,19 +29,67 @@ import ( "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" ) -func prepHorSep(horSep *[]int, maxLenObjNr, maxLenID, maxLenSize, maxLenFilters int) string { +// Images returns all embedded images of ctx. +func Images(ctx *model.Context, selectedPages types.IntSet) ([]map[int]model.Image, *ImageListMaxLengths, error) { + pageNrs := []int{} + for k, v := range selectedPages { + if !v { + continue + } + pageNrs = append(pageNrs, k) + } + sort.Ints(pageNrs) + + mm := []map[int]model.Image{} + var ( + maxLenObjNr, maxLenID, maxLenSize, maxLenFilters int + ) + + for _, i := range pageNrs { + m, err := ExtractPageImages(ctx, i, true) + if err != nil { + return nil, nil, err + } + if len(m) == 0 { + continue + } + for _, i := range m { + s := strconv.Itoa(i.ObjNr) + if len(s) > maxLenObjNr { + maxLenObjNr = len(s) + } + if len(i.Name) > maxLenID { + maxLenID = len(i.Name) + } + lenSize := len(types.ByteSize(i.Size).String()) + if lenSize > maxLenSize { + maxLenSize = lenSize + } + if len(i.Filter) > maxLenFilters { + maxLenFilters = len(i.Filter) + } + } + mm = append(mm, m) + } + + maxLen := &ImageListMaxLengths{ObjNr: maxLenObjNr, ID: maxLenID, Size: maxLenSize, Filters: maxLenFilters} + + return mm, maxLen, nil +} + +func prepHorSep(horSep *[]int, maxLen *ImageListMaxLengths) string { s := "Page Obj# " - if maxLenObjNr > 4 { - s += strings.Repeat(" ", maxLenObjNr-4) - *horSep = append(*horSep, 10+maxLenObjNr-4) + if maxLen.ObjNr > 4 { + s += strings.Repeat(" ", maxLen.ObjNr-4) + *horSep = append(*horSep, 10+maxLen.ObjNr-4) } else { *horSep = append(*horSep, 10) } s += draw.VBar + " Id " - if maxLenID > 2 { - s += strings.Repeat(" ", maxLenID-2) - *horSep = append(*horSep, 4+maxLenID-2) + if maxLen.ID > 2 { + s += strings.Repeat(" ", maxLen.ID-2) + *horSep = append(*horSep, 4+maxLen.ID-2) } else { *horSep = append(*horSep, 4) } @@ -53,15 +101,15 @@ func prepHorSep(horSep *[]int, maxLenObjNr, maxLenID, maxLenSize, maxLenFilters *horSep = append(*horSep, 7, 8, 28) s += draw.VBar + " " - if maxLenSize > 4 { - s += strings.Repeat(" ", maxLenSize-4) - *horSep = append(*horSep, 6+maxLenSize-4) + if maxLen.Size > 4 { + s += strings.Repeat(" ", maxLen.Size-4) + *horSep = append(*horSep, 6+maxLen.Size-4) } else { *horSep = append(*horSep, 6) } s += "Size " + draw.VBar + " Filters" - if maxLenFilters > 7 { - *horSep = append(*horSep, 8+maxLenFilters-7) + if maxLen.Filters > 7 { + *horSep = append(*horSep, 8+maxLen.Filters-7) } else { *horSep = append(*horSep, 8) } @@ -78,7 +126,7 @@ func sortedObjNrs(ii map[int]model.Image) []int { return objNrs } -func listImages(ctx *model.Context, mm []map[int]model.Image, maxLenObjNr, maxLenID, maxLenSize, maxLenFilters int) ([]string, int, int64, error) { +func listImages(ctx *model.Context, mm []map[int]model.Image, maxLen *ImageListMaxLengths) ([]string, int, int64, error) { ss := []string{} first := true j, size := 0, int64(0) @@ -86,7 +134,7 @@ func listImages(ctx *model.Context, mm []map[int]model.Image, maxLenObjNr, maxLe horSep := []int{} for _, ii := range mm { if first { - s := prepHorSep(&horSep, maxLenObjNr, maxLenID, maxLenSize, maxLenFilters) + s := prepHorSep(&horSep, maxLen) ss = append(ss, s) first = false } @@ -130,20 +178,20 @@ func listImages(ctx *model.Context, mm []map[int]model.Image, maxLenObjNr, maxLe } s := strconv.Itoa(img.ObjNr) - fill1 := strings.Repeat(" ", maxLenObjNr-len(s)) - if maxLenObjNr < 4 { - fill1 += strings.Repeat(" ", 4-maxLenObjNr) + fill1 := strings.Repeat(" ", maxLen.ObjNr-len(s)) + if maxLen.ObjNr < 4 { + fill1 += strings.Repeat(" ", 4-maxLen.ObjNr) } - fill2 := strings.Repeat(" ", maxLenID-len(img.Name)) - if maxLenID < 2 { - fill2 += strings.Repeat(" ", 2-maxLenID-len(img.Name)) + fill2 := strings.Repeat(" ", maxLen.ID-len(img.Name)) + if maxLen.ID < 2 { + fill2 += strings.Repeat(" ", 2-maxLen.ID-len(img.Name)) } sizeStr := types.ByteSize(img.Size).String() - fill3 := strings.Repeat(" ", maxLenSize-len(sizeStr)) - if maxLenSize < 4 { - fill3 = strings.Repeat(" ", 4-maxLenSize) + fill3 := strings.Repeat(" ", maxLen.Size-len(sizeStr)) + if maxLen.Size < 4 { + fill3 = strings.Repeat(" ", 4-maxLen.Size) } ss = append(ss, fmt.Sprintf("%4s %s%s %s %s%s %s %s %s %s %s %5d %s %5d %s %10s %d %s %s %s %s%s %s %s", @@ -165,50 +213,19 @@ func listImages(ctx *model.Context, mm []map[int]model.Image, maxLenObjNr, maxLe return ss, j, size, nil } -// ListImages returns a list of embedded images. -func ListImages(ctx *model.Context, selectedPages types.IntSet) ([]string, error) { - pageNrs := []int{} - for k, v := range selectedPages { - if !v { - continue - } - pageNrs = append(pageNrs, k) - } - sort.Ints(pageNrs) +type ImageListMaxLengths struct { + ObjNr, ID, Size, Filters int +} - mm := []map[int]model.Image{} - var ( - maxLenObjNr, maxLenID, maxLenSize, maxLenFilters int - ) +// ListImages returns a formatted list of embedded images. +func ListImages(ctx *model.Context, selectedPages types.IntSet) ([]string, error) { - for _, i := range pageNrs { - m, err := ExtractPageImages(ctx, i, true) - if err != nil { - return nil, err - } - if len(m) == 0 { - continue - } - for _, i := range m { - s := strconv.Itoa(i.ObjNr) - if len(s) > maxLenObjNr { - maxLenObjNr = len(s) - } - if len(i.Name) > maxLenID { - maxLenID = len(i.Name) - } - lenSize := len(types.ByteSize(i.Size).String()) - if lenSize > maxLenSize { - maxLenSize = lenSize - } - if len(i.Filter) > maxLenFilters { - maxLenFilters = len(i.Filter) - } - } - mm = append(mm, m) + mm, maxLen, err := Images(ctx, selectedPages) + if err != nil { + return nil, err } - ss, j, size, err := listImages(ctx, mm, maxLenObjNr, maxLenID, maxLenSize, maxLenFilters) + ss, j, size, err := listImages(ctx, mm, maxLen) if err != nil { return nil, err } diff --git a/pkg/pdfcpu/info.go b/pkg/pdfcpu/info.go index ecefee418..11cd55de4 100644 --- a/pkg/pdfcpu/info.go +++ b/pkg/pdfcpu/info.go @@ -18,6 +18,7 @@ package pdfcpu import ( "fmt" + "sort" "time" "github.com/pdfcpu/pdfcpu/pkg/log" @@ -258,104 +259,146 @@ func appendPageBoxesInfo(ss *[]string, pb model.PageBoundaries, unit string, cur appendNotEqualMediaAndCropBoxInfo(ss, pb, unit, currUnit) } -func pageInfo(ctx *model.Context, selectedPages types.IntSet) ([]string, error) { - unit := ctx.UnitString() +func pageInfo(info *PDFInfo, selectedPages types.IntSet) ([]string, error) { + + ss := []string{} + if len(selectedPages) > 0 { - // TODO ctx.PageBoundaries(selectedPages) - pbs, err := ctx.PageBoundaries() - if err != nil { - return nil, err - } - ss := []string{} - for i, pb := range pbs { + for i, pb := range info.PageBoundaries { if _, found := selectedPages[i+1]; !found { continue } - appendPageBoxesInfo(&ss, pb, unit, ctx.Unit, i) + appendPageBoxesInfo(&ss, pb, info.UnitString, info.Unit, i) } return ss, nil } - pd, err := ctx.PageDims() - if err != nil { - return nil, err + s := "Page size:" + for d := range info.PageDimensions { + dc := d.ConvertToUnit(info.Unit) + ss = append(ss, fmt.Sprintf("%21s %.2f x %.2f %s", s, dc.Width, dc.Height, info.UnitString)) + s = "" } + return ss, nil +} - m := map[types.Dim]bool{} - for _, d := range pd { - m[d] = true - } +type PDFInfo struct { + FileName string `json:"source,omitempty"` + Version string `json:"version"` + PageCount int `json:"pages"` + PageBoundaries []model.PageBoundaries `json:"-"` + PageDimensions map[types.Dim]bool `json:"-"` + Title string `json:"title"` + Author string `json:"author"` + Subject string `json:"subject"` + Producer string `json:"producer"` + Creator string `json:"creator"` + CreationDate string `json:"creationDate"` + ModificationDate string `json:"modificationDate"` + Keywords []string `json:"keywords"` + Properties map[string]string `json:"properties"` + Tagged bool `json:"tagged"` + Hybrid bool `json:"hybrid"` + Linearized bool `json:"linearized"` + UsingXRefStreams bool `json:"usingXRefStreams"` + UsingObjectStreams bool `json:"usingObjectStreams"` + Watermarked bool `json:"watermarked"` + Thumbnails bool `json:"thumbnails"` + Form bool `json:"form"` + Signatures bool `json:"signatures"` + AppendOnly bool `json:"appendOnly"` + Outlines bool `json:"bookmarks"` + Names bool `json:"names"` + Encrypted bool `json:"encrypted"` + Permissions int `json:"permissions"` + Attachments []model.Attachment `json:"attachments,omitempty"` + Unit types.DisplayUnit `json:"-"` + UnitString string `json:"-"` +} - ss := []string{} - s := "Page size:" - for d := range m { - dc := ctx.ConvertToUnit(d) - ss = append(ss, fmt.Sprintf("%21s %.2f x %.2f %s", s, dc.Width, dc.Height, unit)) - s = "" +func (info PDFInfo) renderKeywords(ss *[]string) error { + for i, l := range info.Keywords { + if i == 0 { + *ss = append(*ss, fmt.Sprintf("%20s: %s", "Keywords", l)) + continue + } + *ss = append(*ss, fmt.Sprintf("%20s %s", "", l)) } + return nil +} - return ss, nil +func (info PDFInfo) renderProperties(ss *[]string) error { + first := true + for k, v := range info.Properties { + if first { + *ss = append(*ss, fmt.Sprintf("%20s: %s = %s", "Properties", k, v)) + first = false + continue + } + *ss = append(*ss, fmt.Sprintf("%20s %s = %s", "", k, v)) + } + return nil } -func addFlagsToInfoDigestPart1(ctx *model.Context, ss *[]string, separator string) { +func (info PDFInfo) renderFlagsPart1(ss *[]string, separator string) { *ss = append(*ss, separator) s := "No" - if ctx.Tagged { + if info.Tagged { s = "Yes" } *ss = append(*ss, fmt.Sprintf(" Tagged: %s", s)) s = "No" - if ctx.Read.Hybrid { + if info.Hybrid { s = "Yes" } *ss = append(*ss, fmt.Sprintf(" Hybrid: %s", s)) s = "No" - if ctx.Read.Linearized { + if info.Linearized { s = "Yes" } *ss = append(*ss, fmt.Sprintf(" Linearized: %s", s)) s = "No" - if ctx.Read.UsingXRefStreams { + if info.UsingXRefStreams { s = "Yes" } *ss = append(*ss, fmt.Sprintf(" Using XRef streams: %s", s)) s = "No" - if ctx.Read.UsingObjectStreams { + if info.UsingObjectStreams { s = "Yes" } *ss = append(*ss, fmt.Sprintf("Using object streams: %s", s)) } -func addFlagsToInfoDigestPart2(ctx *model.Context, ss *[]string, separator string) { +func (info PDFInfo) renderFlagsPart2(ss *[]string, separator string) { s := "No" - if ctx.Watermarked { + if info.Watermarked { s = "Yes" } *ss = append(*ss, fmt.Sprintf(" Watermarked: %s", s)) s = "No" - if len(ctx.PageThumbs) > 0 { + if info.Thumbnails { s = "Yes" } *ss = append(*ss, fmt.Sprintf(" Thumbnails: %s", s)) s = "No" - if ctx.AcroForm != nil { + if info.Form { s = "Yes" } - *ss = append(*ss, fmt.Sprintf(" Acroform: %s", s)) - if ctx.AcroForm != nil { - if ctx.SignatureExist || ctx.AppendOnly { + *ss = append(*ss, fmt.Sprintf(" Form: %s", s)) + if info.Form { + if info.Signatures || info.AppendOnly { *ss = append(*ss, " SignaturesExist: Yes") s = "No" - if ctx.AppendOnly { + if info.AppendOnly { s = "Yes" } *ss = append(*ss, fmt.Sprintf(" AppendOnly: %s", s)) @@ -363,13 +406,13 @@ func addFlagsToInfoDigestPart2(ctx *model.Context, ss *[]string, separator strin } s = "No" - if ctx.Outlines != nil { + if info.Outlines { s = "Yes" } *ss = append(*ss, fmt.Sprintf(" Outlines: %s", s)) s = "No" - if len(ctx.Names) > 0 { + if info.Names { s = "Yes" } *ss = append(*ss, fmt.Sprintf(" Names: %s", s)) @@ -377,60 +420,138 @@ func addFlagsToInfoDigestPart2(ctx *model.Context, ss *[]string, separator strin *ss = append(*ss, separator) s = "No" - if ctx.Encrypt != nil { + if info.Encrypted { s = "Yes" } *ss = append(*ss, fmt.Sprintf("%20s: %s", "Encrypted", s)) } -func addFlagsToInfoDigest(ctx *model.Context, ss *[]string, separator string) { - addFlagsToInfoDigestPart1(ctx, ss, separator) - addFlagsToInfoDigestPart2(ctx, ss, separator) +func (info *PDFInfo) renderFlags(ss *[]string, separator string) { + info.renderFlagsPart1(ss, separator) + info.renderFlagsPart2(ss, separator) } -// InfoDigest returns info about ctx. -func InfoDigest(ctx *model.Context, selectedPages types.IntSet) ([]string, error) { - var separator = draw.HorSepLine([]int{44}) +func (info *PDFInfo) renderPermissions(ss *[]string) { + l := PermissionsList(info.Permissions) + if len(l) == 1 { + *ss = append(*ss, fmt.Sprintf("%20s: %s", "Permissions", l[0])) + } else { + *ss = append(*ss, fmt.Sprintf("%20s:", "Permissions")) + *ss = append(*ss, l...) + } +} - var ss []string +func (info *PDFInfo) renderAttachments(ss *[]string) { + ss0 := []string{} + for _, a := range info.Attachments { + ss0 = append(ss0, a.FileName) + } + sort.Strings(ss0) + *ss = append(*ss, ss0...) +} + +// Info returns info about ctx. +func Info(ctx *model.Context, fileName string, selectedPages types.IntSet) (*PDFInfo, error) { + + info := &PDFInfo{FileName: fileName, Unit: ctx.Unit, UnitString: ctx.UnitString()} v := ctx.HeaderVersion if ctx.RootVersion != nil { v = ctx.RootVersion } - ss = append(ss, fmt.Sprintf("%20s: %s", "PDF version", v)) - ss = append(ss, fmt.Sprintf("%20s: %d", "Page count", ctx.PageCount)) + info.Version = (*v).String() + + info.PageCount = ctx.PageCount - pi, err := pageInfo(ctx, selectedPages) + // PageBoundaries for selected pages. + pbs, err := ctx.PageBoundaries(selectedPages) if err != nil { return nil, err } - ss = append(ss, pi...) + info.PageBoundaries = pbs - ss = append(ss, fmt.Sprint(separator)) - ss = append(ss, fmt.Sprintf("%20s: %s", "Title", ctx.Title)) - ss = append(ss, fmt.Sprintf("%20s: %s", "Author", ctx.Author)) - ss = append(ss, fmt.Sprintf("%20s: %s", "Subject", ctx.Subject)) - ss = append(ss, fmt.Sprintf("%20s: %s", "PDF Producer", ctx.Producer)) - ss = append(ss, fmt.Sprintf("%20s: %s", "Content creator", ctx.Creator)) - ss = append(ss, fmt.Sprintf("%20s: %s", "Creation date", ctx.CreationDate)) - ss = append(ss, fmt.Sprintf("%20s: %s", "Modification date", ctx.ModDate)) - - if err := addKeywordsToInfoDigest(ctx, &ss); err != nil { + // Media box dimensions for all pages. + pd, err := ctx.PageDims() + if err != nil { + return nil, err + } + m := map[types.Dim]bool{} + for _, d := range pd { + m[d] = true + } + info.PageDimensions = m + + info.Title = ctx.Title + info.Subject = ctx.Subject + info.Producer = ctx.Producer + info.Creator = ctx.Creator + info.CreationDate = ctx.CreationDate + info.ModificationDate = ctx.ModDate + + kwl, err := KeywordsList(ctx.XRefTable) + if err != nil { return nil, err } + info.Keywords = kwl + + info.Properties = ctx.Properties + info.Tagged = ctx.Tagged + info.Hybrid = ctx.Read.Hybrid + info.Linearized = ctx.Read.Linearized + info.UsingXRefStreams = ctx.Read.UsingXRefStreams + info.UsingObjectStreams = ctx.Read.UsingObjectStreams + info.Watermarked = ctx.Watermarked + info.Thumbnails = len(ctx.PageThumbs) > 0 + info.Form = ctx.Form != nil + info.Signatures = ctx.SignatureExist + info.AppendOnly = ctx.AppendOnly + + if ctx.E != nil { + info.Permissions = ctx.E.P + } - if err := ctx.AddPropertiesToInfoDigest(&ss); err != nil { + aa, err := ctx.ListAttachments() + if err != nil { return nil, err } + info.Attachments = aa + + return info, nil +} + +// ListInfo returns formatted info about ctx. +func ListInfo(info *PDFInfo, selectedPages types.IntSet) ([]string, error) { - addFlagsToInfoDigest(ctx, &ss, separator) + var separator = draw.HorSepLine([]int{44}) + + var ss []string - addPermissionsToInfoDigest(ctx, &ss) + if info.FileName != "" { + ss = append(ss, fmt.Sprintf("%20s: %s", "Source", info.FileName)) + } + ss = append(ss, fmt.Sprintf("%20s: %s", "PDF version", info.Version)) + ss = append(ss, fmt.Sprintf("%20s: %d", "Page count", info.PageCount)) - if err := ctx.AddAttachmentsToInfoDigest(&ss); err != nil { + pi, err := pageInfo(info, selectedPages) + if err != nil { return nil, err } + ss = append(ss, pi...) + + ss = append(ss, fmt.Sprint(separator)) + ss = append(ss, fmt.Sprintf("%20s: %s", "Title", info.Title)) + ss = append(ss, fmt.Sprintf("%20s: %s", "Author", info.Author)) + ss = append(ss, fmt.Sprintf("%20s: %s", "Subject", info.Subject)) + ss = append(ss, fmt.Sprintf("%20s: %s", "PDF Producer", info.Producer)) + ss = append(ss, fmt.Sprintf("%20s: %s", "Content creator", info.Creator)) + ss = append(ss, fmt.Sprintf("%20s: %s", "Creation date", info.CreationDate)) + ss = append(ss, fmt.Sprintf("%20s: %s", "Modification date", info.ModificationDate)) + + info.renderKeywords(&ss) + info.renderProperties(&ss) + info.renderFlags(&ss, separator) + info.renderPermissions(&ss) + info.renderAttachments(&ss) return ss, nil } diff --git a/pkg/pdfcpu/keyword.go b/pkg/pdfcpu/keyword.go index c9f6c0d54..cf847dc1b 100644 --- a/pkg/pdfcpu/keyword.go +++ b/pkg/pdfcpu/keyword.go @@ -17,31 +17,12 @@ limitations under the License. package pdfcpu import ( - "fmt" "strings" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" ) -func addKeywordsToInfoDigest(ctx *model.Context, ss *[]string) error { - if len(ctx.Keywords) == 0 { - return nil - } - kwl, err := KeywordsList(ctx.XRefTable) - if err != nil { - return err - } - for i, l := range kwl { - if i == 0 { - *ss = append(*ss, fmt.Sprintf("%20s: %s", "Keywords", l)) - continue - } - *ss = append(*ss, fmt.Sprintf("%20s %s", "", l)) - } - return nil -} - // KeywordsList returns a list of keywords as recorded in the document info dict. func KeywordsList(xRefTable *model.XRefTable) ([]string, error) { ss := strings.FieldsFunc(xRefTable.Keywords, func(c rune) bool { return c == ',' || c == ';' || c == '\r' }) diff --git a/pkg/pdfcpu/merge.go b/pkg/pdfcpu/merge.go index 01ef330a0..60e5ebd12 100644 --- a/pkg/pdfcpu/merge.go +++ b/pkg/pdfcpu/merge.go @@ -449,7 +449,7 @@ func mergeNames(ctxSrc, ctxDest *model.Context) error { return nil } -func mergeAcroForms(ctxSource, ctxDest *model.Context) error { +func mergeForms(ctxSource, ctxDest *model.Context) error { rootDictSource, rootDictDest, err := rootDicts(ctxSource, ctxDest) if err != nil { @@ -466,7 +466,7 @@ func mergeAcroForms(ctxSource, ctxDest *model.Context) error { return err } - // Retrieve ctxSrc AcroForm Fields + // Retrieve ctxSrc Form Fields o, found = dSrc.Find("Fields") if !found { return nil @@ -479,7 +479,7 @@ func mergeAcroForms(ctxSource, ctxDest *model.Context) error { return nil } - // We have a ctxSrc.Acroform with fields. + // We have a ctxSrc.Form with fields. o, found = rootDictDest.Find("AcroForm") if !found { @@ -821,7 +821,7 @@ func MergeXRefTables(fName string, ctxSource, ctxDest *model.Context) (err error log.Debug.Println("appendSourceObjectsToDest") appendSourceObjectsToDest(ctxSource, ctxDest) - if err := mergeAcroForms(ctxSource, ctxDest); err != nil { + if err := mergeForms(ctxSource, ctxDest); err != nil { return err } diff --git a/pkg/pdfcpu/model/box.go b/pkg/pdfcpu/model/box.go index feb199029..797def59f 100644 --- a/pkg/pdfcpu/model/box.go +++ b/pkg/pdfcpu/model/box.go @@ -906,8 +906,7 @@ func (ctx *Context) addPageBoundaryString(i int, pb PageBoundaries, wantPB *Page // ListPageBoundaries lists page boundaries specified in wantPB for selected pages. func (ctx *Context) ListPageBoundaries(selectedPages types.IntSet, wantPB *PageBoundaries) ([]string, error) { - // TODO ctx.PageBoundaries(selectedPages) - pbs, err := ctx.PageBoundaries() + pbs, err := ctx.PageBoundaries(selectedPages) if err != nil { return nil, err } diff --git a/pkg/pdfcpu/model/configuration.go b/pkg/pdfcpu/model/configuration.go index 0f7d2db31..7808c7f40 100644 --- a/pkg/pdfcpu/model/configuration.go +++ b/pkg/pdfcpu/model/configuration.go @@ -60,7 +60,7 @@ type CommandMode int // The available commands. const ( VALIDATE CommandMode = iota - INFO + LISTINFO OPTIMIZE SPLIT MERGECREATE @@ -101,7 +101,11 @@ const ( ROTATE NUP BOOKLET + LISTBOOKMARKS ADDBOOKMARKS + REMOVEBOOKMARKS + IMPORTBOOKMARKS + EXPORTBOOKMARKS LISTIMAGES CREATE DUMP @@ -121,8 +125,8 @@ const ( INSTALLFONTS LISTFONTS RESIZE - POSTER // needed? - NDOWN // needed? + POSTER + NDOWN CUT ) diff --git a/pkg/pdfcpu/model/context.go b/pkg/pdfcpu/model/context.go index efc4a4f4d..686c95306 100644 --- a/pkg/pdfcpu/model/context.go +++ b/pkg/pdfcpu/model/context.go @@ -172,32 +172,7 @@ func (ctx *Context) UnitString() string { // ConvertToUnit converts dimensions in point to inches,cm,mm func (ctx *Context) ConvertToUnit(d types.Dim) types.Dim { - switch ctx.Unit { - case types.INCHES: - return d.ToInches() - case types.CENTIMETRES: - return d.ToCentimetres() - case types.MILLIMETRES: - return d.ToMillimetres() - } - return d -} - -// AddPropertiesToInfoDigest append optional properties to info digest. -func (ctx *Context) AddPropertiesToInfoDigest(ss *[]string) error { - if len(ctx.Properties) == 0 { - return nil - } - first := true - for k, v := range ctx.Properties { - if first { - *ss = append(*ss, fmt.Sprintf("%20s: %s = %s", "Properties", k, v)) - first = false - continue - } - *ss = append(*ss, fmt.Sprintf("%20s %s = %s", "", k, v)) - } - return nil + return d.ConvertToUnit(ctx.Unit) } // ReadContext represents the context for reading a PDF file. diff --git a/pkg/pdfcpu/model/dereference.go b/pkg/pdfcpu/model/dereference.go index e1e46f4ed..1e38e302e 100644 --- a/pkg/pdfcpu/model/dereference.go +++ b/pkg/pdfcpu/model/dereference.go @@ -347,3 +347,23 @@ func (xRefTable *XRefTable) DereferenceStringEntryBytes(d types.Dict, key string return nil, errors.Errorf("pdfcpu: DereferenceStringEntryBytes dict=%s entry=%s, wrong type %T <%v>", d, key, o, o) } + +func (xRefTable *XRefTable) DestName(obj types.Object) (string, error) { + dest, err := xRefTable.Dereference(obj) + if err != nil { + return "", err + } + + var s string + + switch d := dest.(type) { + case types.Name: + s = d.Value() + case types.StringLiteral: + s, err = types.StringLiteralToString(d) + case types.HexLiteral: + s, err = types.HexLiteralToString(d) + } + + return s, err +} diff --git a/pkg/pdfcpu/model/version.go b/pkg/pdfcpu/model/version.go index 638430ed4..c0f435ce8 100644 --- a/pkg/pdfcpu/model/version.go +++ b/pkg/pdfcpu/model/version.go @@ -23,7 +23,7 @@ import ( ) // VersionStr is the current pdfcpu version. -var VersionStr = "v0.4.2 dev" +var VersionStr = "v0.5.0 dev" // Version is a type for the internal representation of PDF versions. type Version int diff --git a/pkg/pdfcpu/model/xreftable.go b/pkg/pdfcpu/model/xreftable.go index 1245cc0f8..bffdfd58a 100644 --- a/pkg/pdfcpu/model/xreftable.go +++ b/pkg/pdfcpu/model/xreftable.go @@ -158,7 +158,7 @@ type XRefTable struct { Optimized bool Watermarked bool - AcroForm types.Dict + Form types.Dict Outlines types.Dict SignatureExist bool AppendOnly bool @@ -2060,7 +2060,14 @@ func (xRefTable *XRefTable) collectMediaBoxAndCropBox(d types.Dict, inhMediaBox, return nil } -func (xRefTable *XRefTable) collectPageBoundariesForPageTree(root *types.IndirectRef, inhMediaBox, inhCropBox **types.Rectangle, pb []PageBoundaries, r int, p *int) error { +func (xRefTable *XRefTable) collectPageBoundariesForPageTree( + root *types.IndirectRef, + inhMediaBox, inhCropBox **types.Rectangle, + pb []PageBoundaries, + r int, + p *int, + selectedPages types.IntSet) error { + d, err := xRefTable.DereferenceDict(*root) if err != nil { return err @@ -2110,13 +2117,19 @@ func (xRefTable *XRefTable) collectPageBoundariesForPageTree(root *types.Indirec switch *pageNodeDict.Type() { case "Pages": - if err = xRefTable.collectPageBoundariesForPageTree(&ir, inhMediaBox, inhCropBox, pb, r, p); err != nil { + if err = xRefTable.collectPageBoundariesForPageTree(&ir, inhMediaBox, inhCropBox, pb, r, p, selectedPages); err != nil { return err } case "Page": - if err = xRefTable.collectPageBoundariesForPageTree(&ir, inhMediaBox, inhCropBox, pb, r, p); err != nil { - return err + collect := len(selectedPages) == 0 + if !collect { + _, collect = selectedPages[(*p)+1] + } + if collect { + if err = xRefTable.collectPageBoundariesForPageTree(&ir, inhMediaBox, inhCropBox, pb, r, p, selectedPages); err != nil { + return err + } } *p++ } @@ -2128,7 +2141,7 @@ func (xRefTable *XRefTable) collectPageBoundariesForPageTree(root *types.Indirec // PageBoundaries returns a sorted slice with page boundaries // for all pages sorted ascending by page number. -func (xRefTable *XRefTable) PageBoundaries() ([]PageBoundaries, error) { +func (xRefTable *XRefTable) PageBoundaries(selectedPages types.IntSet) ([]PageBoundaries, error) { if err := xRefTable.EnsurePageCount(); err != nil { return nil, err } @@ -2143,7 +2156,7 @@ func (xRefTable *XRefTable) PageBoundaries() ([]PageBoundaries, error) { mb := &types.Rectangle{} cb := &types.Rectangle{} pbs := make([]PageBoundaries, xRefTable.PageCount) - if err := xRefTable.collectPageBoundariesForPageTree(root, &mb, &cb, pbs, 0, &i); err != nil { + if err := xRefTable.collectPageBoundariesForPageTree(root, &mb, &cb, pbs, 0, &i, selectedPages); err != nil { return nil, err } return pbs, nil @@ -2152,7 +2165,7 @@ func (xRefTable *XRefTable) PageBoundaries() ([]PageBoundaries, error) { // PageDims returns a sorted slice with effective media box dimensions // for all pages sorted ascending by page number. func (xRefTable *XRefTable) PageDims() ([]types.Dim, error) { - pbs, err := xRefTable.PageBoundaries() + pbs, err := xRefTable.PageBoundaries(nil) if err != nil { return nil, err } diff --git a/pkg/pdfcpu/page.go b/pkg/pdfcpu/page.go index 46585bcaf..615fa1e36 100644 --- a/pkg/pdfcpu/page.go +++ b/pkg/pdfcpu/page.go @@ -102,8 +102,8 @@ func AddPages(ctxSrc, ctxDest *model.Context, pageNrs []int, usePgCache bool) er fieldsSrc, fieldsDest := types.Array{}, types.Array{} - if ctxSrc.AcroForm != nil { - o, _ := ctxSrc.AcroForm.Find("Fields") + if ctxSrc.Form != nil { + o, _ := ctxSrc.Form.Find("Fields") fieldsSrc, err = ctxSrc.DereferenceArray(o) if err != nil { return err @@ -116,8 +116,8 @@ func AddPages(ctxSrc, ctxDest *model.Context, pageNrs []int, usePgCache bool) er return err } - if ctxSrc.AcroForm != nil && len(fieldsDest) > 0 { - d := ctxSrc.AcroForm.Clone().(types.Dict) + if ctxSrc.Form != nil && len(fieldsDest) > 0 { + d := ctxSrc.Form.Clone().(types.Dict) if err := migrateFormDict(d, fieldsDest, ctxSrc, ctxDest, migrated); err != nil { return err } diff --git a/pkg/pdfcpu/primitives/comboBox.go b/pkg/pdfcpu/primitives/comboBox.go index c5854bb34..b929d9a0b 100644 --- a/pkg/pdfcpu/primitives/comboBox.go +++ b/pkg/pdfcpu/primitives/comboBox.go @@ -19,8 +19,6 @@ package primitives import ( "bytes" "fmt" - "strconv" - "strings" "unicode/utf8" "github.com/pdfcpu/pdfcpu/pkg/font" @@ -234,46 +232,22 @@ func (cb *ComboBox) validate() error { return cb.validateTab() } -func (cb *ComboBox) calcFontFromDA(ctx *model.Context, da []string, fonts map[string]types.IndirectRef) (*types.IndirectRef, error) { +func (cb *ComboBox) calcFontFromDA(ctx *model.Context, d types.Dict, fonts map[string]types.IndirectRef) (*types.IndirectRef, error) { - var ( - f FormFont - fontID string - ) - - f.SetCol(color.Black) - for i := 0; i < len(da); i++ { - if da[i] == "Tf" { - fontID = da[i-2][1:] - cb.SetFontID(fontID) - fl, err := strconv.ParseFloat(da[i-1], 64) - if err != nil { - return nil, err - } - if fl == 0 { - // TODO derive size from acroDict DA and then use a default form font size (add to pdfcpu config) - fl = 12 - } - f.Size = int(fl) - continue - } - if da[i] == "rg" { - r, _ := strconv.ParseFloat(da[i-3], 32) - g, _ := strconv.ParseFloat(da[i-2], 32) - b, _ := strconv.ParseFloat(da[i-1], 32) - f.SetCol(color.SimpleColor{R: float32(r), G: float32(g), B: float32(b)}) - } - if da[i] == "g" { - g, _ := strconv.ParseFloat(da[i-1], 32) - f.SetCol(color.SimpleColor{R: float32(g), G: float32(g), B: float32(g)}) + s := d.StringEntry("DA") + if s == nil { + s = ctx.Form.StringEntry("DA") + if s == nil { + return nil, errors.New("pdfcpu: combobox missing \"DA\"") } } - if len(cb.fontID) == 0 { - return nil, errors.New("pdfcpu: unable to detect font id") + fontID, f, err := fontFromDA(*s) + if err != nil { + return nil, err } - cb.Font = &f + cb.Font, cb.fontID = &f, fontID id, name, lang, fontIndRef, err := extractFormFontDetails(ctx, cb.fontID, fonts) if err != nil { @@ -755,15 +729,7 @@ func NewComboBox( cb.BoundingBox = types.RectForDim(bb.Width(), bb.Height()) - s := d.StringEntry("DA") - if s == nil { - s = ctx.AcroForm.StringEntry("DA") - if s == nil { - return nil, nil, errors.New("pdfcpu: combobox missing \"DA\"") - } - } - - fontIndRef, err := cb.calcFontFromDA(ctx, strings.Fields(*s), fonts) + fontIndRef, err := cb.calcFontFromDA(ctx, d, fonts) if err != nil { return nil, nil, err } diff --git a/pkg/pdfcpu/primitives/dateField.go b/pkg/pdfcpu/primitives/dateField.go index e9a2177cd..e681f995a 100644 --- a/pkg/pdfcpu/primitives/dateField.go +++ b/pkg/pdfcpu/primitives/dateField.go @@ -20,8 +20,6 @@ import ( "bytes" "fmt" "io" - "strconv" - "strings" "github.com/pdfcpu/pdfcpu/pkg/font" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" @@ -283,46 +281,22 @@ func (df *DateField) validate() error { return df.validateTab() } -func (df *DateField) calcFontFromDA(ctx *model.Context, da []string, fonts map[string]types.IndirectRef) (*types.IndirectRef, error) { +func (df *DateField) calcFontFromDA(ctx *model.Context, d types.Dict, fonts map[string]types.IndirectRef) (*types.IndirectRef, error) { - var ( - f FormFont - fontID string - ) - - f.SetCol(color.Black) - for i := 0; i < len(da); i++ { - if da[i] == "Tf" { - fontID = da[i-2][1:] - df.SetFontID(fontID) - fl, err := strconv.ParseFloat(da[i-1], 64) - if err != nil { - return nil, err - } - if fl == 0 { - // TODO derive size from acroDict DA and then use a default form font size (add to pdfcpu config) - fl = 12 - } - f.Size = int(fl) - continue - } - if da[i] == "rg" { - r, _ := strconv.ParseFloat(da[i-3], 32) - g, _ := strconv.ParseFloat(da[i-2], 32) - b, _ := strconv.ParseFloat(da[i-1], 32) - f.SetCol(color.SimpleColor{R: float32(r), G: float32(g), B: float32(b)}) - } - if da[i] == "g" { - g, _ := strconv.ParseFloat(da[i-1], 32) - f.SetCol(color.SimpleColor{R: float32(g), G: float32(g), B: float32(g)}) + s := d.StringEntry("DA") + if s == nil { + s = ctx.Form.StringEntry("DA") + if s == nil { + return nil, errors.New("pdfcpu: datefield missing \"DA\"") } } - if len(df.fontID) == 0 { - return nil, errors.New("pdfcpu: unable to detect font id") + fontID, f, err := fontFromDA(*s) + if err != nil { + return nil, err } - df.Font = &f + df.Font, df.fontID = &f, fontID id, name, lang, fontIndRef, err := extractFormFontDetails(ctx, df.fontID, fonts) if err != nil { @@ -893,15 +867,7 @@ func NewDateField( df.BoundingBox = types.RectForDim(bb.Width(), bb.Height()) - s := d.StringEntry("DA") - if s == nil { - s = ctx.AcroForm.StringEntry("DA") - if s == nil { - return nil, nil, errors.New("pdfcpu: datefield missing \"DA\"") - } - } - - fontIndRef, err := df.calcFontFromDA(ctx, strings.Fields(*s), fonts) + fontIndRef, err := df.calcFontFromDA(ctx, d, fonts) if err != nil { return nil, nil, err } diff --git a/pkg/pdfcpu/primitives/font.go b/pkg/pdfcpu/primitives/font.go index 2a3b42e9a..b53fcd10e 100644 --- a/pkg/pdfcpu/primitives/font.go +++ b/pkg/pdfcpu/primitives/font.go @@ -17,6 +17,7 @@ package primitives import ( + "strconv" "strings" "github.com/pdfcpu/pdfcpu/pkg/font" @@ -136,34 +137,34 @@ func (f FormFont) RTL() bool { return types.MemberOf(f.Script, []string{"Arab", "Hebr"}) || types.MemberOf(f.Lang, []string{"ar", "fa", "he"}) } -func FormFontNameAndLangForID(xRefTable *model.XRefTable, indRef types.IndirectRef) (*string, *string, error) { +func FormFontNameAndLangForID(xRefTable *model.XRefTable, indRef types.IndirectRef) (string, string, error) { objNr := int(indRef.ObjectNumber) fontDict, err := xRefTable.DereferenceDict(indRef) if err != nil || fontDict == nil { - return nil, nil, err + return "", "", err } _, fName, err := pdffont.Name(xRefTable, fontDict, objNr) if err != nil { - return nil, nil, err + return "", "", err } - var fLang *string + var fLang string if font.IsUserFont(fName) { fLang, err = pdffont.Lang(xRefTable, fontDict) if err != nil { - return nil, nil, err + return "", "", err } } - return &fName, fLang, nil + return fName, fLang, nil } // FontResDict returns form dict's font resource dict. func FontResDict(xRefTable *model.XRefTable) (types.Dict, error) { - d := xRefTable.AcroForm + d := xRefTable.Form if len(d) == 0 { return nil, nil } @@ -193,6 +194,7 @@ func formFontIndRef(xRefTable *model.XRefTable, fontID string) (*types.IndirectR } for k, v := range d { + //fmt.Printf("%s %s\n", k, v) if strings.HasPrefix(k, fontID) || strings.HasPrefix(fontID, k) { indRef, _ := v.(types.IndirectRef) return &indRef, nil @@ -276,11 +278,11 @@ func ensureCorrectFontIndRef( return nil } -func fontFromAcroDict(xRefTable *model.XRefTable, fName, fLang *string, fontID string) error { +func fontFromAcroDict(xRefTable *model.XRefTable, fontIndRef *types.IndirectRef, fName, fLang *string, fontID string) error { // Use DA fontId from Acrodict - s := xRefTable.AcroForm.StringEntry("DA") + s := xRefTable.Form.StringEntry("DA") if s == nil { if fName != nil { return errors.Errorf("pdfcpu: unsupported font: %s", *fName) @@ -293,7 +295,9 @@ func fontFromAcroDict(xRefTable *model.XRefTable, fName, fLang *string, fontID s for i := 0; i < len(da); i++ { if da[i] == "Tf" { - rootFontID = da[i-2][1:] + if i >= 2 { + rootFontID = da[i-2][1:] + } break } } @@ -306,23 +310,33 @@ func fontFromAcroDict(xRefTable *model.XRefTable, fName, fLang *string, fontID s } fontID = rootFontID - fontIndRef, err := formFontIndRef(xRefTable, fontID) + indRef, err := formFontIndRef(xRefTable, fontID) if err != nil { return err } - fN, fL, err := FormFontNameAndLangForID(xRefTable, *fontIndRef) + *fontIndRef = *indRef + + *fName, *fLang, err = FormFontNameAndLangForID(xRefTable, *indRef) if err != nil { return err } - *fName = *fN + // if fN != nil { + // println("FN: " + *fN) + // } - if fL != nil { - *fLang = *fL - } + // if fL != nil { + // println("FL: " + *fL) + // } + + // *fName = fN - return err + // if fL != nil { + // *fLang = *fL + // } + + return nil } func extractFormFontDetails( @@ -332,38 +346,90 @@ func extractFormFontDetails( xRefTable := ctx.XRefTable - fontIndRef, err := formFontIndRef(xRefTable, fontID) - if err != nil { - return "", "", "", nil, err - } + var ( + fName, fLang string + fontIndRef *types.IndirectRef + err error + ) - var fName, fLang *string + if len(fontID) > 0 { - if fontIndRef != nil { - fName, fLang, err = FormFontNameAndLangForID(xRefTable, *fontIndRef) + fontIndRef, err = formFontIndRef(xRefTable, fontID) if err != nil { return "", "", "", nil, err } - if fName == nil { - return "", "", "", nil, errors.Errorf("pdfcpu: Unable to detect fontName for: %s", fontID) + if fontIndRef != nil { + fName, fLang, err = FormFontNameAndLangForID(xRefTable, *fontIndRef) + if err != nil { + return "", "", "", nil, err + } + + if fName == "" { + return "", "", "", nil, errors.Errorf("pdfcpu: Unable to detect fontName for: %s", fontID) + } } + } - if fontIndRef == nil || !font.SupportedFont(*fName) { - if err = fontFromAcroDict(xRefTable, fName, fLang, fontID); err != nil { + if fontIndRef == nil || !font.SupportedFont(fName) { + var indRef types.IndirectRef + if err = fontFromAcroDict(xRefTable, &indRef, &fName, &fLang, fontID); err != nil { return "", "", "", nil, err } + fontIndRef = &indRef } - var lang string - if fLang != nil { - lang = *fLang + // var lang string + // if fLang != nil { + // lang = *fLang + // } + + if font.IsUserFont(fName) { + err = ensureCorrectFontIndRef(ctx, &fontIndRef, fName, fonts) } - if font.IsUserFont(*fName) { - err = ensureCorrectFontIndRef(ctx, &fontIndRef, *fName, fonts) + return fontID, fName, fLang, fontIndRef, err +} + +func fontFromDA(s string) (string, FormFont, error) { + + da := strings.Fields(s) + + var ( + f FormFont + fontID string + ) + + f.SetCol(color.Black) + + for i := 0; i < len(da); i++ { + if da[i] == "Tf" { + fontID = da[i-2][1:] + //tf.SetFontID(fontID) + fl, err := strconv.ParseFloat(da[i-1], 64) + if err != nil { + return fontID, f, err + } + if fl == 0 { + // TODO derive size from acroDict DA and then use a default form font size (add to pdfcpu config) + fl = 12 + } + f.Size = int(fl) + continue + } + if da[i] == "rg" { + r, _ := strconv.ParseFloat(da[i-3], 32) + g, _ := strconv.ParseFloat(da[i-2], 32) + b, _ := strconv.ParseFloat(da[i-1], 32) + f.SetCol(color.SimpleColor{R: float32(r), G: float32(g), B: float32(b)}) + continue + } + if da[i] == "g" { + g, _ := strconv.ParseFloat(da[i-1], 32) + f.SetCol(color.SimpleColor{R: float32(g), G: float32(g), B: float32(g)}) + } } - return fontID, *fName, lang, fontIndRef, err + return fontID, f, nil } diff --git a/pkg/pdfcpu/primitives/listBox.go b/pkg/pdfcpu/primitives/listBox.go index 05bb62486..6e51b640a 100644 --- a/pkg/pdfcpu/primitives/listBox.go +++ b/pkg/pdfcpu/primitives/listBox.go @@ -20,8 +20,6 @@ import ( "bytes" "fmt" "io" - "strconv" - "strings" "unicode/utf8" "github.com/pdfcpu/pdfcpu/pkg/font" @@ -304,46 +302,22 @@ func (lb *ListBox) validate() error { return lb.validateTab() } -func (lb *ListBox) calcFontFromDA(ctx *model.Context, da []string, fonts map[string]types.IndirectRef) (*types.IndirectRef, error) { +func (lb *ListBox) calcFontFromDA(ctx *model.Context, d types.Dict, fonts map[string]types.IndirectRef) (*types.IndirectRef, error) { - var ( - f FormFont - fontID string - ) - - f.SetCol(color.Black) - for i := 0; i < len(da); i++ { - if da[i] == "Tf" { - fontID = da[i-2][1:] - lb.SetFontID(fontID) - fl, err := strconv.ParseFloat(da[i-1], 64) - if err != nil { - return nil, err - } - if fl == 0 { - // TODO derive size from acroDict DA and then use a default form font size (add to pdfcpu config) - fl = 12 - } - f.Size = int(fl) - continue - } - if da[i] == "rg" { - r, _ := strconv.ParseFloat(da[i-3], 32) - g, _ := strconv.ParseFloat(da[i-2], 32) - b, _ := strconv.ParseFloat(da[i-1], 32) - f.SetCol(color.SimpleColor{R: float32(r), G: float32(g), B: float32(b)}) - } - if da[i] == "g" { - g, _ := strconv.ParseFloat(da[i-1], 32) - f.SetCol(color.SimpleColor{R: float32(g), G: float32(g), B: float32(g)}) + s := d.StringEntry("DA") + if s == nil { + s = ctx.Form.StringEntry("DA") + if s == nil { + return nil, errors.New("pdfcpu: listbox missing \"DA\"") } } - if len(lb.fontID) == 0 { - return nil, errors.New("pdfcpu: unable to detect font id") + fontID, f, err := fontFromDA(*s) + if err != nil { + return nil, err } - lb.Font = &f + lb.Font, lb.fontID = &f, fontID id, name, lang, fontIndRef, err := extractFormFontDetails(ctx, lb.fontID, fonts) if err != nil { @@ -907,15 +881,7 @@ func NewListBox( lb.BoundingBox = types.RectForDim(bb.Width(), bb.Height()) - s := d.StringEntry("DA") - if s == nil { - s = ctx.AcroForm.StringEntry("DA") - if s == nil { - return nil, nil, errors.New("pdfcpu: listbox missing \"DA\"") - } - } - - fontIndRef, err := lb.calcFontFromDA(ctx, strings.Fields(*s), fonts) + fontIndRef, err := lb.calcFontFromDA(ctx, d, fonts) if err != nil { return nil, nil, err } diff --git a/pkg/pdfcpu/primitives/radioButtonGroup.go b/pkg/pdfcpu/primitives/radioButtonGroup.go index 70012be96..58e1af967 100644 --- a/pkg/pdfcpu/primitives/radioButtonGroup.go +++ b/pkg/pdfcpu/primitives/radioButtonGroup.go @@ -29,7 +29,7 @@ import ( // Note: // Mac Preview is unable to save modified radio buttons: -// The Acrofield holding the kid terminal fields for each button does not get the current value assigned to V. +// The form field holding the kid terminal fields for each button does not get the current value assigned to V. // Instead Preview sets V in the widget annotation that corresponds to the selected radio button. // RadioButtonGroup represents a set of radio buttons including positioned labels. diff --git a/pkg/pdfcpu/primitives/textField.go b/pkg/pdfcpu/primitives/textField.go index 55e012e9d..d181fd8c0 100644 --- a/pkg/pdfcpu/primitives/textField.go +++ b/pkg/pdfcpu/primitives/textField.go @@ -20,8 +20,6 @@ import ( "bytes" "fmt" "io" - "strconv" - "strings" "unicode/utf8" @@ -227,46 +225,22 @@ func (tf *TextField) validate() error { return tf.validateTab() } -func (tf *TextField) calcFontFromDA(ctx *model.Context, da []string, fonts map[string]types.IndirectRef) (*types.IndirectRef, error) { +func (tf *TextField) calcFontFromDA(ctx *model.Context, d types.Dict, fonts map[string]types.IndirectRef) (*types.IndirectRef, error) { - var ( - f FormFont - fontID string - ) - - f.SetCol(color.Black) - for i := 0; i < len(da); i++ { - if da[i] == "Tf" { - fontID = da[i-2][1:] - tf.SetFontID(fontID) - fl, err := strconv.ParseFloat(da[i-1], 64) - if err != nil { - return nil, err - } - if fl == 0 { - // TODO derive size from acroDict DA and then use a default form font size (add to pdfcpu config) - fl = 12 - } - f.Size = int(fl) - continue - } - if da[i] == "rg" { - r, _ := strconv.ParseFloat(da[i-3], 32) - g, _ := strconv.ParseFloat(da[i-2], 32) - b, _ := strconv.ParseFloat(da[i-1], 32) - f.SetCol(color.SimpleColor{R: float32(r), G: float32(g), B: float32(b)}) - } - if da[i] == "g" { - g, _ := strconv.ParseFloat(da[i-1], 32) - f.SetCol(color.SimpleColor{R: float32(g), G: float32(g), B: float32(g)}) + s := d.StringEntry("DA") + if s == nil { + s = ctx.Form.StringEntry("DA") + if s == nil { + return nil, errors.New("pdfcpu: textfield missing \"DA\"") } } - if len(tf.fontID) == 0 { - return nil, errors.New("pdfcpu: unable to detect font id") + fontID, f, err := fontFromDA(*s) + if err != nil { + return nil, err } - tf.Font = &f + tf.Font, tf.fontID = &f, fontID id, name, lang, fontIndRef, err := extractFormFontDetails(ctx, tf.fontID, fonts) if err != nil { @@ -910,15 +884,7 @@ func NewTextField( tf.BoundingBox = types.RectForDim(bb.Width(), bb.Height()) - s := d.StringEntry("DA") - if s == nil { - s = ctx.AcroForm.StringEntry("DA") - if s == nil { - return nil, nil, errors.New("pdfcpu: textfield missing \"DA\"") - } - } - - fontIndRef, err := tf.calcFontFromDA(ctx, strings.Fields(*s), fonts) + fontIndRef, err := tf.calcFontFromDA(ctx, d, fonts) if err != nil { return nil, nil, err } diff --git a/pkg/pdfcpu/types/types.go b/pkg/pdfcpu/types/types.go index 2dca87edc..48f9bbf6a 100644 --- a/pkg/pdfcpu/types/types.go +++ b/pkg/pdfcpu/types/types.go @@ -604,6 +604,19 @@ func (d Dim) ToMillimetres() Dim { return Dim{d.Width * userSpaceToMm, d.Height * userSpaceToMm} } +// ConvertToUnit converts d to unit. +func (d Dim) ConvertToUnit(unit DisplayUnit) Dim { + switch unit { + case INCHES: + return d.ToInches() + case CENTIMETRES: + return d.ToCentimetres() + case MILLIMETRES: + return d.ToMillimetres() + } + return d +} + // AspectRatio returns the relation between width and height. func (d Dim) AspectRatio() float64 { return d.Width / d.Height diff --git a/pkg/pdfcpu/validate/action.go b/pkg/pdfcpu/validate/action.go index 276911ab4..3268f9907 100644 --- a/pkg/pdfcpu/validate/action.go +++ b/pkg/pdfcpu/validate/action.go @@ -917,7 +917,7 @@ func validateAdditionalActions(xRefTable *model.XRefTable, dict types.Dict, dict } case "fieldOrAnnot": - // A terminal acro field may be merged with a widget annotation. + // A terminal form field may be merged with a widget annotation. fieldOptions := []string{"K", "F", "V", "C"} annotOptions := []string{"E", "X", "D", "U", "Fo", "Bl", "PO", "PC", "PV", "Pl"} options := append(fieldOptions, annotOptions...) diff --git a/pkg/pdfcpu/validate/form.go b/pkg/pdfcpu/validate/form.go index 4921dffc5..59fc39869 100644 --- a/pkg/pdfcpu/validate/form.go +++ b/pkg/pdfcpu/validate/form.go @@ -17,6 +17,9 @@ limitations under the License. package validate import ( + "strconv" + "strings" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" "github.com/pkg/errors" @@ -125,17 +128,114 @@ func validateAppearanceDict(xRefTable *model.XRefTable, o types.Object) error { return nil } -func validateAcroFieldDictEntries(xRefTable *model.XRefTable, d types.Dict, terminalNode bool, inFieldType *types.Name) (outFieldType *types.Name, err error) { +func validateDA(s string) bool { + // A sequence of valid page-content graphics or text state operators. + // At a minimum, the string shall include a Tf (text font) operator along with its two operands, font and size. + da := strings.Fields(s) + for i := 0; i < len(da); i++ { + if da[i] == "Tf" { + if i < 2 { + return false + } + if da[i-2][0] != '/' { + return false + } + fontID := da[i-2][1:] + if len(fontID) == 0 { + return false + } + if _, err := strconv.ParseFloat(da[i-1], 64); err != nil { + return false + } + continue + } + if da[i] == "rg" { + if i < 3 { + return false + } + if _, err := strconv.ParseFloat(da[i-3], 32); err != nil { + return false + } + if _, err := strconv.ParseFloat(da[i-2], 32); err != nil { + return false + } + if _, err := strconv.ParseFloat(da[i-1], 32); err != nil { + return false + } + } + if da[i] == "g" { + if i < 1 { + return false + } + if _, err := strconv.ParseFloat(da[i-1], 32); err != nil { + return false + } + } + } + + return true +} + +func validateDARelaxed(s string) bool { + // A sequence of valid page-content graphics or text state operators. + // At a minimum, the string shall include a Tf (text font) operator along with its two operands, font and size. + da := strings.Fields(s) + for i := 0; i < len(da); i++ { + if da[i] == "Tf" { + if i < 2 { + return false + } + if da[i-2][0] != '/' { + return false + } + //fontID := da[i-2][1:] + // if len(fontID) == 0 { + // return false + // } + if _, err := strconv.ParseFloat(da[i-1], 64); err != nil { + return false + } + continue + } + if da[i] == "rg" { + if i < 3 { + return false + } + if _, err := strconv.ParseFloat(da[i-3], 32); err != nil { + return false + } + if _, err := strconv.ParseFloat(da[i-2], 32); err != nil { + return false + } + if _, err := strconv.ParseFloat(da[i-1], 32); err != nil { + return false + } + } + if da[i] == "g" { + if i < 1 { + return false + } + if _, err := strconv.ParseFloat(da[i-1], 32); err != nil { + return false + } + } + } + + return true +} + +func validateFormFieldDictEntries(xRefTable *model.XRefTable, d types.Dict, terminalNode bool, inFieldType *types.Name, requiresDA bool) (outFieldType *types.Name, hasDA bool, err error) { - dictName := "acroFieldDict" + dictName := "formFieldDict" // FT: name, Btn,Tx,Ch,Sig validate := func(s string) bool { return types.MemberOf(s, []string{"Btn", "Tx", "Ch", "Sig"}) } fieldType, err := validateNameEntry(xRefTable, d, dictName, "FT", terminalNode && inFieldType == nil, model.V10, validate) if err != nil { - return nil, err + return nil, false, err } + outFieldType = inFieldType if fieldType != nil { outFieldType = fieldType } @@ -143,62 +243,75 @@ func validateAcroFieldDictEntries(xRefTable *model.XRefTable, d types.Dict, term // Parent, required if this is a child in the field hierarchy. _, err = validateIndRefEntry(xRefTable, d, dictName, "Parent", OPTIONAL, model.V10) if err != nil { - return nil, err + return nil, false, err } // T, optional, text string _, err = validateStringEntry(xRefTable, d, dictName, "T", OPTIONAL, model.V10, nil) if err != nil { - return nil, err + return nil, false, err } // TU, optional, text string, since V1.3 _, err = validateStringEntry(xRefTable, d, dictName, "TU", OPTIONAL, model.V13, nil) if err != nil { - return nil, err + return nil, false, err } // TM, optional, text string, since V1.3 _, err = validateStringEntry(xRefTable, d, dictName, "TM", OPTIONAL, model.V13, nil) if err != nil { - return nil, err + return nil, false, err } // Ff, optional, integer _, err = validateIntegerEntry(xRefTable, d, dictName, "Ff", OPTIONAL, model.V10, nil) if err != nil { - return nil, err + return nil, false, err } // V, optional, various _, err = validateEntry(xRefTable, d, dictName, "V", OPTIONAL, model.V10) if err != nil { - return nil, err + return nil, false, err } // DV, optional, various _, err = validateEntry(xRefTable, d, dictName, "DV", OPTIONAL, model.V10) if err != nil { - return nil, err + return nil, false, err } // AA, optional, dict, since V1.2 err = validateAdditionalActions(xRefTable, d, dictName, "AA", OPTIONAL, model.V12, "fieldOrAnnot") if err != nil { - return nil, err + return nil, false, err + } + + validate = validateDA + if xRefTable.ValidationMode == model.ValidationRelaxed { + validate = validateDARelaxed } + if terminalNode && (*outFieldType).Value() == "Tx" { + da, err := validateStringEntry(xRefTable, d, dictName, "DA", terminalNode && requiresDA, model.V10, validate) + if err != nil { + return nil, false, err + } - return outFieldType, nil + hasDA = da != nil && *da != "" + } + + return outFieldType, hasDA, nil } -func validateAcroFieldParts(xRefTable *model.XRefTable, d types.Dict, inFieldType *types.Name) error { +func validateFormFieldParts(xRefTable *model.XRefTable, d types.Dict, inFieldType *types.Name, requiresDA bool) error { // dict represents a terminal field and must have Subtype "Widget" - if _, err := validateNameEntry(xRefTable, d, "acroFieldDict", "Subtype", REQUIRED, model.V10, func(s string) bool { return s == "Widget" }); err != nil { + if _, err := validateNameEntry(xRefTable, d, "formFieldDict", "Subtype", REQUIRED, model.V10, func(s string) bool { return s == "Widget" }); err != nil { return err } // Validate field dict entries. - if _, err := validateAcroFieldDictEntries(xRefTable, d, true, inFieldType); err != nil { + if _, _, err := validateFormFieldDictEntries(xRefTable, d, true, inFieldType, requiresDA); err != nil { return err } @@ -207,18 +320,22 @@ func validateAcroFieldParts(xRefTable *model.XRefTable, d types.Dict, inFieldTyp return err } -func validateAcroFieldKid(xRefTable *model.XRefTable, d types.Dict, o types.Object, inFieldType *types.Name) error { +func validateFormFieldKids(xRefTable *model.XRefTable, d types.Dict, o types.Object, inFieldType *types.Name, requiresDA bool) error { var err error // dict represents a non terminal field. if d.Subtype() != nil && *d.Subtype() == "Widget" { - return errors.New("pdfcpu: validateAcroFieldKid: non terminal field can not be widget annotation") + return errors.New("pdfcpu: validateFormFieldKids: non terminal field can not be widget annotation") } // Validate field entries. var xInFieldType *types.Name - if xInFieldType, err = validateAcroFieldDictEntries(xRefTable, d, false, inFieldType); err != nil { + var hasDA bool + if xInFieldType, hasDA, err = validateFormFieldDictEntries(xRefTable, d, false, inFieldType, requiresDA); err != nil { return err } + if requiresDA && hasDA { + requiresDA = false + } // Recurse over kids. a, err := xRefTable.DereferenceArray(o) @@ -229,7 +346,7 @@ func validateAcroFieldKid(xRefTable *model.XRefTable, d types.Dict, o types.Obje for _, value := range a { ir, ok := value.(types.IndirectRef) if !ok { - return errors.New("pdfcpu: validateAcroFieldKid: corrupt kids array: entries must be indirect reference") + return errors.New("pdfcpu: validateFormFieldKids: corrupt kids array: entries must be indirect reference") } valid, err := xRefTable.IsValid(ir) if err != nil { @@ -237,7 +354,7 @@ func validateAcroFieldKid(xRefTable *model.XRefTable, d types.Dict, o types.Obje } if !valid { - if err = validateAcroFieldDict(xRefTable, ir, xInFieldType); err != nil { + if err = validateFormFieldDict(xRefTable, ir, xInFieldType, requiresDA); err != nil { return err } } @@ -246,7 +363,7 @@ func validateAcroFieldKid(xRefTable *model.XRefTable, d types.Dict, o types.Obje return nil } -func validateAcroFieldDict(xRefTable *model.XRefTable, ir types.IndirectRef, inFieldType *types.Name) error { +func validateFormFieldDict(xRefTable *model.XRefTable, ir types.IndirectRef, inFieldType *types.Name, requiresDA bool) error { d, err := xRefTable.DereferenceDict(ir) if err != nil || d == nil { return err @@ -263,13 +380,13 @@ func validateAcroFieldDict(xRefTable *model.XRefTable, ir types.IndirectRef, inF } if o, ok := d.Find("Kids"); ok { - return validateAcroFieldKid(xRefTable, d, o, inFieldType) + return validateFormFieldKids(xRefTable, d, o, inFieldType, requiresDA) } - return validateAcroFieldParts(xRefTable, d, inFieldType) + return validateFormFieldParts(xRefTable, d, inFieldType, requiresDA) } -func validateAcroFormFields(xRefTable *model.XRefTable, o types.Object) error { +func validateFormFields(xRefTable *model.XRefTable, o types.Object, requiresDA bool) error { a, err := xRefTable.DereferenceArray(o) if err != nil || len(a) == 0 { @@ -280,7 +397,7 @@ func validateAcroFormFields(xRefTable *model.XRefTable, o types.Object) error { ir, ok := value.(types.IndirectRef) if !ok { - return errors.New("pdfcpu: validateAcroFormFields: corrupt form field array entry") + return errors.New("pdfcpu: validateFormFields: corrupt form field array entry") } valid, err := xRefTable.IsValid(ir) @@ -289,7 +406,7 @@ func validateAcroFormFields(xRefTable *model.XRefTable, o types.Object) error { } if !valid { - if err = validateAcroFieldDict(xRefTable, ir, nil); err != nil { + if err = validateFormFieldDict(xRefTable, ir, nil, requiresDA); err != nil { return err } } @@ -299,7 +416,7 @@ func validateAcroFormFields(xRefTable *model.XRefTable, o types.Object) error { return nil } -func validateAcroFormCO(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version) error { +func validateFormCO(xRefTable *model.XRefTable, o types.Object, sinceVersion model.Version, requiresDA bool) error { // see 12.6.3 Trigger Events // Array of indRefs to field dicts with calculation actions, since V1.3 @@ -310,10 +427,10 @@ func validateAcroFormCO(xRefTable *model.XRefTable, o types.Object, sinceVersion return err } - return validateAcroFormFields(xRefTable, o) + return validateFormFields(xRefTable, o, requiresDA) } -func validateAcroFormXFA(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { +func validateFormXFA(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { // see 12.7.8 @@ -341,7 +458,7 @@ func validateAcroFormXFA(xRefTable *model.XRefTable, d types.Dict, sinceVersion for _, v := range o { if v == nil { - return errors.New("pdfcpu: validateAcroFormXFA: array entry is nil") + return errors.New("pdfcpu: validateFormXFA: array entry is nil") } o, err := xRefTable.Dereference(v) @@ -353,14 +470,14 @@ func validateAcroFormXFA(xRefTable *model.XRefTable, d types.Dict, sinceVersion _, ok := o.(types.StringLiteral) if !ok { - return errors.New("pdfcpu: validateAcroFormXFA: even array must be a string") + return errors.New("pdfcpu: validateFormXFA: even array must be a string") } } else { _, ok := o.(types.StreamDict) if !ok { - return errors.New("pdfcpu: validateAcroFormXFA: odd array entry must be a streamDict") + return errors.New("pdfcpu: validateFormXFA: odd array entry must be a streamDict") } } @@ -369,7 +486,7 @@ func validateAcroFormXFA(xRefTable *model.XRefTable, d types.Dict, sinceVersion } default: - return errors.New("pdfcpu: validateAcroFormXFA: needs to be streamDict or array") + return errors.New("pdfcpu: validateFormXFA: needs to be streamDict or array") } return xRefTable.ValidateVersion("AcroFormXFA", sinceVersion) @@ -377,17 +494,17 @@ func validateAcroFormXFA(xRefTable *model.XRefTable, d types.Dict, sinceVersion func validateQ(i int) bool { return i >= 0 && i <= 2 } -func validateAcroFormEntryCO(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version) error { +func validateFormEntryCO(xRefTable *model.XRefTable, d types.Dict, sinceVersion model.Version, requiresDA bool) error { o, ok := d.Find("CO") if !ok { return nil } - return validateAcroFormCO(xRefTable, o, sinceVersion) + return validateFormCO(xRefTable, o, sinceVersion, requiresDA) } -func validateAcroFormEntryDR(xRefTable *model.XRefTable, d types.Dict) error { +func validateFormEntryDR(xRefTable *model.XRefTable, d types.Dict) error { o, ok := d.Find("DR") if !ok { @@ -399,7 +516,7 @@ func validateAcroFormEntryDR(xRefTable *model.XRefTable, d types.Dict) error { return err } -func validateAcroForm(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { +func validateForm(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { // => 12.7.2 Interactive Form Dictionary @@ -409,26 +526,34 @@ func validateAcroForm(xRefTable *model.XRefTable, rootDict types.Dict, required } // Version check - err = xRefTable.ValidateVersion("AcroForm", sinceVersion) - if err != nil { + if err = xRefTable.ValidateVersion("AcroForm", sinceVersion); err != nil { return err } // Fields, required, array of indirect references o, ok := d.Find("Fields") if !ok { + // Fix empty AcroForm dict. rootDict.Delete("AcroForm") return nil } - xRefTable.AcroForm = d + xRefTable.Form = d + + dictName := "acroFormDict" - err = validateAcroFormFields(xRefTable, o) + // DA: optional, string + da, err := validateStringEntry(xRefTable, d, dictName, "DA", OPTIONAL, model.V10, validateDA) if err != nil { return err } - dictName := "acroFormDict" + requiresDA := da == nil || len(*da) == 0 + + err = validateFormFields(xRefTable, o, requiresDA) + if err != nil { + return err + } // NeedAppearances: optional, boolean _, err = validateBooleanEntry(xRefTable, d, dictName, "NeedAppearances", OPTIONAL, model.V10, nil) @@ -448,19 +573,13 @@ func validateAcroForm(xRefTable *model.XRefTable, rootDict types.Dict, required } // CO: arra - err = validateAcroFormEntryCO(xRefTable, d, model.V13) + err = validateFormEntryCO(xRefTable, d, model.V13, requiresDA) if err != nil { return err } // DR, optional, resource dict - err = validateAcroFormEntryDR(xRefTable, d) - if err != nil { - return err - } - - // DA: optional, string - _, err = validateStringEntry(xRefTable, d, dictName, "DA", OPTIONAL, model.V10, nil) + err = validateFormEntryDR(xRefTable, d) if err != nil { return err } @@ -472,5 +591,5 @@ func validateAcroForm(xRefTable *model.XRefTable, rootDict types.Dict, required } // XFA: optional, since 1.5, stream or array - return validateAcroFormXFA(xRefTable, d, sinceVersion) + return validateFormXFA(xRefTable, d, sinceVersion) } diff --git a/pkg/pdfcpu/validate/xReftable.go b/pkg/pdfcpu/validate/xReftable.go index c9890a022..79f348c2b 100644 --- a/pkg/pdfcpu/validate/xReftable.go +++ b/pkg/pdfcpu/validate/xReftable.go @@ -985,7 +985,7 @@ func validateRootObject(xRefTable *model.XRefTable) error { {validateOpenAction, OPTIONAL, model.V11}, {validateRootAdditionalActions, OPTIONAL, model.V14}, {validateURI, OPTIONAL, model.V11}, - {validateAcroForm, OPTIONAL, model.V12}, + {validateForm, OPTIONAL, model.V12}, {validateRootMetadata, OPTIONAL, model.V14}, {validateStructTree, OPTIONAL, model.V13}, {validateMarkInfo, OPTIONAL, model.V14}, diff --git a/pkg/samples/bookmarks/bookmarkTree.json b/pkg/samples/bookmarks/bookmarkTree.json new file mode 100644 index 000000000..bd38cb233 --- /dev/null +++ b/pkg/samples/bookmarks/bookmarkTree.json @@ -0,0 +1,62 @@ +{ + "header": { + "source": "bookmarkTree.pdf", + "version": "pdfcpu v0.5.0 dev", + "creation": "2023-08-19 10:12:08 CEST", + "title": "The Center of Why?\"", + "author": "Alan Kay", + "creator": "Acrobat PDFMaker 5.0 for Word", + "producer": "pdfcpu v0.5.0 dev", + "subject": "2004 Kyoto Prize Commorative Lecture" + }, + "bookmarks": [ + { + "title": "Page 1: Level 1", + "page": 1, + "color": { + "R": 0, + "G": 1, + "B": 0 + }, + "kids": [ + { + "title": "Page 2: Level 1.1", + "page": 2 + }, + { + "title": "Page 3: Level 1.2", + "page": 3, + "kids": [ + { + "title": "Page 4: Level 1.2.1", + "page": 4 + } + ] + } + ] + }, + { + "title": "Page 5: Level 2", + "page": 5, + "color": { + "R": 0, + "G": 0, + "B": 1 + }, + "kids": [ + { + "title": "Page 6: Level 2.1", + "page": 6 + }, + { + "title": "Page 7: Level 2.2", + "page": 7 + }, + { + "title": "Page 8: Level 2.3", + "page": 8 + } + ] + } + ] +} \ No newline at end of file