Skip to content

Commit 32b00fa

Browse files
danmolitorclaude
andcommitted
Add redact/rasterize to Go SDK, remove deprecated sign shim
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4366507 commit 32b00fa

4 files changed

Lines changed: 212 additions & 10 deletions

File tree

‎packages/go-sdk/client.go‎

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,63 @@ func (f *Forme) Certify(pdfBytes []byte, certificatePem, privateKeyPem string, o
118118
return respBody, nil
119119
}
120120

121-
// Sign is deprecated. Use Certify instead.
122-
func (f *Forme) Sign(pdfBytes []byte, certificatePem, privateKeyPem string, opts CertifyOptions) ([]byte, error) {
123-
return f.Certify(pdfBytes, certificatePem, privateKeyPem, opts)
121+
// Redact removes sensitive content from a PDF.
122+
// Returns the redacted PDF bytes.
123+
func (f *Forme) Redact(pdfBytes []byte, opts RedactOptions) ([]byte, error) {
124+
body := map[string]any{
125+
"pdf": base64.StdEncoding.EncodeToString(pdfBytes),
126+
}
127+
if len(opts.Redactions) > 0 {
128+
body["redactions"] = opts.Redactions
129+
}
130+
if len(opts.Patterns) > 0 {
131+
body["patterns"] = opts.Patterns
132+
}
133+
if len(opts.Presets) > 0 {
134+
body["presets"] = opts.Presets
135+
}
136+
if opts.Template != "" {
137+
body["template"] = opts.Template
138+
}
139+
140+
respBody, _, err := f.doJSON("POST", "/v1/redact", body)
141+
if err != nil {
142+
return nil, err
143+
}
144+
return respBody, nil
145+
}
146+
147+
// Rasterize converts PDF pages to PNG images.
148+
// Returns a list of PNG image bytes, one per page.
149+
func (f *Forme) Rasterize(pdfBytes []byte, opts RasterizeOptions) ([][]byte, error) {
150+
body := map[string]any{
151+
"pdf": base64.StdEncoding.EncodeToString(pdfBytes),
152+
}
153+
if opts.DPI > 0 {
154+
body["dpi"] = opts.DPI
155+
}
156+
157+
respBody, _, err := f.doJSON("POST", "/v1/rasterize", body)
158+
if err != nil {
159+
return nil, err
160+
}
161+
162+
var envelope struct {
163+
Pages []string `json:"pages"`
164+
}
165+
if err := json.Unmarshal(respBody, &envelope); err != nil {
166+
return nil, fmt.Errorf("failed to parse rasterize response: %w", err)
167+
}
168+
169+
pages := make([][]byte, len(envelope.Pages))
170+
for i, page := range envelope.Pages {
171+
decoded, err := base64.StdEncoding.DecodeString(page)
172+
if err != nil {
173+
return nil, fmt.Errorf("failed to decode page %d: %w", i, err)
174+
}
175+
pages[i] = decoded
176+
}
177+
return pages, nil
124178
}
125179

126180
// Merge combines multiple PDFs into a single PDF via multipart upload.

‎packages/go-sdk/forme_test.go‎

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package forme
22

33
import (
4+
"encoding/base64"
45
"encoding/json"
56
"fmt"
67
"io"
@@ -600,6 +601,130 @@ func TestRenderS3ReturnsURL(t *testing.T) {
600601
}
601602
}
602603

604+
func TestRedactSendsCorrectBody(t *testing.T) {
605+
var gotBody []byte
606+
var gotPath, gotContentType string
607+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
608+
gotPath = r.URL.Path
609+
gotContentType = r.Header.Get("Content-Type")
610+
gotBody, _ = io.ReadAll(r.Body)
611+
w.Header().Set("Content-Type", "application/pdf")
612+
w.Write([]byte("%PDF-redacted"))
613+
}))
614+
defer server.Close()
615+
616+
client := New("forme_sk_test", WithBaseURL(server.URL))
617+
page := 2
618+
result, err := client.Redact(
619+
[]byte("%PDF-original"),
620+
RedactOptions{
621+
Patterns: []RedactionPattern{
622+
{Pattern: `\d{3}-\d{2}-\d{4}`, PatternType: "Regex", Color: "#000000"},
623+
{Pattern: "John Doe", PatternType: "Literal", Page: &page},
624+
},
625+
Presets: []string{"ssn", "email"},
626+
},
627+
)
628+
if err != nil {
629+
t.Fatalf("unexpected error: %v", err)
630+
}
631+
if string(result) != "%PDF-redacted" {
632+
t.Fatalf("expected redacted PDF, got %q", result)
633+
}
634+
if gotPath != "/v1/redact" {
635+
t.Fatalf("expected '/v1/redact', got %q", gotPath)
636+
}
637+
if gotContentType != "application/json" {
638+
t.Fatalf("expected 'application/json', got %q", gotContentType)
639+
}
640+
641+
var body map[string]any
642+
json.Unmarshal(gotBody, &body)
643+
if body["pdf"] == "" {
644+
t.Fatal("expected pdf to be base64-encoded")
645+
}
646+
647+
patterns, ok := body["patterns"].([]any)
648+
if !ok || len(patterns) != 2 {
649+
t.Fatalf("expected 2 patterns, got %v", body["patterns"])
650+
}
651+
p0 := patterns[0].(map[string]any)
652+
if p0["pattern"] != `\d{3}-\d{2}-\d{4}` {
653+
t.Fatalf("unexpected pattern: %v", p0["pattern"])
654+
}
655+
if p0["pattern_type"] != "Regex" {
656+
t.Fatalf("unexpected pattern_type: %v", p0["pattern_type"])
657+
}
658+
p1 := patterns[1].(map[string]any)
659+
if p1["page"] != float64(2) {
660+
t.Fatalf("expected page=2, got %v", p1["page"])
661+
}
662+
663+
presets, ok := body["presets"].([]any)
664+
if !ok || len(presets) != 2 {
665+
t.Fatalf("expected 2 presets, got %v", body["presets"])
666+
}
667+
if presets[0] != "ssn" || presets[1] != "email" {
668+
t.Fatalf("unexpected presets: %v", presets)
669+
}
670+
671+
if _, ok := body["redactions"]; ok {
672+
t.Fatal("expected redactions to be omitted when empty")
673+
}
674+
if _, ok := body["template"]; ok {
675+
t.Fatal("expected template to be omitted when empty")
676+
}
677+
}
678+
679+
func TestRasterizeReturnsPages(t *testing.T) {
680+
page1 := []byte("PNG-page-1-data")
681+
page2 := []byte("PNG-page-2-data")
682+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
683+
// Verify path and method
684+
if r.URL.Path != "/v1/rasterize" {
685+
t.Errorf("expected '/v1/rasterize', got %q", r.URL.Path)
686+
}
687+
if r.Method != "POST" {
688+
t.Errorf("expected POST, got %q", r.Method)
689+
}
690+
691+
// Verify DPI is sent
692+
body, _ := io.ReadAll(r.Body)
693+
var reqBody map[string]any
694+
json.Unmarshal(body, &reqBody)
695+
if reqBody["dpi"] != float64(300) {
696+
t.Errorf("expected dpi=300, got %v", reqBody["dpi"])
697+
}
698+
if reqBody["pdf"] == "" {
699+
t.Error("expected pdf to be base64-encoded")
700+
}
701+
702+
w.Header().Set("Content-Type", "application/json")
703+
json.NewEncoder(w).Encode(map[string]any{
704+
"pages": []string{
705+
base64.StdEncoding.EncodeToString(page1),
706+
base64.StdEncoding.EncodeToString(page2),
707+
},
708+
})
709+
}))
710+
defer server.Close()
711+
712+
client := New("forme_sk_test", WithBaseURL(server.URL))
713+
pages, err := client.Rasterize([]byte("%PDF-original"), RasterizeOptions{DPI: 300})
714+
if err != nil {
715+
t.Fatalf("unexpected error: %v", err)
716+
}
717+
if len(pages) != 2 {
718+
t.Fatalf("expected 2 pages, got %d", len(pages))
719+
}
720+
if string(pages[0]) != "PNG-page-1-data" {
721+
t.Fatalf("expected page 1 data, got %q", pages[0])
722+
}
723+
if string(pages[1]) != "PNG-page-2-data" {
724+
t.Fatalf("expected page 2 data, got %q", pages[1])
725+
}
726+
}
727+
603728
// roundTripperFunc is a helper to create custom http.RoundTripper from a function.
604729
type roundTripperFunc func(*http.Request) (*http.Response, error)
605730

