Slide thumbnails (PNG/JPEG) from PowerPoint .pptx files via headless
LibreOffice — usable as a library, a CLI, or a tiny HTTP
microservice (Docker).
Why a microservice? LibreOffice adds hundreds of MB to an application image. Running it once, in its own small static container, keeps your app image slim and your CI fast — your app just POSTs the pptx and gets images back.
Pipeline: soffice --headless renders the pptx to a temporary PDF, then
poppler's pdftocairo rasterizes each page (aspect-ratio-preserving scaling
built in — no ImageMagick needed). Concurrent conversions are safe: every call
gets an isolated LibreOffice user profile, and the server queues requests
beyond a configurable concurrency limit.
docker build -t pptx-thumbnailer .
docker run --rm -p 3000:3000 pptx-thumbnailer
# or: docker compose up -dThe request body is the pptx file — no multipart forms.
# All slides as JSON (base64-encoded images)
curl --data-binary @deck.pptx 'http://localhost:3000/thumbnails?width=400'
# All slides as a zip of slide-<n>.png
curl --data-binary @deck.pptx -H 'Accept: application/zip' \
'http://localhost:3000/thumbnails?width=400' -o slides.zip
# One slide as a plain PNG
curl --data-binary @deck.pptx \
'http://localhost:3000/thumbnails?slide=1&as=image' -o slide-1.png
# Health (checks that soffice + pdftocairo are present)
curl http://localhost:3000/healthz| Query param | Default | Meaning |
|---|---|---|
width |
400 |
Target width in px (aspect ratio preserved) |
dpi |
– | Render at a fixed dpi instead of a fixed width |
slide |
– | Only convert slide n (1-based) |
format |
png |
png or jpeg |
as |
json |
json, zip or image (single slide) |
JSON response shape:
{
"slideCount": 12,
"format": "png",
"slides": [
{ "index": 1, "file": "slide-1.png", "data": "<base64>" }
]
}Errors: 400 bad request, 413 upload too large, 422 conversion failed,
503 (healthz) tools missing.
Faithful rendering (line breaks, text overflow) depends on the fonts a deck uses being available — LibreOffice silently substitutes missing ones, and a substitute with different glyph widths wraps text differently. The image ships metric-compatible replacements for the common Office fonts: Liberation (Arial/Times New Roman/Courier New) and Carlito/Caladea (Calibri/Cambria).
For customer-specific fonts (e.g. licensed corporate fonts, which cannot be
distributed with the image), mount them into the container — fontconfig scans
/usr/local/share/fonts automatically, no rebuild or extra configuration
needed:
docker run --rm -p 3000:3000 \
-v ./customer-fonts:/usr/local/share/fonts/custom:ro \
pptx-thumbnailer(or uncomment the volumes section in docker-compose.yml). Any
.ttf/.otf files in the mounted directory are picked up on the next
conversion. Alternatively, decks saved in PowerPoint with "Embed fonts in the
file" enabled render correctly without any server-side setup, since
LibreOffice uses the embedded fonts.
| Variable | Default | Meaning |
|---|---|---|
PORT / HOST |
3000 / 0.0.0.0 |
Listen address |
THUMBNAILER_CONCURRENCY |
1 |
Parallel LibreOffice conversions |
MAX_UPLOAD_MB |
100 |
Reject larger uploads (413) |
TIMEOUT_MS |
120000 |
Per-tool timeout |
SOFFICE_BIN |
soffice |
LibreOffice binary |
PDFTOCAIRO_BIN |
pdftocairo |
poppler binary |
npm install pptx-thumbnailerimport { ThumbnailerClient } from 'pptx-thumbnailer';
const client = new ThumbnailerClient('http://thumbnailer:3000');
// In-memory
const slides = await client.thumbnails('deck.pptx', { width: 400 });
// slides: [{ index: 1, file: 'slide-1.png', data: <Buffer> }, ...]
// Or straight to disk: writes slide-1.png, slide-2.png, ...
await client.thumbnailsToDir(buffer, './out/slides/42', { width: 400 });Requires libreoffice (impress) and poppler-utils on the host:
import { convertPptxToImages } from 'pptx-thumbnailer';
const slides = await convertPptxToImages('deck.pptx', { width: 400 });npx pptx-thumbnailer deck.pptx -o ./thumbs --width 400 # local conversion
npx pptx-thumbnailer serve --port 3000 # run the serviceMIT