Awesome Go

picoloom

CategoryText Processing
SubcategoryMarkup Languages
Stars86

Markdown-to-PDF converter with CLI and Go library APIs

About picoloom

This large README is shown as a plain-text preview. Read the full README at the source

# Picoloom

[![Go Reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/alnah/picoloom/v2)
[![Go Report Card](https://img.shields.io/badge/go%20report-A+-brightgreen)](https://goreportcard.com/report/github.com/alnah/picoloom/v2)
[![Build Status](https://img.shields.io/github/actions/workflow/status/alnah/picoloom/ci.yml?branch=main)](https://github.com/alnah/picoloom/actions)
[![Coverage](https://img.shields.io/codecov/c/github/alnah/picoloom)](https://codecov.io/gh/alnah/picoloom)
[![License](https://img.shields.io/badge/License-BSD--3--Clause-blue.svg)](LICENSE)

> If you need something between the complexity of Pandoc and the speed of Markdown-to-PDF converters with limited styling options, Picoloom might be a good fit. It is a small, opinionated Go library and CLI for converting Markdown to PDF that I created to share teaching materials with my French students. I designed it to be easy enough, fast enough, and polished enough for that purpose. It supports cover pages, tables of contents, watermarks, signatures, and more. You can also use CSS themes and custom assets. It supports parallel batch processing. Under the hood, it uses Chrome, and it does not rely on LaTeX. I hope it will be useful to you as well.

[See example outputs](examples/)

![Example PDF outputs](examples/illustration.gif)

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Features](#features)
- [CLI Reference](#cli-reference)
- [Environment Variables](#environment-variables)
- [Configuration](#configuration)
- [Config Init Wizard](#config-init-wizard)
- [Library Usage](#library-usage)
- [Troubleshooting](#troubleshooting)
- [Known Limitations](#known-limitations)
- [Sponsors](#sponsors)
- [Contributing](#contributing)

## Installation

```bash
go install github.com/alnah/picoloom/v2/cmd/picoloom@v2
```

The current Go module path is `github.com/alnah/picoloom/v2`.

For reproducible CI or pinned installs, use an exact version:

```bash
go install github.com/alnah/picoloom/v2/cmd/[email protected]
```

Using `@v2` avoids the extra legacy v1 module lookup/downloading noise that can happen with `@latest` on versioned Go modules.

<details>
<summary>Other installation methods</summary>

### Homebrew

```bash
brew tap alnah/tap
brew install alnah/tap/picoloom
```

Update later with:

```bash
brew upgrade alnah/tap/picoloom
```

On a fresh machine without Chrome installed yet, `picoloom doctor` stays strict by
default. Use `picoloom doctor --allow-managed-browser` to validate the managed
Chromium bootstrap path used on first run.

### Docker

```bash
docker pull ghcr.io/alnah/picoloom:latest
```

### Binary Download

Download pre-built binaries from [GitHub Releases](https://github.com/alnah/picoloom/releases).

</details>

## Requirements

- Go 1.26+
- Chrome/Chromium (downloaded automatically on first run)
- Homebrew users can install the CLI from `alnah/tap/picoloom`

> **Docker/CI users:** See [Troubleshooting](#troubleshooting) for setup instructions.

## Quick Start

### CLI

```bash
picoloom convert document.md                # Single file
picoloom convert ./docs/ -o ./output/       # Batch convert
picoloom convert -c work document.md        # With config
picoloom config init                        # Create config with wizard
```

### Library

```go
package main

import (
    "context"
    "log"
    "os"

    "github.com/alnah/picoloom/v2"
)

func main() {
    conv, err := picoloom.NewConverter()
    if err != nil {
        log.Fatal(err)
    }
    defer conv.Close()

    result, err := conv.Convert(context.Background(), picoloom.Input{
        Markdown: "# Hello World\n\nGenerated with Picoloom.",
    })
    if err != nil {
        log.Fatal(err)
    }

    os.WriteFile("output.pdf", result.PDF, 0644)
}
```

The `Convert()` method returns a `ConvertResult` containing:
- `result.PDF` - the generated PDF bytes
- `result.HTML` - the intermediate HTML (useful for debugging)

Use `Input.HTMLOnly: true` to skip PDF generation and only produce HTML.

## Features

- **CLI + Library** - Use as `picoloom` command or import in Go, with shell completion
- **Batch conversion** - Process directories with parallel workers
- **Cover pages** - Title, subtitle, logo, author, organization, date, version
- **Table of contents** - Auto-generated from headings with configurable depth
- **Frontmatter stripping** - YAML frontmatter (`---` blocks) stripped before conversion
- **Custom styling** - Embedded themes or your own CSS ([some limitations](#known-limitations))
- **Page settings** - Size (letter, A4, legal), orientation, margins
- **Signatures** - Name, title, email, photo, links
- **Footers** - Page numbers, dates, status text
- **Watermarks** - Diagonal background text (BRAND, etc.)
- **Admonitions** - Obsidian alerts and Pandoc fences, with types, nesting, and custom accents ([syntax](#admonitions))

<details>
<summary>Admonitions</summary>

<a id="admonitions"></a>

Callouts are always enabled and need no configuration. Two syntaxes are supported.

Obsidian-style alerts mark the first line of a blockquote:

```markdown
> [!NOTE]
> Body text.

> [!TIP] Custom title
> Body text.
```

Pandoc-style fences use three or more colons, close with a line of colons, and can nest:

```markdown
::: warning Deployment window
Body text.
:::
```

| Type | Aliases | Default title |
|------|---------|---------------|
| `note` | `info` | Note |
| `tip` | `success` | Tip |
| `important` | - | Important |
| `warning` | - | Warning |
| `caution` | `danger` | Caution |

Custom accents override a single variable per type in your CSS:

```css
.admonition-note {
  --admonition-accent: #8250df;
}
```

Types are case-insensitive. Titles are plain text, taken from the first line, and HTML-escaped. An unknown alert type stays a regular blockquote; an unknown fence type stays a paragraph.

</details>

## CLI Reference

```bash
picoloom convert document.md                # Single file
picoloom convert ./docs/ -o ./output/       # Batch convert
picoloom convert -c work document.md        # With config
picoloom convert --style technical doc.md   # With style
picoloom config init                        # Interactive config wizard
```

<details>
<summary>All flags</summary>

```
picoloom <command> [flags] [args]

Commands:
  convert      Convert markdown files to PDF
  config       Manage configuration files
  doctor       Check system configuration
  completion   Generate shell completion script
  version      Show version information
  help         Show help for a command

picoloom convert <input> [flags]

Input/Output:
  -o, --output <path>       Output file or directory
  -c, --config <name>       Config file name or path
  -w, --workers <n>         Parallel workers (0 = auto)
  -t, --timeout <duration>  PDF generation timeout (default: 30s)
                            Examples: 30s, 2m, 1m30s

Author:
      --author-name <s>     Author name
      --author-title <s>    Author professional title
      --author-email <s>    Author email
      --author-org <s>      Organization name
      --author-phone <s>    Author phone number
      --author-address <s>  Author postal address
      --author-dept <s>     Author department

Document:
      --doc-title <s>       Document title ("" = auto from H1)
      --doc-subtitle <s>    Document subtitle
      --doc-version <s>     Version string
      --doc-date <s>        Date (see Date Formats section)
      --doc-client <s>      Client name
      --doc-project <s>     Project name
      --doc-type <s>        Document type
      --doc-id <s>          Document ID/reference
      --doc-desc <s>        Document description

Page:
  -p, --page-size <s>       letter, a4, legal (default: letter)
      --orientation <s>     portrait, landscape (default: portrait)
      --margin <f>          Margin in inches (default: 0.5)

Footer:
      --footer-position <s> left, center, right (default: right)
      --footer-text <s>     Custom footer text
      --footer-page-number  Show page numbers
      --footer-doc-id       Show document ID in footer
      --no-footer           Disable footer

Cover:
      --cover-logo <path>   Logo path or URL
      --cover-dept          Show author department on cover
      --no-cover            Disable cover page

Signature:
      --sig-image <path>    Signature image path
      --no-signature        Disable signature block

Table of Contents:
      --toc-title <s>       TOC heading text
      --toc-min-depth <n>   Min heading depth (1-6, default: 2)
                            1=H1, 2=H2, etc. Use 2 to skip title
      --toc-max-depth <n>   Max heading depth (1-6, default: 3)
      --no-toc              Disable table of contents

Watermark:
      --wm-text <s>         Watermark text
      --wm-color <s>        Color hex (default: #888888)
      --wm-opacity <f>      Opacity 0.0-1.0 (default: 0.1)
      --wm-angle <f>        Angle in degrees (default: -45)
      --no-watermark        Disable watermark

Page Breaks:
      --break-before <s>    Break before headings: h1,h2,h3
      --orphans <n>         Min lines at page bottom (default: 2)
      --widows <n>          Min lines at page top (default: 2)
      --no-page-breaks      Disable page break features

Assets & Styling:
      --style <name|path>   CSS style name or file path (default: default)
                            Name: uses embedded or custom asset (e.g., "technical")
                            Path: reads file directly (contains / or \)
      --template <name|path> Template set name or directory path
      --asset-path <dir>    Custom asset directory (overrides config)
      --no-style            Disable CSS styling

Debug Output:
      --html                Output HTML alongside PDF
      --html-only           Output HTML only, skip PDF generation

Output Control:
  -q, --quiet               Only show errors
  -v, --verbose             Show detailed timing

picoloom config init [flags]

Config Init:
      --output <path>       Output path for generated config (default: ./picoloom.yaml)
      --force               Overwrite destination if it exists
      --no-input            Use defaults without interactive prompts
```

</details>

<details>
<summary>Examples</summary>

```bash
# Single file with custom output
picoloom convert -o report.pdf input.md

# Batch with config
picoloom convert -c work ./docs/ -o ./pdfs/

# Custom CSS, no footer
picoloom convert --style ./custom.css --no-footer document.md

# A4 landscape with 1-inch margins
picoloom convert -p a4 --orientation landscape --margin 1.0 document.md

# With watermark
picoloom convert --wm-text "DRAFT" --wm-opacity 0.15 document.md

# Override document title
picoloom convert --doc-title "Final Report" document.md

# Page breaks before H1 and H2 headings
picoloom convert --break-before h1,h2 document.md

# Use embedded style by name
picoloom convert --style technical document.md

# Debug: output HTML alongside PDF
picoloom convert --html document.md

# Debug: output HTML only (no PDF)
picoloom convert --html-only document.md

# Use custom assets directory
picoloom convert --asset-path ./my-assets document.md

# Interactive config wizard
picoloom config init

# Non-interactive config generation (CI/scripts)
picoloom config init --no-input --output ./configs/work.yaml --force
```

</details>

<details>
<summary>Shell Completion</summary>

Generate shell completion scripts for tab-completion of commands, flags, and file arguments:

```bash
# Bash - add to ~/.bashrc
eval "$(picoloom completion bash)"

# Zsh - add to ~/.zshrc
eval "$(picoloom completion zsh)"

# Fish - save to completions directory
picoloom completion fish > ~/.config/fish/completions/picoloom.fish

# PowerShell - add to $PROFILE
picoloom completion powershell | Out-String | Invoke-Expression
```

</details>

<details>
<summary>Exit Codes</summary>

| Code | Name | Description |
|------|------|-------------|
| 0 | Success | Conversion completed successfully |
| 1 | General | Unexpected or unclassified error |
| 2 | Usage | Invalid flags, configuration, or validation failure |
| 3 | I/O | File not found, permission denied, write failure |
| 4 | Browser | Chrome not found, connection failed, timeout |

Example usage in scripts:

```bash
picoloom convert document.md
case $? in
    0) echo "Success" ;;
    2) echo "Check your flags or config" ;;
    3) echo "Check file permissions" ;;
    4) echo "Check Chrome installation" ;;
    *) echo "Unknown error" ;;
esac
```

</details>

<details>
<summary>Doctor Command</summary>

Diagnose system configuration before running conversions:

```bash
picoloom doctor           # Human-readable output
picoloom doctor --json    # JSON output for CI/scripts
picoloom doctor --allow-managed-browser
```

Checks performed:
- Chrome/Chromium: binary exists, version, sandbox status
- Environment: container detection (Docker, Podman, Kubernetes)
- System: temp directory writability

Use `--allow-managed-browser` on fresh Homebrew installs when Chromium may be
downloaded on first run instead of being installed locally ahead of time.

Exit codes:
- `0` - All checks passed (including warnings)
- `1` - Errors found (conversion will likely fail)

Example CI usage:

```bash
# Fail pipeline early if setup is broken
picoloom doctor --json | jq -e '.status != "errors"' || exit 1
```

</details>

<details>
<summary>Docker</summary>

```bash
# Convert a single file
docker run --rm -v $(pwd):/data ghcr.io/alnah/picoloom convert document.md

# Convert with output path
docker run --rm -v $(pwd):/data ghcr.io/alnah/picoloom convert -o output.pdf input.md

# Batch convert directory
docker run --rm -v $(pwd):/data ghcr.io/alnah/picoloom convert ./docs/ -o ./pdfs/
```

> **Note:** The official Docker image has all dependencies pre-installed. For custom images, see [Troubleshooting](#troubleshooting).

</details>

## Environment Variables

Environment variables provide CI/CD-friendly configuration without requiring YAML files.

**Priority:** CLI flags > config file > environment variables > defaults

### PICOLOOM Variables

| Variable | Description |
|----------|-------------|
| `PICOLOOM_CONFIG` | Config file path (e.g., `/app/config.yaml`) |
| `PICOLOOM_INPUT_DIR` | Default input directory |
| `PICOLOOM_OUTPUT_DIR` | Default output directory |
| `PICOLOOM_TIMEOUT` | PDF generation timeout (e.g., `2m`, `90s`) |
| `PICOLOOM_STYLE` | CSS style name or path (e.g., `technical`) |
| `PICOLOOM_WORKERS` | Parallel workers (e.g., `4`) |
| `PICOLOOM_AUTHOR_NAME` | Author name for cover/signature |
| `PICOLOOM_AUTHOR_ORG` | Organization name |
| `PICOLOOM_AUTHOR_EMAIL` | Author email |
| `PICOLOOM_DOC_VERSION` | Document version |
| `PICOLOOM_DOC_DATE` | Document date (supports `auto`) |
| `PICOLOOM_DOC_ID` | Document ID |
| `PICOLOOM_PAGE_SIZE` | Page size: `letter`, `a4`, `legal` |
| `PICOLOOM_COVER_LOGO` | Cover logo path/URL (auto-enables cover) |
| `PICOLOOM_WATERMARK_TEXT` | Watermark text (auto-enables watermark) |
| `PICOLOOM_CONTAINER` | Set to `1` to force container detection (for `picoloom doctor`) |

Legacy `MD2PDF_*` variables are still accepted as fallback. Unknown `PICOLOOM_*` or `MD2PDF_*` variables trigger a warning to catch typos.

<details>
<summary>CI/CD Examples</summary>

**GitHub Actions:**
```yaml
- name: Generate PDFs
  env:
    PICOLOOM_STYLE: technical
    PICOLOOM_AUTHOR_ORG: ${{ github.repository_owner }}
    PICOLOOM_DOC_VERSION: ${{ github.ref_name }}
    PICOLOOM_WATERMARK_TEXT: ${{ github.ref_name == 'main' && '' || 'DRAFT' }}
  run: picoloom convert ./docs/ -o ./output/
```

**GitLab CI:**
```yaml
pdf:
  variables:
    PICOLOOM_STYLE: corporate
    PICOLOOM_OUTPUT_DIR: ./artifacts/pdf
    PICOLOOM_DOC_DATE: auto
  script:
    - picoloom convert ./docs/
```

**Docker:**
```bash
docker run --rm \
  -e PICOLOOM_STYLE=technical \
  -e PICOLOOM_AUTHOR_ORG="Acme Corp" \
  -e ROD_NO_SANDBOX=1 \
  -v $(pwd):/data \
  ghcr.io/alnah/picoloom convert ./docs/
```

</details>

### Browser Variables (go-rod)

| Variable | Default | Description |
|----------|---------|-------------|
| `ROD_NO_SANDBOX` | - | Set to `1` to disable Chrome sandbox (required for Docker/CI) |
| `ROD_BROWSER_BIN` | - | Path to custom Chrome/Chromium binary |

These are used by the underlying [go-rod](https://github.com/go-rod/rod) browser automation library. Error messages will suggest these variables when browser issues are detected in CI/Docker environments.

## Configuration

Config files are searched in the current directory first, then in the user config directory:

| OS      | User Config Directory                      |
| ------- | ------------------------------------------ |
| Linux   | `~/.config/picoloom/`                      |
| macOS   | `~/Library/Application Support/picoloom/`  |
| Windows | `%APPDATA%\picoloom\`                      |

Supported formats: `.yaml`, `.yml`

Legacy fallbacks are still supported during the migration: `./md2pdf.yaml`, `~/.config/go-md2pdf/`, and `MD2PDF_*`.

## Config Init Wizard

Use the wizard to generate a valid config file without writing YAML manually:

```bash
# Interactive wizard (TTY required)
picoloom config init

# Custom destination
picoloom config init --output ./configs/work.yaml

# Non-interactive defaults (CI/scripts)
picoloom config init --no-input --output ./configs/work.yaml --force
```

Wizard behavior:
- Prompts are in English and include available options plus an example value.
- Type `?` at a prompt to display inline help and a YAML snippet.
- Interactive mode collects style, author fields, page size, and optional signature/watermark/cover settings.
- Interactive mode shows a summary and YAML preview before write confirmation.
- Without `--force`, existing files are preserved; with `--force`, overwrite is explicit and safe.

| Option                  | Type   | Default      | Description                              |
| ----------------------- | ------ | ------------ | ---------------------------------------- |
| `input.defaultDir`      | string | -            | Default input directory                  |
| `output.defaultDir`     | string | -            | Default output directory                 |
| `timeout`               | string | `"30s"`      | PDF generation timeout (e.g., "30s", "2m") |
| `style`                 | string | `"default"`  | CSS style name or path                   |
| `assets.basePath`       | string | -            | Custom assets directory (styles, templates) |
| `author.name`           | string | -            | Author name (used by cover, signature)   |
| `author.title`          | string | -            | Author professional title                |
| `author.email`          | string | -            | Author email                             |
| `author.organization`   | string | -            | Organization name                        |
| `author.phone`          | string | -            | Contact phone number                     |
| `author.address`        | string | -            | Postal address (multiline via YAML `\|`) |
| `author.department`     | string | -            | Department name                          |
| `document.title`        | string | -            | Document title ("" = auto from H1)       |
| `document.subtitle`     | string | -            | Document subtitle                        |
| `document.version`      | string | -            | Version string (used in cover, footer)   |
| `document.date`         | string | -            | Date (see [Date Formats](#date-formats)) |
| `document.clientName`   | string | -            | Client/customer name                     |
| `document.projectName`  | string | -            | Project name                             |
| `document.documentType` | string | -            | Document type (e.g., "Specification")    |
| `document.documentID`   | string | -            | Document ID (e.g., "DOC-2025-001")       |

Frequently Asked Questions

What is picoloom?

picoloom is a Text Processing library for the Go programming language. Markdown-to-PDF converter with CLI and Go library APIs

How do I install picoloom?

Install picoloom with the Go module system using `go get alnah/picoloom`. Check the repository for the current installation instructions.

What category does picoloom belong to?

picoloom is listed under Text Processing, specifically Markup Languages.

← Back to Markup Languages