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.