572 lines
20 KiB
Go
572 lines
20 KiB
Go
package lib
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/draw"
|
|
"image/gif"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"io"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/pdfcpu/pdfcpu/pkg/api"
|
|
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
|
|
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types"
|
|
)
|
|
|
|
// Watermark applies per-request watermarks at serve time.
|
|
// Stored files are always clean originals; watermarks are injected on read.
|
|
//
|
|
// Supported types:
|
|
// - PDF: diagonal text watermark (pdfcpu)
|
|
// - Word (.docx): watermark shape in header
|
|
// - Images: text overlay in bottom-right
|
|
// - Other: pass-through unchanged
|
|
|
|
// Watermark dispatches to the appropriate watermarking function based on MIME type.
|
|
func Watermark(in []byte, mimeType string, label string) ([]byte, error) {
|
|
switch mimeType {
|
|
case "application/pdf":
|
|
return WatermarkPDF(in, label)
|
|
case "image/jpeg", "image/png", "image/gif":
|
|
return WatermarkImage(in, mimeType, label)
|
|
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
|
return WatermarkDOCX(in, label)
|
|
default:
|
|
return in, nil // pass through unsupported types
|
|
}
|
|
}
|
|
|
|
// WatermarkPDF applies a diagonal text watermark to every page of a PDF.
|
|
// Uses pdfcpu for PDF manipulation.
|
|
// Label format: "CONFIDENTIAL — {UserName} — {Date} — {ProjectName}"
|
|
func WatermarkPDF(in []byte, label string) ([]byte, error) {
|
|
if len(in) == 0 {
|
|
return nil, fmt.Errorf("empty input")
|
|
}
|
|
|
|
// Build watermark description string for pdfcpu
|
|
// Format: "text, fontname:Helvetica, fontsize:36, color:0.5 0.5 0.5, opacity:0.3, rotation:45, diagonal:1, scale:1 abs"
|
|
wmDesc := fmt.Sprintf("%s, fontname:Helvetica, fontsize:36, color:0.5 0.5 0.5, opacity:0.3, rotation:45, diagonal:1, scale:1 abs",
|
|
escapeWatermarkText(label))
|
|
|
|
wm, err := api.TextWatermark(wmDesc, "", true, false, types.POINTS)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create watermark: %w", err)
|
|
}
|
|
|
|
// Apply watermark to all pages
|
|
var out bytes.Buffer
|
|
inReader := bytes.NewReader(in)
|
|
|
|
conf := model.NewDefaultConfiguration()
|
|
conf.ValidationMode = model.ValidationRelaxed
|
|
|
|
if err := api.AddWatermarks(inReader, &out, nil, wm, conf); err != nil {
|
|
return nil, fmt.Errorf("apply watermark: %w", err)
|
|
}
|
|
|
|
return out.Bytes(), nil
|
|
}
|
|
|
|
// escapeWatermarkText escapes special characters for pdfcpu watermark text.
|
|
func escapeWatermarkText(text string) string {
|
|
// Escape commas and colons which have special meaning in pdfcpu
|
|
text = strings.ReplaceAll(text, ",", "\\,")
|
|
text = strings.ReplaceAll(text, ":", "\\:")
|
|
return text
|
|
}
|
|
|
|
// WatermarkImage applies a text watermark to an image.
|
|
// Supports JPEG, PNG, and GIF (first frame only for GIF).
|
|
// The watermark is placed in the bottom-right corner with semi-transparent white text and dark shadow.
|
|
func WatermarkImage(in []byte, mimeType string, label string) ([]byte, error) {
|
|
if len(in) == 0 {
|
|
return nil, fmt.Errorf("empty input")
|
|
}
|
|
|
|
reader := bytes.NewReader(in)
|
|
|
|
// Handle GIF specially (only watermark first frame)
|
|
if mimeType == "image/gif" {
|
|
return watermarkGIF(reader, label)
|
|
}
|
|
|
|
// Decode image
|
|
img, format, err := image.Decode(reader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode image: %w", err)
|
|
}
|
|
|
|
// Create a new RGBA image to draw on
|
|
bounds := img.Bounds()
|
|
rgba := image.NewRGBA(bounds)
|
|
draw.Draw(rgba, bounds, img, bounds.Min, draw.Src)
|
|
|
|
// Draw watermark text
|
|
drawWatermarkText(rgba, label)
|
|
|
|
// Encode output
|
|
var out bytes.Buffer
|
|
switch format {
|
|
case "jpeg":
|
|
if err := jpeg.Encode(&out, rgba, &jpeg.Options{Quality: 90}); err != nil {
|
|
return nil, fmt.Errorf("encode jpeg: %w", err)
|
|
}
|
|
case "png":
|
|
if err := png.Encode(&out, rgba); err != nil {
|
|
return nil, fmt.Errorf("encode png: %w", err)
|
|
}
|
|
default:
|
|
// For other formats decoded by image.Decode, output as PNG
|
|
if err := png.Encode(&out, rgba); err != nil {
|
|
return nil, fmt.Errorf("encode png: %w", err)
|
|
}
|
|
}
|
|
|
|
return out.Bytes(), nil
|
|
}
|
|
|
|
// watermarkGIF handles GIF watermarking (first frame only).
|
|
func watermarkGIF(reader io.Reader, label string) ([]byte, error) {
|
|
g, err := gif.DecodeAll(reader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode gif: %w", err)
|
|
}
|
|
|
|
if len(g.Image) == 0 {
|
|
return nil, fmt.Errorf("gif has no frames")
|
|
}
|
|
|
|
// Watermark first frame
|
|
firstFrame := g.Image[0]
|
|
bounds := firstFrame.Bounds()
|
|
rgba := image.NewRGBA(bounds)
|
|
draw.Draw(rgba, bounds, firstFrame, bounds.Min, draw.Src)
|
|
|
|
drawWatermarkText(rgba, label)
|
|
|
|
// Convert back to paletted image
|
|
paletted := image.NewPaletted(bounds, firstFrame.Palette)
|
|
draw.Draw(paletted, bounds, rgba, bounds.Min, draw.Src)
|
|
g.Image[0] = paletted
|
|
|
|
var out bytes.Buffer
|
|
if err := gif.EncodeAll(&out, g); err != nil {
|
|
return nil, fmt.Errorf("encode gif: %w", err)
|
|
}
|
|
|
|
return out.Bytes(), nil
|
|
}
|
|
|
|
// drawWatermarkText draws watermark text on an RGBA image.
|
|
// Uses a simple pixel-based text rendering (8x8 bitmap font style).
|
|
// Text is placed in bottom-right corner with shadow effect.
|
|
func drawWatermarkText(img *image.RGBA, label string) {
|
|
bounds := img.Bounds()
|
|
width := bounds.Dx()
|
|
height := bounds.Dy()
|
|
|
|
// Calculate font scale based on image size
|
|
scale := 1
|
|
if width > 1000 {
|
|
scale = 2
|
|
}
|
|
if width > 2000 {
|
|
scale = 3
|
|
}
|
|
|
|
// Measure text
|
|
charWidth := 6 * scale
|
|
charHeight := 10 * scale
|
|
textWidth := len(label) * charWidth
|
|
textHeight := charHeight
|
|
|
|
// Position: bottom-right with padding
|
|
padding := 10 * scale
|
|
x := width - textWidth - padding
|
|
y := height - textHeight - padding
|
|
|
|
// Clamp to image bounds
|
|
if x < padding {
|
|
x = padding
|
|
}
|
|
if y < padding {
|
|
y = padding
|
|
}
|
|
|
|
// Draw shadow (dark, offset by 1-2 pixels)
|
|
shadowColor := color.RGBA{0, 0, 0, 180}
|
|
for i := 1; i <= scale; i++ {
|
|
drawText(img, label, x+i, y+i, charWidth, charHeight, shadowColor, scale)
|
|
}
|
|
|
|
// Draw main text (semi-transparent white)
|
|
textColor := color.RGBA{255, 255, 255, 200}
|
|
drawText(img, label, x, y, charWidth, charHeight, textColor, scale)
|
|
}
|
|
|
|
// drawText draws text using a simple bitmap font approach.
|
|
// This is a basic implementation that renders readable ASCII text.
|
|
func drawText(img *image.RGBA, text string, startX, startY, charWidth, charHeight int, c color.Color, scale int) {
|
|
bounds := img.Bounds()
|
|
|
|
for i, ch := range text {
|
|
x := startX + i*charWidth
|
|
if x >= bounds.Max.X {
|
|
break
|
|
}
|
|
|
|
// Get character bitmap
|
|
bitmap := getCharBitmap(ch)
|
|
if bitmap == nil {
|
|
continue
|
|
}
|
|
|
|
// Draw character
|
|
for row := 0; row < 8; row++ {
|
|
for col := 0; col < 5; col++ {
|
|
if bitmap[row]&(1<<(4-col)) != 0 {
|
|
// Draw scaled pixel
|
|
for sy := 0; sy < scale; sy++ {
|
|
for sx := 0; sx < scale; sx++ {
|
|
px := x + col*scale + sx
|
|
py := startY + row*scale + sy
|
|
if px >= bounds.Min.X && px < bounds.Max.X && py >= bounds.Min.Y && py < bounds.Max.Y {
|
|
img.Set(px, py, c)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// getCharBitmap returns an 8-row bitmap for a character (5 bits per row).
|
|
// Basic ASCII font supporting uppercase, lowercase, digits, and common punctuation.
|
|
func getCharBitmap(ch rune) []byte {
|
|
// 5x8 bitmap font - each byte represents one row, 5 bits used
|
|
fonts := map[rune][]byte{
|
|
'A': {0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11, 0x00},
|
|
'B': {0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E, 0x00},
|
|
'C': {0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E, 0x00},
|
|
'D': {0x1E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1E, 0x00},
|
|
'E': {0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F, 0x00},
|
|
'F': {0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10, 0x00},
|
|
'G': {0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0E, 0x00},
|
|
'H': {0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11, 0x00},
|
|
'I': {0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E, 0x00},
|
|
'J': {0x07, 0x02, 0x02, 0x02, 0x02, 0x12, 0x0C, 0x00},
|
|
'K': {0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11, 0x00},
|
|
'L': {0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F, 0x00},
|
|
'M': {0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11, 0x00},
|
|
'N': {0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11, 0x00},
|
|
'O': {0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E, 0x00},
|
|
'P': {0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10, 0x00},
|
|
'Q': {0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D, 0x00},
|
|
'R': {0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11, 0x00},
|
|
'S': {0x0E, 0x11, 0x10, 0x0E, 0x01, 0x11, 0x0E, 0x00},
|
|
'T': {0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00},
|
|
'U': {0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E, 0x00},
|
|
'V': {0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04, 0x00},
|
|
'W': {0x11, 0x11, 0x11, 0x15, 0x15, 0x1B, 0x11, 0x00},
|
|
'X': {0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11, 0x00},
|
|
'Y': {0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04, 0x00},
|
|
'Z': {0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F, 0x00},
|
|
'a': {0x00, 0x00, 0x0E, 0x01, 0x0F, 0x11, 0x0F, 0x00},
|
|
'b': {0x10, 0x10, 0x1E, 0x11, 0x11, 0x11, 0x1E, 0x00},
|
|
'c': {0x00, 0x00, 0x0E, 0x11, 0x10, 0x11, 0x0E, 0x00},
|
|
'd': {0x01, 0x01, 0x0F, 0x11, 0x11, 0x11, 0x0F, 0x00},
|
|
'e': {0x00, 0x00, 0x0E, 0x11, 0x1F, 0x10, 0x0E, 0x00},
|
|
'f': {0x06, 0x09, 0x08, 0x1C, 0x08, 0x08, 0x08, 0x00},
|
|
'g': {0x00, 0x00, 0x0F, 0x11, 0x0F, 0x01, 0x0E, 0x00},
|
|
'h': {0x10, 0x10, 0x1E, 0x11, 0x11, 0x11, 0x11, 0x00},
|
|
'i': {0x04, 0x00, 0x0C, 0x04, 0x04, 0x04, 0x0E, 0x00},
|
|
'j': {0x02, 0x00, 0x06, 0x02, 0x02, 0x12, 0x0C, 0x00},
|
|
'k': {0x10, 0x10, 0x12, 0x14, 0x18, 0x14, 0x12, 0x00},
|
|
'l': {0x0C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E, 0x00},
|
|
'm': {0x00, 0x00, 0x1A, 0x15, 0x15, 0x15, 0x15, 0x00},
|
|
'n': {0x00, 0x00, 0x1E, 0x11, 0x11, 0x11, 0x11, 0x00},
|
|
'o': {0x00, 0x00, 0x0E, 0x11, 0x11, 0x11, 0x0E, 0x00},
|
|
'p': {0x00, 0x00, 0x1E, 0x11, 0x1E, 0x10, 0x10, 0x00},
|
|
'q': {0x00, 0x00, 0x0F, 0x11, 0x0F, 0x01, 0x01, 0x00},
|
|
'r': {0x00, 0x00, 0x16, 0x19, 0x10, 0x10, 0x10, 0x00},
|
|
's': {0x00, 0x00, 0x0F, 0x10, 0x0E, 0x01, 0x1E, 0x00},
|
|
't': {0x08, 0x08, 0x1C, 0x08, 0x08, 0x09, 0x06, 0x00},
|
|
'u': {0x00, 0x00, 0x11, 0x11, 0x11, 0x11, 0x0F, 0x00},
|
|
'v': {0x00, 0x00, 0x11, 0x11, 0x11, 0x0A, 0x04, 0x00},
|
|
'w': {0x00, 0x00, 0x11, 0x11, 0x15, 0x15, 0x0A, 0x00},
|
|
'x': {0x00, 0x00, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x00},
|
|
'y': {0x00, 0x00, 0x11, 0x11, 0x0F, 0x01, 0x0E, 0x00},
|
|
'z': {0x00, 0x00, 0x1F, 0x02, 0x04, 0x08, 0x1F, 0x00},
|
|
'0': {0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E, 0x00},
|
|
'1': {0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E, 0x00},
|
|
'2': {0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F, 0x00},
|
|
'3': {0x0E, 0x11, 0x01, 0x06, 0x01, 0x11, 0x0E, 0x00},
|
|
'4': {0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02, 0x00},
|
|
'5': {0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E, 0x00},
|
|
'6': {0x06, 0x08, 0x10, 0x1E, 0x11, 0x11, 0x0E, 0x00},
|
|
'7': {0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08, 0x00},
|
|
'8': {0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E, 0x00},
|
|
'9': {0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C, 0x00},
|
|
' ': {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
|
'.': {0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00},
|
|
',': {0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x02, 0x04},
|
|
'-': {0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00},
|
|
'_': {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00},
|
|
':': {0x00, 0x0C, 0x0C, 0x00, 0x0C, 0x0C, 0x00, 0x00},
|
|
'/': {0x01, 0x02, 0x02, 0x04, 0x08, 0x08, 0x10, 0x00},
|
|
'@': {0x0E, 0x11, 0x17, 0x15, 0x17, 0x10, 0x0E, 0x00},
|
|
'(': {0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02, 0x00},
|
|
')': {0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08, 0x00},
|
|
}
|
|
|
|
if bitmap, ok := fonts[ch]; ok {
|
|
return bitmap
|
|
}
|
|
// Return a small square for unknown characters
|
|
return []byte{0x00, 0x1F, 0x11, 0x11, 0x11, 0x1F, 0x00, 0x00}
|
|
}
|
|
|
|
// WatermarkDOCX adds a text watermark to a Word document.
|
|
// DOCX is a ZIP file; we unzip, modify word/document.xml to add a watermark shape in the header, and rezip.
|
|
func WatermarkDOCX(in []byte, label string) ([]byte, error) {
|
|
if len(in) == 0 {
|
|
return nil, fmt.Errorf("empty input")
|
|
}
|
|
|
|
reader := bytes.NewReader(in)
|
|
zipReader, err := zip.NewReader(reader, int64(len(in)))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open zip: %w", err)
|
|
}
|
|
|
|
var out bytes.Buffer
|
|
zipWriter := zip.NewWriter(&out)
|
|
|
|
// Track which relationship files we've seen
|
|
var hasHeader1 bool
|
|
headerRelId := "rIdWatermarkHeader"
|
|
|
|
// First pass: check if header1.xml exists
|
|
for _, f := range zipReader.File {
|
|
if f.Name == "word/header1.xml" {
|
|
hasHeader1 = true
|
|
break
|
|
}
|
|
}
|
|
|
|
for _, f := range zipReader.File {
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open %s: %w", f.Name, err)
|
|
}
|
|
|
|
content, err := io.ReadAll(rc)
|
|
rc.Close()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read %s: %w", f.Name, err)
|
|
}
|
|
|
|
// Modify content based on file
|
|
switch f.Name {
|
|
case "word/document.xml":
|
|
// Add header reference to document if not already present
|
|
if !hasHeader1 {
|
|
content = addHeaderReferenceToDocument(content, headerRelId)
|
|
}
|
|
case "word/header1.xml":
|
|
// Add watermark to existing header
|
|
content = addWatermarkToHeader(content, label)
|
|
case "word/_rels/document.xml.rels":
|
|
// Add relationship for header if we created one
|
|
if !hasHeader1 {
|
|
content = addHeaderRelationship(content, headerRelId)
|
|
}
|
|
case "[Content_Types].xml":
|
|
// Ensure header content type is registered
|
|
if !hasHeader1 {
|
|
content = ensureHeaderContentType(content)
|
|
}
|
|
}
|
|
|
|
// Write modified content
|
|
w, err := zipWriter.Create(f.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create %s: %w", f.Name, err)
|
|
}
|
|
if _, err := w.Write(content); err != nil {
|
|
return nil, fmt.Errorf("write %s: %w", f.Name, err)
|
|
}
|
|
}
|
|
|
|
// If no header existed, create one with the watermark
|
|
if !hasHeader1 {
|
|
header := createWatermarkHeader(label)
|
|
w, err := zipWriter.Create("word/header1.xml")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create header: %w", err)
|
|
}
|
|
if _, err := w.Write(header); err != nil {
|
|
return nil, fmt.Errorf("write header: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := zipWriter.Close(); err != nil {
|
|
return nil, fmt.Errorf("close zip: %w", err)
|
|
}
|
|
|
|
return out.Bytes(), nil
|
|
}
|
|
|
|
// createWatermarkHeader creates a new header XML with a diagonal watermark shape.
|
|
func createWatermarkHeader(label string) []byte {
|
|
// VML shape for diagonal watermark text
|
|
// The shape uses a text path to render the watermark diagonally
|
|
header := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<w:hdr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
|
xmlns:v="urn:schemas-microsoft-com:vml"
|
|
xmlns:o="urn:schemas-microsoft-com:office:office"
|
|
xmlns:w10="urn:schemas-microsoft-com:office:word">
|
|
<w:p>
|
|
<w:pPr>
|
|
<w:pStyle w:val="Header"/>
|
|
</w:pPr>
|
|
<w:r>
|
|
<w:pict>
|
|
<v:shapetype id="_x0000_t136" coordsize="21600,21600" o:spt="136" adj="10800" path="m@7,l@8,m@5,21600l@6,21600e">
|
|
<v:formulas>
|
|
<v:f eqn="sum #0 0 10800"/>
|
|
<v:f eqn="prod #0 2 1"/>
|
|
<v:f eqn="sum 21600 0 @1"/>
|
|
<v:f eqn="sum 0 0 @2"/>
|
|
<v:f eqn="sum 21600 0 @3"/>
|
|
<v:f eqn="if @0 @3 0"/>
|
|
<v:f eqn="if @0 21600 @1"/>
|
|
<v:f eqn="if @0 0 @2"/>
|
|
<v:f eqn="if @0 @4 21600"/>
|
|
<v:f eqn="mid @5 @6"/>
|
|
<v:f eqn="mid @8 @5"/>
|
|
<v:f eqn="mid @7 @8"/>
|
|
<v:f eqn="mid @6 @7"/>
|
|
<v:f eqn="sum @6 0 @5"/>
|
|
</v:formulas>
|
|
<v:path textpathok="t" o:connecttype="custom" o:connectlocs="@9,0;@10,10800;@11,21600;@12,10800" o:connectangles="270,180,90,0"/>
|
|
<v:textpath on="t" fitshape="t"/>
|
|
<v:handles>
|
|
<v:h position="#0,bottomRight" xrange="6629,14971"/>
|
|
</v:handles>
|
|
<o:lock v:ext="edit" text="t" shapetype="t"/>
|
|
</v:shapetype>
|
|
<v:shape id="PowerPlusWaterMarkObject" o:spid="_x0000_s2049" type="#_x0000_t136"
|
|
style="position:absolute;margin-left:0;margin-top:0;width:527.85pt;height:131.95pt;rotation:315;z-index:-251658752;mso-position-horizontal:center;mso-position-horizontal-relative:margin;mso-position-vertical:center;mso-position-vertical-relative:margin"
|
|
o:allowincell="f" fillcolor="silver" stroked="f">
|
|
<v:fill opacity=".5"/>
|
|
<v:textpath style="font-family:"Calibri";font-size:1pt" string="%s"/>
|
|
<w10:wrap anchorx="margin" anchory="margin"/>
|
|
</v:shape>
|
|
</w:pict>
|
|
</w:r>
|
|
</w:p>
|
|
</w:hdr>`, escapeXML(label))
|
|
return []byte(header)
|
|
}
|
|
|
|
// addWatermarkToHeader adds a watermark shape to an existing header.
|
|
func addWatermarkToHeader(content []byte, label string) []byte {
|
|
// Insert watermark shape before </w:hdr>
|
|
watermarkShape := fmt.Sprintf(`
|
|
<w:p>
|
|
<w:r>
|
|
<w:pict xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w10="urn:schemas-microsoft-com:office:word">
|
|
<v:shapetype id="_x0000_t136" coordsize="21600,21600" o:spt="136" path="m@7,l@8,m@5,21600l@6,21600e">
|
|
<v:textpath on="t" fitshape="t"/>
|
|
</v:shapetype>
|
|
<v:shape type="#_x0000_t136"
|
|
style="position:absolute;margin-left:0;margin-top:0;width:500pt;height:130pt;rotation:315;z-index:-251658752;mso-position-horizontal:center;mso-position-horizontal-relative:margin;mso-position-vertical:center;mso-position-vertical-relative:margin"
|
|
fillcolor="silver" stroked="f">
|
|
<v:fill opacity=".5"/>
|
|
<v:textpath style="font-family:Calibri;font-size:1pt" string="%s"/>
|
|
<w10:wrap anchorx="margin" anchory="margin"/>
|
|
</v:shape>
|
|
</w:pict>
|
|
</w:r>
|
|
</w:p>`, escapeXML(label))
|
|
|
|
// Find </w:hdr> and insert before it
|
|
str := string(content)
|
|
if idx := strings.LastIndex(str, "</w:hdr>"); idx != -1 {
|
|
str = str[:idx] + watermarkShape + str[idx:]
|
|
}
|
|
return []byte(str)
|
|
}
|
|
|
|
// addHeaderReferenceToDocument adds a header reference to the document.xml.
|
|
func addHeaderReferenceToDocument(content []byte, relId string) []byte {
|
|
str := string(content)
|
|
|
|
// Find the sectPr element and add headerReference
|
|
// Look for <w:sectPr or create one before </w:body>
|
|
headerRef := fmt.Sprintf(`<w:headerReference w:type="default" r:id="%s"/>`, relId)
|
|
|
|
sectPrPattern := regexp.MustCompile(`(<w:sectPr[^>]*>)`)
|
|
if sectPrPattern.MatchString(str) {
|
|
str = sectPrPattern.ReplaceAllString(str, "${1}"+headerRef)
|
|
} else {
|
|
// No sectPr exists, add one before </w:body>
|
|
sectPr := fmt.Sprintf(`<w:sectPr xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">%s</w:sectPr>`, headerRef)
|
|
if idx := strings.LastIndex(str, "</w:body>"); idx != -1 {
|
|
str = str[:idx] + sectPr + str[idx:]
|
|
}
|
|
}
|
|
|
|
// Ensure r namespace is declared
|
|
if !strings.Contains(str, `xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"`) {
|
|
str = strings.Replace(str, `<w:document `, `<w:document xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" `, 1)
|
|
}
|
|
|
|
return []byte(str)
|
|
}
|
|
|
|
// addHeaderRelationship adds the header relationship to document.xml.rels.
|
|
func addHeaderRelationship(content []byte, relId string) []byte {
|
|
str := string(content)
|
|
rel := fmt.Sprintf(`<Relationship Id="%s" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" Target="header1.xml"/>`, relId)
|
|
|
|
// Insert before </Relationships>
|
|
if idx := strings.LastIndex(str, "</Relationships>"); idx != -1 {
|
|
str = str[:idx] + rel + str[idx:]
|
|
}
|
|
return []byte(str)
|
|
}
|
|
|
|
// ensureHeaderContentType ensures the header content type is in [Content_Types].xml.
|
|
func ensureHeaderContentType(content []byte) []byte {
|
|
str := string(content)
|
|
if strings.Contains(str, "header1.xml") {
|
|
return content
|
|
}
|
|
|
|
override := `<Override PartName="/word/header1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml"/>`
|
|
if idx := strings.LastIndex(str, "</Types>"); idx != -1 {
|
|
str = str[:idx] + override + str[idx:]
|
|
}
|
|
return []byte(str)
|
|
}
|
|
|
|
// escapeXML escapes special XML characters.
|
|
func escapeXML(s string) string {
|
|
s = strings.ReplaceAll(s, "&", "&")
|
|
s = strings.ReplaceAll(s, "<", "<")
|
|
s = strings.ReplaceAll(s, ">", ">")
|
|
s = strings.ReplaceAll(s, "'", "'")
|
|
s = strings.ReplaceAll(s, "\"", """)
|
|
return s
|
|
}
|