LibPDF

Drawing

Draw text, shapes, and images on PDF pages.

Drawing

This guide covers drawing operations on PDF pages.

Coordinate System

PDF uses a coordinate system where:

  • Origin (0, 0) is at the bottom-left corner
  • X increases going right
  • Y increases going up
  • Units are points (1 inch = 72 points)
(0, 792) ─────────────── (612, 792)
    │                         │
    │     US Letter Page      │
    │                         │
(0, 0) ─────────────────  (612, 0)

Colors

Import the color helpers:

import { rgb, cmyk, grayscale } from "@libpdf/core";

// RGB (values 0-1)
const red = rgb(1, 0, 0);
const blue = rgb(0, 0, 1);
const custom = rgb(0.2, 0.4, 0.8);

// CMYK (values 0-1)
const processBlack = cmyk(0, 0, 0, 1);

// Grayscale (0 = black, 1 = white)
const gray = grayscale(0.5);
const black = grayscale(0);
const white = grayscale(1);

Draw Text

Basic Text

page.drawText("Hello, World!", {
  x: 50,
  y: 700,
  size: 24,
});

Text Options

page.drawText("Styled Text", {
  x: 50,
  y: 650,
  size: 16,
  color: rgb(0.2, 0.4, 0.8),
  font: customFont, // Embedded font
  lineHeight: 24, // Line height in points
  maxWidth: 200, // Word wrap width
  opacity: 0.8, // 0-1
});

Multi-line Text

page.drawText("Line 1\nLine 2\nLine 3", {
  x: 50,
  y: 600,
  size: 12,
  lineHeight: 18, // Line height in points
});

Word Wrap

const longText =
  "This is a very long paragraph that will automatically wrap to fit within the specified maximum width.";

page.drawText(longText, {
  x: 50,
  y: 550,
  size: 12,
  maxWidth: 200, // Wrap at 200 points
  lineHeight: 16, // Line height in points
});

Text with Custom Fonts

const fontBytes = await readFile("fonts/OpenSans-Regular.ttf");
const font = pdf.embedFont(fontBytes); // Synchronous

page.drawText("Custom Font", {
  x: 50,
  y: 500,
  size: 14,
  font,
});

Draw Shapes

Rectangle

// Filled rectangle
page.drawRectangle({
  x: 50,
  y: 400,
  width: 200,
  height: 100,
  color: rgb(0.9, 0.9, 0.9),
});

// Outlined rectangle
page.drawRectangle({
  x: 50,
  y: 280,
  width: 200,
  height: 100,
  borderColor: rgb(0, 0, 0),
  borderWidth: 2,
});

// Filled with border
page.drawRectangle({
  x: 300,
  y: 400,
  width: 200,
  height: 100,
  color: rgb(1, 0.9, 0.9),
  borderColor: rgb(0.8, 0, 0),
  borderWidth: 1,
});

Rounded Rectangle

page.drawRectangle({
  x: 50,
  y: 150,
  width: 200,
  height: 100,
  color: rgb(0.9, 0.95, 1),
  borderColor: rgb(0.3, 0.5, 0.8),
  borderWidth: 1,
  cornerRadius: 10,
});

Circle

page.drawCircle({
  x: 150, // Center X
  y: 500, // Center Y
  radius: 50,
  color: rgb(0.8, 0.9, 1),
  borderColor: rgb(0, 0.3, 0.6),
  borderWidth: 2,
});

Ellipse

page.drawEllipse({
  x: 150, // Center X
  y: 400, // Center Y
  xRadius: 80,
  yRadius: 40,
  color: rgb(1, 0.9, 0.8),
  borderColor: rgb(0.6, 0.3, 0),
  borderWidth: 1,
});

Line

page.drawLine({
  start: { x: 50, y: 300 },
  end: { x: 250, y: 350 },
  color: rgb(0, 0, 0),
  thickness: 1,
});

// Dashed line
page.drawLine({
  start: { x: 50, y: 280 },
  end: { x: 250, y: 280 },
  color: rgb(0.5, 0.5, 0.5),
  thickness: 1,
  dashArray: [5, 3], // 5pt dash, 3pt gap
});

Draw Images

Embed and Draw

// JPEG
const jpegBytes = await readFile("photo.jpg");
const jpegImage = pdf.embedJpeg(jpegBytes);

