From 9cdf2fd31351f481e93420436aa05c1cb42d995b Mon Sep 17 00:00:00 2001 From: Horst Rutter Date: Tue, 10 Oct 2023 23:45:18 +0200 Subject: [PATCH] add pagemode, pagelayout commands --- cmd/pdfcpu/init.go | 28 ++++ cmd/pdfcpu/process.go | 94 ++++++++++++ cmd/pdfcpu/usage.go | 228 ++++++++++++++++++++++++------ pkg/api/api.go | 1 + pkg/api/attach.go | 1 + pkg/api/box.go | 1 + pkg/api/cut.go | 3 + pkg/api/keyword.go | 8 +- pkg/api/merge.go | 5 + pkg/api/pageLayout.go | 193 +++++++++++++++++++++++++ pkg/api/pageMode.go | 193 +++++++++++++++++++++++++ pkg/api/property.go | 8 +- pkg/api/test/pageLayout_test.go | 70 +++++++++ pkg/api/test/pageMode_test.go | 70 +++++++++ pkg/cli/cli.go | 46 ++++++ pkg/cli/cmd.go | 85 +++++++++++ pkg/cli/process.go | 33 +++++ pkg/cli/test/pageLayout_test.go | 74 ++++++++++ pkg/cli/test/pageMode_test.go | 74 ++++++++++ pkg/pdfcpu/doc.go | 85 +++++------ pkg/pdfcpu/info.go | 18 +++ pkg/pdfcpu/model/configuration.go | 7 +- pkg/pdfcpu/model/document.go | 112 +++++++++++++++ pkg/pdfcpu/model/xreftable.go | 3 + pkg/pdfcpu/validate/info.go | 11 ++ pkg/pdfcpu/validate/xReftable.go | 24 +++- 26 files changed, 1380 insertions(+), 95 deletions(-) create mode 100644 pkg/api/pageLayout.go create mode 100644 pkg/api/pageMode.go create mode 100644 pkg/api/test/pageLayout_test.go create mode 100644 pkg/api/test/pageMode_test.go create mode 100644 pkg/cli/test/pageLayout_test.go create mode 100644 pkg/cli/test/pageMode_test.go create mode 100644 pkg/pdfcpu/model/document.go diff --git a/cmd/pdfcpu/init.go b/cmd/pdfcpu/init.go index 8bd995ec..92026d0b 100644 --- a/cmd/pdfcpu/init.go +++ b/cmd/pdfcpu/init.go @@ -193,6 +193,30 @@ func initWatermarkCmdMap() commandMap { return m } +func initPageModeCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "list": {processListPageModeCommand, nil, "", ""}, + "set": {processSetPageModeCommand, nil, "", ""}, + "reset": {processResetPageModeCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + +func initPageLayoutCmdMap() commandMap { + m := newCommandMap() + for k, v := range map[string]command{ + "list": {processListPageLayoutCommand, nil, "", ""}, + "set": {processSetPageLayoutCommand, nil, "", ""}, + "reset": {processResetPageLayoutCommand, nil, "", ""}, + } { + m.register(k, v) + } + return m +} + func initCommandMap() { annotsCmdMap := initAnnotsCmdMap() attachCmdMap := initAttachCmdMap() @@ -208,6 +232,8 @@ func initCommandMap() { propertiesCmdMap := initPropertiesCmdMap() stampCmdMap := initStampCmdMap() watermarkCmdMap := initWatermarkCmdMap() + pageModeCmdMap := initPageModeCmdMap() + pageLayoutCmdMap := initPageLayoutCmdMap() cmdMap = newCommandMap() @@ -240,6 +266,8 @@ func initCommandMap() { "ndown": {processNDownCommand, nil, usageNDown, usageLongNDown}, "nup": {processNUpCommand, nil, usageNUp, usageLongNUp}, "optimize": {processOptimizeCommand, nil, usageOptimize, usageLongOptimize}, + "pagelayout": {nil, pageLayoutCmdMap, usagePageLayout, usageLongPageLayout}, + "pagemode": {nil, pageModeCmdMap, usagePageMode, usageLongPageMode}, "pages": {nil, pagesCmdMap, usagePages, usageLongPages}, "paper": {printPaperSizes, nil, usagePaper, usageLongPaper}, "permissions": {nil, permissionsCmdMap, usagePerm, usageLongPerm}, diff --git a/cmd/pdfcpu/process.go b/cmd/pdfcpu/process.go index c9ac5e95..bd329092 100644 --- a/cmd/pdfcpu/process.go +++ b/cmd/pdfcpu/process.go @@ -2316,3 +2316,97 @@ func processRemoveBookmarksCommand(conf *model.Configuration) { process(cli.RemoveBookmarksCommand(inFile, outFile, conf)) } + +func processListPageLayoutCommand(conf *model.Configuration) { + if len(flag.Args()) != 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usagePageLayoutList) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + process(cli.ListPageLayoutCommand(inFile, conf)) +} + +func processSetPageLayoutCommand(conf *model.Configuration) { + if len(flag.Args()) != 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usagePageLayoutSet) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + v := flag.Arg(1) + + if !validate.DocumentPageLayout(v) { + fmt.Fprintln(os.Stderr, "invalid page layout, use one of: SinglePage, TwoColumnLeft, TwoColumnRight, TwoPageLeft, TwoPageRight") + os.Exit(1) + } + + process(cli.SetPageLayoutCommand(inFile, "", v, conf)) +} + +func processResetPageLayoutCommand(conf *model.Configuration) { + if len(flag.Args()) != 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usagePageLayoutReset) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + process(cli.ResetPageLayoutCommand(inFile, "", conf)) +} + +func processListPageModeCommand(conf *model.Configuration) { + if len(flag.Args()) != 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usagePageModeList) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + process(cli.ListPageModeCommand(inFile, conf)) +} + +func processSetPageModeCommand(conf *model.Configuration) { + if len(flag.Args()) != 2 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usagePageModeSet) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + + v := flag.Arg(1) + + if !validate.DocumentPageMode(v) { + fmt.Fprintln(os.Stderr, "invalid page mode, use one of: UseNone, UseThumb, FullScreen, UseOC, UseAttachments") + os.Exit(1) + } + + process(cli.SetPageModeCommand(inFile, "", v, conf)) +} + +func processResetPageModeCommand(conf *model.Configuration) { + if len(flag.Args()) != 1 || selectedPages != "" { + fmt.Fprintf(os.Stderr, "usage: %s\n", usagePageModeReset) + os.Exit(1) + } + + inFile := flag.Arg(0) + if conf.CheckFileNameExt { + ensurePDFExtension(inFile) + } + process(cli.ResetPageModeCommand(inFile, "", conf)) +} diff --git a/cmd/pdfcpu/usage.go b/cmd/pdfcpu/usage.go index 041a4fad..7d829325 100644 --- a/cmd/pdfcpu/usage.go +++ b/cmd/pdfcpu/usage.go @@ -25,47 +25,50 @@ Usage: 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 - collect create custom sequence of selected pages - config print configuration - create create PDF content including forms via JSON - crop set cropbox for selected pages - cut custom cut pages horizontally or vertically - decrypt remove password protection - encrypt set password protection - extract extract images, fonts, content, pages or metadata - fonts install, list supported fonts, create cheat sheets - form list, remove fields, lock, unlock, reset, export, fill form via JSON or CSV - grid rearrange pages or images for enhanced browsing experience - images list images for selected pages - import import/convert images to PDF - info print file info - keywords list, add, remove keywords - merge concatenate PDFs - ndown cut selected pages into n pages symmetrically - nup rearrange pages or images for reduced number of pages - optimize optimize PDF by getting rid of redundant page resources - pages insert, remove selected pages - paper print list of supported paper sizes - permissions list, set user access permissions - portfolio list, add, remove, extract portfolio entries with optional description - poster cut selected pages into poster using paper size or dimensions - properties list, add, remove document properties - resize scale selected pages - rotate rotate selected pages - selectedpages print definition of the -pages flag - split split up a PDF by span or bookmark - stamp add, remove, update Unicode text, image or PDF stamps for selected pages - trim create trimmed version of selected pages - validate validate PDF against PDF 32000-1:2008 (PDF 1.7) - version print version - watermark add, remove, update Unicode text, image or PDF watermarks for selected pages + 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 + collect create custom sequence of selected pages + config print configuration + create create PDF content including forms via JSON + crop set cropbox for selected pages + cut custom cut pages horizontally or vertically + decrypt remove password protection + encrypt set password protection + extract extract images, fonts, content, pages or metadata + fonts install, list supported fonts, create cheat sheets + form list, remove fields, lock, unlock, reset, export, fill form via JSON or CSV + grid rearrange pages or images for enhanced browsing experience + images list images for selected pages + import import/convert images to PDF + info print file info + keywords list, add, remove keywords + merge concatenate PDFs + ndown cut selected pages into n pages symmetrically + nup rearrange pages or images for reduced number of pages + optimize optimize PDF by getting rid of redundant page resources + pagelayout list, set, reset page layout for opened document + pagemode list, set, reset page mode for opened document + pages insert, remove selected pages + paper print list of supported paper sizes + permissions list, set user access permissions + portfolio list, add, remove, extract portfolio entries with optional description + poster cut selected pages into poster using paper size or dimensions + properties list, add, remove document properties + resize scale selected pages + rotate rotate selected pages + selectedpages print definition of the -pages flag + split split up a PDF by span or bookmark + stamp add, remove, update Unicode text, image or PDF stamps for selected pages + trim create trimmed version of selected pages + validate validate PDF against PDF 32000-1:2008 (PDF 1.7) + version print version + viewpreferences list, set, reset viewing preferences for opened document + watermark add, remove, update Unicode text, image or PDF watermarks for selected pages All instantly recognizable command prefixes are supported eg. val for validation One letter Unix style abbreviations supported for flags and command parameters. @@ -1322,4 +1325,147 @@ description ... scalefactor, dimensions, formsize, enforce, border, bgcolor outFile ... output PDF file outFileJSON ... output PDF file ` + + usagePageLayoutList = "pdfcpu pagelayout list inFile" + usagePageLayoutSet = "pdfcpu pagelayout set inFile value" + usagePageLayoutReset = "pdfcpu pagelayout reset inFile" + generalFlags + + usagePageLayout = "usage: " + usagePageLayoutList + + "\n " + usagePageLayoutSet + + "\n " + usagePageLayoutReset + + usageLongPageLayout = `Manage the page layout which shall be used when the document is opened: + + inFile ... input PDF file + value ... one of: + + SinglePage ... Display one page at a time (default) + TwoColumnLeft ... Display the pages in two columns, with odd- numbered pages on the left + TwoColumnRight ... Display the pages in two columns, with odd- numbered pages on the right + TwoPageLeft ... Display the pages two at a time, with odd-numbered pages on the left + TwoPageRight ... Display the pages two at a time, with odd-numbered pages on the right + + Eg. set page layout: + pdfcpu pagelayout set test.pdf TwoPageLeft + + remove page layout: + pdfcpu pagelayout remove test.pdf +` + + usagePageModeList = "pdfcpu pagemode list inFile" + usagePageModeSet = "pdfcpu pagemode set inFile value" + usagePageModeReset = "pdfcpu pagemode reset inFile" + generalFlags + + usagePageMode = "usage: " + usagePageModeList + + "\n " + usagePageModeSet + + "\n " + usagePageModeReset + + usageLongPageMode = `Manage how the document shall be displayed when opened: + + inFile ... input PDF file + value ... one of: + + UseNone ... Neither document outline nor thumbnail images visible (default) + UseOutlines ... Document outline visible + UseThumbs ... Thumbnail images visible + FullScreen ... Full-screen mode, with no menu bar, window controls, or any other window visible + UseOC ... Optional content group panel visible (since PDF 1.5) + UseAttachments ... Attachments panel visible (since PDF 1.6) + + Eg. set page mode: + pdfcpu pagemode set test.pdf UseOutlines + + remove page mode: + pdfcpu pagemode remove test.pdf + ` + + usageViewPrefList = "pdfcpu viewpreferences list inFile" + usageViewPrefSet = "pdfcpu viewpreferences set inFile inFileJSON | nameValuePair..." + usageViewPrefReset = "pdfcpu viewpreferences reset inFile" + generalFlags + + usageViewPrefMode = "usage: " + usageViewPrefList + + "\n " + usageViewPrefSet + + "\n " + usageViewPrefReset + + usageLongViewPrefMode = `Manage the way the document shall be displayed on the screen and shall be printed: + + inFile ... input PDF file + inFileJSON ... input JSON file containing viewing preferences + nameValuePair ... 'name = value' + + The preferences are: + + HideToolbar ... Hide tool bars when the document is active (default=false). + HideMenubar ... Hide the menu bar when the document is active (default=false). + HideWindowUI ... Hide user interface elements in the document’s window (default=false). + FitWindow ... Resize the document’s window to fit the size of the first displayed page (default=false). + CenterWindow ... Position the document’s window in the centre of the screen (default=false). + DisplayDocTitle ... true: The window’s title bar should display the document title taken from the dc:title element of the XMP metadata stream. + false: The title bar should display the name of the PDF file containing the document (default=false). + + NonFullScreenPageMode ... How to display the document on exiting full-screen mode: + UseNone = Neither document outline nor thumbnail images visible (=default) + UseOutlines = Documentoutlinevisible + UseThumbs = Thumbnail images visible + UseOC = Optional content group panel visible + + Direction ... The predominant logical content order for text + L2R = Left to right (=default) + R2L = Right to left (including vertical writing systems, such as Chinese, Japanese, and Korean) + + Duplex ... The paper handling option that shall be used when printing the file from the print dialogue (since PDF 1.7): + Simplex = Print single-sided + DuplexFlipShortEdge = Duplex and flip on the short edge of the sheet + DuplexFlipLongEdge = Duplex and flip on the long edge of the sheet + + PickTrayByPDFSize ... Whether the PDF page size shall be used to select the input paper tray. + PrintPageRange ... The page numbers used to initialize the print dialogue box when the file is printed (since PDF 1.7). + The array shall contain an even number of integers to be interpreted in pairs, with each pair specifying + the first and last pages in a sub-range of pages to be printed. The first page of the PDF file shall be denoted by 1. + NumCopies ... The number of copies that shall be printed when the print dialog is opened for this file (since PDF 1.7). + Enforce ... Array of names of Viewer preference settings that shall be enforced by PDF processors and + that shall not be overridden by subsequent selections in the application user interface (since PDF 2.0). + Possible values: PrintScaling + + Eg. list viewer preferences: + pdfcpu viewpref list test.pdf + + remove viewer preferences: + pdfcpu viewpref remove test.pdf + + set printer preferences: + pdfcpu viewpref add test.pdf 'duplex = duplexFlipShortEdge' 'printPageRange = 1 4 10 20' 'numcopies = 3' + + set viewer preferences: + pdfcpu viewpref add test.pdf 'fitwindow = true', 'hidetoolbar = t', 'centerwindow = yes', 'hidemenubar = y' + + set viewer preferences via JSON: + pdfcpu viewpref add test.pdf viewpref.json + + and eg. viewpref.json (each preferences is optional!): + + { + "viewingPreferences": { + "HideToolBar": true, + "HideMenuBar": false, + "HideWindowUI": false, + "FitWindow": true, + "CenterWindow": true, + "DisplayDocTitle": true, + "NonFullScreenPageMode": "UseThumbs", + "Direction": "R2L", + "Duplex": "Simplex", + "PickTrayByPDFSize": false, + "PrintPageRange": [ + 1, 4, + 10, 20 + ], + "NumCopies": 3, + "Enforce": [ + "PrintScaling" + ] + } + } + + ` ) diff --git a/pkg/api/api.go b/pkg/api/api.go index a8fba284..ce0ab827 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -139,6 +139,7 @@ func readAndValidate(rs io.ReadSeeker, conf *model.Configuration, from1 time.Tim return ctx, dur1, dur2, nil } +// ReadValidateAndOptimize returns the model.Context of rs ready for processing. 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 { diff --git a/pkg/api/attach.go b/pkg/api/attach.go index 2dda598e..f7fc163a 100644 --- a/pkg/api/attach.go +++ b/pkg/api/attach.go @@ -28,6 +28,7 @@ import ( "github.com/pkg/errors" ) +// Attachments returns rs's attachments. func Attachments(rs io.ReadSeeker, conf *model.Configuration) ([]model.Attachment, error) { if rs == nil { return nil, errors.New("pdfcpu: Attachments: missing rs") diff --git a/pkg/api/box.go b/pkg/api/box.go index afe43c09..288924b3 100644 --- a/pkg/api/box.go +++ b/pkg/api/box.go @@ -42,6 +42,7 @@ func Box(s string, u types.DisplayUnit) (*model.Box, error) { return model.ParseBox(s, u) } +// Boxes returns rs's page boundaries for selected pages of rs. func Boxes(rs io.ReadSeeker, selectedPages []string, conf *model.Configuration) ([]model.PageBoundaries, error) { if rs == nil { return nil, errors.New("pdfcpu: Boxes: missing rs") diff --git a/pkg/api/cut.go b/pkg/api/cut.go index d40432ce..be763685 100644 --- a/pkg/api/cut.go +++ b/pkg/api/cut.go @@ -50,6 +50,7 @@ func prepareForCut(rs io.ReadSeeker, selectedPages []string, conf *model.Configu return ctxSrc, pages, nil } +// Poster applies cut for selected pages of rs and generates corresponding poster tiles in outDir. 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") @@ -125,6 +126,7 @@ func PosterFile(inFile, outDir, outFile string, selectedPages []string, cut *mod return Poster(f, outDir, outFile, selectedPages, cut, conf) } +// NDown applies n & cutConf for selected pages of rs and writes results to outDir. 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") @@ -218,6 +220,7 @@ func validateCut(cut *model.Cut) error { return nil } +// Cut applies cutConf for selected pages of rs and writes results to outDir. 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") diff --git a/pkg/api/keyword.go b/pkg/api/keyword.go index e1d08f00..6f61b399 100644 --- a/pkg/api/keyword.go +++ b/pkg/api/keyword.go @@ -48,7 +48,7 @@ func Keywords(rs io.ReadSeeker, conf *model.Configuration) ([]string, error) { return pdfcpu.KeywordsList(ctx.XRefTable) } -// AddKeywords embeds files into a PDF context read from rs and writes the result to w. +// AddKeywords adds keywords to rs's infodict 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") @@ -88,7 +88,7 @@ func AddKeywords(rs io.ReadSeeker, w io.Writer, files []string, conf *model.Conf return nil } -// AddKeywordsFile embeds files into a PDF context read from inFile and writes the result to outFile. +// AddKeywordsFile adds keywords to inFile's infodict and writes the result to outFile. func AddKeywordsFile(inFile, outFile string, files []string, conf *model.Configuration) (err error) { var f1, f2 *os.File @@ -128,7 +128,7 @@ func AddKeywordsFile(inFile, outFile string, files []string, conf *model.Configu return AddKeywords(f1, f2, files, conf) } -// RemoveKeywords deletes embedded files from a PDF context read from rs and writes the result to w. +// RemoveKeywords deletes keywords from rs's infodict 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") @@ -171,7 +171,7 @@ func RemoveKeywords(rs io.ReadSeeker, w io.Writer, keywords []string, conf *mode return nil } -// RemoveKeywordsFile deletes embedded files from a PDF context read from inFile and writes the result to outFile. +// RemoveKeywordsFile deletes keywords from inFile's infodict and writes the result to outFile. func RemoveKeywordsFile(inFile, outFile string, keywords []string, conf *model.Configuration) (err error) { var f1, f2 *os.File diff --git a/pkg/api/merge.go b/pkg/api/merge.go index 02cd044e..9537f437 100644 --- a/pkg/api/merge.go +++ b/pkg/api/merge.go @@ -94,6 +94,9 @@ func prepDestContext(destFile string, rs io.ReadSeeker, conf *model.Configuratio return ctxDest, nil } +// Merge concatenates inFiles. +// if destFile is supplied it appends the result to destfile (=MERGEAPPEND) +// if no destFile supplied it writes the result to the first entry of inFiles (=MERGECREATE). func Merge(destFile string, inFiles []string, w io.Writer, conf *model.Configuration) error { if w == nil { return errors.New("pdfcpu: Merge: Please provide w") @@ -159,6 +162,7 @@ func Merge(destFile string, inFiles []string, w io.Writer, conf *model.Configura return WriteContext(ctxDest, w) } +// MergeCreateFile merges inFiles and writes the result to outFile. func MergeCreateFile(inFiles []string, outFile string, conf *model.Configuration) (err error) { f, err := os.Create(outFile) if err != nil { @@ -176,6 +180,7 @@ func MergeCreateFile(inFiles []string, outFile string, conf *model.Configuration return Merge("", inFiles, f, conf) } +// MergeAppendFile appends inFiles to outFile. func MergeAppendFile(inFiles []string, outFile string, conf *model.Configuration) (err error) { tmpFile := outFile overWrite := false diff --git a/pkg/api/pageLayout.go b/pkg/api/pageLayout.go new file mode 100644 index 00000000..6e6d5bf6 --- /dev/null +++ b/pkg/api/pageLayout.go @@ -0,0 +1,193 @@ +/* + 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 api + +import ( + "io" + "os" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// ListPageLayout lists rs's page layout. +func ListPageLayout(rs io.ReadSeeker, conf *model.Configuration) (*model.PageLayout, error) { + if rs == nil { + return nil, errors.New("pdfcpu: PageLayout: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTPAGELAYOUT + + ctx, _, _, err := readAndValidate(rs, conf, time.Now()) + if err != nil { + return nil, err + } + + return ctx.PageLayout, nil +} + +// ListPageLayoutFile lists inFile's page layout. +func ListPageLayoutFile(inFile string, conf *model.Configuration) (*model.PageLayout, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return ListPageLayout(f, conf) +} + +// SetPageLayout sets rs's page layout and writes the result to w. +func SetPageLayout(rs io.ReadSeeker, w io.Writer, val model.PageLayout, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: SetPageLayout: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.SETPAGELAYOUT + + ctx, _, _, err := readAndValidate(rs, conf, time.Now()) + if err != nil { + return err + } + + ctx.RootDict["PageLayout"] = types.Name(val.String()) + + if err = WriteContext(ctx, w); err != nil { + return err + } + + return nil +} + +// SetPageLayoutFile sets inFile's page layout and writes the result to outFile. +func SetPageLayoutFile(inFile, outFile string, val model.PageLayout, 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 SetPageLayout(f1, f2, val, conf) +} + +// ResetPageLayout resets rs's page layout and writes the result to w. +func ResetPageLayout(rs io.ReadSeeker, w io.Writer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ResetPageLayout: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.RESETPAGELAYOUT + + ctx, _, _, err := readAndValidate(rs, conf, time.Now()) + if err != nil { + return err + } + + delete(ctx.RootDict, "PageLayout") + + if err = WriteContext(ctx, w); err != nil { + return err + } + + return nil +} + +// ResetPageLayoutFile resets inFile's page layout and writes the result to outFile. +func ResetPageLayoutFile(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 ResetPageLayout(f1, f2, conf) +} diff --git a/pkg/api/pageMode.go b/pkg/api/pageMode.go new file mode 100644 index 00000000..a61c6229 --- /dev/null +++ b/pkg/api/pageMode.go @@ -0,0 +1,193 @@ +/* + 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 api + +import ( + "io" + "os" + "time" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" + "github.com/pkg/errors" +) + +// ListPageMode lists rs's page mode. +func ListPageMode(rs io.ReadSeeker, conf *model.Configuration) (*model.PageMode, error) { + if rs == nil { + return nil, errors.New("pdfcpu: PageMode: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.LISTPAGEMODE + + ctx, _, _, err := readAndValidate(rs, conf, time.Now()) + if err != nil { + return nil, err + } + + return ctx.PageMode, nil +} + +// ListPageModeFile lists inFile's page mode. +func ListPageModeFile(inFile string, conf *model.Configuration) (*model.PageMode, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, err + } + defer f.Close() + + return ListPageMode(f, conf) +} + +// SetPageMode sets rs's page mode and writes the result to w. +func SetPageMode(rs io.ReadSeeker, w io.Writer, val model.PageMode, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: SetPageMode: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.SETPAGEMODE + + ctx, _, _, err := readAndValidate(rs, conf, time.Now()) + if err != nil { + return err + } + + ctx.RootDict["PageMode"] = types.Name(val.String()) + + if err = WriteContext(ctx, w); err != nil { + return err + } + + return nil +} + +// SetPageModeFile sets inFile's page mode and writes the result to outFile. +func SetPageModeFile(inFile, outFile string, val model.PageMode, 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 SetPageMode(f1, f2, val, conf) +} + +// ResetPageMode resets rs's page mode and writes the result to w. +func ResetPageMode(rs io.ReadSeeker, w io.Writer, conf *model.Configuration) error { + if rs == nil { + return errors.New("pdfcpu: ResetPageMode: missing rs") + } + + if conf == nil { + conf = model.NewDefaultConfiguration() + } else { + conf.ValidationMode = model.ValidationRelaxed + } + conf.Cmd = model.RESETPAGEMODE + + ctx, _, _, err := readAndValidate(rs, conf, time.Now()) + if err != nil { + return err + } + + delete(ctx.RootDict, "PageMode") + + if err = WriteContext(ctx, w); err != nil { + return err + } + + return nil +} + +// ResetPageModeFile resets inFile's page mode and writes the result to outFile. +func ResetPageModeFile(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 ResetPageMode(f1, f2, conf) +} diff --git a/pkg/api/property.go b/pkg/api/property.go index 57faeee0..4371357f 100644 --- a/pkg/api/property.go +++ b/pkg/api/property.go @@ -48,7 +48,7 @@ func Properties(rs io.ReadSeeker, conf *model.Configuration) (map[string]string, return ctx.Properties, nil } -// AddProperties embeds files into a PDF context read from rs and writes the result to w. +// AddProperties adds properties to rs's infodict 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") @@ -88,7 +88,7 @@ func AddProperties(rs io.ReadSeeker, w io.Writer, properties map[string]string, return nil } -// AddPropertiesFile embeds files into a PDF context read from inFile and writes the result to outFile. +// AddPropertiesFile adds properties to inFile's infodict and writes the result to outFile. func AddPropertiesFile(inFile, outFile string, properties map[string]string, conf *model.Configuration) (err error) { var f1, f2 *os.File @@ -128,7 +128,7 @@ func AddPropertiesFile(inFile, outFile string, properties map[string]string, con return AddProperties(f1, f2, properties, conf) } -// RemoveProperties deletes embedded files from a PDF context read from rs and writes the result to w. +// RemoveProperties deletes properties from rs's infodict 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") @@ -171,7 +171,7 @@ func RemoveProperties(rs io.ReadSeeker, w io.Writer, properties []string, conf * return nil } -// RemovePropertiesFile deletes embedded files from a PDF context read from inFile and writes the result to outFile. +// RemovePropertiesFile deletes properties from inFile's infodict and writes the result to outFile. func RemovePropertiesFile(inFile, outFile string, properties []string, conf *model.Configuration) (err error) { var f1, f2 *os.File diff --git a/pkg/api/test/pageLayout_test.go b/pkg/api/test/pageLayout_test.go new file mode 100644 index 00000000..f03ede97 --- /dev/null +++ b/pkg/api/test/pageLayout_test.go @@ -0,0 +1,70 @@ +/* +Copyright 2023 The pdf 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/pdfcpu/model" +) + +func TestPageLayout(t *testing.T) { + msg := "testPageLayout" + + fileName := "test.pdf" + inFile := filepath.Join(outDir, fileName) + copyFile(t, filepath.Join(inDir, fileName), inFile) + + pageLayout := model.PageLayoutTwoColumnLeft + + pl, err := api.ListPageLayoutFile(inFile, nil) + if err != nil { + t.Fatalf("%s %s: list pageLayout: %v\n", msg, inFile, err) + } + if pl != nil { + t.Fatalf("%s %s: list pageLayout, unexpected: %s\n", msg, inFile, pl) + } + + if err := api.SetPageLayoutFile(inFile, "", pageLayout, nil); err != nil { + t.Fatalf("%s %s: set pageLayout: %v\n", msg, inFile, err) + } + + pl, err = api.ListPageLayoutFile(inFile, nil) + if err != nil { + t.Fatalf("%s %s: list pageLayout: %v\n", msg, inFile, err) + } + if pl == nil { + t.Fatalf("%s %s: list pageLayout, missing page layout\n", msg, inFile) + } + if *pl != pageLayout { + t.Fatalf("%s %s: list pageLayout, want:%s, got:%s\n", msg, inFile, pageLayout, *pl) + } + + if err := api.ResetPageLayoutFile(inFile, "", nil); err != nil { + t.Fatalf("%s %s: reset pageLayout: %v\n", msg, inFile, err) + } + + pl, err = api.ListPageLayoutFile(inFile, nil) + if err != nil { + t.Fatalf("%s %s: list page layout: %v\n", msg, inFile, err) + } + if pl != nil { + t.Fatalf("%s %s: list page layout, unexpected: %s\n", msg, inFile, pl) + } +} diff --git a/pkg/api/test/pageMode_test.go b/pkg/api/test/pageMode_test.go new file mode 100644 index 00000000..5fe63b31 --- /dev/null +++ b/pkg/api/test/pageMode_test.go @@ -0,0 +1,70 @@ +/* +Copyright 2023 The pdf 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/pdfcpu/model" +) + +func TestPageMode(t *testing.T) { + msg := "testPageMode" + + fileName := "test.pdf" + inFile := filepath.Join(outDir, fileName) + copyFile(t, filepath.Join(inDir, fileName), inFile) + + pageMode := model.PageModeUseOutlines + + pl, err := api.ListPageModeFile(inFile, nil) + if err != nil { + t.Fatalf("%s %s: list pageMode: %v\n", msg, inFile, err) + } + if pl != nil { + t.Fatalf("%s %s: list pageMode, unexpected: %s\n", msg, inFile, pl) + } + + if err := api.SetPageModeFile(inFile, "", pageMode, nil); err != nil { + t.Fatalf("%s %s: set pageMode: %v\n", msg, inFile, err) + } + + pl, err = api.ListPageModeFile(inFile, nil) + if err != nil { + t.Fatalf("%s %s: list pageMode: %v\n", msg, inFile, err) + } + if pl == nil { + t.Fatalf("%s %s: list pageMode, missing page mode\n", msg, inFile) + } + if *pl != pageMode { + t.Fatalf("%s %s: list pageMode, want:%s, got:%s\n", msg, inFile, pageMode, *pl) + } + + if err := api.ResetPageModeFile(inFile, "", nil); err != nil { + t.Fatalf("%s %s: reset pageMode: %v\n", msg, inFile, err) + } + + pl, err = api.ListPageModeFile(inFile, nil) + if err != nil { + t.Fatalf("%s %s: list pageMode: %v\n", msg, inFile, err) + } + if pl != nil { + t.Fatalf("%s %s: list pageMode, unexpected: %s\n", msg, inFile, pl) + } +} diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index b1e1dc98..06929df0 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -362,3 +362,49 @@ func ImportBookmarks(cmd *Command) ([]string, error) { func RemoveBookmarks(cmd *Command) ([]string, error) { return nil, api.RemoveBookmarksFile(*cmd.InFile, *cmd.OutFile, cmd.Conf) } + +// ListPageLayout returns inFile's page layout. +func ListPageLayout(cmd *Command) ([]string, error) { + pl, err := api.ListPageLayoutFile(*cmd.InFile, cmd.Conf) + var ss []string + if pl != nil { + ss = append(ss, pl.String()) + } else { + ss = append(ss, "No page layout set, PDF viewers will default to \"SinglePage\"") + } + return ss, err +} + +// SetPageLayout sets inFile's page layout. +func SetPageLayout(cmd *Command) ([]string, error) { + pageLayout := model.PageLayoutFor(cmd.StringVal) + return nil, api.SetPageLayoutFile(*cmd.InFile, *cmd.OutFile, *pageLayout, cmd.Conf) +} + +// ResetPageLayout resets inFile's page layout. +func ResetPageLayout(cmd *Command) ([]string, error) { + return nil, api.ResetPageLayoutFile(*cmd.InFile, *cmd.OutFile, cmd.Conf) +} + +// ListPageMode returns inFile's page mode. +func ListPageMode(cmd *Command) ([]string, error) { + pm, err := api.ListPageModeFile(*cmd.InFile, cmd.Conf) + var ss []string + if pm != nil { + ss = append(ss, pm.String()) + } else { + ss = append(ss, "No page mode set, PDF viewers will default to \"UseNone\"") + } + return ss, err +} + +// SetPageMode sets inFile's page mode. +func SetPageMode(cmd *Command) ([]string, error) { + pageMode := model.PageModeFor(cmd.StringVal) + return nil, api.SetPageModeFile(*cmd.InFile, *cmd.OutFile, *pageMode, cmd.Conf) +} + +// ResetPageMode resets inFile's page mode. +func ResetPageMode(cmd *Command) ([]string, error) { + return nil, api.ResetPageModeFile(*cmd.InFile, *cmd.OutFile, cmd.Conf) +} diff --git a/pkg/cli/cmd.go b/pkg/cli/cmd.go index 1ac667dc..e667259c 100644 --- a/pkg/cli/cmd.go +++ b/pkg/cli/cmd.go @@ -36,6 +36,7 @@ type Command struct { PageSelection []string PWOld *string PWNew *string + StringVal string IntVal int BoolVal bool IntVals []int @@ -122,6 +123,12 @@ var cmdMap = map[model.CommandMode]func(cmd *Command) ([]string, error){ model.EXPORTBOOKMARKS: processBookmarks, model.IMPORTBOOKMARKS: processBookmarks, model.REMOVEBOOKMARKS: processBookmarks, + model.LISTPAGEMODE: processPageMode, + model.SETPAGEMODE: processPageMode, + model.RESETPAGEMODE: processPageMode, + model.LISTPAGELAYOUT: processPageLayout, + model.SETPAGELAYOUT: processPageLayout, + model.RESETPAGELAYOUT: processPageLayout, } // ValidateCommand creates a new command to validate a file. @@ -1043,3 +1050,81 @@ func RemoveBookmarksCommand(inFile, outFile string, conf *model.Configuration) * OutFile: &outFile, Conf: conf} } + +// ListPageLayoutCommand creates a new command to list the document page layout. +func ListPageLayoutCommand(inFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTPAGELAYOUT + return &Command{ + Mode: model.LISTPAGELAYOUT, + InFile: &inFile, + Conf: conf} +} + +// SetPageLayoutCommand creates a new command to set the document page layout. +func SetPageLayoutCommand(inFile, outFile, value string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.SETPAGELAYOUT + return &Command{ + Mode: model.SETPAGELAYOUT, + InFile: &inFile, + OutFile: &outFile, + StringVal: value, + Conf: conf} +} + +// ResetPageLayoutCommand creates a new command to reset the document page layout. +func ResetPageLayoutCommand(inFile, outFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.RESETPAGELAYOUT + return &Command{ + Mode: model.RESETPAGELAYOUT, + InFile: &inFile, + OutFile: &outFile, + Conf: conf} +} + +// ListPageModeCommand creates a new command to list the document page mode. +func ListPageModeCommand(inFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.LISTPAGEMODE + return &Command{ + Mode: model.LISTPAGEMODE, + InFile: &inFile, + Conf: conf} +} + +// SetPageModeCommand creates a new command to set the document page mode. +func SetPageModeCommand(inFile, outFile, value string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.SETPAGEMODE + return &Command{ + Mode: model.SETPAGEMODE, + InFile: &inFile, + OutFile: &outFile, + StringVal: value, + Conf: conf} +} + +// ResetPageModeCommand creates a new command to reset the document page mode. +func ResetPageModeCommand(inFile, outFile string, conf *model.Configuration) *Command { + if conf == nil { + conf = model.NewDefaultConfiguration() + } + conf.Cmd = model.RESETPAGEMODE + return &Command{ + Mode: model.RESETPAGEMODE, + InFile: &inFile, + OutFile: &outFile, + Conf: conf} +} diff --git a/pkg/cli/process.go b/pkg/cli/process.go index 3ac58a69..3e662fa0 100644 --- a/pkg/cli/process.go +++ b/pkg/cli/process.go @@ -224,5 +224,38 @@ func processBookmarks(cmd *Command) (out []string, err error) { case model.REMOVEBOOKMARKS: return RemoveBookmarks(cmd) } + + return nil, nil +} + +func processPageLayout(cmd *Command) (out []string, err error) { + switch cmd.Mode { + + case model.LISTPAGELAYOUT: + return ListPageLayout(cmd) + + case model.SETPAGELAYOUT: + return SetPageLayout(cmd) + + case model.RESETPAGELAYOUT: + return ResetPageLayout(cmd) + } + + return nil, nil +} + +func processPageMode(cmd *Command) (out []string, err error) { + switch cmd.Mode { + + case model.LISTPAGEMODE: + return ListPageMode(cmd) + + case model.SETPAGEMODE: + return SetPageMode(cmd) + + case model.RESETPAGEMODE: + return ResetPageMode(cmd) + } + return nil, nil } diff --git a/pkg/cli/test/pageLayout_test.go b/pkg/cli/test/pageLayout_test.go new file mode 100644 index 00000000..485eb03b --- /dev/null +++ b/pkg/cli/test/pageLayout_test.go @@ -0,0 +1,74 @@ +/* +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" + "strings" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +func TestPageLayout(t *testing.T) { + msg := "testPageLayout" + + pageLayout := "TwoColumnLeft" + + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(outDir, "test.pdf") + + cmd := cli.ListPageLayoutCommand(inFile, conf) + ss, err := cli.Process(cmd) + if err != nil { + t.Fatalf("%s %s: list pageLayout: %v\n", msg, inFile, err) + } + if len(ss) > 0 && !strings.HasPrefix(ss[0], "No page layout") { + t.Fatalf("%s %s: list pageLayout, unexpected: %s\n", msg, inFile, ss[0]) + } + + cmd = cli.SetPageLayoutCommand(inFile, outFile, pageLayout, nil) + if _, err = cli.Process(cmd); err != nil { + t.Fatalf("%s %s: set pageLayout: %v\n", msg, outFile, err) + } + + cmd = cli.ListPageLayoutCommand(outFile, conf) + ss, err = cli.Process(cmd) + if err != nil { + t.Fatalf("%s %s: list pageLayout: %v\n", msg, outFile, err) + } + if len(ss) == 0 { + t.Fatalf("%s %s: list pageLayout, missing page layout\n", msg, outFile) + } + if ss[0] != pageLayout { + t.Fatalf("%s %s: list pageLayout, want:%s, got:%s\n", msg, outFile, pageLayout, ss[0]) + } + + cmd = cli.ResetPageLayoutCommand(outFile, "", nil) + if _, err = cli.Process(cmd); err != nil { + t.Fatalf("%s %s: reset pageLayout: %v\n", msg, outFile, err) + } + + cmd = cli.ListPageLayoutCommand(outFile, conf) + ss, err = cli.Process(cmd) + if err != nil { + t.Fatalf("%s %s: list pageLayout: %v\n", msg, outFile, err) + } + if len(ss) > 0 && !strings.HasPrefix(ss[0], "No page layout") { + t.Fatalf("%s %s: list pageLayout, unexpected: %s\n", msg, outFile, ss[0]) + } +} diff --git a/pkg/cli/test/pageMode_test.go b/pkg/cli/test/pageMode_test.go new file mode 100644 index 00000000..fe67d2d3 --- /dev/null +++ b/pkg/cli/test/pageMode_test.go @@ -0,0 +1,74 @@ +/* +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" + "strings" + "testing" + + "github.com/pdfcpu/pdfcpu/pkg/cli" +) + +func TestPageMode(t *testing.T) { + msg := "testPageMode" + + pageMode := "UseOutlines" + + inFile := filepath.Join(inDir, "test.pdf") + outFile := filepath.Join(outDir, "test.pdf") + + cmd := cli.ListPageModeCommand(inFile, conf) + ss, err := cli.Process(cmd) + if err != nil { + t.Fatalf("%s %s: list pageMode: %v\n", msg, inFile, err) + } + if len(ss) > 0 && !strings.HasPrefix(ss[0], "No page mode") { + t.Fatalf("%s %s: list pageMode, unexpected: %s\n", msg, inFile, ss[0]) + } + + cmd = cli.SetPageModeCommand(inFile, outFile, pageMode, nil) + if _, err = cli.Process(cmd); err != nil { + t.Fatalf("%s %s: set pageMode: %v\n", msg, outFile, err) + } + + cmd = cli.ListPageModeCommand(outFile, conf) + ss, err = cli.Process(cmd) + if err != nil { + t.Fatalf("%s %s: list pageMode: %v\n", msg, outFile, err) + } + if len(ss) == 0 { + t.Fatalf("%s %s: list pageMode, missing pageMode\n", msg, outFile) + } + if ss[0] != pageMode { + t.Fatalf("%s %s: list pageMode, want:%s, got:%s\n", msg, outFile, pageMode, ss[0]) + } + + cmd = cli.ResetPageModeCommand(outFile, "", nil) + if _, err = cli.Process(cmd); err != nil { + t.Fatalf("%s %s: reset pageMode: %v\n", msg, outFile, err) + } + + cmd = cli.ListPageModeCommand(outFile, conf) + ss, err = cli.Process(cmd) + if err != nil { + t.Fatalf("%s %s: list pageMode: %v\n", msg, outFile, err) + } + if len(ss) > 0 && !strings.HasPrefix(ss[0], "No page mode") { + t.Fatalf("%s %s: list pageMode, unexpected: %s\n", msg, outFile, ss[0]) + } +} diff --git a/pkg/pdfcpu/doc.go b/pkg/pdfcpu/doc.go index ff31d5d8..853989cd 100644 --- a/pkg/pdfcpu/doc.go +++ b/pkg/pdfcpu/doc.go @@ -4,46 +4,49 @@ It provides an API and a command line interface. Supported are all versions up t 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 - collect create custom sequence of selected pages - config print configuration - create create PDF content including forms via JSON - crop set cropbox for selected pages - cut custom cut pages horizontally or vertically - decrypt remove password protection - encrypt set password protection - extract extract images, fonts, content, pages or metadata - fonts install, list supported fonts, create cheat sheets - form list, remove fields, lock, unlock, reset, export, fill form via JSON or CSV - grid rearrange pages or images for enhanced browsing experience - images list images for selected pages - import import/convert images to PDF - info print file info - keywords list, add, remove keywords - merge concatenate PDFs - ndown cut selected pages into n pages symmetrically - nup rearrange pages or images for reduced number of pages - optimize optimize PDF by getting rid of redundant page resources - pages insert, remove selected pages - paper print list of supported paper sizes - permissions list, set user access permissions - portfolio list, add, remove, extract portfolio entries with optional description - poster cut selected pages into poster using paper size or dimensions - properties list, add, remove document properties - resize scale selected pages - rotate rotate selected pages - selectedpages print definition of the -pages flag - split split up a PDF by span or bookmark - stamp add, remove, update Unicode text, image or PDF stamps for selected pages - trim create trimmed version of selected pages - validate validate PDF against PDF 32000-1:2008 (PDF 1.7) - version print version - watermark add, remove, update Unicode text, image or PDF watermarks for selected pages + 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 + collect create custom sequence of selected pages + config print configuration + create create PDF content including forms via JSON + crop set cropbox for selected pages + cut custom cut pages horizontally or vertically + decrypt remove password protection + encrypt set password protection + extract extract images, fonts, content, pages or metadata + fonts install, list supported fonts, create cheat sheets + form list, remove fields, lock, unlock, reset, export, fill form via JSON or CSV + grid rearrange pages or images for enhanced browsing experience + images list images for selected pages + import import/convert images to PDF + info print file info + keywords list, add, remove keywords + merge concatenate PDFs + ndown cut selected pages into n pages symmetrically + nup rearrange pages or images for reduced number of pages + optimize optimize PDF by getting rid of redundant page resources + pagelayout list, set, reset page layout for opened document + pagemode list, set, reset page mode for opened document + pages insert, remove selected pages + paper print list of supported paper sizes + permissions list, set user access permissions + portfolio list, add, remove, extract portfolio entries with optional description + poster cut selected pages into poster using paper size or dimensions + properties list, add, remove document properties + resize scale selected pages + rotate rotate selected pages + selectedpages print definition of the -pages flag + split split up a PDF by span or bookmark + stamp add, remove, update Unicode text, image or PDF stamps for selected pages + trim create trimmed version of selected pages + validate validate PDF against PDF 32000-1:2008 (PDF 1.7) + version print version + viewpreferences list, set, reset viewing preferences for opened document + watermark add, remove, update Unicode text, image or PDF watermarks for selected pages */ package pdfcpu diff --git a/pkg/pdfcpu/info.go b/pkg/pdfcpu/info.go index 3f7681fa..d55395c7 100644 --- a/pkg/pdfcpu/info.go +++ b/pkg/pdfcpu/info.go @@ -321,6 +321,8 @@ type PDFInfo struct { Creator string `json:"creator"` CreationDate string `json:"creationDate"` ModificationDate string `json:"modificationDate"` + PageMode string `json:"pageMode"` + PageLayout string `json:"pageLayout"` Keywords []string `json:"keywords"` Properties map[string]string `json:"properties"` Tagged bool `json:"tagged"` @@ -511,6 +513,16 @@ func Info(ctx *model.Context, fileName string, selectedPages types.IntSet) (*PDF info.CreationDate = ctx.CreationDate info.ModificationDate = ctx.ModDate + info.PageMode = "" + if ctx.PageMode != nil { + info.PageMode = ctx.PageMode.String() + } + + info.PageLayout = "" + if ctx.PageLayout != nil { + info.PageLayout = ctx.PageLayout.String() + } + kwl, err := KeywordsList(ctx.XRefTable) if err != nil { return nil, err @@ -572,6 +584,12 @@ func ListInfo(info *PDFInfo, selectedPages types.IntSet) ([]string, error) { 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)) + if info.PageMode != "" { + ss = append(ss, fmt.Sprintf("%20s: %s", "Page mode", info.PageMode)) + } + if info.PageLayout != "" { + ss = append(ss, fmt.Sprintf("%20s: %s", "Page Layout", info.PageLayout)) + } info.renderKeywords(&ss) info.renderProperties(&ss) diff --git a/pkg/pdfcpu/model/configuration.go b/pkg/pdfcpu/model/configuration.go index 61fa596b..fee3bece 100644 --- a/pkg/pdfcpu/model/configuration.go +++ b/pkg/pdfcpu/model/configuration.go @@ -128,6 +128,12 @@ const ( POSTER NDOWN CUT + LISTPAGELAYOUT + SETPAGELAYOUT + RESETPAGELAYOUT + LISTPAGEMODE + SETPAGEMODE + RESETPAGEMODE ) // Configuration of a Context. @@ -297,7 +303,6 @@ func NewDefaultConfiguration() *Configuration { if err != nil { path = os.TempDir() } - println(path) if err = EnsureDefaultConfigAt(path); err == nil { c := *loadedDefaultConfig return &c diff --git a/pkg/pdfcpu/model/document.go b/pkg/pdfcpu/model/document.go new file mode 100644 index 00000000..df42969e --- /dev/null +++ b/pkg/pdfcpu/model/document.go @@ -0,0 +1,112 @@ +/* +Copyright 2021 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 model + +import "strings" + +type PageMode int + +const ( + PageModeUseNone PageMode = iota + PageModeUseOutlines + PageModeUseThumb + PageModeFullScreen + PageModeUseOC + PageModeUseAttachments +) + +func (pm PageMode) String() string { + switch pm { + case PageModeUseNone: + return "UseNone" // = default + case PageModeUseOutlines: + return "UseOutlines" + case PageModeUseThumb: + return "UseThumb" + case PageModeFullScreen: + return "FullScreen" + case PageModeUseOC: + return "UseOC" + case PageModeUseAttachments: + return "UseAttachments" + default: + return "?" + } +} + +func PageModeFor(s string) *PageMode { + var pm PageMode + switch strings.ToLower(s) { + case "usenone": + pm = PageModeUseNone + case "useoutlines": + pm = PageModeUseOutlines + case "usethumb": + pm = PageModeUseThumb + case "fullscreen": + pm = PageModeFullScreen + case "useoc": + pm = PageModeUseOC + case "useattachments": + pm = PageModeUseAttachments + } + return &pm +} + +type PageLayout int + +const ( + PageLayoutSinglePage PageLayout = iota + PageLayoutTwoColumnLeft + PageLayoutTwoColumnRight + PageLayoutTwoPageLeft + PageLayoutTwoPageRight +) + +func (pl PageLayout) String() string { + switch pl { + case PageLayoutSinglePage: + return "SinglePage" // = default + case PageLayoutTwoColumnLeft: + return "TwoColumnLeft" + case PageLayoutTwoColumnRight: + return "TwoColumnRight" + case PageLayoutTwoPageLeft: + return "TwoPageLeft" + case PageLayoutTwoPageRight: + return "TwoPageRight" + default: + return "?" + } +} + +func PageLayoutFor(s string) *PageLayout { + var pl PageLayout + switch strings.ToLower(s) { + case "singlepage": + pl = PageLayoutSinglePage + case "twocolumnleft": + pl = PageLayoutTwoColumnLeft + case "twocolumnright": + pl = PageLayoutTwoColumnRight + case "twopageleft": + pl = PageLayoutTwoPageLeft + case "twopageright": + pl = PageLayoutTwoPageRight + } + return &pl +} diff --git a/pkg/pdfcpu/model/xreftable.go b/pkg/pdfcpu/model/xreftable.go index 3822a0c1..6650a5be 100644 --- a/pkg/pdfcpu/model/xreftable.go +++ b/pkg/pdfcpu/model/xreftable.go @@ -127,6 +127,9 @@ type XRefTable struct { ModDate string Properties map[string]string + PageLayout *PageLayout + PageMode *PageMode + // Linearization section (not yet supported) OffsetPrimaryHintTable *int64 OffsetOverflowHintTable *int64 diff --git a/pkg/pdfcpu/validate/info.go b/pkg/pdfcpu/validate/info.go index 34ab8422..adc0b239 100644 --- a/pkg/pdfcpu/validate/info.go +++ b/pkg/pdfcpu/validate/info.go @@ -17,6 +17,7 @@ limitations under the License. package validate import ( + "strings" "unicode/utf8" "github.com/pdfcpu/pdfcpu/pkg/log" @@ -204,3 +205,13 @@ func validateDocumentInfoObject(xRefTable *model.XRefTable) error { return nil } + +// DocumentPageLayout returns true for valid page layout values. +func DocumentPageLayout(s string) bool { + return types.MemberOf(strings.ToLower(s), []string{"singlepage", "twocolumnleft", "twocolumnright", "twopageleft", "twopageright"}) +} + +// DocumentPageMode returns true for valid page mode values. +func DocumentPageMode(s string) bool { + return types.MemberOf(strings.ToLower(s), []string{"usenone", "useoutlines", "usethumbs", "fullscreen", "useoc", "useattachments"}) +} diff --git a/pkg/pdfcpu/validate/xReftable.go b/pkg/pdfcpu/validate/xReftable.go index ffb31cef..0a8833a3 100644 --- a/pkg/pdfcpu/validate/xReftable.go +++ b/pkg/pdfcpu/validate/xReftable.go @@ -323,8 +323,16 @@ func pageLayoutValidator(v model.Version) func(s string) bool { } func validatePageLayout(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { - _, err := validateNameEntry(xRefTable, rootDict, "rootDict", "PageLayout", required, sinceVersion, pageLayoutValidator(xRefTable.Version())) - return err + n, err := validateNameEntry(xRefTable, rootDict, "rootDict", "PageLayout", required, sinceVersion, pageLayoutValidator(xRefTable.Version())) + if err != nil { + return err + } + + if n != nil { + xRefTable.PageLayout = model.PageLayoutFor(n.String()) + } + + return nil } func pageModeValidator(v model.Version) func(s string) bool { @@ -342,8 +350,16 @@ func pageModeValidator(v model.Version) func(s string) bool { } func validatePageMode(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error { - _, err := validateNameEntry(xRefTable, rootDict, "rootDict", "PageMode", required, sinceVersion, pageModeValidator(xRefTable.Version())) - return err + n, err := validateNameEntry(xRefTable, rootDict, "rootDict", "PageMode", required, sinceVersion, pageModeValidator(xRefTable.Version())) + if err != nil { + return err + } + + if n != nil { + xRefTable.PageMode = model.PageModeFor(n.String()) + } + + return nil } func validateOpenAction(xRefTable *model.XRefTable, rootDict types.Dict, required bool, sinceVersion model.Version) error {