first commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
dst
|
||||||
|
*pdf
|
||||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module pdfmerge
|
||||||
|
|
||||||
|
go 1.25.4
|
||||||
|
|
||||||
|
require golang.org/x/image v0.34.0
|
||||||
2
go.sum
Normal file
2
go.sum
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||||
|
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||||
11
internal/pdfmerge/consts.go
Normal file
11
internal/pdfmerge/consts.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package pdfmerge
|
||||||
|
|
||||||
|
var size, margin string
|
||||||
|
var scaleH, scaleW, landscape bool
|
||||||
|
var JPEGQuality int
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultSize = "IMG-SIZE"
|
||||||
|
DefaultMargin = "0,0,0,0"
|
||||||
|
VERSION = "1.2.0"
|
||||||
|
)
|
||||||
41
internal/pdfmerge/dir.go
Normal file
41
internal/pdfmerge/dir.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package pdfmerge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"pdfmerge/internal/pdf/creator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DirSource struct {
|
||||||
|
path string
|
||||||
|
files []Mergeable
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s DirSource) MergeTo(c *creator.Creator) (err error) {
|
||||||
|
for _, mergeableFile := range s.files {
|
||||||
|
err = mergeableFile.MergeTo(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DirSource) scanMergeables() error {
|
||||||
|
filesInDir, err := os.ReadDir(s.path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, file := range filesInDir {
|
||||||
|
if file.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mergableFiles, err := getMergeableFile(filepath.Join(s.path, file.Name()), []int{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.files = append(s.files, mergableFiles)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
129
internal/pdfmerge/img.go
Normal file
129
internal/pdfmerge/img.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package pdfmerge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"pdfmerge/internal/pdf/creator"
|
||||||
|
|
||||||
|
"pdfmerge/internal/pdf/core"
|
||||||
|
|
||||||
|
"golang.org/x/image/tiff"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pageMargin [4]float64
|
||||||
|
var pageSize creator.PageSize
|
||||||
|
var sizeHasSet, marginHasSet = false, false
|
||||||
|
var tiffExts = []string{".tiff", ".TIFF", ".tif", ".TIF"}
|
||||||
|
|
||||||
|
type ImgSource struct {
|
||||||
|
source
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ImgSource) MergeTo(c *creator.Creator) error {
|
||||||
|
return addImage(s.source, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addImage(s source, c *creator.Creator) error {
|
||||||
|
img, err := createImage(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following funcs must be called in sequence
|
||||||
|
setEncoding(img, s)
|
||||||
|
setMargin(img, c)
|
||||||
|
setSize(img, c)
|
||||||
|
|
||||||
|
c.NewPage()
|
||||||
|
err = c.Draw(img)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createImage(s source) (*creator.Image, error) {
|
||||||
|
if in_array(s.ext, tiffExts) {
|
||||||
|
f, _ := os.Open(s.path)
|
||||||
|
i, err := tiff.Decode(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return creator.NewImageFromGoImage(i)
|
||||||
|
}
|
||||||
|
return creator.NewImageFromFile(s.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setMargin(img *creator.Image, c *creator.Creator) error {
|
||||||
|
if !marginHasSet {
|
||||||
|
for i, m := range strings.Split(margin, ",") {
|
||||||
|
floatVal, err := strconv.ParseFloat(m, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error: -m|--margin MUST be 4 comma separated int/float numbers. %s found", m)
|
||||||
|
}
|
||||||
|
|
||||||
|
pageMargin[i] = floatVal * creator.PPI
|
||||||
|
}
|
||||||
|
if len(pageMargin) != 4 {
|
||||||
|
return fmt.Errorf("error: -m|--margin MUST be 4 comma separated int/float numbers. %s provided", margin)
|
||||||
|
}
|
||||||
|
marginHasSet = true
|
||||||
|
}
|
||||||
|
c.SetPageMargins(pageMargin[0], pageMargin[1], pageMargin[2], pageMargin[3])
|
||||||
|
img.SetPos(pageMargin[0], pageMargin[3])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSize(img *creator.Image, c *creator.Creator) error {
|
||||||
|
if size == DefaultSize {
|
||||||
|
// Width height with adding margin
|
||||||
|
w := img.Width() + pageMargin[0] + pageMargin[1]
|
||||||
|
h := img.Height() + pageMargin[2] + pageMargin[3]
|
||||||
|
|
||||||
|
pageSize = creator.PageSize{w, h}
|
||||||
|
} else {
|
||||||
|
sizeHasSet = true
|
||||||
|
switch size {
|
||||||
|
case "A4":
|
||||||
|
pageSize = creator.PageSizeA4
|
||||||
|
case "A3":
|
||||||
|
pageSize = creator.PageSizeA3
|
||||||
|
case "Legal":
|
||||||
|
pageSize = creator.PageSizeLegal
|
||||||
|
case "Letter":
|
||||||
|
pageSize = creator.PageSizeLetter
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("error: -s|--size MUST be one of A4, A3, Legal or Letter. %s given", size)
|
||||||
|
}
|
||||||
|
|
||||||
|
if scaleH {
|
||||||
|
img.ScaleToHeight(pageSize[1] - (pageMargin[2] + pageMargin[3]))
|
||||||
|
} else if scaleW {
|
||||||
|
img.ScaleToWidth(pageSize[0] - (pageMargin[0] + pageMargin[1]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if landscape {
|
||||||
|
c.SetPageSize(creator.PageSize{pageSize[1], pageSize[0]})
|
||||||
|
} else {
|
||||||
|
c.SetPageSize(pageSize)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set appropriate encoding for JPEG and TIFF
|
||||||
|
// MUST be called before changing image size
|
||||||
|
func setEncoding(img *creator.Image, s source) {
|
||||||
|
switch s.mime {
|
||||||
|
case "image/jpeg":
|
||||||
|
encoder := core.NewDCTEncoder()
|
||||||
|
encoder.Quality = JPEGQuality
|
||||||
|
// Encoder dimensions must match the raw image data dimensions.
|
||||||
|
encoder.Width = int(img.Width())
|
||||||
|
encoder.Height = int(img.Height())
|
||||||
|
img.SetEncoder(encoder)
|
||||||
|
}
|
||||||
|
}
|
||||||
82
internal/pdfmerge/pdf.go
Normal file
82
internal/pdfmerge/pdf.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package pdfmerge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"pdfmerge/internal/pdf/creator"
|
||||||
|
|
||||||
|
pdf "pdfmerge/internal/pdf/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PDFSource struct {
|
||||||
|
source
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PDFSource) MergeTo(c *creator.Creator) error {
|
||||||
|
f, _ := os.Open(s.path)
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
return addPdfPages(f, s.pages, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getReader(rs io.ReadSeeker) (*pdf.PdfReader, error) {
|
||||||
|
|
||||||
|
pdfReader, err := pdf.NewPdfReader(rs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
isEncrypted, err := pdfReader.IsEncrypted()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isEncrypted {
|
||||||
|
auth, err := pdfReader.Decrypt([]byte(""))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !auth {
|
||||||
|
return nil, errors.New("cannot merge encrypted, password protected document")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pdfReader, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addPdfPages(file *os.File, pages []int, c *creator.Creator) error {
|
||||||
|
pdfReader, err := getReader(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pages) > 0 {
|
||||||
|
for _, pageNo := range pages {
|
||||||
|
if page, pageErr := pdfReader.GetPage(pageNo); pageErr != nil {
|
||||||
|
return pageErr
|
||||||
|
} else {
|
||||||
|
err = c.AddPage(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
numPages, err := pdfReader.GetNumPages()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := 0; i < numPages; i++ {
|
||||||
|
pageNum := i + 1
|
||||||
|
|
||||||
|
page, err := pdfReader.GetPage(pageNum)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = c.AddPage(page); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
122
internal/pdfmerge/source.go
Normal file
122
internal/pdfmerge/source.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package pdfmerge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"pdfmerge/internal/pdf/creator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Mergeable interface {
|
||||||
|
MergeTo(c *creator.Creator) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type source struct {
|
||||||
|
path, sourceType, mime, ext string
|
||||||
|
pages []int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate new source file from input argument
|
||||||
|
func NewSource(input string) (Mergeable, error) {
|
||||||
|
fileInputParts := strings.Split(input, "~")
|
||||||
|
|
||||||
|
path := fileInputParts[0]
|
||||||
|
var inputSource Mergeable
|
||||||
|
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch mode := info.Mode(); {
|
||||||
|
case mode.IsDir():
|
||||||
|
inputSource = getMergeableDir(path)
|
||||||
|
case mode.IsRegular():
|
||||||
|
pages := []int{}
|
||||||
|
if len(fileInputParts) > 1 {
|
||||||
|
pages = parsePageNums(fileInputParts[1])
|
||||||
|
}
|
||||||
|
inputSource, err = getMergeableFile(path, pages)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputSource, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMergeableDir(path string) Mergeable {
|
||||||
|
dir := DirSource{path: path}
|
||||||
|
dir.scanMergeables()
|
||||||
|
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMergeableFile(path string, pages []int) (Mergeable, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot read source file: %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
ext := filepath.Ext(f.Name())
|
||||||
|
mime, err := getMimeType(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error in getting mime type of file: %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceType, err := getFileType(mime, ext)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error : %s (%s)", err.Error(), path)
|
||||||
|
}
|
||||||
|
|
||||||
|
source := source{path, sourceType, mime, ext, pages}
|
||||||
|
|
||||||
|
var m Mergeable
|
||||||
|
switch sourceType {
|
||||||
|
case "image":
|
||||||
|
m = ImgSource{source}
|
||||||
|
case "pdf":
|
||||||
|
m = PDFSource{source}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFileType(mime, ext string) (string, error) {
|
||||||
|
pdfExts := []string{".pdf", ".PDF"}
|
||||||
|
imgExts := []string{".jpg", ".jpeg", ".gif", ".png", ".tiff", ".tif", ".JPG", ".JPEG", ".GIF", ".PNG", ".TIFF", ".TIF"}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case mime == "application/pdf":
|
||||||
|
return "pdf", nil
|
||||||
|
case mime[:6] == "image/":
|
||||||
|
return "image", nil
|
||||||
|
case mime == "application/octet-stream" && in_array(ext, pdfExts):
|
||||||
|
return "pdf", nil
|
||||||
|
case mime == "application/octet-stream" && in_array(ext, imgExts):
|
||||||
|
return "image", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "error", errors.New("file type not acceptable. ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePageNums(pagesInput string) []int {
|
||||||
|
pages := []int{}
|
||||||
|
|
||||||
|
for _, e := range strings.Split(pagesInput, ",") {
|
||||||
|
pageNo, err := strconv.Atoi(strings.Trim(e, " \n"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("invalid format! Example of a file input with page numbers: path/to/abc.pdf~1,2,3,5,6")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
pages = append(pages, pageNo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages
|
||||||
|
}
|
||||||
41
internal/pdfmerge/util.go
Normal file
41
internal/pdfmerge/util.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package pdfmerge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
func in_array(val, array any) bool {
|
||||||
|
return at_array(val, array) != -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func at_array(val, array any) (index int) {
|
||||||
|
index = -1
|
||||||
|
|
||||||
|
switch reflect.TypeOf(array).Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
s := reflect.ValueOf(array)
|
||||||
|
|
||||||
|
for i := 0; i < s.Len(); i++ {
|
||||||
|
if reflect.DeepEqual(val, s.Index(i).Interface()) {
|
||||||
|
index = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMimeType(file *os.File) (string, error) {
|
||||||
|
// Only the first 512 bytes are used to sniff the content type.
|
||||||
|
buffer := make([]byte, 512)
|
||||||
|
_, readError := file.Read(buffer)
|
||||||
|
if readError != nil {
|
||||||
|
return "error", readError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always returns a valid content-type and "application/octet-stream" if no others seemed to match.
|
||||||
|
return http.DetectContentType(buffer), nil
|
||||||
|
}
|
||||||
25
pdfmerge.go
Normal file
25
pdfmerge.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package pdfmerge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
merge "pdfmerge/internal/pdfmerge"
|
||||||
|
|
||||||
|
"pdfmerge/internal/pdf/creator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Pdfmerge(inputpath []string, outputpath string) error {
|
||||||
|
c := creator.New()
|
||||||
|
|
||||||
|
for _, arg := range inputpath {
|
||||||
|
source, err := merge.NewSource(arg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = source.MergeTo(c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error: %s (%s)", err.Error(), arg)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.WriteToFile(outputpath)
|
||||||
|
}
|
||||||
11
pdfmerge_test.go
Normal file
11
pdfmerge_test.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package pdfmerge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMerge(t *testing.T) {
|
||||||
|
t.Log("start pdf merge test")
|
||||||
|
file := []string{"./dst"}
|
||||||
|
Pdfmerge(file, "output.pdf")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user