2358 lines
51 KiB
Go
2358 lines
51 KiB
Go
/*
|
|
Copyright 2018 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 pdfcpu
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"image"
|
|
"io"
|
|
"io/ioutil"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf16"
|
|
|
|
"github.com/pdfcpu/pdfcpu/pkg/filter"
|
|
"github.com/pdfcpu/pdfcpu/pkg/font"
|
|
"github.com/pdfcpu/pdfcpu/pkg/log"
|
|
"github.com/pdfcpu/pdfcpu/pkg/types"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const stampWithBBox = false
|
|
|
|
const (
|
|
degToRad = math.Pi / 180
|
|
radToDeg = 180 / math.Pi
|
|
)
|
|
|
|
// Watermark mode
|
|
const (
|
|
WMText = iota
|
|
WMImage
|
|
WMPDF
|
|
)
|
|
|
|
// Rotation along one of 2 diagonals
|
|
const (
|
|
NoDiagonal = iota
|
|
DiagonalLLToUR
|
|
DiagonalULToLR
|
|
)
|
|
|
|
// RenderMode represents the text rendering mode (see 9.3.6)
|
|
type RenderMode int
|
|
|
|
// Render mode
|
|
const (
|
|
RMFill RenderMode = iota
|
|
RMStroke
|
|
RMFillAndStroke
|
|
)
|
|
|
|
var (
|
|
errNoContent = errors.New("pdfcpu: page without content")
|
|
errNoWatermark = errors.New("pdfcpu: no watermarks found")
|
|
errCorruptOCGs = errors.New("pdfcpu: OCProperties: corrupt OCGs element")
|
|
)
|
|
|
|
type watermarkParamMap map[string]func(string, *Watermark) error
|
|
|
|
// Handle applies parameter completion and if successful
|
|
// parses the parameter values into import.
|
|
func (m watermarkParamMap) Handle(paramPrefix, paramValueStr string, imp *Watermark) error {
|
|
var param string
|
|
|
|
// Completion support
|
|
for k := range m {
|
|
if !strings.HasPrefix(k, paramPrefix) {
|
|
continue
|
|
}
|
|
if len(param) > 0 {
|
|
return errors.Errorf("pdfcpu: ambiguous parameter prefix \"%s\"", paramPrefix)
|
|
}
|
|
param = k
|
|
}
|
|
|
|
if param == "" {
|
|
return errors.Errorf("pdfcpu: unknown parameter prefix \"%s\"", paramPrefix)
|
|
}
|
|
|
|
return m[param](paramValueStr, imp)
|
|
}
|
|
|
|
var wmParamMap = watermarkParamMap{
|
|
"aligntext": parseTextHorAlignment,
|
|
"backgroundcolor": parseBackgroundColor,
|
|
"bgcolor": parseBackgroundColor,
|
|
"border": parseBorder,
|
|
"color": parseFillColor,
|
|
"diagonal": parseDiagonal,
|
|
"fillcolor": parseFillColor,
|
|
"fontname": parseFontName,
|
|
"margins": parseMargins,
|
|
"mode": parseRenderMode,
|
|
"offset": parsePositionOffsetWM,
|
|
"opacity": parseOpacity,
|
|
"points": parseFontSize,
|
|
"position": parsePositionAnchorWM,
|
|
"rendermode": parseRenderMode,
|
|
"rotation": parseRotation,
|
|
"scalefactor": parseScaleFactorWM,
|
|
"strokecolor": parseStrokeColor,
|
|
}
|
|
|
|
// SimpleColor is a simple rgb wrapper.
|
|
type SimpleColor struct {
|
|
R, G, B float32 // intensities between 0 and 1.
|
|
}
|
|
|
|
func (sc SimpleColor) String() string {
|
|
return fmt.Sprintf("r=%1.1f g=%1.1f b=%1.1f", sc.R, sc.G, sc.B)
|
|
}
|
|
|
|
// NewSimpleColor returns a SimpleColor for rgb in the form 0x00RRGGBB
|
|
func NewSimpleColor(rgb uint32) SimpleColor {
|
|
r := float32((rgb>>16)&0xFF) / 255
|
|
g := float32((rgb>>8)&0xFF) / 255
|
|
b := float32(rgb&0xFF) / 255
|
|
return SimpleColor{r, g, b}
|
|
}
|
|
|
|
// Some popular colors.
|
|
var (
|
|
Black = SimpleColor{}
|
|
White = SimpleColor{R: 1, G: 1, B: 1}
|
|
Gray = SimpleColor{.5, .5, .5}
|
|
LightGray = SimpleColor{.9, .9, .9}
|
|
)
|
|
|
|
type formCache map[types.Rectangle]*IndirectRef
|
|
|
|
type pdfResources struct {
|
|
content []byte
|
|
resDict *IndirectRef
|
|
bb *Rectangle
|
|
}
|
|
|
|
// Watermark represents the basic structure and command details for the commands "Stamp" and "Watermark".
|
|
type Watermark struct {
|
|
// configuration
|
|
Mode int // WMText, WMImage or WMPDF
|
|
TextString string // raw display text.
|
|
TextLines []string // display multiple lines of text.
|
|
FileName string // display pdf page or png image.
|
|
Page int // the page number of a PDF file. 0 means multistamp/multiwatermark.
|
|
OnTop bool // if true this is a STAMP else this is a WATERMARK.
|
|
InpUnit DisplayUnit // input display unit.
|
|
Pos anchor // position anchor, one of tl,tc,tr,l,c,r,bl,bc,br.
|
|
Dx, Dy int // anchor offset.
|
|
HAlign *HAlignment // horizonal alignment for text watermarks.
|
|
FontName string // supported are Adobe base fonts only. (as of now: Helvetica, Times-Roman, Courier)
|
|
FontSize int // font scaling factor.
|
|
ScaledFontSize int // font scaling factor for a specific page
|
|
Color SimpleColor // text fill color(=non stroking color) for backwards compatibility.
|
|
FillColor SimpleColor // text fill color(=non stroking color).
|
|
StrokeColor SimpleColor // text stroking color
|
|
BgColor *SimpleColor // text bounding box background color
|
|
MLeft, MRight int // left and right bounding box margin
|
|
MTop, MBot int // top and bottom bounding box margin
|
|
BorderWidth int // Border width, visible if BgColor is set.
|
|
BorderStyle LineJoinStyle // Border style (bounding box corner style), visible if BgColor is set.
|
|
BorderColor *SimpleColor // border color
|
|
Rotation float64 // rotation to apply in degrees. -180 <= x <= 180
|
|
Diagonal int // paint along the diagonal.
|
|
UserRotOrDiagonal bool // true if one of rotation or diagonal provided overriding the default.
|
|
Opacity float64 // opacity of the watermark. 0 <= x <= 1
|
|
RenderMode RenderMode // fill=0, stroke=1 fill&stroke=2
|
|
Scale float64 // relative scale factor: 0 <= x <= 1, absolute scale factor: 0 <= x
|
|
ScaleEff float64 // effective scale factor
|
|
ScaleAbs bool // true for absolute scaling.
|
|
Update bool // true for updating instead of adding a page watermark.
|
|
|
|
// resources
|
|
ocg, extGState, font, image *IndirectRef
|
|
|
|
// image or PDF watermark
|
|
width, height int // image or page dimensions.
|
|
bbPDF *Rectangle
|
|
|
|
// PDF watermark
|
|
pdfRes map[int]pdfResources
|
|
|
|
// page specific
|
|
bb *Rectangle // bounding box of the form representing this watermark.
|
|
vp *Rectangle // page dimensions.
|
|
pageRot float64 // page rotation in effect.
|
|
form *IndirectRef // Forms are dependent on given page dimensions.
|
|
|
|
// house keeping
|
|
objs IntSet // objects for which wm has been applied already.
|
|
fCache formCache // form cache.
|
|
}
|
|
|
|
// DefaultWatermarkConfig returns the default configuration.
|
|
func DefaultWatermarkConfig() *Watermark {
|
|
return &Watermark{
|
|
Page: 0,
|
|
FontName: "Helvetica",
|
|
FontSize: 24,
|
|
Pos: Center,
|
|
Scale: 0.5,
|
|
ScaleAbs: false,
|
|
Color: Gray,
|
|
StrokeColor: Gray,
|
|
FillColor: Gray,
|
|
Diagonal: DiagonalLLToUR,
|
|
Opacity: 1.0,
|
|
RenderMode: RMFill,
|
|
pdfRes: map[int]pdfResources{},
|
|
objs: IntSet{},
|
|
fCache: formCache{},
|
|
TextLines: []string{},
|
|
}
|
|
}
|
|
|
|
func (wm Watermark) typ() string {
|
|
if wm.isImage() {
|
|
return "image"
|
|
}
|
|
if wm.isPDF() {
|
|
return "pdf"
|
|
}
|
|
return "text"
|
|
}
|
|
|
|
func (wm Watermark) String() string {
|
|
var s string
|
|
if !wm.OnTop {
|
|
s = "not "
|
|
}
|
|
|
|
t := wm.TextString
|
|
if len(t) == 0 {
|
|
t = wm.FileName
|
|
}
|
|
|
|
sc := "relative"
|
|
if wm.ScaleAbs {
|
|
sc = "absolute"
|
|
}
|
|
|
|
bbox := ""
|
|
if wm.bb != nil {
|
|
bbox = (*wm.bb).String()
|
|
}
|
|
|
|
vp := ""
|
|
if wm.vp != nil {
|
|
vp = (*wm.vp).String()
|
|
}
|
|
|
|
return fmt.Sprintf("Watermark: <%s> is %son top, typ:%s\n"+
|
|
"%s %d points\n"+
|
|
"PDFpage#: %d\n"+
|
|
"scaling: %.1f %s\n"+
|
|
"color: %s\n"+
|
|
"rotation: %.1f\n"+
|
|
"diagonal: %d\n"+
|
|
"opacity: %.1f\n"+
|
|
"renderMode: %d\n"+
|
|
"bbox:%s\n"+
|
|
"vp:%s\n"+
|
|
"pageRotation: %.1f\n",
|
|
t, s, wm.typ(),
|
|
wm.FontName, wm.FontSize,
|
|
wm.Page,
|
|
wm.Scale, sc,
|
|
wm.Color,
|
|
wm.Rotation,
|
|
wm.Diagonal,
|
|
wm.Opacity,
|
|
wm.RenderMode,
|
|
bbox,
|
|
vp,
|
|
wm.pageRot,
|
|
)
|
|
}
|
|
|
|
// OnTopString returns "watermark" or "stamp" whichever applies.
|
|
func (wm Watermark) OnTopString() string {
|
|
s := "watermark"
|
|
if wm.OnTop {
|
|
s = "stamp"
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (wm Watermark) multiStamp() bool {
|
|
return wm.Page == 0
|
|
}
|
|
|
|
func (wm Watermark) calcMaxTextWidth() float64 {
|
|
var maxWidth float64
|
|
for _, l := range wm.TextLines {
|
|
w := font.TextWidth(l, wm.FontName, wm.ScaledFontSize)
|
|
if w > maxWidth {
|
|
maxWidth = w
|
|
}
|
|
}
|
|
return maxWidth
|
|
}
|
|
|
|
func (wm Watermark) textDescriptor() TextDescriptor {
|
|
td := TextDescriptor{
|
|
Text: wm.TextString,
|
|
FontName: wm.FontName,
|
|
FontSize: wm.FontSize,
|
|
Scale: wm.Scale,
|
|
ScaleAbs: wm.ScaleAbs,
|
|
RMode: wm.RenderMode,
|
|
StrokeCol: wm.StrokeColor,
|
|
FillCol: wm.FillColor,
|
|
ShowBackground: true,
|
|
}
|
|
if wm.BgColor != nil {
|
|
td.ShowTextBB = true
|
|
td.BackgroundCol = *wm.BgColor
|
|
}
|
|
return td
|
|
}
|
|
|
|
func parseTextHorAlignment(s string, wm *Watermark) error {
|
|
var a HAlignment
|
|
switch s {
|
|
case "l":
|
|
a = AlignLeft
|
|
case "r":
|
|
a = AlignRight
|
|
case "c":
|
|
a = AlignCenter
|
|
case "j":
|
|
a = AlignJustify
|
|
default:
|
|
return errors.Errorf("pdfcpu: unknown horizontal alignment (l,r,c,j): %s", s)
|
|
}
|
|
|
|
wm.HAlign = &a
|
|
|
|
return nil
|
|
}
|
|
|
|
func parsePositionAnchorWM(s string, wm *Watermark) error {
|
|
a, err := parsePositionAnchor(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
wm.Pos = a
|
|
return nil
|
|
}
|
|
|
|
func parsePositionOffsetWM(s string, wm *Watermark) error {
|
|
d := strings.Split(s, " ")
|
|
if len(d) != 2 {
|
|
return errors.Errorf("pdfcpu: illegal position offset string: need 2 numeric values, %s\n", s)
|
|
}
|
|
|
|
f, err := strconv.ParseFloat(d[0], 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
wm.Dx = int(toUserSpace(f, wm.InpUnit))
|
|
|
|
f, err = strconv.ParseFloat(d[1], 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
wm.Dy = int(toUserSpace(f, wm.InpUnit))
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseScaleFactorWM(s string, wm *Watermark) (err error) {
|
|
wm.Scale, wm.ScaleAbs, err = parseScaleFactor(s)
|
|
return err
|
|
}
|
|
|
|
func parseFontName(s string, wm *Watermark) error {
|
|
if !font.SupportedFont(s) {
|
|
return errors.Errorf("pdfcpu: %s is unsupported, please refer to \"pdfcpu fonts list\".\n", s)
|
|
}
|
|
wm.FontName = s
|
|
return nil
|
|
}
|
|
|
|
func parseFontSize(s string, wm *Watermark) error {
|
|
fs, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
wm.FontSize = fs
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseScaleFactor(s string) (float64, bool, error) {
|
|
ss := strings.Split(s, " ")
|
|
if len(ss) > 2 {
|
|
return 0, false, errors.Errorf("pdfcpu: invalid scale string %s: 0.0 < i <= 1.0 {rel} | 0.0 < i {abs}\n", s)
|
|
}
|
|
|
|
sc, err := strconv.ParseFloat(ss[0], 64)
|
|
if err != nil {
|
|
return 0, false, errors.Errorf("pdfcpu: scale factor must be a float value: %s\n", ss[0])
|
|
}
|
|
|
|
if sc <= 0 {
|
|
return 0, false, errors.Errorf("pdfcpu: invalid scale value %.2f: 0.0 < i <= 1.0 {rel} | 0.0 < i {abs}\n", sc)
|
|
}
|
|
|
|
var scaleAbs bool
|
|
|
|
if len(ss) == 1 {
|
|
// Assume relative scaling for sc <= 1 and absolute scaling for sc > 1.
|
|
scaleAbs = sc > 1
|
|
return sc, scaleAbs, nil
|
|
}
|
|
|
|
switch ss[1] {
|
|
case "a", "abs":
|
|
scaleAbs = true
|
|
|
|
case "r", "rel":
|
|
scaleAbs = false
|
|
|
|
default:
|
|
return 0, false, errors.Errorf("pdfcpu: illegal scale mode: abs|rel, %s\n", ss[1])
|
|
}
|
|
|
|
if !scaleAbs && sc > 1 {
|
|
return 0, false, errors.Errorf("pdfcpu: invalid relative scale value %.2f: 0.0 < i <= 1\n", sc)
|
|
}
|
|
|
|
return sc, scaleAbs, nil
|
|
}
|
|
|
|
func parseHexColor(hexCol string) (SimpleColor, error) {
|
|
var sc SimpleColor
|
|
if len(hexCol) != 7 || hexCol[0] != '#' {
|
|
return sc, errors.Errorf("pdfcpu: invalid hex color string: #FFFFFF, %s\n", hexCol)
|
|
}
|
|
b, err := hex.DecodeString(hexCol[1:])
|
|
if err != nil || len(b) != 3 {
|
|
return sc, errors.Errorf("pdfcpu: invalid hex color string: #FFFFFF, %s\n", hexCol)
|
|
}
|
|
return SimpleColor{float32(b[0]) / 255, float32(b[1]) / 255, float32(b[2]) / 255}, nil
|
|
}
|
|
|
|
func parseColor(s string) (SimpleColor, error) {
|
|
var sc SimpleColor
|
|
|
|
cs := strings.Split(s, " ")
|
|
if len(cs) != 1 && len(cs) != 3 {
|
|
return sc, errors.Errorf("pdfcpu: illegal color string: 3 intensities 0.0 <= i <= 1.0 or #FFFFFF, %s\n", s)
|
|
}
|
|
|
|
if len(cs) == 1 {
|
|
// #FFFFFF to uint32
|
|
return parseHexColor(cs[0])
|
|
}
|
|
|
|
r, err := strconv.ParseFloat(cs[0], 32)
|
|
if err != nil {
|
|
return sc, errors.Errorf("red must be a float value: %s\n", cs[0])
|
|
}
|
|
if r < 0 || r > 1 {
|
|
return sc, errors.New("pdfcpu: red: a color value is an intensity between 0.0 and 1.0")
|
|
}
|
|
sc.R = float32(r)
|
|
|
|
g, err := strconv.ParseFloat(cs[1], 32)
|
|
if err != nil {
|
|
return sc, errors.Errorf("pdfcpu: green must be a float value: %s\n", cs[1])
|
|
}
|
|
if g < 0 || g > 1 {
|
|
return sc, errors.New("pdfcpu: green: a color value is an intensity between 0.0 and 1.0")
|
|
}
|
|
sc.G = float32(g)
|
|
|
|
b, err := strconv.ParseFloat(cs[2], 32)
|
|
if err != nil {
|
|
return sc, errors.Errorf("pdfcpu: blue must be a float value: %s\n", cs[2])
|
|
}
|
|
if b < 0 || b > 1 {
|
|
return sc, errors.New("pdfcpu: blue: a color value is an intensity between 0.0 and 1.0")
|
|
}
|
|
sc.B = float32(b)
|
|
|
|
return sc, nil
|
|
}
|
|
|
|
func parseStrokeColor(s string, wm *Watermark) error {
|
|
c, err := parseColor(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
wm.StrokeColor = c
|
|
return nil
|
|
}
|
|
|
|
func parseFillColor(s string, wm *Watermark) error {
|
|
c, err := parseColor(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
wm.FillColor = c
|
|
return nil
|
|
}
|
|
|
|
func parseBackgroundColor(s string, wm *Watermark) error {
|
|
c, err := parseColor(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
wm.BgColor = &c
|
|
return nil
|
|
}
|
|
|
|
func parseRotation(s string, wm *Watermark) error {
|
|
if wm.UserRotOrDiagonal {
|
|
return errors.New("pdfcpu: please specify rotation or diagonal (r or d)")
|
|
}
|
|
|
|
r, err := strconv.ParseFloat(s, 64)
|
|
if err != nil {
|
|
return errors.Errorf("pdfcpu: rotation must be a float value: %s\n", s)
|
|
}
|
|
if r < -180 || r > 180 {
|
|
return errors.Errorf("pdfcpu: illegal rotation: -180 <= r <= 180 degrees, %s\n", s)
|
|
}
|
|
|
|
wm.Rotation = r
|
|
wm.Diagonal = NoDiagonal
|
|
wm.UserRotOrDiagonal = true
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseDiagonal(s string, wm *Watermark) error {
|
|
if wm.UserRotOrDiagonal {
|
|
return errors.New("pdfcpu: please specify rotation or diagonal (r or d)")
|
|
}
|
|
|
|
d, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
return errors.Errorf("pdfcpu: illegal diagonal value: allowed 1 or 2, %s\n", s)
|
|
}
|
|
if d != DiagonalLLToUR && d != DiagonalULToLR {
|
|
return errors.New("pdfcpu: diagonal: 1..lower left to upper right, 2..upper left to lower right")
|
|
}
|
|
|
|
wm.Diagonal = d
|
|
wm.Rotation = 0
|
|
wm.UserRotOrDiagonal = true
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseOpacity(s string, wm *Watermark) error {
|
|
o, err := strconv.ParseFloat(s, 64)
|
|
if err != nil {
|
|
return errors.Errorf("pdfcpu: opacity must be a float value: %s\n", s)
|
|
}
|
|
if o < 0 || o > 1 {
|
|
return errors.Errorf("pdfcpu: illegal opacity: 0.0 <= r <= 1.0, %s\n", s)
|
|
}
|
|
wm.Opacity = o
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseRenderMode(s string, wm *Watermark) error {
|
|
m, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
return errors.Errorf("pdfcpu: illegal render mode value: allowed 0,1,2, %s\n", s)
|
|
}
|
|
rm := RenderMode(m)
|
|
if rm != RMFill && rm != RMStroke && rm != RMFillAndStroke {
|
|
return errors.New("pdfcpu: valid rendermodes: 0..fill, 1..stroke, 2..fill&stroke")
|
|
}
|
|
wm.RenderMode = rm
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseMargins(s string, wm *Watermark) error {
|
|
var err error
|
|
|
|
m := strings.Split(s, " ")
|
|
if len(m) == 0 || len(m) > 4 {
|
|
return errors.Errorf("pdfcpu: margins: need 1,2,3 or 4 int values, %s\n", s)
|
|
}
|
|
|
|
f, err := strconv.ParseFloat(m[0], 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
i := int(toUserSpace(f, wm.InpUnit))
|
|
|
|
if len(m) == 1 {
|
|
wm.MLeft = i
|
|
wm.MRight = i
|
|
wm.MTop = i
|
|
wm.MBot = i
|
|
return nil
|
|
}
|
|
|
|
f, err = strconv.ParseFloat(m[1], 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
j := int(toUserSpace(f, wm.InpUnit))
|
|
|
|
if len(m) == 2 {
|
|
wm.MTop, wm.MBot = i, i
|
|
wm.MLeft, wm.MRight = j, j
|
|
return nil
|
|
}
|
|
|
|
f, err = strconv.ParseFloat(m[2], 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
k := int(toUserSpace(f, wm.InpUnit))
|
|
|
|
if len(m) == 3 {
|
|
wm.MTop = i
|
|
wm.MLeft, wm.MRight = j, j
|
|
wm.MBot = k
|
|
return nil
|
|
}
|
|
|
|
f, err = strconv.ParseFloat(m[3], 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
l := int(toUserSpace(f, wm.InpUnit))
|
|
|
|
wm.MTop = i
|
|
wm.MRight = j
|
|
wm.MBot = k
|
|
wm.MLeft = l
|
|
return nil
|
|
}
|
|
|
|
func parseBorder(s string, wm *Watermark) error {
|
|
// w
|
|
// w r g b
|
|
// w #c
|
|
// w round
|
|
// w round r g b
|
|
// w round #c
|
|
|
|
var err error
|
|
|
|
b := strings.Split(s, " ")
|
|
if len(b) == 0 || len(b) > 5 {
|
|
return errors.Errorf("pdfcpu: borders: need 1,2,3,4 or 5 int values, %s\n", s)
|
|
}
|
|
|
|
wm.BorderWidth, err = strconv.Atoi(b[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if wm.BorderWidth == 0 {
|
|
return errors.New("pdfcpu: borders: need width > 0")
|
|
}
|
|
|
|
if len(b) == 1 {
|
|
return nil
|
|
}
|
|
|
|
if strings.HasPrefix("round", b[1]) {
|
|
wm.BorderStyle = LJRound
|
|
if len(b) == 2 {
|
|
return nil
|
|
}
|
|
c, err := parseColor(strings.Join(b[2:], " "))
|
|
wm.BorderColor = &c
|
|
return err
|
|
}
|
|
|
|
c, err := parseColor(strings.Join(b[1:], " "))
|
|
wm.BorderColor = &c
|
|
return err
|
|
}
|
|
|
|
func parseWatermarkDetails(mode int, modeParm, s string, onTop bool, u DisplayUnit) (*Watermark, error) {
|
|
wm := DefaultWatermarkConfig()
|
|
wm.OnTop = onTop
|
|
wm.InpUnit = u
|
|
|
|
ss := strings.Split(s, ",")
|
|
if len(ss) > 0 && len(ss[0]) == 0 {
|
|
setWatermarkType(mode, modeParm, wm)
|
|
return wm, nil
|
|
}
|
|
|
|
for _, s := range ss {
|
|
|
|
ss1 := strings.Split(s, ":")
|
|
if len(ss1) != 2 {
|
|
return nil, parseWatermarkError(onTop)
|
|
}
|
|
|
|
paramPrefix := strings.TrimSpace(ss1[0])
|
|
paramValueStr := strings.TrimSpace(ss1[1])
|
|
|
|
if err := wmParamMap.Handle(paramPrefix, paramValueStr, wm); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return wm, setWatermarkType(mode, modeParm, wm)
|
|
}
|
|
|
|
// ParseTextWatermarkDetails parses a text Watermark/Stamp command string into an internal structure.
|
|
func ParseTextWatermarkDetails(text, desc string, onTop bool, u DisplayUnit) (*Watermark, error) {
|
|
return parseWatermarkDetails(WMText, text, desc, onTop, u)
|
|
}
|
|
|
|
// ParseImageWatermarkDetails parses a text Watermark/Stamp command string into an internal structure.
|
|
func ParseImageWatermarkDetails(fileName, desc string, onTop bool, u DisplayUnit) (*Watermark, error) {
|
|
return parseWatermarkDetails(WMImage, fileName, desc, onTop, u)
|
|
}
|
|
|
|
// ParsePDFWatermarkDetails parses a text Watermark/Stamp command string into an internal structure.
|
|
func ParsePDFWatermarkDetails(fileName, desc string, onTop bool, u DisplayUnit) (*Watermark, error) {
|
|
return parseWatermarkDetails(WMPDF, fileName, desc, onTop, u)
|
|
}
|
|
|
|
func (wm Watermark) calcMinFontSize(w float64) int {
|
|
var minSize int
|
|
for _, l := range wm.TextLines {
|
|
w := font.Size(l, wm.FontName, w)
|
|
if minSize == 0.0 {
|
|
minSize = w
|
|
}
|
|
if w < minSize {
|
|
minSize = w
|
|
}
|
|
}
|
|
return minSize
|
|
}
|
|
|
|
// IsText returns true if the watermark content is text.
|
|
func (wm Watermark) isText() bool {
|
|
return wm.Mode == WMText
|
|
}
|
|
|
|
// IsPDF returns true if the watermark content is PDF.
|
|
func (wm Watermark) isPDF() bool {
|
|
return wm.Mode == WMPDF
|
|
}
|
|
|
|
// IsImage returns true if the watermark content is an image.
|
|
func (wm Watermark) isImage() bool {
|
|
return wm.Mode == WMImage
|
|
}
|
|
|
|
func (wm *Watermark) calcBoundingBox(pageNr int) {
|
|
bb := RectForDim(float64(wm.width), float64(wm.height))
|
|
|
|
if wm.isPDF() {
|
|
wm.bbPDF = wm.pdfRes[wm.Page].bb
|
|
if wm.multiStamp() {
|
|
i := pageNr
|
|
if i > len(wm.pdfRes) {
|
|
i = len(wm.pdfRes)
|
|
}
|
|
wm.bbPDF = wm.pdfRes[i].bb
|
|
}
|
|
wm.width = int(wm.bbPDF.Width())
|
|
wm.height = int(wm.bbPDF.Height())
|
|
bb = wm.bbPDF
|
|
}
|
|
|
|
ar := bb.AspectRatio()
|
|
|
|
if wm.ScaleAbs {
|
|
w1 := wm.Scale * bb.Width()
|
|
bb.UR.X = bb.LL.X + w1
|
|
bb.UR.Y = bb.LL.Y + w1/ar
|
|
wm.bb = bb
|
|
wm.ScaleEff = wm.Scale
|
|
return
|
|
}
|
|
|
|
if ar >= 1 {
|
|
// Landscape
|
|
w1 := wm.Scale * wm.vp.Width()
|
|
bb.UR.X = bb.LL.X + w1
|
|
bb.UR.Y = bb.LL.Y + w1/ar
|
|
wm.ScaleEff = w1 / float64(wm.width)
|
|
} else {
|
|
// Portrait
|
|
h1 := wm.Scale * wm.vp.Height()
|
|
bb.UR.Y = bb.LL.Y + h1
|
|
bb.UR.X = bb.LL.X + h1*ar
|
|
wm.ScaleEff = h1 / float64(wm.height)
|
|
}
|
|
|
|
wm.bb = bb
|
|
return
|
|
}
|
|
|
|
func (wm *Watermark) calcTransformMatrix() matrix {
|
|
var sin, cos float64
|
|
r := wm.Rotation
|
|
|
|
if wm.Diagonal != NoDiagonal {
|
|
|
|
// Calculate the angle of the diagonal with respect of the aspect ratio of the bounding box.
|
|
r = math.Atan(wm.vp.Height()/wm.vp.Width()) * float64(radToDeg)
|
|
|
|
if wm.bb.AspectRatio() < 1 {
|
|
r -= 90
|
|
}
|
|
|
|
if wm.Diagonal == DiagonalULToLR {
|
|
r = -r
|
|
}
|
|
|
|
}
|
|
|
|
sin = math.Sin(float64(r) * float64(degToRad))
|
|
cos = math.Cos(float64(r) * float64(degToRad))
|
|
|
|
var dy float64
|
|
if !wm.isImage() && !wm.isPDF() {
|
|
dy = wm.bb.LL.Y
|
|
}
|
|
ll := lowerLeftCorner(wm.vp.Width(), wm.vp.Height(), wm.bb.Width(), wm.bb.Height(), wm.Pos)
|
|
|
|
dx := ll.X + wm.bb.Width()/2 + float64(wm.Dx) + sin*(wm.bb.Height()/2+dy) - cos*wm.bb.Width()/2
|
|
dy = ll.Y + wm.bb.Height()/2 + float64(wm.Dy) - cos*(wm.bb.Height()/2+dy) - sin*wm.bb.Width()/2
|
|
|
|
return calcTransformMatrix(1, 1, sin, cos, dx, dy)
|
|
}
|
|
|
|
func onTopString(onTop bool) string {
|
|
e := "watermark"
|
|
if onTop {
|
|
e = "stamp"
|
|
}
|
|
return e
|
|
}
|
|
|
|
func parseWatermarkError(onTop bool) error {
|
|
s := onTopString(onTop)
|
|
return errors.Errorf("Invalid %s configuration string. Please consult pdfcpu help %s.\n", s, s)
|
|
}
|
|
|
|
func setWatermarkType(mode int, s string, wm *Watermark) error {
|
|
wm.Mode = mode
|
|
|
|
switch mode {
|
|
case WMText:
|
|
wm.TextString = s
|
|
if font.IsCoreFont(wm.FontName) {
|
|
bb := []byte{}
|
|
for _, r := range s {
|
|
// Unicode => char code
|
|
b := byte(0x20) // better use glyph: .notdef
|
|
if r <= 0xff {
|
|
b = byte(r)
|
|
}
|
|
bb = append(bb, b)
|
|
}
|
|
s = string(bb)
|
|
} else {
|
|
bb := []byte{}
|
|
u := utf16.Encode([]rune(s))
|
|
for _, i := range u {
|
|
bb = append(bb, byte((i>>8)&0xFF))
|
|
bb = append(bb, byte(i&0xFF))
|
|
}
|
|
s = string(bb)
|
|
}
|
|
s = strings.ReplaceAll(s, "\\n", "\n")
|
|
for _, l := range strings.FieldsFunc(s, func(c rune) bool { return c == 0x0a }) {
|
|
wm.TextLines = append(wm.TextLines, l)
|
|
}
|
|
|
|
case WMImage:
|
|
ext := strings.ToLower(filepath.Ext(s))
|
|
if !MemberOf(ext, []string{".jpg", ".jpeg", ".png", ".tif", ".tiff"}) {
|
|
return errors.New("imageFileName has to have one of these extensions: jpg, jpeg, png, tif, tiff")
|
|
}
|
|
wm.FileName = s
|
|
|
|
case WMPDF:
|
|
i := strings.LastIndex(s, ":")
|
|
if i < 1 {
|
|
// No Colon.
|
|
if strings.ToLower(filepath.Ext(s)) != ".pdf" {
|
|
return errors.Errorf("%s is not a PDF file", s)
|
|
}
|
|
wm.FileName = s
|
|
return nil
|
|
}
|
|
// We have at least one Colon.
|
|
if strings.ToLower(filepath.Ext(s)) == ".pdf" {
|
|
// We have an absolute DOS filename.
|
|
wm.FileName = s
|
|
return nil
|
|
}
|
|
// We expect a page number on the right side of the right most Colon.
|
|
var err error
|
|
pageNumberStr := s[i+1:]
|
|
wm.Page, err = strconv.Atoi(pageNumberStr)
|
|
if err != nil {
|
|
return errors.Errorf("illegal PDF page number: %s\n", pageNumberStr)
|
|
}
|
|
fileName := s[:i]
|
|
if strings.ToLower(filepath.Ext(fileName)) != ".pdf" {
|
|
return errors.Errorf("%s is not a PDF file", fileName)
|
|
}
|
|
wm.FileName = fileName
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func migrateIndRef(ir *IndirectRef, ctxSource, ctxDest *Context, migrated map[int]int) (Object, error) {
|
|
o, err := ctxSource.Dereference(*ir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if o != nil {
|
|
o = o.Clone()
|
|
}
|
|
|
|
objNrNew, err := ctxDest.InsertObject(o)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
objNr := ir.ObjectNumber.Value()
|
|
migrated[objNr] = objNrNew
|
|
ir.ObjectNumber = Integer(objNrNew)
|
|
return o, nil
|
|
}
|
|
|
|
func migrateObject(o Object, ctxSource, ctxDest *Context, migrated map[int]int) (Object, error) {
|
|
var err error
|
|
switch o := o.(type) {
|
|
case IndirectRef:
|
|
objNr := o.ObjectNumber.Value()
|
|
if migrated[objNr] > 0 {
|
|
o.ObjectNumber = Integer(migrated[objNr])
|
|
return o, nil
|
|
}
|
|
o1, err := migrateIndRef(&o, ctxSource, ctxDest, migrated)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := migrateObject(o1, ctxSource, ctxDest, migrated); err != nil {
|
|
return nil, err
|
|
}
|
|
return o, nil
|
|
|
|
case Dict:
|
|
for k, v := range o {
|
|
if o[k], err = migrateObject(v, ctxSource, ctxDest, migrated); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return o, nil
|
|
|
|
case StreamDict:
|
|
for k, v := range o.Dict {
|
|
if o.Dict[k], err = migrateObject(v, ctxSource, ctxDest, migrated); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return o, nil
|
|
|
|
case Array:
|
|
for k, v := range o {
|
|
if o[k], err = migrateObject(v, ctxSource, ctxDest, migrated); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return o, nil
|
|
}
|
|
|
|
return o, nil
|
|
}
|
|
|
|
func createPDFRes(ctx, otherCtx *Context, pageNr int, migrated map[int]int, wm *Watermark) error {
|
|
pdfRes := pdfResources{}
|
|
xRefTable := ctx.XRefTable
|
|
otherXRefTable := otherCtx.XRefTable
|
|
|
|
// Locate page dict & resource dict of PDF stamp.
|
|
consolidateRes := true
|
|
d, inhPAttrs, err := otherXRefTable.PageDict(pageNr, consolidateRes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d == nil {
|
|
return errors.Errorf("pdfcpu: unknown page number: %d\n", pageNr)
|
|
}
|
|
|
|
// Retrieve content stream bytes of page dict.
|
|
pdfRes.content, err = otherXRefTable.PageContent(d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Migrate external resource dict into ctx.
|
|
if _, err = migrateObject(inhPAttrs.resources, otherCtx, ctx, migrated); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create an object for resource dict in xRefTable.
|
|
ir, err := xRefTable.IndRefForNewObject(inhPAttrs.resources)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pdfRes.resDict = ir
|
|
pdfRes.bb = viewPort(inhPAttrs)
|
|
wm.pdfRes[pageNr] = pdfRes
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ctx *Context) createPDFResForWM(wm *Watermark) error {
|
|
// Note: The stamp pdf is assumed to be valid!
|
|
otherCtx, err := ReadFile(wm.FileName, NewDefaultConfiguration())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := otherCtx.EnsurePageCount(); err != nil {
|
|
return nil
|
|
}
|
|
|
|
migrated := map[int]int{}
|
|
|
|
if !wm.multiStamp() {
|
|
if err := createPDFRes(ctx, otherCtx, wm.Page, migrated, wm); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
j := otherCtx.PageCount
|
|
if ctx.PageCount < otherCtx.PageCount {
|
|
j = ctx.PageCount
|
|
}
|
|
for i := 1; i <= j; i++ {
|
|
if err := createPDFRes(ctx, otherCtx, i, migrated, wm); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func createImageResource(xRefTable *XRefTable, r io.Reader) (*IndirectRef, int, int, error) {
|
|
bb, err := ioutil.ReadAll(r)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
|
|
var sd *StreamDict
|
|
r = bytes.NewReader(bb)
|
|
|
|
// We identify JPG via its magic bytes.
|
|
if bytes.HasPrefix(bb, []byte("\xff\xd8")) {
|
|
// Process JPG by wrapping byte stream into DCTEncoded object stream.
|
|
c, _, err := image.DecodeConfig(r)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
|
|
sd, err = ReadJPEG(xRefTable, bb, c)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
|
|
} else {
|
|
// Process other formats by decoding into an image
|
|
// and subsequent object stream encoding,
|
|
img, _, err := image.Decode(r)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
|
|
sd, err = imgToImageDict(xRefTable, img)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
}
|
|
|
|
w := *sd.IntEntry("Width")
|
|
h := *sd.IntEntry("Height")
|
|
|
|
indRef, err := xRefTable.IndRefForNewObject(*sd)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
|
|
return indRef, w, h, nil
|
|
}
|
|
|
|
func (ctx *Context) createImageResForWM(wm *Watermark) (err error) {
|
|
f, err := os.Open(wm.FileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
wm.image, wm.width, wm.height, err = createImageResource(ctx.XRefTable, f)
|
|
return err
|
|
}
|
|
|
|
func (ctx *Context) createFontResForWM(wm *Watermark) (err error) {
|
|
// TODO Take existing font dicts into account.
|
|
if font.IsUserFont(wm.FontName) {
|
|
// Dummy call in order to setup used glyphs.
|
|
WriteMultiLine(new(bytes.Buffer), RectForFormat("A4"), nil, setupTextDescriptor(wm))
|
|
}
|
|
wm.font, err = createFontDict(ctx.XRefTable, wm.FontName)
|
|
return err
|
|
}
|
|
|
|
func (ctx *Context) createResourcesForWM(wm *Watermark) error {
|
|
if wm.isPDF() {
|
|
return ctx.createPDFResForWM(wm)
|
|
}
|
|
if wm.isImage() {
|
|
return ctx.createImageResForWM(wm)
|
|
}
|
|
return ctx.createFontResForWM(wm)
|
|
}
|
|
|
|
func (ctx *Context) ensureOCG(onTop bool) (*IndirectRef, error) {
|
|
name := "Background"
|
|
subt := "BG"
|
|
if onTop {
|
|
name = "Watermark"
|
|
subt = "FG"
|
|
}
|
|
|
|
d := Dict(
|
|
map[string]Object{
|
|
"Name": StringLiteral(name),
|
|
"Type": Name("OCG"),
|
|
"Usage": Dict(
|
|
map[string]Object{
|
|
"PageElement": Dict(map[string]Object{"Subtype": Name(subt)}),
|
|
"View": Dict(map[string]Object{"ViewState": Name("ON")}),
|
|
"Print": Dict(map[string]Object{"PrintState": Name("ON")}),
|
|
"Export": Dict(map[string]Object{"ExportState": Name("ON")}),
|
|
},
|
|
),
|
|
},
|
|
)
|
|
|
|
return ctx.IndRefForNewObject(d)
|
|
}
|
|
|
|
func (ctx *Context) prepareOCPropertiesInRoot(onTop bool) (*IndirectRef, error) {
|
|
rootDict, err := ctx.Catalog()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if o, ok := rootDict.Find("OCProperties"); ok {
|
|
|
|
d, err := ctx.DereferenceDict(o)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
o, found := d.Find("OCGs")
|
|
if found {
|
|
a, err := ctx.DereferenceArray(o)
|
|
if err != nil {
|
|
return nil, errCorruptOCGs
|
|
}
|
|
|
|
ir, ok := a[0].(IndirectRef)
|
|
if !ok {
|
|
return nil, errCorruptOCGs
|
|
}
|
|
return &ir, nil
|
|
}
|
|
}
|
|
|
|
ir, err := ctx.ensureOCG(onTop)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
optionalContentConfigDict := Dict(
|
|
map[string]Object{
|
|
"AS": Array{
|
|
Dict(
|
|
map[string]Object{
|
|
"Category": NewNameArray("View"),
|
|
"Event": Name("View"),
|
|
"OCGs": Array{*ir},
|
|
},
|
|
),
|
|
Dict(
|
|
map[string]Object{
|
|
"Category": NewNameArray("Print"),
|
|
"Event": Name("Print"),
|
|
"OCGs": Array{*ir},
|
|
},
|
|
),
|
|
Dict(
|
|
map[string]Object{
|
|
"Category": NewNameArray("Export"),
|
|
"Event": Name("Export"),
|
|
"OCGs": Array{*ir},
|
|
},
|
|
),
|
|
},
|
|
"ON": Array{*ir},
|
|
"Order": Array{},
|
|
"RBGroups": Array{},
|
|
},
|
|
)
|
|
|
|
d := Dict(
|
|
map[string]Object{
|
|
"OCGs": Array{*ir},
|
|
"D": optionalContentConfigDict,
|
|
},
|
|
)
|
|
|
|
rootDict.Update("OCProperties", d)
|
|
return ir, nil
|
|
}
|
|
|
|
func (ctx *Context) createFormResDict(pageNr int, wm *Watermark) (*IndirectRef, error) {
|
|
if wm.isPDF() {
|
|
i := wm.Page
|
|
if wm.multiStamp() {
|
|
maxStampPageNr := len(wm.pdfRes)
|
|
i = pageNr
|
|
if pageNr > maxStampPageNr {
|
|
i = maxStampPageNr
|
|
}
|
|
}
|
|
return wm.pdfRes[i].resDict, nil
|
|
}
|
|
|
|
if wm.isImage() {
|
|
d := Dict(
|
|
map[string]Object{
|
|
"ProcSet": NewNameArray("PDF", "Text", "ImageB", "ImageC", "ImageI"),
|
|
"XObject": Dict(map[string]Object{"Im0": *wm.image}),
|
|
},
|
|
)
|
|
return ctx.IndRefForNewObject(d)
|
|
}
|
|
|
|
d := Dict(
|
|
map[string]Object{
|
|
"Font": Dict(map[string]Object{"F1": *wm.font}),
|
|
"ProcSet": NewNameArray("PDF", "Text", "ImageB", "ImageC", "ImageI"),
|
|
},
|
|
)
|
|
|
|
return ctx.IndRefForNewObject(d)
|
|
}
|
|
|
|
func cachedForm(wm *Watermark) bool {
|
|
return !wm.isPDF() || !wm.multiStamp()
|
|
}
|
|
|
|
func pdfFormContent(w io.Writer, pageNr int, wm *Watermark) error {
|
|
cs := wm.pdfRes[wm.Page].content
|
|
if wm.multiStamp() {
|
|
maxStampPageNr := len(wm.pdfRes)
|
|
i := pageNr
|
|
if pageNr > maxStampPageNr {
|
|
i = maxStampPageNr
|
|
}
|
|
cs = wm.pdfRes[i].content
|
|
}
|
|
sc := wm.Scale
|
|
if !wm.ScaleAbs {
|
|
sc = wm.bb.Width() / float64(wm.width)
|
|
}
|
|
|
|
// Scale & translate into origin
|
|
|
|
m1 := identMatrix
|
|
m1[0][0] = sc
|
|
m1[1][1] = sc
|
|
|
|
m2 := identMatrix
|
|
m2[2][0] = -wm.bb.LL.X * wm.ScaleEff
|
|
m2[2][1] = -wm.bb.LL.Y * wm.ScaleEff
|
|
|
|
m := m1.multiply(m2)
|
|
|
|
fmt.Fprintf(w, "%.2f %.2f %.2f %.2f %.2f %.2f cm ", m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1])
|
|
|
|
_, err := w.Write(cs)
|
|
return err
|
|
}
|
|
|
|
func imageFormContent(w io.Writer, wm *Watermark) {
|
|
fmt.Fprintf(w, "q %f 0 0 %f 0 0 cm /Im0 Do Q", wm.bb.Width(), wm.bb.Height()) // TODO dont need Q
|
|
}
|
|
|
|
func formContent(w io.Writer, pageNr int, wm *Watermark) error {
|
|
switch true {
|
|
case wm.isPDF():
|
|
return pdfFormContent(w, pageNr, wm)
|
|
case wm.isImage():
|
|
imageFormContent(w, wm)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setupTextDescriptor(wm *Watermark) TextDescriptor {
|
|
// Set horizontal alignment.
|
|
var hAlign HAlignment
|
|
if wm.HAlign == nil {
|
|
// Use alignment implied by anchor.
|
|
_, _, hAlign, _ = anchorPosAndAlign(wm.Pos, RectForDim(0, 0))
|
|
} else {
|
|
// Use manual alignment.
|
|
hAlign = *wm.HAlign
|
|
}
|
|
|
|
// Set effective position and vertical alignment.
|
|
x, y, _, vAlign := anchorPosAndAlign(BottomLeft, wm.vp)
|
|
td := wm.textDescriptor()
|
|
td.X, td.Y, td.HAlign, td.VAlign, td.FontKey = x, y, hAlign, vAlign, "F1"
|
|
|
|
// Set margins.
|
|
td.MLeft = float64(wm.MLeft)
|
|
td.MRight = float64(wm.MRight)
|
|
td.MTop = float64(wm.MTop)
|
|
td.MBot = float64(wm.MBot)
|
|
|
|
// Set border.
|
|
td.BorderWidth = float64(wm.BorderWidth)
|
|
td.BorderStyle = wm.BorderStyle
|
|
if wm.BorderColor != nil {
|
|
td.ShowBorder = true
|
|
td.BorderCol = *wm.BorderColor
|
|
}
|
|
return td
|
|
}
|
|
|
|
func drawBoundingBox(b bytes.Buffer, wm *Watermark, bb *Rectangle) {
|
|
urx := bb.UR.X
|
|
ury := bb.UR.Y
|
|
if wm.isPDF() {
|
|
sc := wm.Scale
|
|
if !wm.ScaleAbs {
|
|
sc = bb.Width() / float64(wm.width)
|
|
}
|
|
urx /= sc
|
|
ury /= sc
|
|
}
|
|
fmt.Fprintf(&b, "[]0 d 2 w %.2f %.2f m %.2f %.2f l %.2f %.2f l %.2f %.2f l s ",
|
|
bb.LL.X, bb.LL.Y,
|
|
urx, bb.LL.Y,
|
|
urx, ury,
|
|
bb.LL.X, ury,
|
|
)
|
|
}
|
|
|
|
func (ctx *Context) createForm(pageNr int, wm *Watermark, withBB bool) error {
|
|
var b bytes.Buffer
|
|
|
|
if wm.isImage() || wm.isPDF() {
|
|
wm.calcBoundingBox(pageNr)
|
|
} else {
|
|
td := setupTextDescriptor(wm)
|
|
// Render td into b and return the bounding box.
|
|
wm.bb = WriteMultiLine(&b, wm.vp, nil, td)
|
|
}
|
|
|
|
// The forms bounding box is dependent on the page dimensions.
|
|
bb := wm.bb
|
|
|
|
if cachedForm(wm) || pageNr > len(wm.pdfRes) {
|
|
// Use cached form.
|
|
ir, ok := wm.fCache[*bb.Rectangle]
|
|
if ok {
|
|
wm.form = ir
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if wm.isImage() || wm.isPDF() {
|
|
if err := formContent(&b, pageNr, wm); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
ir, err := ctx.createFormResDict(pageNr, wm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bbox := bb.CroppedCopy(0)
|
|
bbox.Translate(-bb.LL.X, -bb.LL.Y)
|
|
|
|
// Paint bounding box
|
|
if withBB {
|
|
drawBoundingBox(b, wm, bbox)
|
|
}
|
|
|
|
sd := StreamDict{
|
|
Dict: Dict(
|
|
map[string]Object{
|
|
"Type": Name("XObject"),
|
|
"Subtype": Name("Form"),
|
|
"BBox": bbox.Array(),
|
|
"Matrix": NewNumberArray(1, 0, 0, 1, 0, 0),
|
|
"OC": *wm.ocg,
|
|
"Resources": *ir,
|
|
},
|
|
),
|
|
Content: b.Bytes(),
|
|
FilterPipeline: []PDFFilter{{Name: filter.Flate, DecodeParms: nil}},
|
|
}
|
|
|
|
sd.InsertName("Filter", filter.Flate)
|
|
|
|
if err = sd.Encode(); err != nil {
|
|
return err
|
|
}
|
|
|
|
ir, err = ctx.IndRefForNewObject(sd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
wm.form = ir
|
|
|
|
if cachedForm(wm) || pageNr >= len(wm.pdfRes) {
|
|
// Cache form.
|
|
wm.fCache[*wm.bb.Rectangle] = ir
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ctx *Context) createExtGStateForStamp(opacity float64) (*IndirectRef, error) {
|
|
d := Dict(
|
|
map[string]Object{
|
|
"Type": Name("ExtGState"),
|
|
"CA": Float(opacity),
|
|
"ca": Float(opacity),
|
|
},
|
|
)
|
|
|
|
return ctx.IndRefForNewObject(d)
|
|
}
|
|
|
|
func (ctx *Context) insertPageResourcesForWM(pageDict Dict, wm *Watermark, gsID, xoID string) error {
|
|
resourceDict := Dict(
|
|
map[string]Object{
|
|
"ExtGState": Dict(map[string]Object{gsID: *wm.extGState}),
|
|
"XObject": Dict(map[string]Object{xoID: *wm.form}),
|
|
},
|
|
)
|
|
|
|
pageDict.Insert("Resources", resourceDict)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ctx *Context) updatePageResourcesForWM(resDict Dict, wm *Watermark, gsID, xoID *string) error {
|
|
o, ok := resDict.Find("ExtGState")
|
|
if !ok {
|
|
resDict.Insert("ExtGState", Dict(map[string]Object{*gsID: *wm.extGState}))
|
|
} else {
|
|
d, _ := ctx.DereferenceDict(o)
|
|
for i := 0; i < 1000; i++ {
|
|
*gsID = "GS" + strconv.Itoa(i)
|
|
if _, found := d.Find(*gsID); !found {
|
|
break
|
|
}
|
|
}
|
|
d.Insert(*gsID, *wm.extGState)
|
|
}
|
|
|
|
o, ok = resDict.Find("XObject")
|
|
if !ok {
|
|
resDict.Insert("XObject", Dict(map[string]Object{*xoID: *wm.form}))
|
|
} else {
|
|
d, _ := ctx.DereferenceDict(o)
|
|
for i := 0; i < 1000; i++ {
|
|
*xoID = "Fm" + strconv.Itoa(i)
|
|
if _, found := d.Find(*xoID); !found {
|
|
break
|
|
}
|
|
}
|
|
d.Insert(*xoID, *wm.form)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func wmContent(wm *Watermark, gsID, xoID string) []byte {
|
|
m := wm.calcTransformMatrix()
|
|
insertOCG := " /Artifact <</Subtype /Watermark /Type /Pagination >>BDC q %.2f %.2f %.2f %.2f %.2f %.2f cm /%s gs /%s Do Q EMC "
|
|
var b bytes.Buffer
|
|
fmt.Fprintf(&b, insertOCG, m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1], gsID, xoID)
|
|
return b.Bytes()
|
|
}
|
|
|
|
func (ctx *Context) insertPageContentsForWM(pageDict Dict, wm *Watermark, gsID, xoID string) error {
|
|
sd, _ := ctx.NewStreamDictForBuf(wmContent(wm, gsID, xoID))
|
|
if err := sd.Encode(); err != nil {
|
|
return err
|
|
}
|
|
|
|
ir, err := ctx.IndRefForNewObject(*sd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pageDict.Insert("Contents", *ir)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ctx *Context) updatePageContentsForWM(obj Object, wm *Watermark, gsID, xoID string) error {
|
|
var entry *XRefTableEntry
|
|
var objNr int
|
|
|
|
ir, ok := obj.(IndirectRef)
|
|
if ok {
|
|
objNr = ir.ObjectNumber.Value()
|
|
if wm.objs[objNr] {
|
|
// wm already applied to this content stream.
|
|
return nil
|
|
}
|
|
genNr := ir.GenerationNumber.Value()
|
|
entry, _ = ctx.FindTableEntry(objNr, genNr)
|
|
obj = entry.Object
|
|
}
|
|
|
|
switch o := obj.(type) {
|
|
|
|
case StreamDict:
|
|
|
|
err := patchContentForWM(&o, gsID, xoID, wm, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
entry.Object = o
|
|
wm.objs[objNr] = true
|
|
|
|
case Array:
|
|
|
|
// Get stream dict for first element.
|
|
o1 := o[0]
|
|
ir, _ := o1.(IndirectRef)
|
|
objNr = ir.ObjectNumber.Value()
|
|
genNr := ir.GenerationNumber.Value()
|
|
entry, _ := ctx.FindTableEntry(objNr, genNr)
|
|
sd, _ := (entry.Object).(StreamDict)
|
|
|
|
if len(o) == 1 || !wm.OnTop {
|
|
|
|
if wm.objs[objNr] {
|
|
// wm already applied to this content stream.
|
|
return nil
|
|
}
|
|
|
|
err := patchContentForWM(&sd, gsID, xoID, wm, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
entry.Object = sd
|
|
wm.objs[objNr] = true
|
|
return nil
|
|
}
|
|
|
|
if wm.objs[objNr] {
|
|
// wm already applied to this content stream.
|
|
} else {
|
|
// Patch first content stream.
|
|
err := patchFirstContentForWM(&sd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
entry.Object = sd
|
|
wm.objs[objNr] = true
|
|
}
|
|
|
|
// Patch last content stream.
|
|
o1 = o[len(o)-1]
|
|
|
|
ir, _ = o1.(IndirectRef)
|
|
objNr = ir.ObjectNumber.Value()
|
|
if wm.objs[objNr] {
|
|
// wm already applied to this content stream.
|
|
return nil
|
|
}
|
|
|
|
genNr = ir.GenerationNumber.Value()
|
|
entry, _ = ctx.FindTableEntry(objNr, genNr)
|
|
sd, _ = (entry.Object).(StreamDict)
|
|
|
|
err := patchContentForWM(&sd, gsID, xoID, wm, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
entry.Object = sd
|
|
wm.objs[objNr] = true
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func viewPort(a *InheritedPageAttrs) *Rectangle {
|
|
visibleRegion := a.mediaBox
|
|
if a.cropBox != nil {
|
|
visibleRegion = a.cropBox
|
|
}
|
|
return visibleRegion
|
|
}
|
|
|
|
func (ctx *Context) addPageWatermark(i int, wm *Watermark) error {
|
|
if i > ctx.PageCount {
|
|
return errors.Errorf("pdfcpu: invalid page number: %d", i)
|
|
}
|
|
|
|
log.Debug.Printf("addPageWatermark page:%d\n", i)
|
|
if wm.Update {
|
|
log.Debug.Println("Updating")
|
|
if _, err := ctx.removePageWatermark(i); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
consolidateRes := false
|
|
d, inhPAttrs, err := ctx.PageDict(i, consolidateRes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
wm.vp = viewPort(inhPAttrs)
|
|
|
|
if err = ctx.createForm(i, wm, stampWithBBox); err != nil {
|
|
return err
|
|
}
|
|
|
|
wm.pageRot = float64(inhPAttrs.rotate)
|
|
|
|
log.Debug.Printf("\n%s\n", wm)
|
|
|
|
gsID := "GS0"
|
|
xoID := "Fm0"
|
|
|
|
if inhPAttrs.resources == nil {
|
|
err = ctx.insertPageResourcesForWM(d, wm, gsID, xoID)
|
|
} else {
|
|
err = ctx.updatePageResourcesForWM(inhPAttrs.resources, wm, &gsID, &xoID)
|
|
d.Update("Resources", inhPAttrs.resources)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
obj, found := d.Find("Contents")
|
|
if !found {
|
|
return ctx.insertPageContentsForWM(d, wm, gsID, xoID)
|
|
}
|
|
|
|
return ctx.updatePageContentsForWM(obj, wm, gsID, xoID)
|
|
}
|
|
|
|
func patchContentForWM(sd *StreamDict, gsID, xoID string, wm *Watermark, saveGState bool) error {
|
|
err := sd.Decode()
|
|
if err == filter.ErrUnsupportedFilter {
|
|
log.Info.Println("unsupported filter: unable to patch content with watermark.")
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bb := wmContent(wm, gsID, xoID)
|
|
|
|
if wm.OnTop {
|
|
if saveGState {
|
|
sd.Content = append([]byte("q "), sd.Content...)
|
|
}
|
|
sd.Content = append(sd.Content, []byte(" Q")...)
|
|
sd.Content = append(sd.Content, bb...)
|
|
} else {
|
|
sd.Content = append(bb, sd.Content...)
|
|
}
|
|
|
|
return sd.Encode()
|
|
}
|
|
|
|
func patchFirstContentForWM(sd *StreamDict) error {
|
|
err := sd.Decode()
|
|
if err == filter.ErrUnsupportedFilter {
|
|
log.Info.Println("unsupported filter: unable to patch content with watermark.")
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sd.Content = append([]byte("q "), sd.Content...)
|
|
|
|
return sd.Encode()
|
|
}
|
|
|
|
func (ctx *Context) createResourcesForWMMap(m map[int]*Watermark, ocgIndRef, extGStateIndRef *IndirectRef, onTop bool, opacity float64) (map[string]*[]int, error) {
|
|
fm := map[string]*[]int{}
|
|
for i, wm := range m {
|
|
wm.ocg = ocgIndRef
|
|
wm.extGState = extGStateIndRef
|
|
wm.OnTop = onTop
|
|
wm.Opacity = opacity
|
|
if wm.isText() {
|
|
if font.IsUserFont(wm.FontName) {
|
|
// Dummy call in order to setup used glyphs.
|
|
WriteMultiLine(new(bytes.Buffer), RectForFormat("A4"), nil, setupTextDescriptor(wm))
|
|
}
|
|
ii, found := fm[wm.FontName]
|
|
if !found {
|
|
fm[wm.FontName] = &[]int{i}
|
|
} else {
|
|
*ii = append(*ii, i)
|
|
}
|
|
continue
|
|
}
|
|
if wm.isImage() {
|
|
if err := ctx.createImageResForWM(wm); err != nil {
|
|
return nil, err
|
|
}
|
|
continue
|
|
}
|
|
if err := ctx.createPDFResForWM(wm); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return fm, nil
|
|
}
|
|
|
|
// AddWatermarksMap adds watermarks in m to corresponding pages.
|
|
func (ctx *Context) AddWatermarksMap(m map[int]*Watermark) error {
|
|
var (
|
|
onTop bool
|
|
opacity float64
|
|
)
|
|
for _, wm := range m {
|
|
onTop = wm.OnTop
|
|
opacity = wm.Opacity
|
|
break
|
|
}
|
|
|
|
ocgIndRef, err := ctx.prepareOCPropertiesInRoot(onTop)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
extGStateIndRef, err := ctx.createExtGStateForStamp(opacity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fm, err := ctx.createResourcesForWMMap(m, ocgIndRef, extGStateIndRef, onTop, opacity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for k, v := range fm {
|
|
// TODO Take existing font dicts into account.
|
|
ir, err := createFontDict(ctx.XRefTable, k)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, pageNr := range *v {
|
|
m[pageNr].font = ir
|
|
}
|
|
}
|
|
|
|
for k, wm := range m {
|
|
if err := ctx.addPageWatermark(k, wm); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
ctx.EnsureVersionForWriting()
|
|
return nil
|
|
}
|
|
|
|
// AddWatermarks adds watermarks to all pages selected.
|
|
func (ctx *Context) AddWatermarks(selectedPages IntSet, wm *Watermark) error {
|
|
log.Debug.Printf("AddWatermarks wm:\n%s\n", wm)
|
|
var err error
|
|
if wm.ocg, err = ctx.prepareOCPropertiesInRoot(wm.OnTop); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = ctx.createResourcesForWM(wm); err != nil {
|
|
return err
|
|
}
|
|
|
|
if wm.extGState, err = ctx.createExtGStateForStamp(wm.Opacity); err != nil {
|
|
return err
|
|
}
|
|
|
|
if selectedPages == nil || len(selectedPages) == 0 {
|
|
selectedPages = IntSet{}
|
|
for i := 1; i <= ctx.PageCount; i++ {
|
|
selectedPages[i] = true
|
|
}
|
|
}
|
|
|
|
for k, v := range selectedPages {
|
|
if v {
|
|
if err = ctx.addPageWatermark(k, wm); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx.EnsureVersionForWriting()
|
|
return nil
|
|
}
|
|
|
|
func (ctx *Context) removeResDictEntry(d *Dict, entry string, ids []string, i int) error {
|
|
o, ok := d.Find(entry)
|
|
if !ok {
|
|
return errors.Errorf("pdfcpu: page %d: corrupt resource dict", i)
|
|
}
|
|
|
|
d1, err := ctx.DereferenceDict(o)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, id := range ids {
|
|
o, ok := d1.Find(id)
|
|
if ok {
|
|
err = ctx.deleteObject(o)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d1.Delete(id)
|
|
}
|
|
}
|
|
|
|
if d1.Len() == 0 {
|
|
d.Delete(entry)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ctx *Context) removeExtGStates(d *Dict, ids []string, i int) error {
|
|
return ctx.removeResDictEntry(d, "ExtGState", ids, i)
|
|
}
|
|
|
|
func (ctx *Context) removeForms(d *Dict, ids []string, i int) error {
|
|
return ctx.removeResDictEntry(d, "XObject", ids, i)
|
|
}
|
|
|
|
func removeArtifacts(sd *StreamDict, i int) (ok bool, extGStates []string, forms []string, err error) {
|
|
err = sd.Decode()
|
|
if err == filter.ErrUnsupportedFilter {
|
|
log.Info.Printf("unsupported filter: unable to patch content with watermark for page %d\n", i)
|
|
return false, nil, nil, nil
|
|
}
|
|
if err != nil {
|
|
return false, nil, nil, err
|
|
}
|
|
|
|
var patched bool
|
|
|
|
// Watermarks may begin or end the content stream.
|
|
|
|
for {
|
|
s := string(sd.Content)
|
|
beg := strings.Index(s, "/Artifact <</Subtype /Watermark /Type /Pagination >>BDC")
|
|
if beg < 0 {
|
|
break
|
|
}
|
|
|
|
end := strings.Index(s[beg:], "EMC")
|
|
if end < 0 {
|
|
break
|
|
}
|
|
|
|
// Check for usage of resources.
|
|
t := s[beg : beg+end]
|
|
|
|
i := strings.Index(t, "/GS")
|
|
if i > 0 {
|
|
j := i + 3
|
|
k := strings.Index(t[j:], " gs")
|
|
if k > 0 {
|
|
extGStates = append(extGStates, "GS"+t[j:j+k])
|
|
}
|
|
}
|
|
|
|
i = strings.Index(t, "/Fm")
|
|
if i > 0 {
|
|
j := i + 3
|
|
k := strings.Index(t[j:], " Do")
|
|
if k > 0 {
|
|
forms = append(forms, "Fm"+t[j:j+k])
|
|
}
|
|
}
|
|
|
|
// TODO Remove whitespace until 0x0a
|
|
sd.Content = append(sd.Content[:beg], sd.Content[beg+end+3:]...)
|
|
patched = true
|
|
}
|
|
|
|
if patched {
|
|
err = sd.Encode()
|
|
}
|
|
|
|
return patched, extGStates, forms, err
|
|
}
|
|
|
|
func (ctx *Context) removeArtifactsFromPage(sd *StreamDict, resDict *Dict, i int) (bool, error) {
|
|
// Remove watermark artifacts and locate id's
|
|
// of used extGStates and forms.
|
|
ok, extGStates, forms, err := removeArtifacts(sd, i)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
|
|
// Remove obsolete extGStates from page resource dict.
|
|
err = ctx.removeExtGStates(resDict, extGStates, i)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Remove obsolete extGStatesforms from page resource dict.
|
|
return true, ctx.removeForms(resDict, forms, i)
|
|
}
|
|
|
|
func (ctx *Context) locatePageContentAndResourceDict(i int) (Object, Dict, error) {
|
|
consolidateRes := false
|
|
d, _, err := ctx.PageDict(i, consolidateRes)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
o, found := d.Find("Resources")
|
|
if !found {
|
|
return nil, nil, errors.Errorf("pdfcpu: page %d: no resource dict found\n", i)
|
|
}
|
|
|
|
resDict, err := ctx.DereferenceDict(o)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
o, found = d.Find("Contents")
|
|
if !found {
|
|
return nil, nil, errors.Errorf("pdfcpu: page %d: no page watermark found", i)
|
|
}
|
|
|
|
return o, resDict, nil
|
|
}
|
|
|
|
func (ctx *Context) removePageWatermark(i int) (bool, error) {
|
|
o, resDict, err := ctx.locatePageContentAndResourceDict(i)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
found := false
|
|
var entry *XRefTableEntry
|
|
|
|
ir, ok := o.(IndirectRef)
|
|
if ok {
|
|
objNr := ir.ObjectNumber.Value()
|
|
genNr := ir.GenerationNumber.Value()
|
|
entry, _ = ctx.FindTableEntry(objNr, genNr)
|
|
o = entry.Object
|
|
}
|
|
|
|
switch o := o.(type) {
|
|
|
|
case StreamDict:
|
|
ok, err := ctx.removeArtifactsFromPage(&o, &resDict, i)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !found && ok {
|
|
found = true
|
|
}
|
|
entry.Object = o
|
|
|
|
case Array:
|
|
// Get stream dict for first element.
|
|
o1 := o[0]
|
|
ir, _ := o1.(IndirectRef)
|
|
objNr := ir.ObjectNumber.Value()
|
|
genNr := ir.GenerationNumber.Value()
|
|
entry, _ := ctx.FindTableEntry(objNr, genNr)
|
|
sd, _ := (entry.Object).(StreamDict)
|
|
|
|
ok, err := ctx.removeArtifactsFromPage(&sd, &resDict, i)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !found && ok {
|
|
found = true
|
|
entry.Object = sd
|
|
}
|
|
|
|
if len(o) > 1 {
|
|
// Get stream dict for last element.
|
|
o1 := o[len(o)-1]
|
|
ir, _ := o1.(IndirectRef)
|
|
objNr = ir.ObjectNumber.Value()
|
|
genNr := ir.GenerationNumber.Value()
|
|
entry, _ := ctx.FindTableEntry(objNr, genNr)
|
|
sd, _ := (entry.Object).(StreamDict)
|
|
|
|
ok, err = ctx.removeArtifactsFromPage(&sd, &resDict, i)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !found && ok {
|
|
found = true
|
|
entry.Object = sd
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/*
|
|
Supposedly the form needs a PieceInfo in order to be recognized by Acrobat like so:
|
|
|
|
<PieceInfo, <<
|
|
<ADBE_CompoundType, <<
|
|
<DocSettings, (61 0 R)>
|
|
<LastModified, (D:20190830152436+02'00')>
|
|
<Private, Watermark>
|
|
>>>
|
|
>>>
|
|
|
|
*/
|
|
|
|
return found, nil
|
|
}
|
|
|
|
func (ctx *Context) locateOCGs() (Array, error) {
|
|
rootDict, err := ctx.Catalog()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
o, ok := rootDict.Find("OCProperties")
|
|
if !ok {
|
|
return nil, errNoWatermark
|
|
}
|
|
|
|
d, err := ctx.DereferenceDict(o)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
o, found := d.Find("OCGs")
|
|
if !found {
|
|
return nil, errNoWatermark
|
|
}
|
|
|
|
return ctx.DereferenceArray(o)
|
|
}
|
|
|
|
// RemoveWatermarks removes watermarks for all pages selected.
|
|
func (ctx *Context) RemoveWatermarks(selectedPages IntSet) error {
|
|
log.Debug.Printf("RemoveWatermarks\n")
|
|
|
|
a, err := ctx.locateOCGs()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
found := false
|
|
|
|
for _, o := range a {
|
|
d, err := ctx.DereferenceDict(o)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if o == nil {
|
|
continue
|
|
}
|
|
|
|
if *d.Type() != "OCG" {
|
|
continue
|
|
}
|
|
|
|
n := d.StringEntry("Name")
|
|
if n == nil {
|
|
continue
|
|
}
|
|
|
|
if *n != "Background" && *n != "Watermark" {
|
|
continue
|
|
}
|
|
|
|
found = true
|
|
break
|
|
}
|
|
|
|
if !found {
|
|
return errNoWatermark
|
|
}
|
|
|
|
var removedSmth bool
|
|
|
|
for k, v := range selectedPages {
|
|
if !v {
|
|
continue
|
|
}
|
|
|
|
ok, err := ctx.removePageWatermark(k)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if ok {
|
|
removedSmth = true
|
|
}
|
|
}
|
|
|
|
if !removedSmth {
|
|
return errNoWatermark
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func detectArtifacts(sd *StreamDict) (bool, error) {
|
|
if err := sd.Decode(); err != nil {
|
|
return false, err
|
|
}
|
|
// Watermarks may begin or end the content stream.
|
|
i := strings.Index(string(sd.Content), "/Artifact <</Subtype /Watermark /Type /Pagination >>BDC")
|
|
return i >= 0, nil
|
|
}
|
|
|
|
func (ctx *Context) findPageWatermarks(pageDictIndRef *IndirectRef) (bool, error) {
|
|
d, err := ctx.DereferenceDict(*pageDictIndRef)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
o, found := d.Find("Contents")
|
|
if !found {
|
|
return false, errNoContent
|
|
}
|
|
|
|
var entry *XRefTableEntry
|
|
|
|
ir, ok := o.(IndirectRef)
|
|
if ok {
|
|
objNr := ir.ObjectNumber.Value()
|
|
genNr := ir.GenerationNumber.Value()
|
|
entry, _ = ctx.FindTableEntry(objNr, genNr)
|
|
o = entry.Object
|
|
}
|
|
|
|
switch o := o.(type) {
|
|
|
|
case StreamDict:
|
|
return detectArtifacts(&o)
|
|
|
|
case Array:
|
|
// Get stream dict for first element.
|
|
o1 := o[0]
|
|
ir, _ := o1.(IndirectRef)
|
|
objNr := ir.ObjectNumber.Value()
|
|
genNr := ir.GenerationNumber.Value()
|
|
entry, _ := ctx.FindTableEntry(objNr, genNr)
|
|
sd, _ := (entry.Object).(StreamDict)
|
|
ok, err := detectArtifacts(&sd)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if ok {
|
|
return true, nil
|
|
}
|
|
|
|
if len(o) > 1 {
|
|
// Get stream dict for last element.
|
|
o1 := o[len(o)-1]
|
|
ir, _ := o1.(IndirectRef)
|
|
objNr = ir.ObjectNumber.Value()
|
|
genNr := ir.GenerationNumber.Value()
|
|
entry, _ := ctx.FindTableEntry(objNr, genNr)
|
|
sd, _ := (entry.Object).(StreamDict)
|
|
return detectArtifacts(&sd)
|
|
}
|
|
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (ctx *Context) detectPageTreeWatermarks(root *IndirectRef) error {
|
|
d, err := ctx.DereferenceDict(*root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
kids := d.ArrayEntry("Kids")
|
|
if kids == nil {
|
|
return nil
|
|
}
|
|
|
|
for _, o := range kids {
|
|
|
|
if ctx.Watermarked {
|
|
return nil
|
|
}
|
|
|
|
if o == nil {
|
|
continue
|
|
}
|
|
|
|
// Dereference next page node dict.
|
|
ir, ok := o.(IndirectRef)
|
|
if !ok {
|
|
return errors.Errorf("pdfcpu: detectPageTreeWatermarks: corrupt page node dict")
|
|
}
|
|
|
|
pageNodeDict, err := ctx.DereferenceDict(ir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch *pageNodeDict.Type() {
|
|
|
|
case "Pages":
|
|
// Recurse over sub pagetree.
|
|
if err := ctx.detectPageTreeWatermarks(&ir); err != nil {
|
|
return err
|
|
}
|
|
|
|
case "Page":
|
|
found, err := ctx.findPageWatermarks(&ir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if found {
|
|
ctx.Watermarked = true
|
|
return nil
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DetectPageTreeWatermarks checks xRefTable's page tree for watermarks
|
|
// and records the result to xRefTable.Watermarked.
|
|
func (ctx *Context) DetectPageTreeWatermarks() error {
|
|
root, err := ctx.Pages()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.detectPageTreeWatermarks(root)
|
|
}
|
|
|
|
// DetectWatermarks checks ctx for watermarks
|
|
// and records the result to xRefTable.Watermarked.
|
|
func (ctx *Context) DetectWatermarks() error {
|
|
a, err := ctx.locateOCGs()
|
|
if err != nil {
|
|
if err == errNoWatermark {
|
|
ctx.Watermarked = false
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
found := false
|
|
|
|
for _, o := range a {
|
|
d, err := ctx.DereferenceDict(o)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if o == nil {
|
|
continue
|
|
}
|
|
|
|
if *d.Type() != "OCG" {
|
|
continue
|
|
}
|
|
|
|
n := d.StringEntry("Name")
|
|
if n == nil {
|
|
continue
|
|
}
|
|
|
|
if *n != "Background" && *n != "Watermark" {
|
|
continue
|
|
}
|
|
|
|
found = true
|
|
break
|
|
}
|
|
|
|
if !found {
|
|
ctx.Watermarked = false
|
|
return nil
|
|
}
|
|
|
|
return ctx.DetectPageTreeWatermarks()
|
|
}
|