Generate and read back official documents
A business system submits official-document JSON and gets an editable document laid out to GB/T 9704; after editing, the same data format reads back.
A business system, such as forms with a back-end service, only assembles one JSON value following the protocol on this page.
@quirekit/official-document lays out the head, red rule, title, structure numbering, sign-off, imprint and page numbers to
GB/T 9704-2012. The result edits in Classic like any document and exports to Word, PDF and OFD. Its elements carry
GB/T 33476.2 official-document fields, so the edited document reads back as the same JSON for the business system.
@quirekit/official-document is a commercial component; obtain the distribution and license first, see Commercial use.
1. Flow
- The business system builds official-document JSON from the form (sections 2-8).
- The front end validates it, compiles it and loads the document into Classic.
- People edit and export online; when needed, read the content back as JSON (section 10) for the business system to store.
import { validateOfficialDocumentData } from '@quirekit/official-document'
import { readOfficialDocumentData } from '@quirekit/official-document/classic'
const checked = validateOfficialDocumentData(await response.json())
if (!checked.ok) showIssues(checked.issues) // each issue has a path and a Chinese message
else instance.editor?.commands.composeOfficialDocument(checked.data) // replaces the whole document in one undoable edit
// After editing, read the data back with its seals and pictures
const extracted = readOfficialDocumentData(instance.editor!)
if (extracted.ok) await saveToBusinessSystem(extracted.data, extracted.warnings)
composeOfficialDocument comes with the official-document extension: @quirekit/classic-ai installs it, and otherwise
install createClassicOfficialDocumentPlugin() from @quirekit/official-document/classic. It registers the data's seals and
pictures with the editor's media before replacing the document, and returns false without changing the document when the
data is invalid. Without the command, register collectOfficialDocumentImages(data) with the media first, then load the
result of compileOfficialDocument(data); read back with extractOfficialDocumentData(instance.getJSON(), { images }).
Saving, versions and file conversion are covered in Save, open and convert files.
Word files can also be produced without a browser, by compiling in a Node service and exporting with the Classic DOCX service. Node has no browser font measurement, so whether the even-page imprint shares the last body page is judged with estimated glyph widths; check the file in Word before delivery.
2. Complete example
A notice with a copy number, classification and urgency:
{
"version": 1,
"kind": "notice",
"issuer": { "agency": "示例市政务服务管理局" },
"copyNumber": "000123",
"secrecy": { "level": "秘密", "term": "1年" },
"urgency": "加急",
"documentNumber": "示政发〔2026〕12号",
"title": "示例市政务服务管理局关于2027年元旦放假安排的通知",
"recipients": ["各处室", "各直属单位"],
"body": [
{ "type": "paragraph", "text": "根据国务院办公厅通知精神,结合工作实际,现将2027年元旦放假安排通知如下。" },
{ "type": "heading", "level": 1, "title": "放假时间" },
{ "type": "paragraph", "text": "2027年1月1日至3日放假调休,共3天。" },
{ "type": "heading", "level": 1, "title": "工作要求" },
{ "type": "heading", "level": 2, "title": "严格值班值守。", "text": "各单位要安排好节日期间值班。" },
{ "type": "heading", "level": 2, "title": "做好安全检查。", "text": "节前开展一次安全隐患排查。" },
{ "type": "paragraph", "text": "特此通知。" }
],
"attachments": ["值班安排表", "联系人名单"],
"date": "2026-12-20",
"note": "联系人:李某,电话:12345678",
"imprint": { "cc": ["示例市人民政府办公室"], "office": "示例市政务服务管理局办公室", "date": "2026-12-20" }
}
3. Fields
Only kind, issuer.agency and body are required. Omit fields without content: an optional field written as null
or an empty string is rejected.
| Field | Type | Meaning and rules |
|---|---|---|
version | 1 | Protocol version; may be omitted |
kind | kind value | See section 5 |
format | "letter" | Letter format (section 6), usually for a letter (函); omitted uses the document format |
issuer.agency | text | Issuing agency's full or standard short name, used in the sign-off; the lead agency of a joint document |
issuer.joint | text array | Co-issuing agencies of a joint document, in order after the lead agency |
issuer.mark | text | Agency mark; defaults to agency + 文件 (+ 令 for an order, + 会议纪要 for minutes); \n breaks lines |
copyNumber | digit string | Copy number, usually six digits such as 000123 |
secrecy.level | 秘密, 机密, 绝密 | Classification level |
secrecy.term | text | Period, declassification time or condition such as 1年; set as 机密★1年 |
urgency | 特急, 加急 | Urgency; telegrams also use 特提 and 平急 |
documentNumber | text | Document number with the year in hexagonal brackets, such as 示政发〔2026〕12号; an order number 第1号 or minutes issue 第1期 |
signers | text array | Signers of an upward document (request, report), one per issuing agency in agency order |
title | text | Title; a long title breaks before 关于 automatically, or use \n; not used for orders and minutes |
adoption | text | Adoption statement such as (2026年10月17日……会议通过), for resolutions and similar kinds |
recipients | text array | Main recipients, one agency per item, without 、 or :; not used for announcements, public notices and communiqués |
body | block array | See section 4 |
attachments | text array | Attachment names without trailing punctuation; numbered when more than one |
signature | sign-off | See section 7 |
date | YYYY-MM-DD | Date of the document, written as 2026年12月20日; omitted leaves 年 月 日 to fill in |
note | text | Note, without brackets |
attendees | list array | Minutes attendance: { "label": "出席", "names": ["张某", "李某"] }, labels 出席, 请假, 列席 |
imprint.cc | text array | Copied agencies |
imprint.office | text | Printing and distributing office, usually the issuer's general office; omitted in the letter format |
imprint.date | YYYY-MM-DD | Printing date, omitted in the letter format; omit the whole imprint when the document has none |
4. Body
body lists blocks in reading order. There are four kinds:
| Block | Form | Layout |
|---|---|---|
| Paragraph | { "type": "paragraph", "text": "…" } | FangSong 16 pt with a two-character indent; do not start with spaces |
| Heading | { "type": "heading", "level": 1, "title": "…" } | Levels 1-4 are numbered 一、, (一), 1. and (1); lower levels restart under each upper heading |
| Table | { "type": "table", "title": "…", "header": [[…]], "rows": [[…]] } | Full body width with 0.5 pt single rules; title centred above (SimHei), header rows in SimHei, centred and repeated on every page; rows never split |
| Image | { "type": "image", "src": "data:image/png;base64,…", "caption": "…" } | On its own line, centred, caption centred below |
- Do not write ordinals in titles; they are generated. The first heading must be level 1.
- When body text continues on the heading's line, end
titlewith 。 and put the text intext:{ "type": "heading", "level": 2, "title": "严格值班值守。", "text": "各单位要安排好节日期间值班。" }.
Tables:
{
"type": "table",
"title": "表1 2027年培训安排",
"header": [["时间", "内容", "对象"]],
"rows": [["3月", "公文格式", "各处室"], ["4月", "公文处理\n办文流程", "办公室"]],
"columns": [{ "width": 1, "align": "center" }, { "width": 3 }, { "width": 2, "align": "center" }],
"fontSize": 14
}
| Field | Meaning |
|---|---|
title | Table title, optional |
header | Header rows, optional; repeated on every page the table spans |
rows | Body rows; every row, header rows included, has one string per column, without merged cells; \n in a cell starts a new paragraph |
columns | One entry per column: width is relative (equal by default), align is start (default), center or end |
fontSize | Table text size: 16 (三号, default), 14 (四号) or 12 (小四) |
Images: src is a PNG or JPEG data URL; width is in mm, by default the image's size at 96 dpi and at most the 156 mm body
width, the height following the image; caption is optional. Images live in the editor's media like seals, see section 7.
Horizontal tables (GB/T 9704 §8) are not supported yet.
5. Document kinds
kind | Kind | Notes |
|---|---|---|
resolution | 决议 | Usually unsigned, often with adoption |
decision | 决定 | |
order | 命令(令) | Order head without title or recipients; documentNumber is the order number; signed by a leader |
communique | 公报 | Usually without recipients or sign-off |
announcement | 公告 | Usually without recipients |
public-notice | 通告 | Usually without recipients |
opinion | 意见 | |
notice | 通知 | |
circular | 通报 | |
report | 报告 | Upward document; write signers |
request | 请示 | Upward document; write signers |
reply | 批复 | |
proposal | 议案 | Signed by a leader |
letter | 函 | |
minutes | 纪要 | Minutes head without title or recipients; write attendees; usually unsigned |
Orders and minutes cannot be joint documents; orders take no copy number, classification or urgency.
6. Letter format
With "format": "letter" the document follows GB/T 9704 10.1: the agency mark is the agency name without 文件, its top edge
30 mm below the page top; 4 mm below it a red double rule (thick over thin), and 20 mm above the page bottom another (thin over
thick), both 170 mm long. Copy number, classification and urgency sit flush left and the document number flush right, with the
title two lines below; the first page shows no page number; the imprint lists copied agencies only (imprint with cc) at the
bottom of the last page. A joint letter uses the lead agency's mark, while the sign-off still lists every agency. The letter format
takes no signers and is not available for orders or minutes.
{
"kind": "letter",
"format": "letter",
"issuer": { "agency": "示例市政务服务管理局" },
"documentNumber": "示政函〔2026〕5号",
"title": "关于商请协助开展公文规范培训的函",
"recipients": ["示例市档案局"],
"body": [{ "type": "paragraph", "text": "专此函达。" }],
"date": "2026-12-20",
"imprint": { "cc": ["示例市财政局"] }
}
7. Sign-off and joint documents
Without signature the document is signed by the agency (the unsealed layout). The other forms:
- Sealed:
{ "type": "sealed" }, see "Seals and signature stamps" below - Signers' signature stamps:
{ "type": "stamp", "stamps": [...] }, see below - Leader sign-off (orders, proposals):
{ "type": "leader", "position": "市长", "name": "张某" } - No sign-off (resolutions, communiqués, minutes):
{ "type": "none" }
For a joint document, list the co-issuing agencies in issuer.joint: the agency names stack in the head with 文件 centred on
their right, and the sign-off lists the lead agency first and the others below. Signers of an upward document sit two per line
in agency order:
{
"kind": "request",
"issuer": { "agency": "示例市政务服务管理局", "joint": ["示例市财政局"] },
"documentNumber": "示政请〔2026〕3号",
"signers": ["张某", "李某"],
"title": "关于联合开展公文规范培训的请示",
"recipients": ["示例市人民政府"],
"body": [
{ "type": "paragraph", "text": "为提高公文办理质量,拟联合开展公文规范培训。现将有关事项请示如下。" },
{ "type": "heading", "level": 1, "title": "培训安排" },
{ "type": "paragraph", "text": "拟于2027年3月组织一次专题培训。" },
{ "type": "paragraph", "text": "妥否,请批示。" }
],
"date": "2026-12-20"
}
Seals and signature stamps
Laid out to GB/T 9704-2012 §7.3.5.1 and §7.3.5.3, for either setup:
- With an e-signature system: give no images. The document lays out the sign-off and reserves the seal positions (invisible,
moving with pagination); after export the e-signature system seals at the coordinates
locateOfficialSealsreports. - Without one: give the seal images (PNG data URLs with transparency) in the data; they are placed in the document and included in Word, PDF and OFD exports.
{ "type": "sealed", "seals": ["data:image/png;base64,iVBORw0..."], "diameter": 42 }
| Field | Meaning |
|---|---|
seals | One per issuing agency in agency order; omit to reserve every position. The count must equal the number of agencies |
diameter | Seal diameter in mm, 20–60, default 42 |
A sealed date stands four characters from the right, the agency is centred on it, and the seal is concentric with them, its top half a line below the body. A joint document sets its seals in a row in agency order, the last one on the date and the others to its left, 2 mm apart; when the agency names are too long to sit under their seals side by side, the seals stack one per row.
{ "type": "stamp", "stamps": [{ "position": "示例市人民政府市长", "image": "data:image/png;base64,iVBORw0..." }], "height": 15 }
| Field | Meaning |
|---|---|
stamps | One per issuing agency in agency order: position is the signer's full title, image the stamp, omitted to reserve the position |
height | Stamp height in mm, 8–30, default 15; the width follows the image, or 2.5 × the height without one |
The stamp stands two blank lines below the body and four characters from the right, the title two characters to its left and centred on it, and the date one blank line below the stamp, four characters from the right; a joint document has one agency per row.
In the browser, @quirekit/official-document/classic reports the positions:
import { locateOfficialSeals } from "@quirekit/official-document/classic";
// [{ kind: "seal", number: 1, reserved: true, page: 1, x: 120.68, y: 215.62, width: 42, height: 42 }]
// page counts from 1; x, y, width and height are millimetres from the page's top-left corner, as in PDF export.
const positions = locateOfficialSeals(instance.editor!);
Images are kept in the editor's media rather than in the document JSON; composeOfficialDocument registers them before replacing
the document.
8. Back-end validation
schema/official-document.schema.json in the distribution (JSON Schema 2020-12) describes the field structure, so a back end
can validate first with a JSON Schema 2020-12 validator in its own language. validateOfficialDocumentData also checks
the rules a schema cannot state:
- every kind except orders and minutes needs a title;
- dates must be real calendar days;
- the first heading is level 1;
- orders take no copy number, classification or urgency, and orders and minutes cannot be joint documents;
- there are no more signers than issuing agencies, and
attendeesis for minutes only; - the letter format is not for orders or minutes and takes no signers; outside it, an imprint needs
officeanddate; - there are as many seals (
seals) or signature stamps (stamps) as issuing agencies; - every table row, header rows included, has one cell per column.
A failed validation returns every problem with a JSON Pointer path and a Chinese message, ready to show next to the form field:
[
{ "path": "/issuer", "message": "缺少必填字段:agency。" },
{ "path": "/date", "message": "格式不对,日期请写成 YYYY-MM-DD。" }
]
9. Integration notes
The interface is language-neutral: the business system builds JSON that follows this protocol in any language and hands it to the front end over HTTP. When assembling the data:
- field names use the protocol's camelCase, such as
documentNumberandfontSize; - leave unset fields out rather than writing
nullor empty strings (a paragraph'stextmust not be empty); - write dates as
2026-12-20; - headings carry no ordinals such as 一、 or (一); the program numbers them;
- every table row, header rows included, has one cell per column, and there are as many seals or signature stamps as issuing agencies;
- seals, signature stamps and body pictures are data URLs: PNG with transparency for seals and stamps, PNG or JPEG for pictures.
A back end can first validate the structure with schema/official-document.schema.json from the distribution (section 8).
Read-back JSON has the same structure, so the types used to generate it also deserialize it.
10. Read back
extractOfficialDocumentData(document, options?) reads editor content (instance.getJSON()) or an imported DOCX and returns
{ ok: true, data, warnings } or { ok: false, issues }:
- Only text inside the official-document fields is read; paragraphs started outside them are not, while paragraphs added inside the body are.
- Pending revisions read as if all were accepted; the document's revisions are not changed.
- Body tables and pictures read back as table and image blocks; content the protocol cannot hold, such as a table with merged
cells, is skipped with its position in
warnings, and an unreadable date stays empty with a warning. Column widths read back as the twips they resolved to, in the same proportions. - Joint agencies come from the sign-off; a mismatch with the names in the head produces a warning.
- A document edited in Microsoft Word reads back after import (verified with Mac Word 16.109.2).
- While editing online, Document elements on the View ribbon opens a right-side panel listing the document's elements by head,
body and imprint, edited like a form; changes go into the matching fields and can be undone. The body and attachment note show
a summary and can be located. Import
@quirekit/official-document/styles.cssfor the panel's styles. - Seal and signature-stamp images live in the editor's media and read back only with
{ images: mediaResolver }; in the browser,readOfficialDocumentData(editor)from@quirekit/official-document/classicpasses it. An unreadable image produces a warning and reads back as a reserved position.
Unedited data compiles back into the same document.
11. Current scope
Supported: the fifteen kinds above in the document and letter formats, copy number, classification and term, urgency, signers, joint documents, four heading levels, body tables and pictures, attachment notes, agency, sealed (reserved or with images), signature-stamp, leader and no sign-off, notes, minutes attendance, copied agencies and printing, odd/even page numbers, and Word/PDF/OFD export.
Not yet supported: horizontal tables and merged table cells. Whether WPS and other official-document systems recognize the fields is unverified; PDF and OFD exports contain the layout only, without the fields.