Turn any photo into a perforated postage-stamp PNG, from the command line.
Stampy is a TypeScript port of the original Python stampy tool. It takes
an arbitrary input image, fits it into a stamp-shaped canvas, applies an
optional photo effect, draws a themed border with a caption and
denomination, and punches perforation holes around the edge so the result
looks like a real postage stamp.
npm install -g @ishk9/stampy
# or, run without installing:
npx @ishk9/stampystampy # interactive wizard
stampy create photo.jpg # one-shotIf -o is omitted the output is written next to the source as
<input>-stamp.png.
stampy [--verbose]
create INPUT [-o OUTPUT] [-c "CAPTION"] [-d "$1"]
[-t THEME] [-e EFFECT]
[-W INNER_W] [-H INNER_H] [-b BORDER]
[--no-perforations] [--perf-radius N] [--perf-spacing N]
themes
effects
wizard
stampystampy create cat.jpgstampy create cat.jpg -o cat-stamp.png -c "POSTES * PARIS" -d "$1"stampy create cat.jpg -t midnight -e vintage --perf-spacing 38stampy create cat.jpg --no-perforations -W 1200 -H 1200| Key | Label | Border | Background |
|---|---|---|---|
classic |
Classic Red | rgb(180, 30, 50) | rgb(255, 252, 240) |
midnight |
Midnight Blue | rgb(25, 55, 120) | rgb(245, 248, 255) |
forest |
Forest Green | rgb(35, 95, 60) | rgb(250, 252, 245) |
ink |
Ink Black | rgb(30, 30, 30) | rgb(252, 250, 245) |
burgundy |
Burgundy Gold | rgb(110, 25, 50) | rgb(252, 244, 220) |
Run stampy themes to print this table from the live registry.
| Key | Description |
|---|---|
none |
Pass-through; the photo's tones are preserved as-is. |
sepia |
Warm, brown-toned colorisation. |
grayscale |
Luminance-only conversion. |
vintage |
Faded, low-contrast colorisation with a warm cast. |
Run stampy effects to print this table.
src/
├── cli.ts # CLI entrypoint (commander)
├── cli/commands/ # `create`, `themes`, `effects`, `wizard`
├── application/
│ ├── makeStamp.ts # MakeStampUseCase
│ └── pipelineBuilder.ts # StampPipelineBuilder
├── domain/
│ ├── models.ts # Effect, StampSpec, makeStampSpec
│ ├── themes.ts # ThemeRegistry, defaultThemeRegistry
│ └── errors.ts # StampyError hierarchy
├── effects/
│ ├── base.ts # EffectStrategy interface
│ ├── strategies.ts # None / Sepia / Grayscale / Vintage
│ └── registry.ts # EffectRegistry, defaultEffectRegistry
├── pipeline/
│ ├── base.ts # StampImage, StampProcessor, Pipeline
│ ├── orientation.ts # EXIF normalisation hook
│ ├── effect.ts # Effect step (delegates to strategy)
│ ├── fit.ts # Cover-fit into innerSize
│ ├── frame.ts # Coloured border + concentric strokes
│ ├── caption.ts # Top caption text
│ ├── denomination.ts # Bottom-corner price chip
│ └── perforation.ts # Punch transparent holes
├── rendering/
│ ├── fonts.ts # FontProvider, SystemFontProvider
│ └── canvas.ts # @napi-rs/canvas helpers
└── infrastructure/
└── imageRepository.ts # SharpImageRepository (load / save)
- Strategy —
EffectStrategylets each photo effect (NoneEffect,SepiaEffect,GrayscaleEffect,VintageEffect) implement one tonal transform with a single-method interface. - Registry —
EffectRegistryandThemeRegistryare the only places that know about all available strategies and themes; consumers ask by key. - Pipes-and-Filters — the rendering pipeline is a sequence of
StampProcessorsteps that passStampImagefrom one to the next. - Composite —
Pipeline implements StampProcessor, so a whole pipeline is interchangeable with a single step. - Builder —
StampPipelineBuilderhides the construction order and dependencies of the default pipeline. - Repository —
ImageRepositoryabstracts I/O; onlySharpImageRepositoryknows aboutsharp. - Use Case —
MakeStampUseCaseis the single application entrypoint and depends only on abstractions. - Command — each CLI subcommand is its own module under
cli/commands/. - Dependency Injection — the CLI wires the registries, font provider, repository, and pipeline at the composition root and passes them down through constructors.
| Principle | Where it shows up |
|---|---|
| SRP | Each pipeline step does one thing (FitProcessor resizes, FrameProcessor draws the border, etc.). |
| OCP | Adding a new effect is a registry entry plus a strategy class — no other code changes. |
| LSP | All StampProcessor implementations are interchangeable; Pipeline is one. |
| ISP | Narrow interfaces (EffectStrategy.apply, FontProvider.fontSpec, ImageRepository.load/save). |
| DIP | MakeStampUseCase depends on ImageRepository and StampProcessor interfaces, not on sharp or @napi-rs/canvas. |
import {
defaultEffectRegistry,
defaultThemeRegistry,
Effect,
makeStampSpec,
MakeStampUseCase,
SharpImageRepository,
StampPipelineBuilder,
SystemFontProvider,
} from "@ishk9/stampy";
const themes = defaultThemeRegistry();
const effects = defaultEffectRegistry();
const fonts = new SystemFontProvider();
const repository = new SharpImageRepository();
const pipeline = new StampPipelineBuilder(effects, fonts).build();
const useCase = new MakeStampUseCase(repository, pipeline);
const spec = makeStampSpec({
theme: themes.get("classic"),
caption: "POSTES",
denomination: "$1",
effect: Effect.SEPIA,
});
const result = await useCase.execute("cat.jpg", "cat-stamp.png", spec);
console.log(`wrote ${result.outputPath} (${result.width} x ${result.height})`);A runnable version of this snippet lives at
examples/library-use.ts.
npm install
npm run dev -- create cat.jpg # via tsx
npm run build && npm start -- create cat.jpg
npm testThe test suite is vitest plus tsx-style ESM resolution, so local
imports keep their .js extensions even though the sources are .ts.