Modern print format designer for Frappe using the Typst typesetting system. Build PDF‑native print formats with a Vue 3 visual builder and real-time preview.
Alpha / Work in progress. Expect frequent changes while features are still settling.
Why Typst instead of HTML/CSS? Typst provides superior PDF typography, precise layout control, and professional typesetting features that are difficult to achieve with browser-based rendering. Perfect for invoices, reports, certificates, and other print-critical documents.
- Features
- Requirements
- Installation
- Quick Start
- Usage
- Font Configuration
- Architecture
- Development
- API Reference
- Known Limitations
- Roadmap
- Troubleshooting
- FAQ
- Testing
- Contributing
- License
- Visual Print Format Builder - Drag-and-drop interface for designing print layouts
- Real-time Typst Preview - See PDF output as you design with live SVG preview
- Native PDF Generation - High-quality PDFs via Typst CLI (no browser printing)
- DocType Integration - Create custom formats for any Frappe DocType
- Letterhead Support - Use Letter Head documents with automatic image handling
- Logo Support - Add company logo to page.
- Custom Fonts - Support for system fonts, custom fonts, and bundled fonts
- Print Preview Page - Dedicated preview page for testing formats with actual documents
- QR Code Integration - Automatic QR code generation for documents
- Raw Typst Mode - Advanced users can write Typst markup directly
- Dual-Mode Report Builder - Basic visual controls for report templates with Advanced Raw Typst mode for full customization
- Report Mode Guardrails - Safe Basic/Advanced switching with signature checks to prevent accidental overwrite of custom Typst
Typst must be installed on your system before installing the app.
brew install typstcurl -L -o typst.tar.xz https://github.com/typst/typst/releases/latest/download/typst-x86_64-unknown-linux-musl.tar.xz
tar -xf typst.tar.xz
sudo mv typst-x86_64-unknown-linux-musl/typst /usr/local/bin/Windows users: Frappe requires WSL. Install Typst inside your WSL environment using the Linux commands above.
Or download from Typst Releases
typst --version
# Should output: typst 0.11.0 or higher- Frappe: v15 or later (all Python/Node.js dependencies already satisfied)
- Python dependency:
pyqrcode(installed with the app; required for QR SVG generation)
See Requirements above - Typst must be installed first.
cd ~/frappe-bench
bench get-app https://github.com/agatho-daemon/crispy_print --branch develop
bench --site your-site install-app crispy_print
bench restart# Check app is installed
bench --site your-site list-apps | grep crispy_print
# Test Typst integration
bench --site your-site console>>> from crispy_print.api import get_typst_local_fonts
>>> fonts = get_typst_local_fonts()
>>> print(len(fonts), "fonts available")If you're developing the app:
cd apps/crispy_print/crispy_print/public/js
yarn install
cd ~/frappe-bench
bench build --app crispy_print
⚠️ CRITICAL: You must set at least one format as Default for a DocType. The Typst print button appears on document forms only when a default format exists for that DocType.
Step-by-step:
-
Create a new format
- Navigate to: Desk → Crispy Print → Crispy Format
- Click New
- Enter Name (e.g., "Sales Invoice Modern")
- Select DocType (e.g., "Sales Invoice")
- Select Module (e.g., "Crispy Print")
- Save
-
Set as default
⚠️ (Required for button to appear!)- Check the "Set as Default" checkbox
- Save again
-
Design your layout
- Click Open Builder button
- Add sections: Click "+ Add Section"
- Drag fields: From right pane to layout grid
- Configure fields: Click field to edit label, style, alignment
- Add tables: Drag table fields (e.g., "items") for line items
- Adjust columns: Split sections into 1-4 columns
-
Configure page settings (left sidebar)
- Paper Size: A4, Letter, etc.
- Margins: Adjust spacing
- Fonts: Select font family
- Letterhead: Optional background image
- QR Code: Enable for verification
-
Save and test
- Click Save
- Open any document of that DocType (e.g., Sales Invoice)
- Look for Typst button in toolbar (top-right)
- Click to preview and download PDF
Once a default format exists, the Typst button appears automatically on all documents of that DocType.
From Document:
- Open document (e.g., SI-2024-001)
- Click Typst button in toolbar
- Preview opens with your default format
- Click Download PDF
Direct URL:
/app/crispy-print/{doctype}/{docname}/{format_name}
The app automatically includes fonts from crispy_print/public/vendor/typst/. These fonts are available to all print formats without additional configuration.
Typst automatically discovers system fonts. On Linux, it searches standard directories like /usr/share/fonts and ~/.local/share/fonts.
To add custom fonts outside the Frappe bench:
Add to your shell profile (~/.bashrc, ~/.zshrc, or ~/.profile):
export TYPST_FONT_PATHS="/path/to/custom/fonts:/another/font/path"Then restart your bench:
bench restartSet the dedicated Typst font directory:
export TYPST_FONT_DIR="$HOME/.fonts/typst"Create the directory and add fonts:
mkdir -p ~/.fonts/typst
cp /path/to/font.ttf ~/.fonts/typst/You can also configure the Typst binary path in site_config.json:
{
"TYPST_BIN": "/usr/local/bin/typst"
}The app's get_typst_local_fonts() API method returns all available fonts by running:
typst fontsThis includes:
- System fonts
- Fonts in
TYPST_FONT_PATHS - Bundled fonts from
crispy_print/public/vendor/typst/
This app uses Frappe v15's native esbuild bundler - no separate Vite or webpack setup required.
- Bundle Entry:
crispy_print/public/js/crispy_print.bundle.js - Build Command:
bench build --app crispy_print - Output: Single JavaScript bundle with inlined CSS (~1.7MB)
- Plugin: Uses
frappe-vue-styleto automatically inline Vue SFC styles
crispy_print/
├── crispy_print/
│ ├── api.py # Whitelisted API methods
│ ├── hooks.py # App hooks
│ ├── public/
│ │ ├── js/
│ │ │ ├── crispy_print.bundle.js # Main entry (builder)
│ │ │ ├── crispy_preview.bundle.js # Preview page entry
│ │ │ ├── components/ # Reusable Vue components
│ │ │ ├── composables/ # Vue composables (useStore)
│ │ │ ├── pages/
│ │ │ │ ├── CrispyPFB.vue # Print Format Builder
│ │ │ │ └── CrispyPP.vue # Print Preview
│ │ │ ├── typst/
│ │ │ │ ├── createTypstWorker.ts # Web worker factory
│ │ │ │ ├── setupWorker.ts # Worker orchestration
│ │ │ │ ├── JSONToTypst.ts # Layout → Typst translator
│ │ │ │ └── worker.ts # Typst compilation worker
│ │ │ └── utils/
│ │ │ ├── layout.ts # Layout type definitions
│ │ │ └── formatLoader.ts # Format data utilities
│ │ └── vendor/typst/ # Bundled fonts (optional)
│ ├── doctype/
│ │ └── crispy_format/ # Crispy Format DocType
│ └── page/
│ ├── crispy_print_builder/ # Builder page (Frappe desk)
│ └── typst_print/ # Preview page (Frappe desk)
├── pyproject.toml # Python dependencies & config
└── README.md
Pages:
- Crispy Format Builder (
/app/crispy-format-builder) - 4-pane builder; DocType uses visual layout, Report supports Basic + Advanced Raw Typst modes - Crispy Print Preview (
/app/crispy-print/{doctype}/{docname}/{format}) - Document preview page
Core Files:
api.py- Backend API: Typst compilation, font discovery, letterhead handlingCrispyPFB.vue- Main builder component with drag-drop layout editorCrispyPP.vue- Preview component with format/settings controlsuseStore.ts- Centralized state management (layout, page settings, metadata)setupWorker.ts- Typst worker lifecycle and compilation orchestrationJSONToTypst.ts- Translates JSON layout structure to Typst markupworker.ts- Web Worker for async Typst compilation via APIformatLoader.ts- Utilities for loading Crispy Format documents
For TypeScript/Vue IntelliSense in VS Code and the color picker bundle (@simonwep/pickr):
cd apps/crispy_print/crispy_print/public/js
yarn installThis installs local dependencies used by the frontend bundle:
package.json,yarn.lock- Type dependenciestsconfig.json- TypeScript configurationnode_modules/- Type definitions
Note: Required for building.
# Build the app
bench build --app crispy_print
# Clear cache after changes
bench clear-cache && bench clear-website-cache
# Development workflow
bench start # Run with auto-reload enabledFrontend Tests (TypeScript/Vue):
cd apps/crispy_print/crispy_print/public/js
# Run all tests
yarn test:unit
# Run specific batch
yarn test:unit:batch6
# Watch mode
yarn test:unit -- --watchBackend Tests (Python):
# Run all tests
bench --site your-site run-tests --app crispy_print
# Run specific module
bench --site your-site run-tests --module crispy_print.tests.test_api
# Run DocType tests
bench --site your-site run-tests --doctype "Crispy Format"Test Coverage:
- Frontend: 67 tests across 27 test files (100% passing)
- Backend: 29 tests across 2 test files (100% passing)
- Total: 96 tests
See TEST_COVERAGE.md for details.
All Vue components use <style scoped> blocks. CSS is automatically extracted and inlined:
<template>
<div class="my-component">Content</div>
</template>
<script setup lang="ts">
// Component logic with Composition API
</script>
<style scoped>
/* Scoped styles - automatically inlined */
.my-component {
padding: 1rem;
}
</style>For dynamically created DOM (e.g., .typst-page elements), apply styles via JavaScript:
const page = document.createElement("div")
page.className = "typst-page"
page.style.marginBottom = "1.5rem"
page.style.boxShadow = "0 4px 12px rgba(148, 163, 184, 0.25)"The typical workflow for using Crispy Print:
- Create format → Design in builder → Save
- Set as default → To enable Typst button for DocType
- Open document → Click
typstbutton → Download PDF
For Crispy Format Type = Report, builder now supports two editing modes:
- Basic mode: non-technical controls generate a managed Typst report template
- Advanced mode: direct Raw Typst editing
Key behavior:
- Basic → Advanced is always allowed.
- Advanced → Basic only unlocks full Basic editing when the template is still Basic-managed.
- If custom Raw Typst changes are detected, Basic opens in read-only with options to:
- stay in Advanced mode, or
- reset/regenerate the Basic template.
This keeps report editing accessible while protecting advanced customizations.
You can create multiple formats for the same DocType without setting them as default:
- Access via direct URL:
/app/crispy-print/{doctype}/{docname}/{format_name} - Or programmatically via API (see API Reference)
Invoice with Logo and Table:
- Section 1 (2 columns): Company logo left, Invoice details right
- Section 2: Customer information
- Section 3: Items table (drag "items" table field)
- Section 4: Totals (align right)
- Section 5 (footer): Terms and conditions
Certificate:
- Enable letterhead background
- Section 1: Centered title
- Section 2: Recipient name (large font)
- Section 3: Certificate text
- Section 4: Signatures (3 columns)
- Add QR code in corner for verification
Report with Headers:
- Configure page header in settings
- Section 1: Report title and date
- Section 2: Summary metrics (4 columns)
- Section 3: Data table
- Section 4: Charts (if using HTML fields)
For a concise endpoint list with arguments and return shapes, see:
docs/api-reference.mddocs/typst-cookbook.md
All methods are accessible via frappe.call() from client-side.
Returns list of available font families.
@frappe.whitelist()
def get_typst_local_fonts() -> list[str]Returns: list[str] - Font family names
Example:
fonts = frappe.call('crispy_print.api.get_typst_local_fonts')
# ['EB Garamon', 'Roboto', 'Liberation Sans', ...]Compiles Typst source to PDF or SVG.
@frappe.whitelist()
def compile_typst(
typst_source: str,
output_format: str = "svg",
letterhead_image: str = None,
qr_data: str = None,
qr_filename: str = None
) -> dictParameters:
typst_source(str, required) - Typst markup codeoutput_format(str) - "pdf" or "svg" (default: "svg")letterhead_image(str) - Path to letterhead image (e.g., "/files/letterhead.png")qr_data(str) - Data to encode in QR codeqr_filename(str) - QR SVG filename
Returns: dict
# PDF format:
{
"success": True,
"format": "pdf",
"pdf_data": "base64_encoded_pdf_string"
}
# SVG format:
{
"success": True,
"format": "svg",
"svg_pages": ["<svg>...</svg>", "<svg>...</svg>"],
"page_count": 2
}Raises: frappe.ValidationError if compilation fails
Example:
result = frappe.call('crispy_print.api.compile_typst',
typst_source='#set page(paper: "a4")\n= Hello Typst',
output_format='pdf'
)
pdf_bytes = base64.b64decode(result['pdf_data'])Returns document with server-side formatted field values.
@frappe.whitelist()
def get_formatted_doc(doctype: str, name: str) -> dictParameters:
doctype(str, required) - DocType namename(str, required) - Document name
Returns: dict - Document with formatted fields (currency, dates, etc.)
Example:
doc = frappe.call('crispy_print.api.get_formatted_doc',
doctype='Sales Invoice',
name='SI-2024-001'
)
# doc['grand_total'] is now formatted as "1,234.56"Get all Crispy Formats for a DocType.
@frappe.whitelist()
def get_crispy_formats_for_doctype(doctype: str) -> list[dict]Returns: list[dict] - List of format names and DocTypes
Get DocTypes that have default Crispy Formats set.
@frappe.whitelist()
def get_default_doctypes() -> list[str]Returns: list[str] - List of DocType names
Returns canonical backend defaults for report Basic mode controls.
@frappe.whitelist()
def get_default_report_builder_config(generic_report_type: str | None = None) -> dictParameters:
generic_report_type(str, optional) - e.g."Grid","Tree","Summary","Minimal"
Returns: dict
{
"mode": "basic",
"preset": "grid", # or tree/summary/minimal
"show_filters": True,
"show_footer_total": True,
"header_fill": "#B3D7FF",
"header_text_weight": "bold",
"font_family": "Inter 18pt",
"font_size_pt": 9,
"row_striping": False,
"row_stripe_fill": "#F8FBFF",
"column_align_strategy": "auto",
"table_inset_x_pt": 8,
"table_inset_y_pt": 6,
"table_stroke_top_pt": 1,
"table_stroke_body_pt": 0.5,
"raw_signature": None
}Used by the frontend as the server-side single source of truth for report builder defaults.
These functions are available globally in the builder and preview pages.
Mounts the Vue 3 print format builder app.
window.mountCrispyPrint(containerId: string, formatName: string): voidUsed internally by: Frappe page loader
Initializes the Typst compilation web worker.
window.setupWorker(
formatName: string,
previewContainer: HTMLElement,
adapter: object
): () => voidReturns: Teardown function to cleanup worker
Used internally by: Preview page
As an alpha release, Crispy Print has several known limitations:
- Typst CLI Required: Must be installed separately; app won't work without it
- Frappe v15+ Only: Not tested for compatibility with older Frappe versions
- Server-Side Rendering: All PDF compilation happens on the server (no client-side rendering)
- Font Discovery: Depends on system font configuration and
TYPST_FONT_PATHSenvironment variable
- Limited Field Types: Currently supports basic fields; complex custom fields may not render correctly
- Fixed Grid System: 4-column layout structure cannot be customized
- No Conditional Visibility: Cannot hide/show elements based on document conditions
- Report Basic Mode Scope: Basic mode intentionally covers common report patterns; complex custom report logic still requires Advanced Raw Typst mode
- Raw Typst Mode Limitations:
- Requires knowledge of Typst syntax
- No visual preview while editing raw code
- Syntax errors not caught until compilation
- 🔄 Tip: Use the Refresh button in the preview pane to recompile after editing raw Typst code
- QR Code Format: Only SVG format supported (no PNG/bitmap QR codes)
- Table Styling: Limited compared to full Typst table capabilities
- Browser Preview: SVG rendering in browser preview (actual PDF downloads are native Typst output)
- Image Formats: Letterhead images must be in formats supported by Typst (PNG, JPEG, SVG)
- No Real-Time Collaboration: Multiple users cannot edit the same format simultaneously
- Text Editor / HTML Fields: Content is converted to plain text (HTML stripped) before rendering
- No Built-in Letterhead Editor: Must use existing Frappe Letter Head documents
- Fixed Branding Position: Logo and QR code placements use absolute positioning
- LTR Languages Only: Builder UI and text direction currently support left-to-right languages only (English, Spanish, French, etc.). RTL support (Arabic, Hebrew) not yet implemented. Multi-language content is possible via Raw Typst Mode if document fields contain the target language data.
- No Jinja Support: Completely different from standard Frappe Print Formats - uses JSON layouts instead of Jinja templates
- No Python Scripts: Cannot execute custom Python code like standard Print Formats
- Export Only: Formats cannot be imported/exported between sites (yet)
- No Version History: Previous versions of formats are not stored
- Web Worker Compilation: Initial compilation may take 2-3 seconds for complex layouts
- SVG Preview Size: Multi-page SVG previews can be memory-intensive in browser
- Font Loading: Large custom font collections may slow down font discovery API
The following features are planned but not yet available:
- Support Frappe Format Field Templates in Typst output
- Native rendering with safe fallback for unsupported legacy templates
- Batch printing from list view
- Progress indicator for multi-document compilation
- Configurable batch size limits
- Custom page break controls
- Multi-language format support
- Format import/export
- Advanced table styling options
- Client-side PDF rendering
- Format version history
-
Template variable/expression support
Error running typst fonts: [Errno 2] No such file or directory: 'typst'
Solution: Install Typst CLI (see Requirements section)
Check available fonts: ``# Format Not Appearing in Document
If Typst button doesn't show or format isn't available:
-
Verify installation:
bench --site your-site list-apps | grep crispy_print -
Check format has layout_json:
- Open Crispy Format document
- Ensure it was saved via builder (not manually created)
-
Verify permissions:
- User needs read access to Crispy Format doctype
- Check Role Permission Manager
-
Check browser console for API errors
"Typst compiler not found"
Solution: Install Typst CLI (see Requirements)
"Compilation timed out"
Causes:
- Server under heavy load
- Infinite loop in raw Typst code
Solutions:
- Simplify layout
- Check raw Typst syntax
"Letterhead image not found"
Causes:
- Letter Head document doesn't have image field
- Image file deleted from /files/
Solution:
- Re-upload letterhead image
- Verify file path in Letter Head document
Enable debug logging:
# In site_config.json
{
"developer_mode": 1,
"logging": 2
}Check logs:
tail -f sites/your-site/logs/frappe.logQ: Can I use Crispy Print with ERPNext?
A: Yes! It's designed for ERPNext and supports Sales Invoice, Purchase Invoice, Quotation, etc.
Q: Does it work offline?
A: PDF compilation requires server access (runs Typst CLI server-side). Preview requires internet for fonts.
Q: Can I export formats between sites?
A: Not yet. Planned for future release. Currently, you need to recreate formats on each site.
Q: How do I customize fonts?
A: Add fonts to system, set TYPST_FONT_PATHS environment variable, then restart bench. See Font Configuration.
Q: Can I use custom Typst functions?
A: Yes, in Raw Typst mode. But be careful - syntax errors will break compilation.
Q: What's the difference from Print Designer?
A: Print Designer uses HTML/CSS/Jinja. Crispy Print uses Typst for better PDF quality. They're completely separate systems.
Q: Can I mix Jinja and Typst?
A: No. Crispy Print uses JSON layouts, not Jinja templates.
Q: Is it production-ready?
A: It's in alpha. Use for non-critical documents. Test thoroughly before production use.
Q: How do I report bugs?
A: Open an issue on GitHub with Frappe version, Typst version, and error logs.
Q: Does it support multi-language?
A: Not yet. Single language per format. Multi-language support is planned.
This app includes comprehensive test coverage:
- 96 total tests (67 frontend + 29 backend)
- 100% pass rate
- Test frameworks: Vitest (frontend), Frappe Test Runner (backend)
See TEST_COVERAGE.md for detailed coverage report.
This app uses pre-commit hooks for code quality:
cd apps/crispy_print
pre-commit installLinters configured:
- ruff - Python linting and formatting
- eslint - JavaScript linting
- prettier - Code formatting
- pyupgrade - Python syntax modernization
This project has pre-configured GitHub Actions workflows (currently disabled):
- CI - Would run 90 unit tests on
developbranch - Linters - Would run Frappe Semgrep Rules and pip-audit on PRs
Currently: Tests are run manually by developers using yarn test:unit (frontend) and bench run-tests (backend).
If PDF generation takes more than 5 seconds:
- Reduce document complexity: Simplify layouts with fewer nested elements
- Limit table rows: Consider pagination for large tables (100+ rows)
- Optimize images: Use compressed letterhead images (< 1MB)
- Check server resources: Ensure adequate CPU/memory on server
If changes don't appear in preview:
- Clear browser cache:
Ctrl+Shift+R(orCmd+Shift+Ron macOS) - Clear Frappe cache:
bench clear-cache && bench clear-website-cache - Rebuild app:
bench build --app crispy_print - Check browser console for JavaScript errors
This app uses pre-commit for code quality:
cd apps/crispy_print
pre-commit installLinters configured:
- ruff - Python linting and formatting
- eslint - JavaScript linting
- prettier - Code formatting
- pyupgrade - Python syntax modernization
GitHub Actions workflows:
- CI - Runs unit tests on
developbranch - Linters - Runs Frappe Semgrep Rules and pip-audit on PRs
MIT
Built with the assistance of various AI tools. Special thanks to:
Star this repo if you find it useful! ⭐