page.drawImage(jpegImage, {
  x: 50,
  y: 100,
  width: 200,
  height: 150,
});

// PNG (with transparency support)
const pngBytes = await readFile("logo.png");
const pngImage = pdf.embedPng(pngBytes);

page.drawImage(pngImage, {
  x: 300,
  y: 100,
  width: 100,
  height: 100,
});

Preserve Aspect Ratio

// Specify only width - height calculated automatically
page.drawImage(image, {
  x: 50,
  y: 100,
  width: 200,
});

// Or only height
page.drawImage(image, {
  x: 50,
  y: 100,
  height: 150,
});

Image Opacity

page.drawImage(image, {
  x: 50,
  y: 100,
  width: 200,
  height: 150,
  opacity: 0.5,
});

Custom Paths

For complex shapes, use the path builder:

// Triangle using the fluent path API
page
  .drawPath()
  .moveTo(100, 100)
  .lineTo(150, 200)
  .lineTo(50, 200)
  .close()
  .fillAndStroke({
    color: rgb(0.8, 0.2, 0.2),
    borderColor: rgb(0.4, 0, 0),
    borderWidth: 2,
  });

Curves

// Quadratic curve (converted to cubic internally)
page
  .drawPath()
  .moveTo(50, 300)
  .quadraticCurveTo(100, 400, 150, 300) // Control point, end point
  .stroke({ borderColor: rgb(0, 0, 0), borderWidth: 2 });

// Cubic bezier curve
page
  .drawPath()
  .moveTo(200, 300)
  .curveTo(225, 200, 275, 400, 300, 300) // Two control points, end point
  .stroke({ borderColor: rgb(0, 0, 0), borderWidth: 2 });

SVG Paths

Draw vector graphics using SVG path syntax. This is useful for icons, logos, and importing graphics from SVG files.

Basic Usage

Use drawSvgPath() to render SVG path data:

// Simple triangle (filled by default)
page.drawSvgPath("M 0 0 L 60 0 L 30 50 Z", {
  x: 50,
  y: 700,
  color: rgb(1, 0, 0),
});

// Stroked path
page.drawSvgPath("M 0 0 C 20 40 60 40 80 0", {
  x: 150,
  y: 700,
  borderColor: rgb(0, 0, 1),
  borderWidth: 2,
});

Coordinate System

SVG uses a Y-down coordinate system (origin at top-left), while PDF uses Y-up (origin at bottom-left). drawSvgPath() automatically transforms coordinates so SVG paths render correctly.

The x and y options position the path's origin point on the page.

Scaling

Scale paths up or down with the scale option:

// Original size (24x24 icon)
const heartIcon = "M 12 4 C 12 4 8 0 4 4 C 0 8 0 12 12 22 ...";

page.drawSvgPath(heartIcon, { x: 50, y: 700, color: rgb(1, 0, 0) });

// Double size
page.drawSvgPath(heartIcon, { x: 100, y: 700, scale: 2, color: rgb(1, 0, 0) });

// Triple size
page.drawSvgPath(heartIcon, { x: 180, y: 700, scale: 3, color: rgb(1, 0, 0) });

Supported Commands

All SVG path commands are supported:

CommandDescriptionExample
M/mMove toM 10 20
L/lLine toL 100 200
H/hHorizontal lineH 150
V/vVertical lineV 100
C/cCubic bezierC 10 20 30 40 50 60
S/sSmooth cubicS 30 40 50 60
Q/qQuadratic bezierQ 50 100 100 0
T/tSmooth quadraticT 150 0
A/aElliptical arcA 50 50 0 0 1 100 0
Z/zClose pathZ

Uppercase commands use absolute coordinates; lowercase use relative coordinates.

Even-Odd Fill

For paths with holes (like donuts or frames), use the even-odd winding rule:

// Outer square with inner square hole
page.drawSvgPath("M 0 0 L 80 0 L 80 80 L 0 80 Z M 20 20 L 60 20 L 60 60 L 20 60 Z", {
  x: 50,
  y: 600,
  color: rgb(0, 0, 1),
  windingRule: "evenodd",
});

Using with PathBuilder

For more control, use appendSvgPath() on a PathBuilder:

page
  .drawPath()
  .moveTo(50, 500)
  .lineTo(100, 500)
  .appendSvgPath("l 30 30 l 30 -30", { flipY: false }) // Continue with SVG
  .lineTo(200, 500)
  .stroke({ borderColor: rgb(0, 0, 0), borderWidth: 2 });

