Skip to content
Lamina PDF
Unreleased

These docs describe changes that aren’t released yet. For the newest release, see the released docs.

Shapes, colour and images

You’ll draw lines, shapes and images on a page, in colour and with transparency. You need this for anything visual in a PDF: a logo, a chart, the rules of a table or a photo.

Shapes and images need only the base module (init()).

surface.path(p => …) returns the shape unpainted. Paint it with fill, stroke or fillStroke, or call clip to limit what comes after to its inside. A value that makes no shape, such as NaN or a negative line width, fails page.finish() with invalid-geometry.

A colour is gray [g], RGB [r, g, b], CMYK [c, m, y, k] or hex. Opacity and blend modes set how a drawing mixes with what’s already on the page: in the example, multiply darkens where a shape overlaps what’s below it.

  • Image.jpeg(bytes) embeds a JPEG as it is, without decoding it.
  • Image.png(bytes) keeps the PNG’s transparency.
  • Image.rgba(width, height, pixels) takes raw pixels.

A Matrix rotates, scales or moves what you draw. It works in page space, like every other position.

This example draws rectangles, circles, curves and dashed strokes in several colours, with transparency, a clip and a rotation, and places a JPEG and a transparent image:

packages/sdk/examples/images-and-vectors.ts
// Images and vector graphics on one A4 page. It draws shapes and a dashed Bézier curve in gray,
// RGB, CMYK and hex colour, overlaps two translucent discs with a blend mode, clips a JPEG to an
// ellipse and rotates an RGBA image with a soft mask. Needs only the base module.
import { Builder, type BuilderOptions, Image, Matrix, type Surface } from "@laminapdf/sdk";
/** The RGBA gradient's side, in pixels. */
const GLOW_SIDE = 16;
/**
* The document's bytes, with `photo` (a JPEG) on the page.
*
* @param options - can fix the file ID, the producer and the creation date.
*/
export function imagesAndVectors(photo: Uint8Array, options: BuilderOptions = {}): Uint8Array<ArrayBuffer> {
using b = new Builder(options);
const jpeg = Image.jpeg(photo);
const glow = Image.rgba(GLOW_SIDE, GLOW_SIDE, gradient());
const page = b.page("A4");
const s = page.surface();
shapes(s);
overlappingDiscs(s);
images(s, jpeg, glow);
page.finish();
using doc = b.finish();
return doc.save();
}
/** A square gradient whose alpha falls off towards the right. */
function gradient(): Uint8Array {
const out = new Uint8Array(GLOW_SIDE * GLOW_SIDE * 4);
for (let y = 0; y < GLOW_SIDE; y++) {
for (let x = 0; x < GLOW_SIDE; x++) out.set([x * 16, y * 16, 200, 255 - x * 15], (y * GLOW_SIDE + x) * 4);
}
return out;
}
/** A rectangle, a circle and a dashed, round-capped Bézier curve. */
function shapes(s: Surface): void {
s.fillColor([0.9, 0.3, 0.2])
.path((p) => p.rect([56, 56, 160, 100]))
.fill();
s.fillColor([0.8, 0.1, 0, 0])
.path((p) => p.circle([330, 106], 50))
.fill();
s.strokeColor("#2a9d8f")
.path((p) => p.moveTo(56, 220).cubicTo(180, 140, 300, 300, 539, 200))
.stroke({ width: 3, cap: "round", dash: [12, 8] });
}
/** Two translucent discs, the second multiplied onto the first. */
function overlappingDiscs(s: Surface): void {
s.save().opacity(0.6, 1).fillColor([0.3]);
s.path((p) => p.circle([120, 330], 60)).fill();
s.blendMode("multiply").fillColor([1, 0.8, 0]);
s.path((p) => p.circle([190, 330], 60))
.fill()
.restore();
}
/** The photo clipped to an ellipse, and the gradient rotated by 30 degrees. */
function images(s: Surface, photo: Image, glow: Image): void {
s.save()
.path((p) => p.ellipse([300, 270, 239, 120]))
.clip();
s.image(photo, [300, 250, 239, 160]).restore();
s.save().transform(Matrix.after(Matrix.translate(150, 520), Matrix.rotate(30)));
s.image(glow, [-60, -60, 120, 120]).restore();
}

See the output.

Next: links, outlines and metadata.