Zum Inhalt springen

Guide · Go

PDFs zusammenfügen und mit Passwort schützen mit Go

Rechnung, AGB und Datenblatt sollen den Kunden als eine Datei erreichen, und sie soll nicht für jeden lesbar sein, der sie findet. Dieses Go-Programm verbindet bereits gerenderte Dokumente und verschlüsselt das Ergebnis, in zwei Aufrufen und ohne PDF-Bibliothek.

Kostenloser Plan mit 100 Einheiten im Monat, keine Kreditkarte

Was Sie brauchen

  • Einen API-Key. Legen Sie ihn in der App unter API-Schlüssel an und exportieren Sie ihn als FORMFEED_API_KEY. Zum Ausprobieren ist ein Test-Key (ff_test_…) richtig. Siehe Quickstart.
  • Go. Sie brauchen Go 1.21 oder neuer; das Programm nutzt nur die Standardbibliothek.
  • Die Quellen. sources.json neben dem Programm: ein JSON-Array dessen, was in das Dokument kommt, in dieser Reihenfolge. Eine Render-ID (rnd_…) oder der Name eines PDFs in der Workspace-Bibliothek, zwei bis fünfzig davon.
  • Ein Passwort. In FORMFEED_PDF_PASSWORD. Erzeugen Sie eines je Dokument und senden Sie es über einen anderen Kanal als die Datei.
  • Einen Tarif mit PDF-Tools. Die Tools gehören zu Starter und darüber.

Das Programm

Das Programm liest die Quellen, fügt sie zu einem PDF zusammen, verschlüsselt das Ergebnis mit dem Passwort und speichert die fertige Datei.

main.goGo
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
)

type render struct {
	ID          string  `json:"id"`
	DownloadURL string  `json:"download_url"`
	PageCount   int     `json:"page_count"`
	Units       float64 `json:"units"`
	Title       string  `json:"title"`
	Detail      string  `json:"detail"`
}

func api(path string, body any) render {
	encoded, _ := json.Marshal(body)
	request, _ := http.NewRequest("POST", "https://api-eu.formfeed.dev/v1"+path, bytes.NewReader(encoded))
	request.Header.Set("Authorization", "Bearer "+os.Getenv("FORMFEED_API_KEY"))
	request.Header.Set("Content-Type", "application/json")
	response, err := http.DefaultClient.Do(request)
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()

	var answer render
	json.NewDecoder(response.Body).Decode(&answer)
	if response.StatusCode >= 400 {
		fmt.Fprintf(os.Stderr, "%s: %s\n", answer.Title, answer.Detail)
		os.Exit(1)
	}
	return answer
}

func main() {
	file, err := os.ReadFile("sources.json")
	if err != nil {
		panic(err)
	}
	var sources []string
	json.Unmarshal(file, &sources)

	// 1. join the documents, in the order they are given
	merged := api("/pdf/merge", map[string]any{"sources": sources, "filename": "bundle.pdf"})

	// 2. encrypt the result; printing stays allowed, copying and editing do not
	protected := api("/pdf/protect", map[string]any{
		"source":        merged.ID,
		"user_password": os.Getenv("FORMFEED_PDF_PASSWORD"),
		"permissions":   []string{"print"},
	})

	answer, err := http.Get(protected.DownloadURL)
	if err != nil {
		panic(err)
	}
	defer answer.Body.Close()
	out, _ := os.Create("bundle.pdf")
	defer out.Close()
	io.Copy(out, answer.Body)
	fmt.Printf("%s %d page(s), %v unit(s)\n", protected.ID, protected.PageCount, protected.Units)
}

Ausführen

Es gibt die ID des Renderings, die Seitenzahl und die Einheiten aus, und bundle.pdf liegt neben dem Programm. Beim Öffnen fragt es nach dem Passwort; Drucken geht, Text kopieren nicht.

Shell
export FORMFEED_API_KEY=ff_test_...
export FORMFEED_PDF_PASSWORD=$(openssl rand -base64 18)
go run main.go

Was an Go besonders ist

  • json.RawMessage. Die Beispieldaten gehen so in den Body, wie sie auf der Platte liegen, ohne vorher in Go-Typen dekodiert zu werden; das Programm funktioniert deshalb für jede Vorlage.
  • Ein Struct für beide Antworten. Ein Rendering und ein Problem-Dokument sind beide JSON-Objekte, ein einziges Struct mit title und detail neben den Render-Feldern dekodiert also beides.
  • Timeouts. http.DefaultClient hat keinen. Geben Sie dem Client in einem Dienst einen Timeout etwas über 60 Sekunden: Ein synchrones Rendering antwortet, wenn die Datei fertig ist, oder nach 60 Sekunden mit 202, während es im Hintergrund weiterläuft.