‎packages/go-sdk/options.go‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,36 @@ type CertifyOptions struct {
3939
Contact string `json:"contact,omitempty"`
4040
}
4141

42-
// SignOptions is deprecated. Use CertifyOptions instead.
43-
type SignOptions = CertifyOptions
42+
// RedactOptions configures a redact request.
43+
type RedactOptions struct {
44+
Redactions []RedactionRegion `json:"redactions,omitempty"`
45+
Patterns []RedactionPattern `json:"patterns,omitempty"`
46+
Presets []string `json:"presets,omitempty"`
47+
Template string `json:"template,omitempty"`
48+
}
49+
50+
// RedactionRegion specifies a coordinate-based redaction area.
51+
type RedactionRegion struct {
52+
Page int `json:"page"`
53+
X float64 `json:"x"`
54+
Y float64 `json:"y"`
55+
Width float64 `json:"width"`
56+
Height float64 `json:"height"`
57+
Color string `json:"color,omitempty"`
58+
}
59+
60+
// RedactionPattern specifies a text search pattern for redaction.
61+
type RedactionPattern struct {
62+
Pattern string `json:"pattern"`
63+
PatternType string `json:"pattern_type"` // "Literal" or "Regex"
64+
Page *int `json:"page,omitempty"`
65+
Color string `json:"color,omitempty"`
66+
}
67+
68+
// RasterizeOptions configures a rasterize request.
69+
type RasterizeOptions struct {
70+
DPI int `json:"dpi,omitempty"` // 72-300, default 150
71+
}
4472

4573
// JobResult is returned from GetJob.
4674
type JobResult struct {

‎packages/go-sdk/templates/templates.go‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -896,11 +896,6 @@ func (d *DocumentNode) SetCertification(config map[string]any) *DocumentNode {
896896
return d
897897
}
898898

899-
// SetSignature is deprecated. Use SetCertification instead.
900-
func (d *DocumentNode) SetSignature(config map[string]any) *DocumentNode {
901-
return d.SetCertification(config)
902-
}
903-
904899
func (d *DocumentNode) toDict() map[string]any {
905900
doc := map[string]any{
906901
"children": nodesToDicts(d.children),

0 commit comments

Comments
 (0)