feat(canary): pdf template (binary blob + build helper)
Phase 5 first commit. Adds the committed template.pdf binary blob plus
the pure-Go one-shot utility that produced it, matching the docx Phase
4 pattern (cmd/build<format>template/main.go excluded from production).
cmd/buildpdftemplate/main.go assembles a minimal PDF-1.4 by hand:
- Header: %PDF-1.4 + 4 high-bit bytes to mark as binary
- Object 1: Catalog → Pages root
- Object 2: Pages → single Kids ref + Count 1
- Object 3: Page with /AA << /O << /Type /Action /S /URI /URI (...) >> >>
where the URI value is the 76-char placeholder
HONEY_TRACK_URL_PADDED_TO_FIXED_WIDTH______________________________________
- xref table with computed byte offsets
- trailer + startxref + %%EOF
The placeholder is a direct dictionary value (NOT inside a stream), so
substitution can be a plain byte-replace at runtime without breaking the
cross-reference table — spec §9.4 line 1133. No FlateDecode anywhere.
pdfcpu (v0.12.1) is used for validation only — the builder calls
api.Validate before writing, and tests will call it again on substituted
output to confirm the PDF stays well-formed.
Verification at HEAD:
- 477-byte template.pdf
- sha256: 70c6359016ceb780539f8c4497991b98ff82201e35a38d36090d88856027e103
- Reproducible: rebuild produces byte-identical output
- grep -aob HONEY_TRACK_URL → one offset (240)
- file: PDF document, version 1.4, 1 page(s)
- api.Validate passes
go.mod adds pdfcpu as a direct dep with its transitive deps via go mod
tidy. golang.org/x/crypto returns to direct (pdfcpu uses it for AES
encryption support).
This commit is contained in:
parent
13b2604f18
commit
c9c9333294
|
|
@ -0,0 +1,154 @@
|
|||
// ©AngelaMos | 2026
|
||||
// main.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pdfcpu/pdfcpu/pkg/api"
|
||||
)
|
||||
|
||||
const (
|
||||
placeholderRoot = "HONEY_TRACK_URL_PADDED_TO_FIXED_WIDTH"
|
||||
placeholderLength = 76
|
||||
|
||||
pdfHeader = "%PDF-1.4\n%\xe2\xe3\xcf\xd3\n"
|
||||
|
||||
objCatalog = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"
|
||||
objPages = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"
|
||||
|
||||
pageDictFmt = "3 0 obj\n" +
|
||||
"<<\n" +
|
||||
"/Type /Page\n" +
|
||||
"/Parent 2 0 R\n" +
|
||||
"/MediaBox [0 0 612 792]\n" +
|
||||
"/Resources << >>\n" +
|
||||
"/AA << /O << /Type /Action /S /URI /URI (%s) >> >>\n" +
|
||||
">>\nendobj\n"
|
||||
|
||||
xrefHeader = "xref\n0 4\n"
|
||||
xrefFreeRecord = "0000000000 65535 f \n"
|
||||
xrefRecordFmt = "%010d 00000 n \n"
|
||||
|
||||
trailerFmt = "trailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n"
|
||||
|
||||
dirPerm os.FileMode = 0o755
|
||||
filePerm os.FileMode = 0o644
|
||||
)
|
||||
|
||||
func placeholder() string {
|
||||
return placeholderRoot +
|
||||
strings.Repeat("_", placeholderLength-len(placeholderRoot))
|
||||
}
|
||||
|
||||
func main() {
|
||||
out := flag.String("out", "", "output path for template.pdf")
|
||||
flag.Parse()
|
||||
|
||||
if *out == "" {
|
||||
log.Fatal("usage: buildpdftemplate -out <path>")
|
||||
}
|
||||
|
||||
if err := buildTemplate(*out); err != nil {
|
||||
log.Fatalf("build pdf template: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("wrote %s\n", *out)
|
||||
}
|
||||
|
||||
func buildTemplate(out string) (err error) {
|
||||
cleaned := filepath.Clean(out)
|
||||
if mkErr := os.MkdirAll(filepath.Dir(cleaned), dirPerm); mkErr != nil {
|
||||
return fmt.Errorf("mkdir parent: %w", mkErr)
|
||||
}
|
||||
|
||||
pdfBytes := assemblePDF()
|
||||
|
||||
if vErr := api.Validate(
|
||||
bytes.NewReader(pdfBytes),
|
||||
nil,
|
||||
); vErr != nil {
|
||||
return fmt.Errorf("pdfcpu validate: %w", vErr)
|
||||
}
|
||||
|
||||
full := placeholder()
|
||||
if c := bytes.Count(pdfBytes, []byte(full)); c != 1 {
|
||||
return fmt.Errorf(
|
||||
"full placeholder appears %d times, expected exactly 1",
|
||||
c,
|
||||
)
|
||||
}
|
||||
if c := bytes.Count(pdfBytes, []byte(placeholderRoot)); c != 1 {
|
||||
return fmt.Errorf(
|
||||
"placeholder root appears %d times, expected exactly 1",
|
||||
c,
|
||||
)
|
||||
}
|
||||
if len(full) != placeholderLength {
|
||||
return fmt.Errorf(
|
||||
"placeholder length is %d, expected %d",
|
||||
len(full),
|
||||
placeholderLength,
|
||||
)
|
||||
}
|
||||
|
||||
f, oErr := os.OpenFile(
|
||||
cleaned,
|
||||
os.O_CREATE|os.O_TRUNC|os.O_WRONLY,
|
||||
filePerm,
|
||||
)
|
||||
if oErr != nil {
|
||||
return fmt.Errorf("open output: %w", oErr)
|
||||
}
|
||||
defer func() {
|
||||
if cerr := f.Close(); cerr != nil && err == nil {
|
||||
err = fmt.Errorf("close output: %w", cerr)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, wErr := f.Write(pdfBytes); wErr != nil {
|
||||
return fmt.Errorf("write pdf: %w", wErr)
|
||||
}
|
||||
|
||||
sum := sha256.Sum256(pdfBytes)
|
||||
fmt.Printf("sha256: %x\n", sum)
|
||||
fmt.Printf("size: %d bytes\n", len(pdfBytes))
|
||||
return nil
|
||||
}
|
||||
|
||||
func assemblePDF() []byte {
|
||||
pageObj := fmt.Sprintf(pageDictFmt, placeholder())
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(pdfHeader)
|
||||
|
||||
offsets := make([]int, 4)
|
||||
|
||||
offsets[1] = buf.Len()
|
||||
buf.WriteString(objCatalog)
|
||||
|
||||
offsets[2] = buf.Len()
|
||||
buf.WriteString(objPages)
|
||||
|
||||
offsets[3] = buf.Len()
|
||||
buf.WriteString(pageObj)
|
||||
|
||||
xrefOffset := buf.Len()
|
||||
buf.WriteString(xrefHeader)
|
||||
buf.WriteString(xrefFreeRecord)
|
||||
for i := 1; i <= 3; i++ {
|
||||
fmt.Fprintf(&buf, xrefRecordFmt, offsets[i])
|
||||
}
|
||||
|
||||
fmt.Fprintf(&buf, trailerFmt, xrefOffset)
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ require (
|
|||
github.com/knadh/koanf/providers/env v1.1.0
|
||||
github.com/knadh/koanf/providers/file v1.2.1
|
||||
github.com/knadh/koanf/v2 v2.1.2
|
||||
github.com/pdfcpu/pdfcpu v0.12.1
|
||||
github.com/pressly/goose/v3 v3.27.1
|
||||
github.com/redis/go-redis/v9 v9.7.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
|
|
@ -32,6 +33,7 @@ require (
|
|||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
|
|
@ -53,6 +55,9 @@ require (
|
|||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
|
||||
github.com/hhrutter/lzw v1.0.0 // indirect
|
||||
github.com/hhrutter/pkcs7 v0.2.2 // indirect
|
||||
github.com/hhrutter/tiff v1.0.3 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
|
|
@ -61,6 +66,7 @@ require (
|
|||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
|
|
@ -75,6 +81,7 @@ require (
|
|||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
|
|
@ -91,6 +98,7 @@ require (
|
|||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
golang.org/x/image v0.39.0 // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
|
|
@ -98,5 +106,6 @@ require (
|
|||
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
|
|||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
|
|
@ -83,6 +85,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
|
||||
github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
|
||||
github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
|
||||
github.com/hhrutter/pkcs7 v0.2.2 h1:xMoifoVWah1LNym3C0pomEiLmyJyVIBXt/8oTPyPz+8=
|
||||
github.com/hhrutter/pkcs7 v0.2.2/go.mod h1:aEzKz0+ZAlz7YaEMY47jDHL14hVWD6iXt0AgqgAvWgE=
|
||||
github.com/hhrutter/tiff v1.0.3 h1:POV5xITOE1Lt5FvP24ylft0LyCmHmc8GkJ1SVlvUyk0=
|
||||
github.com/hhrutter/tiff v1.0.3/go.mod h1:zZDLVY4cp9za2FLrryAaGszwWYAUM6DrRiBR0l//mxA=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
|
|
@ -119,6 +127,8 @@ github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8S
|
|||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
|
||||
|
|
@ -153,6 +163,10 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
|||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pdfcpu/pdfcpu v0.12.1 h1:HwoN72zJCj+pPbfMDChYBTZrT7SY0VwgUzqeaId3I20=
|
||||
github.com/pdfcpu/pdfcpu v0.12.1/go.mod h1:7KPpVLMavcpliPrtN6o7Kuk3cFtYq8nii3SJnnsK7ps=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
|
|
@ -216,6 +230,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
|||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
|
||||
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
|
|
@ -245,6 +261,8 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
|
|||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
%PDF-1.4
|
||||
%âãÏÓ
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/Parent 2 0 R
|
||||
/MediaBox [0 0 612 792]
|
||||
/Resources << >>
|
||||
/AA << /O << /Type /Action /S /URI /URI (HONEY_TRACK_URL_PADDED_TO_FIXED_WIDTH_______________________________________) >> >>
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 4
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000064 00000 n
|
||||
0000000121 00000 n
|
||||
trailer
|
||||
<< /Size 4 /Root 1 0 R >>
|
||||
startxref
|
||||
334
|
||||
%%EOF
|
||||
Loading…
Reference in New Issue