Skip to content

Commit 4f206b6

Browse files
deon-gracias3ace
andauthored
Added organize feature (#64)
* Added organize feature * Added changes from suggestion Suggestion: https://github.com/unidoc/unipdf-cli/pull/64/files#r1770322097 * Apply suggestions from code review Co-authored-by: Ade Anom A <adeanom@yahoo.com> * Applied suggestions from code review - Comment: https://github.com/unidoc/unipdf-cli/pull/64/files/3898c2938786d339d44b7affb4becfddfc704452#r1770271470 --------- Co-authored-by: Ade Anom A <adeanom@yahoo.com>
1 parent 9dfe4f1 commit 4f206b6

File tree

3 files changed

+282
-1
lines changed

3 files changed

+282
-1
lines changed

internal/cli/organize.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* This file is subject to the terms and conditions defined in
3+
* file 'LICENSE.md', which is part of this source code package.
4+
*/
5+
6+
package cli
7+
8+
import (
9+
"errors"
10+
"fmt"
11+
12+
"github.com/spf13/cobra"
13+
"github.com/unidoc/unipdf-cli/pkg/pdf"
14+
)
15+
16+
const organizeCmdDesc = `Split PDF files.
17+
18+
The command is used to organize one or more page ranges from the input file
19+
and save the result as the output file.
20+
If no page range is specified, all the pages from the input file will be
21+
copied to the output file.
22+
23+
An example of the pages parameter: 1-3,4,6-7
24+
Only pages 1,2,3 (1-3), 4 and 6,7 (6-7) will be present in the output file,
25+
while page number 5 is skipped.
26+
`
27+
28+
var organizeCmdExample = fmt.Sprintf("%s\n%s\n",
29+
fmt.Sprintf("%s organize input_file.pdf output_file.pdf 1-2", appName),
30+
fmt.Sprintf("%s organize -p pass input_file.pd output_file.pdf 1-2,4", appName),
31+
)
32+
33+
// organizeCmd represents the split command.
34+
var organizeCmd = &cobra.Command{
35+
Use: "organize [FLAG]... INPUT_FILE OUTPUT_FILE [PAGES]",
36+
Short: "Organize PDF files",
37+
Long: organizeCmdDesc,
38+
Example: organizeCmdExample,
39+
DisableFlagsInUseLine: true,
40+
Run: func(cmd *cobra.Command, args []string) {
41+
inputPath := args[0]
42+
outputPath := args[1]
43+
password, _ := cmd.Flags().GetString("password")
44+
45+
// Parse page range.
46+
var err error
47+
var pages []int
48+
49+
if len(args) > 2 {
50+
if pages, err = parsePageRangeUnsorted(args[2]); err != nil {
51+
printUsageErr(cmd, "Invalid page range specified\n")
52+
}
53+
}
54+
55+
if err := pdf.Organize(inputPath, outputPath, password, pages); err != nil {
56+
printErr("Error: %s\n", err)
57+
}
58+
59+
fmt.Printf("Successfully organized file %s\n", inputPath)
60+
fmt.Printf("Output file saved to %s\n", outputPath)
61+
},
62+
Args: func(_ *cobra.Command, args []string) error {
63+
if len(args) < 2 {
64+
return errors.New("must provide at least the input and output files")
65+
}
66+
67+
return nil
68+
},
69+
}
70+
71+
func init() {
72+
rootCmd.AddCommand(organizeCmd)
73+
74+
organizeCmd.Flags().StringP("password", "p", "", "input file password")
75+
}

internal/cli/utils.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ func parsePageRange(pageRange string) ([]int, error) {
3939
}
4040

4141
indices := strings.Split(rng, "-")
42-
4342
lenIndices := len(indices)
4443
if lenIndices > 2 {
4544
return nil, errors.New("invalid page range")
@@ -86,6 +85,61 @@ func parsePageRange(pageRange string) ([]int, error) {
8685
return pages, nil
8786
}
8887

88+
func parsePageRangeUnsorted(pageRange string) ([]int, error) {
89+
var pages []int
90+
91+
rngs := strings.Split(removeSpaces(pageRange), ",")
92+
for _, rng := range rngs {
93+
if rng == "" {
94+
continue
95+
}
96+
97+
indices := strings.Split(rng, "-")
98+
lenIndices := len(indices)
99+
if lenIndices > 2 {
100+
return nil, errors.New("invalid page range")
101+
}
102+
if lenIndices == 2 {
103+
start, err := strconv.Atoi(indices[0])
104+
if err != nil {
105+
return nil, errors.New("invalid start page number")
106+
}
107+
if start < 1 {
108+
return nil, errors.New("page range start must be greater than 0")
109+
}
110+
111+
end, err := strconv.Atoi(indices[1])
112+
if err != nil {
113+
return nil, errors.New("invalid end page number")
114+
}
115+
if end < 1 {
116+
return nil, errors.New("page range end must be greater than 0")
117+
}
118+
119+
if start > end {
120+
return nil, errors.New("page range end must be greater than the start")
121+
}
122+
123+
for page := start; page <= end; page++ {
124+
pages = append(pages, page)
125+
}
126+
127+
continue
128+
}
129+
130+
page, err := strconv.Atoi(indices[0])
131+
if err != nil {
132+
return nil, errors.New("invalid page number")
133+
}
134+
135+
pages = append(pages, page)
136+
}
137+
138+
pages = uniqueIntSlice(pages)
139+
140+
return pages, nil
141+
}
142+
89143
func parseInputPaths(inputPaths []string, recursive bool, matcher fileMatcher) ([]string, error) {
90144
var err error
91145
var files []string

pkg/pdf/organize.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* This file is subject to the terms and conditions defined in
3+
* file 'LICENSE.md', which is part of this source code package.
4+
*/
5+
6+
package pdf
7+
8+
import (
9+
"github.com/unidoc/unipdf/v3/common"
10+
unipdf "github.com/unidoc/unipdf/v3/model"
11+
)
12+
13+
// Organize extracts the provided page list from PDF file specified by the
14+
// inputPath parameter then merges the individual pages and saves the
15+
// resulting file at the location specified by the outputPath parameter.
16+
// A password can be passed in for encrypted input files.
17+
func Organize(inputPath, outputPath, password string, pages []int) error {
18+
// Read input file.
19+
pdfReader, _, _, _, err := readPDF(inputPath, password)
20+
if err != nil {
21+
return err
22+
}
23+
24+
// Add selected pages to the writer.
25+
pdfWriter := unipdf.NewPdfWriter()
26+
27+
for i := 0; i < len(pages); i++ {
28+
page, err := pdfReader.GetPage(pages[i])
29+
if err != nil {
30+
return err
31+
}
32+
33+
err = pdfWriter.AddPage(page)
34+
if err != nil {
35+
return err
36+
}
37+
}
38+
39+
// Copy PDF version.
40+
version := pdfReader.PdfVersion()
41+
pdfWriter.SetVersion(version.Major, version.Minor)
42+
43+
// Copy PDF info.
44+
info, err := pdfReader.GetPdfInfo()
45+
if err != nil {
46+
common.Log.Debug("ERROR: %v", err)
47+
} else {
48+
pdfWriter.SetDocInfo(info)
49+
}
50+
51+
// Copy Catalog Metadata.
52+
if meta, ok := pdfReader.GetCatalogMetadata(); ok {
53+
if err := pdfWriter.SetCatalogMetadata(meta); err != nil {
54+
return err
55+
}
56+
}
57+
58+
// Copy catalog mark information.
59+
if markInfo, ok := pdfReader.GetCatalogMarkInfo(); ok {
60+
if err := pdfWriter.SetCatalogMarkInfo(markInfo); err != nil {
61+
return err
62+
}
63+
}
64+
65+
// Copy AcroForm.
66+
err = pdfWriter.SetForms(pdfReader.AcroForm)
67+
if err != nil {
68+
common.Log.Debug("ERROR: %v", err)
69+
return err
70+
}
71+
72+
// Copy viewer preferences.
73+
if pref, ok := pdfReader.GetCatalogViewerPreferences(); ok {
74+
if err := pdfWriter.SetCatalogViewerPreferences(pref); err != nil {
75+
return err
76+
}
77+
}
78+
79+
// Copy language preferences.
80+
if lang, ok := pdfReader.GetCatalogLanguage(); ok {
81+
if err := pdfWriter.SetCatalogLanguage(lang); err != nil {
82+
return err
83+
}
84+
}
85+
86+
// Copy document outlines.
87+
pdfWriter.AddOutlineTree(pdfReader.GetOutlineTree())
88+
89+
// Copy OC Properties.
90+
props, err := pdfReader.GetOCProperties()
91+
if err != nil {
92+
common.Log.Debug("ERROR: %v", err)
93+
} else {
94+
err = pdfWriter.SetOCProperties(props)
95+
if err != nil {
96+
common.Log.Debug("ERROR: %v", err)
97+
}
98+
}
99+
100+
// Copy page labels.
101+
labelObj, err := pdfReader.GetPageLabels()
102+
if err != nil {
103+
common.Log.Debug("ERROR: %v", err)
104+
} else {
105+
err = pdfWriter.SetPageLabels(labelObj)
106+
if err != nil {
107+
common.Log.Debug("ERROR: %v", err)
108+
}
109+
}
110+
111+
// Copy named destinations.
112+
namedDest, err := pdfReader.GetNamedDestinations()
113+
if err != nil {
114+
common.Log.Debug("ERROR: %v", err)
115+
} else {
116+
err = pdfWriter.SetNamedDestinations(namedDest)
117+
if err != nil {
118+
common.Log.Debug("ERROR: %v", err)
119+
}
120+
}
121+
122+
// Copy name dictionary.
123+
nameDict, err := pdfReader.GetNameDictionary()
124+
if err != nil {
125+
common.Log.Debug("ERROR: %v", err)
126+
} else {
127+
err = pdfWriter.SetNameDictionary(nameDict)
128+
if err != nil {
129+
common.Log.Debug("ERROR: %v", err)
130+
}
131+
}
132+
133+
// Copy StructTreeRoot dictionary.
134+
structTreeRoot, found := pdfReader.GetCatalogStructTreeRoot()
135+
if found {
136+
err := pdfWriter.SetCatalogStructTreeRoot(structTreeRoot)
137+
if err != nil {
138+
common.Log.Debug("ERROR: %v", err)
139+
}
140+
}
141+
142+
// Copy global page rotation.
143+
if pdfReader.Rotate != nil {
144+
if err := pdfWriter.SetRotation(*pdfReader.Rotate); err != nil {
145+
common.Log.Debug("ERROR: %v", err)
146+
}
147+
}
148+
149+
// Write output file.
150+
safe := inputPath == outputPath
151+
return writePDF(outputPath, &pdfWriter, safe)
152+
}

0 commit comments

Comments
 (0)