Docs
Getting started
Getting started
FreeReport is one TypeScript package with no dependencies. Install it, import the stylesheet once, and create a viewer in an element that has a height.
npm i @freereportjs/coreimport { createViewer } from '@freereportjs/core';
import '@freereportjs/core/styles.css';
const definition = await fetch('/reports/sales.json').then(r => r.json());
const viewer = createViewer(document.getElementById('viewer'), {
definition,
data: { rows }, // detail rows; other keys are usable in expressions
lang: 'ko',
});
// <div id="viewer" style="height: 720px"></div>Without a build step (CDN)
Load the bundle with a script tag; it defines a global FreeReport. Pin a version (v0.1.0) in production; latest always points to the newest release.
<link rel="stylesheet" href="https://freereport.app/cdn/latest/freereport.css">
<script src="https://freereport.app/cdn/latest/freereport.min.js"></script>
<div id="viewer" style="height: 720px"></div>
<script>
FreeReport.createViewer(document.getElementById('viewer'), { definition, data: { rows } });
</script><link rel="stylesheet" href="https://freereport.app/cdn/v0.1.0/freereport.css">
<script type="module">
import { createViewer } from 'https://freereport.app/cdn/v0.1.0/freereport.esm.js';
createViewer(document.getElementById('viewer'), { definition, data: { rows } });
</script>Frameworks and eGovFrame
The core works in any framework: create the viewer or designer when the component mounts and destroy it when it unmounts. Ready-made components are published on npm: @freereportjs/react, @freereportjs/vue (Vue 3), @freereportjs/svelte (Svelte 5) and @freereportjs/angular, each with a viewer and a designer. Install the core alongside the wrapper, for example npm i @freereportjs/core @freereportjs/react.
- React: <FreeReportViewer> and <FreeReportDesigner>. The viewer ref is { viewer, report }; the designer takes onChange and onSave.
- Vue: the viewer exposes viewer and report; the designer supports v-model:definition and emits change, update:definition and save.
- Svelte: viewer.getViewer() / getReport() and designer.getDesigner(), or onReady.
- Angular: <free-report-viewer> and <free-report-designer> (standalone). The designer emits (definitionChange), so [(definition)] works, and (save).
definition, data, params and zoom update the viewer in place; other props recreate it. Both components default to height: 100% with a 480px minimum.
// npm i @freereportjs/core @freereportjs/react
import { useRef, useState } from 'react';
import { FreeReportDesigner, FreeReportViewer, type ReportDefinition } from '@freereportjs/react';
import '@freereportjs/core/styles.css';
export function SalesReport({ definition, rows }) {
const ref = useRef(null); // { viewer, report }
return <FreeReportViewer ref={ref} definition={definition} data={{ rows }} lang="ko" style={{ height: 720 }} />;
}
export function SalesDesigner({ initial, rows, save }) {
const [def, setDef] = useState<ReportDefinition>(initial);
return <FreeReportDesigner definition={def} data={{ rows }} onChange={setDef} onSave={save} />;
}<!-- npm i @freereportjs/core @freereportjs/vue -->
<script setup>
import { ref } from 'vue';
import { FreeReportDesigner, FreeReportViewer } from '@freereportjs/vue';
import '@freereportjs/core/styles.css';
const props = defineProps(['initial', 'rows']);
const def = ref(props.initial);
const viewer = ref(null); // viewer.value.viewer / viewer.value.report
const save = d => fetch('/api/reports/sales', { method: 'PUT', body: JSON.stringify(d) });
</script>
<template>
<FreeReportViewer ref="viewer" :definition="def" :data="{ rows }" lang="ko" :height="720" />
<FreeReportDesigner v-model:definition="def" :data="{ rows }" @save="save" />
</template><!-- npm i @freereportjs/core @freereportjs/svelte (Svelte 5) -->
<script>
import { FreeReportDesigner, FreeReportViewer } from '@freereportjs/svelte';
import '@freereportjs/core/styles.css';
let { initial, rows, save } = $props();
let def = $state(initial);
let viewer; // viewer.getViewer(), viewer.getReport()
</script>
<FreeReportViewer bind:this={viewer} definition={def} data={{ rows }} lang="ko" style="height: 720px" />
<FreeReportDesigner definition={def} data={{ rows }} onChange={d => (def = d)} onSave={save} />// npm i @freereportjs/core @freereportjs/angular
// angular.json "styles": ["node_modules/@freereportjs/core/dist/styles.css"]
import { Component } from '@angular/core';
import { FreeReportDesignerComponent, FreeReportViewerComponent } from '@freereportjs/angular';
@Component({
selector: 'app-sales-report',
imports: [FreeReportViewerComponent, FreeReportDesignerComponent],
template: `
<free-report-viewer [definition]="def" [data]="data" lang="ko" [height]="720" />
<free-report-designer [(definition)]="def" [data]="data" (save)="save($event)" />`,
})
export class SalesReportComponent {
def = initialDefinition;
data = { rows: [] };
save(def) { /* PUT the JSON to your server */ }
}<%-- reportView.jsp: the viewer in an eGovFrame (Spring + JSP) page --%>
<link rel="stylesheet" href="<c:url value='/static/freereport/freereport.css'/>">
<div id="viewer" style="height: 760px" data-ctx="${fn:escapeXml(pageContext.request.contextPath)}"></div>
<script src="<c:url value='/static/freereport/freereport.min.js'/>"></script>
<script>
var ctx = document.getElementById('viewer').dataset.ctx;
Promise.all([
fetch(ctx + '/report/def/sales.do').then(function (r) { return r.json(); }), // report JSON
fetch(ctx + '/report/data/sales.do').then(function (r) { return r.json(); }) // { rows: [...] } from MyBatis
]).then(function (res) {
FreeReport.createViewer(document.getElementById('viewer'), {
definition: res[0], data: res[1], lang: 'ko',
onOutput: function (e) { /* POST to the output log controller */ }
});
});
</script>eGovFrame sample project
The sample is an eGovFrame 4.3 project (Spring 5.3, MyBatis, JDK 8+) that shows report viewing, printing, PDF and Excel and an output log. The server returns only the report definition and the data as JSON; layout, printing and files are made in the browser, with no report server, ActiveX or installer. For eGovFrame 3.x, change the imports and the groupId from org.egovframe.rte to egovframework.rte.
| Route | Returns | Description |
|---|---|---|
GET /report/view.do?name=sales | JSP | Viewer page (reportView.jsp) |
GET /report/designer.do?name=sales | JSP | Designer page (reportDesigner.jsp) |
GET /report/def/{name}.do | JSON | Report definition: the copy saved in REPORT_DEF, or classpath:reports/{name}.json |
POST /report/def/{name}.do | JSON | Saves the definition from the designer |
GET /report/data/sales.do | JSON | Report data (Service → MyBatis) |
POST /report/log.do | JSON | Records the viewer onOutput event in REPORT_OUTPUT_LOG |
GET /report/logs.do | JSP | The latest 100 output log entries |
The sample project is provided on request: email [email protected].
Building a report
Report definition
A report is a JSON object: page setup, a default font, optional parameters and styles, and bands of positioned elements. The layout engine binds it to data and produces pages; the viewer, printing, PDF and Excel all draw those same pages, so they match.
{
"name": "거래처별 매출",
"page": { "size": "A4", "orientation": "portrait", "margins": 15 },
"font": { "family": "Nanum Gothic", "size": 9 },
"sort": ["customer", "date"],
"bands": {
"title": { "height": 14, "elements": [
{ "type": "text", "x": 0, "y": 0, "w": 180, "h": 10, "text": "거래처별 매출", "style": { "fontSize": 16, "bold": true } }
] },
"columnHeader": { "height": 7, "elements": [
{ "type": "text", "x": 0, "y": 0, "w": 30, "h": 7, "text": "일자", "style": { "bold": true, "border": true } },
{ "type": "text", "x": 30, "y": 0, "w": 110, "h": 7, "text": "거래처", "style": { "bold": true, "border": true } },
{ "type": "text", "x": 140, "y": 0, "w": 40, "h": 7, "text": "금액", "style": { "bold": true, "border": true } }
] },
"detail": { "height": 6, "elements": [
{ "type": "field", "x": 0, "y": 0, "w": 30, "h": 6, "expr": "date", "format": "yyyy-MM-dd", "style": { "border": true } },
{ "type": "field", "x": 30, "y": 0, "w": 110, "h": 6, "expr": "customer", "suppressRepeated": true, "style": { "border": true } },
{ "type": "field", "x": 140, "y": 0, "w": 40, "h": 6, "expr": "amount", "format": "#,##0", "style": { "border": true, "align": "right" } }
] },
"summary": { "height": 8, "elements": [
{ "type": "text", "x": 0, "y": 1, "w": 180, "h": 7, "text": "합계 {sum(amount):#,##0}원 ({koreanAmount(sum(amount))})", "style": { "align": "right", "bold": true } }
] },
"pageFooter": { "height": 8, "elements": [
{ "type": "text", "x": 0, "y": 2, "w": 180, "h": 5, "text": "{page} / {pages}", "style": { "align": "center" } }
] }
}
}| Name | Type | Description |
|---|---|---|
name | string | Report name; used for file names and the output event. |
page | PageSetup | Paper size, orientation and margins. |
font | { family, size, color } | Default font of the report (default Nanum Gothic 9pt). |
styles | Record<string, Style> | Named styles; elements refer to them with styleName. |
params | Record<string, ParamDefinition> | Report parameters shown in the viewer's parameter panel. |
dataSource | string | Which array in the data holds the detail rows (default "rows"). |
sort | string[] | Sort detail rows, e.g. ["region", "-amount"] ("-" = descending). |
filter | Expr | Keep only rows where the expression is truthy. |
bands | object | title, pageHeader, columnHeader, groups, detail, summary, pageFooter, noData. |
pageHeaderSkipFirst | boolean | Do not print the page header on the first page. |
fillEmptyRows | boolean | Fill the rest of each page with blank detail rows (form tables). |
watermark | string | Watermark | Watermark[] | Text or image marks behind (or over) the content. |
emboss | Emboss | Emboss[] | Raised seals, logos or text pressed into the paper. |
All positions and sizes are millimetres from the top-left of the band. Font sizes are points.
Page setup
| Name | Type | Description |
|---|---|---|
size | 'A3' | 'A4' | 'A5' | 'B4' | 'B5' | 'Letter' | 'Legal' | [w, h] | Paper name or [width, height] in mm. Default A4. |
orientation | 'portrait' | 'landscape' | Default portrait. |
margins | number | [top, right, bottom, left] | Margins in mm. Default 15. |
Bands
Bands are printed in this order: title (first page), then on every page the page header and column header, the group headers, one detail band per row, group footers, the summary after the last row, and the page footer at the bottom of every page.
| Name | Type | Description |
|---|---|---|
title | Band | First page only, before everything else (title, letterhead, whole forms). |
pageHeader | Band | Top of every page. |
columnHeader | Band | Below the page header on every page with detail rows (table column titles). |
groups | GroupDefinition[] | Group headers and footers, outermost first. |
detail | Band | Once per detail row. |
summary | Band | After the last row (grand totals, charts). |
pageFooter | Band | Bottom of every page (page numbers, page subtotals). |
noData | Band | Printed instead of the detail rows when there is no data. |
Band properties
| Name | Type | Description |
|---|---|---|
height | number | Height in mm. |
elements | ReportElement[] | Positioned elements; x, y, w, h in mm inside the band. |
canGrow | boolean | Grow when text needs more room (default true for detail and group bands). |
visible | Expr | Print the band only when the expression is truthy. |
pageBreakBefore / pageBreakAfter | boolean | Start a new page before / after the band. |
background | string | Band background color. |
altBackground | string | Detail only: alternate row background (zebra). |
columns / columnGap | number | Detail only: print rows side by side in columns (label sheets, cards); gap in mm. |
Groups and subtotals
A group collects consecutive rows with the same value. Aggregates in its header and footer cover the group; in the page footer they can cover the page; anywhere you can ask for the whole report.
"groups": [{
"by": "region", "sort": "asc", "repeatHeader": true, "keepWithRows": 2,
"header": { "height": 7, "elements": [
{ "type": "text", "x": 0, "y": 0, "w": 180, "h": 7, "text": "■ {region} ({count()}건)", "style": { "bold": true } }
] },
"footer": { "height": 7, "elements": [
{ "type": "text", "x": 0, "y": 0, "w": 140, "h": 7, "text": "{region} 소계", "style": { "align": "center" } },
{ "type": "field", "x": 140, "y": 0, "w": 40, "h": 7, "expr": "sum(amount)", "format": "#,##0", "style": { "align": "right" } }
] }
}],
"pageFooter": { "height": 7, "elements": [
{ "type": "text", "x": 0, "y": 1, "w": 180, "h": 6, "text": "이 페이지 소계 {sum(amount, 'page'):#,##0}원 · 누계 {sum(amount, 'report'):#,##0}원" }
] }| Name | Type | Description |
|---|---|---|
by | Expr | Expression that groups consecutive rows, e.g. "region". |
sort | 'asc' | 'desc' | Sort rows by the group expression first (otherwise rows keep their order). |
header / footer | Band | Printed before / after each group; aggregates cover the group. |
newPage | boolean | Each group starts on a new page. |
repeatHeader | boolean | Repeat the group header on pages where the group continues. |
keepWithRows | number | Keep the header with at least this many rows (default 1). |
resetPageNumber | boolean | Restart page / pages for each group. |
Elements
| Name | Type | Description |
|---|---|---|
type | 'text' | 'field' | 'line' | 'rect' | 'image' | 'barcode' | 'chart' | 'table' | 'crosstab' | Element kind. |
x, y, w, h | number | Position and size inside the band, in mm. |
id | string | Optional id (the designer sets one). |
style / styleName | Style / string | Inline style, merged over the named style. |
visible | Expr | Draw only when truthy. |
canGrow | boolean | Grow downwards when the text needs room (the band grows with it). |
stretch | boolean | Follow the band height when it grows (default for elements touching the band bottom). |
name | string | Column name in Excel "data" mode. |
Element types
| type | Properties | Description |
|---|---|---|
text | { text } | Static or templated text: "합계 {sum(amount):#,##0}원". {{ and }} are literal braces. |
field | { expr, format?, nullText?, suppressRepeated? } | One expression with an optional format; suppressRepeated hides values equal to the row above. |
line | { color?, width?, dash? } | From (x, y) to (x + w, y + h); a horizontal line has h = 0. Width in mm. |
rect | { fill?, color?, width?, radius? } | Rectangle; width 0 for no stroke. |
image | { src? | expr?, fit? } | URL or data: URL, or an expression from data. fit: contain | cover | fill. |
barcode | { expr, format?, showText?, ecc?, color? } | format: 'code128' | 'ean13' | 'qr' | 'datamatrix' | 'pdf417'. ecc: QR error correction L/M/Q/H. |
chart | { chartType, category, series, source?, title?, legend?, format?, showValues?, stacked? } | chartType: 'bar' | 'hbar' | 'line' | 'area' | 'pie' | 'doughnut'. |
table | { source, columns, rowHeight?, showHeader?, headerStyle?, footerLabel?, altBackground? } | A list inside a band (master–detail); columns: { header, expr, width?, format?, align?, sum? }. |
crosstab | { rowKey, columnKey, value, aggregate?, format?, totals?, corner?, source?, cellHeight?, rowHeaderWidth?, headerStyle? } | Rows × columns with an aggregated value per cell and totals. aggregate: sum | count | avg | min | max. |
Styles and borders
Inline styles merge over a named style (styleName), which merges over the report font. Borders take a shorthand per side, so table cells can share edges.
"styles": {
"th": { "bold": true, "align": "center", "background": "#eef1f6", "border": "0.2 solid #8a94a6" },
"num": { "align": "right", "border": { "bottom": "0.15 solid #b9c1cd" } }
},
"bands": { "detail": { "height": 6, "elements": [
{ "type": "field", "x": 140, "y": 0, "w": 40, "h": 6, "expr": "amount", "format": "#,##0", "styleName": "num", "style": { "bold": true } }
] } }| Name | Type | Description |
|---|---|---|
fontFamily, fontSize | string, number | Font family and size in points (default 9). |
bold, italic, underline, strike | boolean | Font styles. |
color, background | string | Text and background color. |
align, valign | 'left' | 'center' | 'right' | 'justify'; 'top' | 'middle' | 'bottom' | Horizontal and vertical alignment. |
padding | number | [t, r, b, l] | Inner padding in mm (default 1 horizontally, 0.5 vertically). |
border | true | "0.3 solid #333" | { top, right, bottom, left } | true = 0.2mm solid black; a CSS-like string; or per side. |
lineHeight | number | Multiple of the font size (default 1.25). |
wrap | boolean | Wrap long text (default true); false cuts at the box edge. |
shrinkToFit | boolean | Shrink the font until one line fits. |
letterSpacing | number | Letter spacing in mm (spaced titles like 견 적 서). |
Data and expressions
Expressions and functions
Fields, visibility rules, filters and text templates use a small, safe expression language: arithmetic, comparisons, && || ! ?? and ?:, member access (customer.name, items[0]) and the functions below. It has no access to globals and cannot assign. In text, write expressions in braces with an optional format after a colon: "{sum(amount):#,##0}원".
price * qty arithmetic
amount >= params.minAmount comparison with a parameter
customer.name ?? '(미지정)' member access and fallback
qty > 10 ? '대량' : '일반' condition
sum(amount) aggregate over the band's scope
sum(amount, 'page') page subtotal
sum(amount) / sum(amount, 'report') share of the grand total
format(date, 'yyyy년 M월 d일 (ddd)') 2026년 10월 15일 (목)
koreanAmount(1230000) 일금 일백이십삼만원정
bizNo('7928703276') 792-87-03276
{page} / {pages} page x / y in a text templateVariables
| Name | Type | Description |
|---|---|---|
page, pages | number | Page number and page count (per group with resetPageNumber). |
rowNum | number | Row number within the group (or report). |
rowIndex | number | Row number within the report. |
groupKey, groupLevel | unknown, number | Current group value and depth. |
now | Date | Time the report was laid out. |
params | object | Parameter values: params.from. |
row, rows, allRows, data | object, array | Current row, rows in scope, all rows, the whole data object. |
Aggregates and scopes
The scope is the second argument: 'group' (the current group), 'page' (rows on the current page — page subtotals) or 'report' (every row). Without it, aggregates use the band's natural scope: the group in group bands, the page in page bands, the whole report elsewhere.
| Name | Type | Description |
|---|---|---|
sum(expr, scope?) | number | Sum over the scope. |
avg / min / max(expr, scope?) | number | Average, minimum, maximum. |
count(expr?, scope?) | number | Rows (or non-empty values). |
distinct(expr, scope?) | number | Number of different values. |
first / last(expr, scope?) | unknown | Value of the first / last row. |
runningSum(expr, scope?) | number | Sum up to and including the current row (default scope report). |
Functions
| Name | Type | Description |
|---|---|---|
format(v, f) | string | Format a number or date: format(date, "yyyy-MM-dd"). |
koreanAmount(n) / korean(n) | string | 1230000 → 일금 일백이십삼만원정. |
bizNo(v), phone(v) | string | 1234567890 → 123-45-67890; 0212345678 → 02-1234-5678. |
round / floor / ceil(v, digits?) | number | Rounding to digits after the point. |
abs, number, string | — | Conversions. |
upper, lower, trim, len, left, right, substr, replace, concat, padStart, join | string | Text functions. |
iif(c, a, b), coalesce(...), isEmpty(v) | — | Conditions and fallbacks (?: and ?? work too). |
date, today, now, year, month, day, addDays, dateDiff, ymd | — | Dates. |
groupSum(rows, key, value), groupCount(rows, key) | array | Totals per key, handy as a chart source. |
min2(a, b), max2(a, b) | number | Smaller / larger of two values. |
Formats
A field's format (or the part after the colon in a template) formats numbers with Excel-style patterns and dates with yyyy, MM, dd, HH, mm, ss patterns.
| Format | Value | Description |
|---|---|---|
#,##0 #,##0.00 0.0% | number | Excel-style number patterns. |
₩#,##0 #,##0원 #,##0,"천" | number | Prefixes, suffixes and thousands scaling. |
#,##0;(#,##0);- | number | Sections: positive; negative; zero. |
yyyy-MM-dd HH:mm:ss | date | Dates and times. |
yyyy년 M월 d일 (ddd) | date | Korean dates with the weekday (월, 화…). |
a h:mm | date | 오전/오후 (AM/PM). |
korean, koreanNumber | number | 일금 ○○원정 / 십이만삼천. |
bizNo, upper, lower | string | Business number and case. |
Parameters
Parameters appear in the viewer's parameter panel and in expressions as params.name. With fetchData the viewer asks your server for the rows whenever the user presses Run.
// definition.params: { from: { label: '시작일', type: 'date', required: true },
// region: { label: '지역', type: 'select', options: [{ value: '' , label: '전체' }, { value: '서울' }] } }
createViewer(el, {
definition,
paramsOpen: true,
fetchData: params => fetch('/api/sales?' + new URLSearchParams(params)).then(r => r.json()),
});| Name | Type | Description |
|---|---|---|
label | string | Label in the parameter panel. |
type | 'string' | 'number' | 'date' | 'boolean' | 'select' | Input type. |
default | unknown | Initial value. |
options | { value, label? }[] | Choices for select. |
required | boolean | Must be filled before 조회 (Run). |
Barcodes, charts and marks
Barcodes
Barcodes are drawn as vector shapes, so they stay sharp at any zoom, in print and in the PDF. Supported: Code128, EAN-13 (12 digits; the check digit is added), QR (error correction L/M/Q/H), DataMatrix and PDF417. With a multi-column detail band (columns: 3) one row becomes one label on a label sheet. Label demo
"detail": { "height": 38, "columns": 3, "columnGap": 3, "elements": [
{ "type": "field", "x": 2, "y": 2, "w": 36, "h": 6, "expr": "name", "style": { "bold": true } },
{ "type": "barcode", "x": 40, "y": 2, "w": 18, "h": 18, "expr": "'https://example.com/a/' + assetNo", "format": "qr", "ecc": "M" },
{ "type": "barcode", "x": 2, "y": 24, "w": 56, "h": 11, "expr": "assetNo", "format": "code128" }
] }Charts, tables, crosstabs
A chart takes rows from its scope (or any array from source), a category per row and one or more series. groupSum() turns rows into totals per key. A table element lists an array inside a band (the items of each invoice); a crosstab tabulates rows × columns with totals. Charts demo · Crosstab demo
{ "type": "chart", "x": 0, "y": 16, "w": 110, "h": 60, "chartType": "bar", "title": "지역별 매출",
"source": "groupSum(rows, 'region', 'amount')", "category": "region",
"series": [{ "expr": "amount", "label": "매출" }], "format": "#,##0" },
{ "type": "table", "x": 0, "y": 20, "w": 180, "h": 20, "source": "items", "altBackground": "#fafbfd",
"columns": [{ "header": "품목", "expr": "name" }, { "header": "수량", "expr": "qty", "width": 20, "align": "right" },
{ "header": "금액", "expr": "qty * price", "width": 40, "format": "#,##0", "sum": true }] },
{ "type": "crosstab", "x": 0, "y": 0, "w": 180, "h": 50, "rowKey": "region", "columnKey": "category",
"value": "amount", "aggregate": "sum", "format": "#,##0", "corner": "지역 / 품목군" }Watermarks and embossing
A watermark is a string or settings, or a list of them. Because the text is a template, a security watermark can carry who printed the page and when, repeated over the page so a photo or copy still shows it.
"watermark": [
{ "text": "{params.user} · {format(now(), 'yyyy-MM-dd HH:mm')} 출력", "repeat": true, "opacity": 0.07, "fontSize": 10, "gap": 40 },
{ "text": "사 본", "fontSize": 110, "color": "#c8102e", "pages": "all" }
],
"emboss": { "text": "(주)세컨드코리아 대표이사 직인 ·", "centerText": "인증", "x": 140, "y": 240, "w": 34 }| Name | Type | Description |
|---|---|---|
text | string | Template, e.g. "{params.user} · {format(now(), 'yyyy-MM-dd HH:mm')}". |
image, imageWidth | string, number | Image mark instead of (or with) text; width in mm. |
repeat, gap | boolean, number | Tile over the page (security pattern); distance in mm (default 45). |
opacity, angle, color, fontSize, bold | — | Default opacity 0.12, angle -35°. |
pages | 'all' | 'first' | 'last' | 'odd' | 'even' | Which pages. |
layer | 'back' | 'front' | Behind the content (default) or over it. |
Web embossing
Embossing draws a raised seal, logo or text with light and shadow, like a stamp pressed into paper. It appears on screen, in print and in the PDF.
| Name | Type | Description |
|---|---|---|
text, centerText | string | Text around a round seal and in its centre (templates allowed). |
image | string | A logo turned into a relief. |
x, y, w, h | number | Position and size in mm (default a 32mm seal near the bottom right). |
shape | 'circle' | 'rect' | 'none' | Outline. |
depth, color, opacity, fontSize, pages | — | Relief depth in mm (default 0.3), paper tone, opacity, pages. |
Viewing and output
Viewer options
The viewer draws pages as SVG when they scroll into view. Keys: PageUp/PageDown, Ctrl+Home/End, Ctrl+P (print), Ctrl + wheel (zoom).
| Name | Type | Description |
|---|---|---|
definition | ReportDefinition | The report (JSON). |
data | ReportData | { rows: [...] } plus any other values used by expressions. |
fetchData | (params) => data | Promise | Load data for the parameters (on start and on 조회). |
params | object | Initial parameter values (over the defaults). |
lang | 'ko' | 'en' | Toolbar language (default ko). |
toolbar | { pages, zoom, print, pdf, xlsx, xlsxData, csv, params } | Buttons to show (csv is off by default). |
zoom | number | 'width' | 'page' | Initial zoom (default width). |
paramsOpen | boolean | Open the parameter panel at start. |
fileName | string | Base name of saved files. |
licenseKey | string | License key (or setLicenseKey once). |
onOutput | (e) => boolean | void | Promise | Before every print or export; return false to cancel. |
pdfSecurity | { userPassword?, ownerPassword?, permissions? } | PDF password and permissions (AES-256). |
author | string | PDF metadata author. |
Viewer methods
| Name | Type | Description |
|---|---|---|
refresh() | Promise<void> | Run the report again. |
setData(data) / setParams(p) / setDefinition(def) | Promise<void> | Change data, parameters or the definition and re-run. |
goTo(page) / setZoom(z) | void | Navigate and zoom. |
print() | Promise<boolean> | Print (false when onOutput cancelled). |
report | Report | The report object below (toPdf, toXlsx…). |
destroy() | void | Remove the viewer. |
Designer
createDesigner mounts the web designer: a toolbox, a data field list, the bands, a property panel, preview and JSON views, undo and redo. Give it sample data so the field list and the preview have something to show. Try the designer
import { createDesigner } from '@freereportjs/core';
import '@freereportjs/core/styles.css';
const designer = createDesigner(document.getElementById('designer'), {
definition, // leave out to start with a blank report
data: { rows }, // sample data for the field list and the preview
lang: 'ko',
onChange: def => localStorage.setItem('draft', JSON.stringify(def)),
onSave: def => fetch('/api/reports/sales', { method: 'PUT', body: JSON.stringify(def) }),
});
designer.setMode('preview'); // 'design' | 'preview' | 'json'
const json = designer.getDefinition();Print, PDF, Excel, CSV
Printing uses the browser at the exact paper size of the report, with the same line breaks as on screen. PDF embeds a subset of the TrueType fonts, so text is searchable and copyable. Excel "form" keeps the printed layout as merged cells with borders; "data" writes one row per record for further work.
import { createReport } from '@freereportjs/core';
const report = createReport(definition, { rows });
await report.ready(); // wait for web fonts, then lay out
await report.print(); // browser print at the paper size
const pdf = await report.toPdf(); // Blob, fonts embedded
const form = await report.toXlsx('form'); // merged cells and borders, like the printout
const data = await report.toXlsx('data'); // one row per record
await report.download('pdf', '매출 보고서'); // or save directly| Name | Type | Description |
|---|---|---|
createReport(def, data, options?) | Report | Bind a definition to data without a viewer. |
ready() | Promise<LayoutResult> | Wait for web fonts, then lay out. Call before the first output in browsers. |
layout() | LayoutResult | Pages, row count and time (cached). |
print({ pages? }) | Promise<boolean> | Print through the browser at the exact paper size. |
toPdf() | Promise<Blob | null> | PDF with embedded font subsets. |
toXlsx('form' | 'data') | Promise<Blob | null> | "form" keeps merged cells and borders; "data" writes one row per record. |
toHwpx() | Promise<Blob | null> | Hangul document (.hwpx, KS X 6101) laid out like the printed pages. |
toCsv() | Promise<Blob | null> | CSV (UTF-8 with BOM, so Excel shows Korean correctly). |
download('pdf' | 'xlsx' | 'xlsx-data' | 'csv' | 'hwpx', name?) | Promise<boolean> | Create and save the file. |
Fonts
Nanum Gothic and Nanum Myeongjo are built in (SIL Open Font License): a web stylesheet for screen and print, TrueType files for PDF, loaded from the jsDelivr CDN when first needed. registerFont adds your own family; host the files yourself on an intranet.
import { registerFont } from '@freereportjs/core';
registerFont('Malgun Gothic', {
css: '/fonts/malgun.css', // @font-face rules for screen and print (optional)
normal: '/fonts/malgun.ttf', // TrueType for the PDF (URL or Uint8Array)
bold: '/fonts/malgunbd.ttf',
});
// then: "font": { "family": "Malgun Gothic" } or "style": { "fontFamily": "Malgun Gothic" }Server, security and operations
Server rendering
renderPdf and renderXlsx lay out and write files without a browser (Node, Deno, Bun, workers). Text is measured with the TrueType fonts that the PDF embeds, so line breaks match. Use it for batch jobs, scheduled reports and e-mail attachments.
// Node 20+, Deno or Bun — no browser needed
import { writeFile } from 'node:fs/promises';
import { loadFonts, renderPdf, renderXlsx } from '@freereportjs/core';
const fonts = await loadFonts(['Nanum Gothic']); // TTF bytes (or read your own files)
const pdf = await renderPdf(definition, { rows }, { fonts, params: { user: 'batch' } });
await writeFile('sales.pdf', pdf);
await writeFile('sales.xlsx', await renderXlsx(definition, { rows }, { fonts, mode: 'form' }));PDF security
pdfSecurity encrypts the PDF with AES-256 (PDF 2.0 standard security). With a user password the file asks for it when opened; permissions limit printing, copying and changes in PDF readers. A random owner password is used when you give none.
createViewer(el, {
definition, data,
pdfSecurity: {
userPassword: '1234', // asked when the PDF is opened (omit: opens freely)
permissions: { print: true, copy: false, modify: false },
},
});Output log hook
onOutput is called before every print and export with the kind, report name, pages, rows and parameters. Write it to your output log (required for personal-data systems in Korea), ask for a reason, or return false to block it. Output control demo
createViewer(el, {
definition, data, params: { user: '홍길동' },
// e = { kind: 'print' | 'pdf' | 'xlsx' | 'xlsx-data' | 'csv', report, pages, rows, params, fileName }
onOutput: async e => {
const res = await fetch('/api/output-log', { method: 'POST', body: JSON.stringify(e) });
return res.ok; // false cancels the print or download
},
});License key
Personal use needs no key. Companies get a key for their domains (FR1.…) and set it once, or pass licenseKey. Without a valid key on a public domain the library prints one notice in the developer console; nothing on screen changes and no feature is turned off. Local and private network addresses never need a key. Pricing
import { setLicenseKey } from '@freereportjs/core';
setLicenseKey('FR1.eyJkb21haW5zIjpb...'); // once, before creating viewers
// or: createViewer(el, { definition, data, licenseKey: 'FR1.eyJkb21haW5zIjpb...' })Help
Asking an AI assistant
When you ask ChatGPT, Claude, Gemini or Cursor to write a FreeReport definition, have it read https://freereport.app/llms-full.txt first: every option, element, function and format in one file. Ask it not to invent options that are not in the file.