muun-recovery/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/stamp.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()
}