Warum zwei Aufrufe

  • Die Reihenfolge zählt. Ein verschlüsseltes PDF lässt sich nicht mehr ändern, das Passwort kommt also zuletzt. Ein bereits geschütztes Dokument zusammenzufügen scheitert.
  • Jeder Schritt ist ein Rendering. Zusammenfügen und Schützen erzeugen je ein eigenes Rendering mit ID, Download-URL und Zeile im Render-Log; das zweite nimmt die ID des ersten als Quelle.
  • Je eine halbe Einheit. Ein Tool kostet mit einem Live-Schlüssel 0,5 Einheiten, unabhängig von der Seitenzahl. Die Renderings, die die Quellen erzeugt haben, wurden gezählt, als sie liefen.
  • Oder in einer Anfrage. Ein Rendering kann dieselben Schritte als post mitbringen und alles in einem Aufruf erledigen. Nehmen Sie die Tools, wenn die Dokumente schon da sind, und post, wenn Sie ohnehin rendern. Siehe PDF-Tools.

Was die Berechtigungen tun

Alles, was nicht in permissions steht, ist verboten (fehlt die Liste, sind alle vier erlaubt: print, copy, modify und annotate): Mit ["print"] darf gedruckt, aber kein Text kopiert, bearbeitet oder kommentiert werden. Ohne user_password kann jeder das Dokument öffnen, und die Berechtigungen gelten trotzdem; das ist die richtige Einstellung für ein Dokument, das öffentlich ist, aber nicht bearbeitet werden soll. Ein owner_password hebt die Beschränkungen auf; ohne Angabe wird ein zufälliges verwendet. PDF-Viewer beachten die Berechtigungen, sie schützen aber nicht vor jemandem, der die Datei öffnen kann: Vertraulich macht sie erst das user_password.

Wenn die API nein sagt

  • Eine Form für jeden Fehler. Fehler sind Problem-Dokumente (RFC 9457) mit type, title, status, detail, einem stabilen code und der request_id, die Sie dem Support nennen; die type-URL führt zu einer Seite über diesen Fehler. Die Liste steht in der Fehlerreferenz.
  • Die üblichen. 401 unauthorized bei fehlendem oder widerrufenem Schlüssel, 404 template_not_found bei falschem Vorlagennamen, 422 template_runtime_error, wenn die Vorlage an den Daten scheitert, 429 rate_limited über den Anfragen pro Sekunde des Tarifs, 402 quota_exceeded, wenn die Einheiten des Free-Tarifs verbraucht sind (ein bezahlter Tarif rechnet weitere Einheiten ab, bis zu seiner Obergrenze).
  • Sicher wiederholen. Senden Sie beim POST einen Idempotency-Key-Header und wiederholen Sie die Anfrage mit demselben Schlüssel, wenn Sie nicht wissen, ob der erste Versuch ankam: 24 Stunden lang liefert der Schlüssel die erste Antwort, statt erneut zu rendern und zu berechnen. Gespeichert wird die Antwort, sobald der erste Versuch beantwortet ist; wiederholen Sie also nicht, solange er noch laufen kann. Auch Probleme unter 500 werden gespeichert, eine korrigierte Anfrage braucht deshalb einen neuen Schlüssel.

Die Aufgabe um dieses Programm

Dieser Guide ist ein Programm. Was die Aufgabe darum herum braucht, von der Vorlage bis zu Limits und Preis, steht auf der Seite Verträge und Vereinbarungen als PDF.

Weitere Guides für Go

FAQ

Woher kommen die Render-IDs?
Aus den Renderings, die Sie vorher gemacht haben: die id jeder Antwort. Sie müssen zum selben Workspace gehören, und ihre Ausgabe muss noch existieren. Dateien, die Sie in die Bibliothek geladen haben, nennen Sie stattdessen beim Namen.
Welche Verschlüsselung wird verwendet?
AES-256, ausgeführt von qpdf. Das Passwort erscheint nie im Render-Log und wird nicht gespeichert: Geht es verloren, lässt sich das Dokument nicht mehr öffnen.
Kann ich auch ein Wasserzeichen setzen?
Ja, /pdf/watermark ist das dritte Tool und nimmt einen Text oder ein Bild. Hängen Sie es vor das Passwort, wie das Zusammenfügen. Siehe PDF-Tools.

Zusammenfügen und schützen in anderen Sprachen

Heute mit dem Rendern beginnen

Der kostenlose Plan enthält 100 Einheiten im Monat und kostenlose Testrenderings. Wechseln Sie den Plan, wenn Sie mehr brauchen.