Generate PDF Thumbnails for Document Previews with Go
Every document management system needs thumbnails. Users expect to see a visual preview before clicking on a file, whether it's a contract, invoice, or report. Without thumbnails, you're stuck showing generic PDF icons.
Generating PDF thumbnails locally requires heavy libraries like ImageMagick or Poppler. These add deployment complexity and consume server resources. For a Go backend, there's no clean native solution.
The simpler approach: convert PDF pages to images via API. Send the PDF URL, get back image URLs. No dependencies, no memory spikes, just clean HTTP calls.
Quick Example
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
const (
apiToken = "YOUR_API_TOKEN"
apiURL = "https://apdf.io/api/pdf/file/to-image"
statusURL = "https://apdf.io/api/job/status/check"
)
// Poll until the async job finishes, then return its result payload as raw JSON.
func waitForJob(jobID string) (json.RawMessage, error) {
payload, _ := json.Marshal(map[string]string{"id": jobID})
client := &http.Client{Timeout: 30 * time.Second}
for i := 0; i < 1200; i++ {
req, _ := http.NewRequest("POST", statusURL, bytes.NewBuffer(payload))
req.Header.Set("Authorization", "Bearer "+apiToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
var status struct {
Status string `json:"status"`
Result json.RawMessage `json:"result"`
Error string `json:"error"`
}
json.NewDecoder(resp.Body).Decode(&status)
resp.Body.Close()
if status.Status == "successful" {
return status.Result, nil
}
if status.Status == "failed" {
return nil, fmt.Errorf("job failed: %s", status.Error)
}
time.Sleep(2 * time.Second)
}
return nil, fmt.Errorf("job did not finish in time")
}
func main() {
payload := map[string]interface{}{
"file": "https://pdfobject.com/pdf/sample.pdf",
"image_type": "jpeg",
"pages": "1",
"dpi": 150,
}
jsonData, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
req.Header.Set("Authorization", "Bearer "+apiToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
var initial struct {
JobID string `json:"job_id"`
}
json.NewDecoder(resp.Body).Decode(&initial)
raw, err := waitForJob(initial.JobID)
if err != nil {
fmt.Println("Error:", err)
return
}
var result []map[string]interface{}
json.Unmarshal(raw, &result)
fmt.Printf("Thumbnail URL: %s\n", result[0]["file"])
}
The API returns an array of images, one per page. For a thumbnail, we only request page 1.
Real-World Scenario: Document Library API
You're building a document library for a SaaS app. When users upload PDFs, you need to generate thumbnails to display in the file browser. Here's a complete Go service that handles this:
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
const (
apiToken = "YOUR_API_TOKEN"
apiURL = "https://apdf.io/api/pdf/file/to-image"
statusURL = "https://apdf.io/api/job/status/check"
)
type ThumbnailResult struct {
Page string `json:"page"`
File string `json:"file"`
Expiration string `json:"expiration"`
}
type ThumbnailRequest struct {
DocumentID string
PDFURL string
}
// Poll until the async job finishes, then return its result payload as raw JSON.
func waitForJob(jobID string) (json.RawMessage, error) {
payload, _ := json.Marshal(map[string]string{"id": jobID})
client := &http.Client{Timeout: 30 * time.Second}
for i := 0; i < 1200; i++ {
req, _ := http.NewRequest("POST", statusURL, bytes.NewBuffer(payload))
req.Header.Set("Authorization", "Bearer "+apiToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
var status struct {
Status string `json:"status"`
Result json.RawMessage `json:"result"`
Error string `json:"error"`
}
json.NewDecoder(resp.Body).Decode(&status)
resp.Body.Close()
if status.Status == "successful" {
return status.Result, nil
}
if status.Status == "failed" {
return nil, fmt.Errorf("job failed: %s", status.Error)
}
time.Sleep(2 * time.Second)
}
return nil, fmt.Errorf("job did not finish in time")
}
func generateThumbnail(pdfURL string) (*ThumbnailResult, error) {
payload := map[string]interface{}{
"file": pdfURL,
"image_type": "jpeg",
"pages": "1",
"dpi": 100, // Lower DPI for faster thumbnails
}
jsonData, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
req.Header.Set("Authorization", "Bearer "+apiToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var initial struct {
JobID string `json:"job_id"`
}
if err := json.NewDecoder(resp.Body).Decode(&initial); err != nil {
return nil, err
}
raw, err := waitForJob(initial.JobID)
if err != nil {
return nil, err
}
var results []ThumbnailResult
if err := json.Unmarshal(raw, &results); err != nil {
return nil, err
}
if len(results) == 0 {
return nil, fmt.Errorf("no thumbnail generated")
}
return &results[0], nil
}
func main() {
// Documents that need thumbnails
documents := []ThumbnailRequest{
{DocumentID: "doc-001", PDFURL: "https://pdfobject.com/pdf/sample.pdf"},
{DocumentID: "doc-002", PDFURL: "https://ontheline.trincoll.edu/images/bookdown/sample-local-pdf.pdf"},
}
for _, doc := range documents {
fmt.Printf("Generating thumbnail for %s...\n", doc.DocumentID)
result, err := generateThumbnail(doc.PDFURL)
if err != nil {
fmt.Printf(" Error: %v\n", err)
continue
}
fmt.Printf(" Thumbnail: %s\n", result.File)
// In production: download and store this image in your storage
// The URL expires after 1 hour
}
}
Note: The thumbnail URLs are valid for 1 hour. Download and store them in your own storage (S3, local disk) for permanent use.
Choosing the Right Settings
The API offers several parameters to tune your thumbnails:
- image_type: Use
jpegfor photos/scans (smaller files),pngfor sharp text/diagrams - dpi: 72-100 for small thumbnails, 150 for medium previews, 300 for full-quality
- pages: Use
1for a single thumbnail, or1-3for a multi-page preview
// High-quality preview (e.g., document viewer)
payload := map[string]interface{}{
"file": pdfURL,
"image_type": "png",
"dpi": 200,
"pages": "1",
}
// Fast, small thumbnail (e.g., file browser grid)
payload := map[string]interface{}{
"file": pdfURL,
"image_type": "jpeg",
"dpi": 72,
"pages": "1",
}
// Multi-page preview carousel
payload := map[string]interface{}{
"file": pdfURL,
"image_type": "jpeg",
"dpi": 150,
"pages": "1-5",
}
Next Steps
Once you have thumbnails working, consider these enhancements:
- Extract first page: Use the Extract Pages endpoint to create a single-page preview PDF.
- Compress before converting: For large PDFs, use the Compress endpoint first to speed up thumbnail generation.
Related tutorials
How to Send "Embedded" PDF Invoice Previews in Emails (using Images)
Learn how to embed PDF invoices directly in HTML emails by converting the first page to an image. Since email clients don't support embedded PDFs, this technique uses the Apdf API to create visual invoice previews that increase engagement and trust with your customers.
Combine Monthly Reports into Quarterly Summaries with Go
Automate report compilation by merging multiple PDF files into quarterly or annual summaries using Go. Learn how to create cover pages, skip existing covers, and build professional combined reports automatically.
Generate PDF Event Tickets with QR Codes in A6 Format using Go
Learn how to generate A6-sized event tickets with embedded QR codes using Go. This tutorial focuses on small-format PDFs (tickets, badges, labels) using CSS print media and the Apdf API. Perfect for event management systems, concerts, and conferences where scanning and compact printing are essential.
Your code made the PDF.
Then it went dark.
Opened, read, re-read, dropped on page 4 — you never see any of it. Share the PDFs you generate through Apdf recipient links, and every signal becomes something you can act on: ping Slack, update the CRM, let an agent follow up. Same account, same API token, one more call.
API · Webhook · MCP