The flipY: false option keeps SVG in PDF coordinates when mixing with PathBuilder.

Drawing Icons

Extract the d attribute from any SVG icon and use it directly:

// From an SVG file: <path d="M12 2l3.09 6.26L22 9.27..." />
const starPath = "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77...";

// Stroked icon (like Lucide/Feather)
page.drawSvgPath(starPath, {
  x: 50,
  y: 600,
  scale: 2,
  borderColor: rgb(0.9, 0.7, 0.1),
  borderWidth: 2,
});

// Filled icon (like Simple Icons)
const githubPath = "M12 .297c-6.63 0-12 5.373-12 12...";

page.drawSvgPath(githubPath, {
  x: 150,
  y: 600,
  scale: 2,
  color: rgb(0.1, 0.1, 0.1),
});

Options Reference

OptionTypeDescription
xnumberX position on page
ynumberY position on page
scalenumberScale factor (default: 1)
colorColorFill color
borderColorColorStroke color
borderWidthnumberStroke width in points
windingRule"nonzero" | "evenodd"Fill rule for overlapping paths
opacitynumberOpacity (0-1)

Drawing Order

Content is drawn in order - later drawings appear on top:

// Background first
page.drawRectangle({
  x: 0,
  y: 0,
  width: 612,
  height: 792,
  color: rgb(0.95, 0.95, 0.95),
});

// Then content
page.drawText("On top of background", {
  x: 50,
  y: 700,
  size: 16,
});

Draw on Existing Pages

const pdf = await PDF.load(existingBytes);
const page = pdf.getPage(0);

// Add watermark
page.drawText("CONFIDENTIAL", {
  x: 200,
  y: 400,
  size: 60,
  color: rgb(1, 0, 0),
  opacity: 0.3,
});

const bytes = await pdf.save();

Transformations

Rotation

Rotate content using the rotate option:

import { degrees } from "@libpdf/core";

// Rotate text around its position
page.drawText("Rotated Text", {
  x: 280,
  y: 400,
  size: 14,
  rotate: degrees(45), // 45 degrees counter-clockwise
});

Complete Example

import { writeFile } from "fs/promises";
import { PDF, rgb } from "@libpdf/core";

const pdf = PDF.create();
const page = pdf.addPage({ size: "a4" });
const { width, height } = page;

// Background
page.drawRectangle({
  x: 0,
  y: 0,
  width,
  height,
  color: rgb(0.98, 0.98, 1),
});

// Header bar
page.drawRectangle({
  x: 0,
  y: height - 60,
  width,
  height: 60,
  color: rgb(0.2, 0.4, 0.8),
});

page.drawText("Document Title", {
  x: 50,
  y: height - 40,
  size: 24,
  color: rgb(1, 1, 1),
});

// Content
page.drawText("Section 1", {
  x: 50,
  y: height - 120,
  size: 18,
  color: rgb(0.2, 0.2, 0.2),
});

page.drawLine({
  start: { x: 50, y: height - 130 },
  end: { x: width - 50, y: height - 130 },
  color: rgb(0.8, 0.8, 0.8),
  thickness: 1,
});

page.drawText("Lorem ipsum dolor sit amet, consectetur adipiscing elit.", {
  x: 50,
  y: height - 160,
  size: 12,
  maxWidth: width - 100,
  lineHeight: 18,
});

await writeFile("styled-document.pdf", await pdf.save());

Pattern Fills

Shape drawing methods support pattern fills in addition to solid colors:

// Create a gradient pattern
const gradient = pdf.createLinearGradient({
  angle: 90,
  length: 100,
  stops: [
    { offset: 0, color: rgb(1, 0, 0) },
    { offset: 1, color: rgb(0, 0, 1) },
  ],
});
const pattern = pdf.createShadingPattern({ shading: gradient });

// Use with drawRectangle, drawCircle, drawEllipse, or drawPath
page.drawRectangle({
  x: 50,
  y: 500,
  width: 200,
  height: 100,
  pattern,
});

// Also works with PathBuilder
page.drawPath().circle(300, 550, 50).fill({ pattern });

See Low-Level Drawing for gradients, tiling patterns, and more.

Next Steps

On this page