O3O DocBuilder và chuyển đổi
o3oscript cho văn bản
Mọi thao tác của kịch bản type = text: khổ giấy, kiểu chữ, đầu và chân trang, đoạn chữ định dạng, tiêu đề, đoạn văn, danh sách, bảng, ảnh và ngắt trang, mỗi thao tác có bảng tham số và ví dụ.
Trong trang này
Kịch bản type = "text" dựng một văn bản. Nội dung nằm trong mảng body (1 tới 20.000 khối) theo thứ tự xuất hiện; mỗi khối có khoá type là một trong heading, paragraph, list, table, image, page_break. Định dạng lưu: docx, odt, pdf.
Khổ giấy và lề: page#
Đối tượng page
sizeA3 | A4 | A5 | Letter | Legal | đối tượngtuỳ chọnMặc định:A4Khổ giấy có tên, hoặc{"width", "height"}tính bằng mm (50 tới 2000).orientationportrait | landscapetuỳ chọnMặc định:portraitHướng giấy.marginobjecttuỳ chọnMặc định:20 mm mỗi cạnhLề trang.margin.top / right / bottom / leftnumber 0–2000 (mm)tuỳ chọnMặc định:20Lề từng cạnh.
Đối tượng style (kiểu chữ mặc định)
fontstringtuỳ chọnFont mặc định.sizenumber 4–400 (pt)tuỳ chọnCỡ chữ mặc định.color#RRGGBBtuỳ chọnMàu chữ mặc định.line_spacingnumber 50–300 (%)tuỳ chọnGiãn dòng; 100 là dòng đơn.
{
"page": {
"size": "A4",
"orientation": "portrait",
"margin": {
"top": 20,
"right": 15,
"bottom": 20,
"left": 30
}
},
"style": {
"font": "Liberation Serif",
"size": 13,
"color": "#000000",
"line_spacing": 120
}
}Đầu trang và chân trang: header, footer#
Đầu trang và chân trang là một dòng, áp cho mọi trang. content ghép từ chuỗi trơn, đối tượng run và trường tự cập nhật.
Đối tượng header hoặc footer
contentarray, 1–50 phần tửbắt buộcMỗi phần tử là chuỗi (≤ 1000 ký tự), đối tượng run, hoặc trường{"field": ...}.alignleft | center | righttuỳ chọnMặc định:centerCăn lề.sizenumber 4–400 (pt)tuỳ chọnCỡ chữ.color#RRGGBBtuỳ chọnMàu chữ.
Trường tự cập nhật
fieldpage_number | page_count | datebắt buộcpage_number: số trang hiện tại;page_count: tổng số trang;date: ngày dựng tài liệu dạng dd/MM/yyyy.
{
"header": {
"content": [
{
"text": "CÔNG TY CỔ PHẦN O3O",
"bold": true
},
" · Tài liệu nội bộ"
],
"align": "left",
"size": 9,
"color": "#555555"
},
"footer": {
"content": [
"Trang ",
{
"field": "page_number"
},
"/",
{
"field": "page_count"
},
" · Ngày in: ",
{
"field": "date"
}
],
"align": "right",
"size": 9
}
}Đoạn chữ định dạng: run#
Nơi nào nhận runs (tiêu đề, đoạn văn, mục danh sách, ô bảng) và nội dung đầu, chân trang đều dùng cùng đối tượng run: một đoạn chữ liền nhau có chung định dạng. Khối có cả text và runs thì runs được dùng.
Đối tượng run
textstring, ≤ 100.000bắt buộcChữ của đoạn.boldbooleantuỳ chọnChữ đậm.italicbooleantuỳ chọnChữ nghiêng.underlinebooleantuỳ chọnGạch chân.strikebooleantuỳ chọnGạch ngang.superscriptbooleantuỳ chọnChỉ số trên.subscriptbooleantuỳ chọnChỉ số dưới.color#RRGGBBtuỳ chọnMàu chữ.background#RRGGBBtuỳ chọnMàu nền (tô sáng).sizenumber 4–400 (pt)tuỳ chọnCỡ chữ.fontstringtuỳ chọnTên font.linkURL, ≤ 2048tuỳ chọnBiến đoạn chữ thành siêu liên kết.
{
"type": "paragraph",
"runs": [
{
"text": "Hạn nộp hồ sơ: "
},
{
"text": "30/09/2026",
"bold": true,
"color": "#C00000"
},
{
"text": ". Hướng dẫn tại "
},
{
"text": "office.o3o.vn",
"link": "https://office.o3o.vn",
"underline": true,
"color": "#1D55B8"
},
{
"text": ". Ký hiệu hoá học của nước là H"
},
{
"text": "2",
"subscript": true
},
{
"text": "O."
}
]
}Tiêu đề: heading#
Khối heading
type"heading"bắt buộcLoại khối.levelinteger 1–6bắt buộcCấp tiêu đề.textstring, ≤ 2000tuỳ chọnChữ trơn. Cầntexthoặcruns.runsarray run, 1–200tuỳ chọnChữ có định dạng riêng.alignleft | center | right | justifytuỳ chọnCăn lề.color#RRGGBBtuỳ chọnMàu chữ.page_break_beforebooleantuỳ chọnMặc định:falseSang trang mới trước tiêu đề.
{
"type": "heading",
"level": 2,
"text": "Điều 3. Giá trị hợp đồng",
"color": "#1D55B8"
}Tiêu đề ánh xạ sang kiểu đoạn Heading 1 tới Heading 6, nên xuất hiện trong ngăn điều hướng của trình soạn thảo và thành dấu trang khi xuất PDF.
Đoạn văn: paragraph#
Khối paragraph
type"paragraph"bắt buộcLoại khối.textstring, ≤ 100.000tuỳ chọnChữ trơn; ký tự xuống dòng\nthành ngắt dòng trong đoạn. Cầntexthoặcruns.runsarray run, 1–2000tuỳ chọnChữ có định dạng riêng.alignleft | center | right | justifytuỳ chọnCăn lề.spacing_beforenumber (mm)tuỳ chọnKhoảng cách trước đoạn.spacing_afternumber (mm)tuỳ chọnKhoảng cách sau đoạn.line_spacingnumber 50–300 (%)tuỳ chọnGiãn dòng riêng của đoạn.indent_leftnumber (mm)tuỳ chọnThụt lề trái.indent_rightnumber (mm)tuỳ chọnThụt lề phải.indent_firstnumber −200–200 (mm)tuỳ chọnThụt dòng đầu; số âm là thụt treo.keep_with_nextbooleantuỳ chọnMặc định:falseGiữ đoạn cùng trang với đoạn sau.page_break_beforebooleantuỳ chọnMặc định:falseSang trang mới trước đoạn.
{
"type": "paragraph",
"text": "Căn cứ Bộ luật Dân sự năm 2015;\nCăn cứ nhu cầu và khả năng của hai bên.",
"align": "justify",
"indent_first": 10,
"spacing_after": 3,
"line_spacing": 130
}Danh sách: list#
Khối list
type"list"bắt buộcLoại khối.stylebullet | numbertuỳ chọnMặc định:bulletGạch đầu dòng hoặc đánh số.itemsarray, 1–5000 mụcbắt buộcMỗi mục là chuỗi (≤ 10.000 ký tự) hoặc đối tượng mục.
Đối tượng mục danh sách
textstring, ≤ 10.000tuỳ chọnChữ trơn. Cầntexthoặcruns.runsarray run, 1–200tuỳ chọnChữ có định dạng riêng.itemsarray, 1–1000 mụctuỳ chọnDanh sách con, cùng kiểu với danh sách cha.
{
"type": "list",
"style": "number",
"items": [
"Bên A thanh toán trong 7 ngày làm việc.",
{
"text": "Bên B có trách nhiệm:",
"items": [
"Bàn giao token bản quyền",
{
"runs": [
{
"text": "Hỗ trợ kỹ thuật "
},
{
"text": "trong giờ hành chính",
"italic": true
}
]
}
]
},
"Hai bên cùng bảo mật thông tin."
]
}Bảng: table#
Khối table
type"table"bắt buộcLoại khối.columnsarray, 1–63 cộttuỳ chọnMỗi cột{"width", "align"}:widthlà bề rộng tương đối 1 tới 1000 (mặc định 1).headerarray ô, 1–63tuỳ chọnHàng tiêu đề.rowsarray hàng, 1–10.000bắt buộcMỗi hàng là mảng 1 tới 63 ô.borderfalse | đối tượng viềntuỳ chọnfalse: không kẻ; đối tượng: kẻ mọi đường, viền ngoài lẫn lưới trong.header_fill#RRGGBBtuỳ chọnMàu nền hàng tiêu đề.header_color#RRGGBBtuỳ chọnMàu chữ hàng tiêu đề.header_boldbooleantuỳ chọnMặc định:trueChữ đậm ở hàng tiêu đề.zebra_fill#RRGGBBtuỳ chọnMàu nền các hàng dữ liệu chẵn.repeat_headerbooleantuỳ chọnMặc định:trueLặp hàng tiêu đề khi bảng sang trang.width_percentnumber 10–100tuỳ chọnMặc định:100Bề rộng bảng theo phần trăm vùng chữ.alignleft | center | righttuỳ chọnMặc định:leftVị trí bảng khiwidth_percentnhỏ hơn 100.sizenumber 4–400 (pt)tuỳ chọnCỡ chữ trong bảng.cell_paddingnumber 0–20 (mm)tuỳ chọnMặc định:1.5Khoảng đệm trong ô.spacing_afternumber (mm)tuỳ chọnKhoảng cách sau bảng.
Ô bảng dạng đối tượng
textstring, ≤ 20.000tuỳ chọnChữ trơn. Cầntexthoặcruns.runsarray run, 1–200tuỳ chọnChữ có định dạng riêng.alignleft | center | right | justifytuỳ chọnCăn ngang.valigntop | middle | bottomtuỳ chọnCăn dọc.boldbooleantuỳ chọnChữ đậm.italicbooleantuỳ chọnChữ nghiêng.color#RRGGBBtuỳ chọnMàu chữ.fill#RRGGBBtuỳ chọnMàu nền ô.
Đối tượng viền
widthnumber 0.25–6 (pt)tuỳ chọnMặc định:0.5Độ dày nét.color#RRGGBBtuỳ chọnMàu nét.
{
"type": "table",
"columns": [
{
"width": 1,
"align": "center"
},
{
"width": 5
},
{
"width": 2,
"align": "right"
}
],
"header": [
"STT",
"Hạng mục",
"Số tiền (đồng)"
],
"rows": [
[
1,
"Thiết kế giao diện",
"8.000.000"
],
[
2,
"Lập trình và kiểm thử",
"24.000.000"
],
[
{
"text": "Cộng",
"bold": true,
"align": "right"
},
null,
{
"text": "32.000.000",
"bold": true,
"fill": "#EAF1FB"
}
]
],
"border": {
"width": 0.5,
"color": "#7F7F7F"
},
"header_fill": "#1D55B8",
"header_color": "#FFFFFF",
"zebra_fill": "#F7F9FC",
"size": 11,
"spacing_after": 4
}Gộp ô trong bảng văn bản.
Ảnh: image#
Khối image
type"image"bắt buộcLoại khối.srcobjectbắt buộcNguồn ảnh:{"url"}hoặc{"base64", "mime"}.widthnumber (mm)tuỳ chọnBề rộng. Chỉ có một cạnh thì cạnh kia theo tỷ lệ gốc.heightnumber (mm)tuỳ chọnChiều cao.alignleft | center | righttuỳ chọnMặc định:leftCăn lề của ảnh.captionstring, ≤ 1000tuỳ chọnChú thích: một đoạn căn giữa, chữ nghiêng, ngay dưới ảnh.altstring, ≤ 1000tuỳ chọnChữ thay thế cho người dùng trình đọc màn hình.
Nguồn ảnh src
urlURL http/https, ≤ 2048tuỳ chọnDocBuilder tự tải ảnh, chịu quy tắc chống SSRF. DùngurlHOẶCbase64.base64stringtuỳ chọnDữ liệu ảnh Base64 chuẩn, KHÔNG có tiền tốdata:, tối đa 10 MB sau giải mã.mimeimage/png | image/jpeg | image/gif | image/svg+xmltuỳ chọnMặc định:image/pngKiểu ảnh khi dùng base64.
{
"type": "image",
"src": {
"url": "https://example.com/images/so-do-to-chuc.png"
},
"width": 120,
"align": "center",
"caption": "Hình 1. Sơ đồ tổ chức",
"alt": "Sơ đồ tổ chức công ty"
}{
"type": "image",
"src": {
"base64": "iVBORw0KGgoAAAANSUhEUgAAAKAAAABaCAIAAACwpMoFAAAAsUlEQVR42u3RMQ0AIAwAwapgIYhhZccMyhiRhoA6aC55BX8x9lXhwgLAAizAAizAAizAgAVYgAVYgAVYgAELsAALsAALsAALMGABFmABFmABVgJ+q6twgAELsAALsAALsAADFmABFmABFmABBizAAizAAizAAizAgAVYgAVYgAVYGbidqcIBBizAAizAAizAAgxYgAVYgAVYgAUYsAALsAALsAALsAADFmABFmABFmClPpawm2fxo+U6AAAAAElFTkSuQmCC",
"mime": "image/png"
},
"height": 20,
"alt": "Ba dải màu"
}Ngắt trang: page_break#
{
"type": "page_break"
}Khối page_break chỉ có khoá type. Muốn một tiêu đề hay đoạn văn luôn bắt đầu ở trang mới, đặt page_break_before: true ngay trên khối đó.
Ví dụ đầy đủ: báo giá#
bao-gia.json dựng một báo giá hai trang: đầu trang, chân trang đánh số, tiêu đề, đoạn chữ định dạng, bảng có hàng cộng, danh sách lồng và ngắt trang. Kịch bản có 28 đơn vị.
{
"o3oscript": 1,
"type": "text",
"meta": {
"title": "Báo giá O3O Office Online",
"author": "Công ty Cổ phần O3O",
"subject": "Báo giá bản quyền theo số kết nối",
"keywords": [
"báo giá",
"O3O Office Online"
],
"lang": "vi-VN"
},
"page": {
"size": "A4",
"orientation": "portrait",
"margin": {
"top": 20,
"right": 20,
"bottom": 20,
"left": 25
}
},
"style": {
"font": "Liberation Sans",
"size": 11,
"line_spacing": 115
},
"header": {
"content": [
{
"text": "O3O Office Online",
"bold": true,
"color": "#1D55B8"
},
" · Báo giá số BG-2026-0917"
],
"align": "left",
"size": 9
},
"footer": {
"content": [
"Trang ",
{
"field": "page_number"
},
" / ",
{
"field": "page_count"
}
],
"align": "center",
"size": 9,
"color": "#666666"
},
"body": [
{
"type": "heading",
"level": 1,
"text": "BÁO GIÁ BẢN QUYỀN",
"align": "center",
"color": "#1D55B8"
},
{
"type": "paragraph",
"align": "center",
"spacing_after": 6,
"runs": [
{
"text": "Số: BG-2026-0917",
"italic": true
},
{
"text": " · Ngày lập: 21/09/2026",
"italic": true
}
]
},
{
"type": "heading",
"level": 2,
"text": "1. Thông tin khách hàng"
},
{
"type": "paragraph",
"runs": [
{
"text": "Kính gửi: ",
"bold": true
},
{
"text": "Công ty TNHH Thương mại và Dịch vụ An Phát"
}
]
},
{
"type": "paragraph",
"spacing_after": 4,
"runs": [
{
"text": "Địa chỉ: ",
"bold": true
},
{
"text": "Số 18 đường Trần Hưng Đạo, quận Hoàn Kiếm, Hà Nội"
}
]
},
{
"type": "heading",
"level": 2,
"text": "2. Chi tiết báo giá"
},
{
"type": "paragraph",
"spacing_after": 3,
"text": "Đơn giá áp cho toàn bộ số kết nối của đơn hàng. Một kết nối là một phiên soạn thảo đồng thời."
},
{
"type": "table",
"columns": [
{
"width": 1,
"align": "center"
},
{
"width": 6
},
{
"width": 2,
"align": "right"
},
{
"width": 2,
"align": "right"
},
{
"width": 3,
"align": "right"
}
],
"header": [
"STT",
"Hạng mục",
"Số kết nối",
"Đơn giá (USD)",
"Thành tiền (USD)"
],
"rows": [
[
1,
"O3O Office Online, bản doanh nghiệp, thuê bao 1 năm",
120,
"5,00",
"600,00"
],
[
2,
"Hỗ trợ triển khai và đấu nối Nextcloud",
null,
null,
"0,00"
],
[
{
"text": "Tổng cộng",
"bold": true,
"align": "right"
},
null,
null,
null,
{
"text": "600,00",
"bold": true,
"fill": "#EAF1FB"
}
]
],
"border": {
"width": 0.5,
"color": "#7F7F7F"
},
"header_fill": "#1D55B8",
"header_color": "#FFFFFF",
"header_bold": true,
"zebra_fill": "#F7F9FC",
"repeat_header": true,
"size": 10,
"spacing_after": 4
},
{
"type": "heading",
"level": 2,
"text": "3. Điều khoản"
},
{
"type": "list",
"style": "number",
"items": [
"Báo giá có hiệu lực 30 ngày kể từ ngày lập.",
"Bản quyền được cấp dưới dạng token ký số, nạp vào máy chủ của khách hàng.",
{
"text": "Phạm vi của bản doanh nghiệp:",
"items": [
"Số kết nối đồng thời theo đơn hàng",
"API DocBuilder đầy đủ",
"Cập nhật trong suốt thời hạn thuê bao"
]
}
]
},
{
"type": "paragraph",
"spacing_before": 8,
"align": "right",
"runs": [
{
"text": "ĐẠI DIỆN BÊN BÁN",
"bold": true
}
]
},
{
"type": "paragraph",
"align": "right",
"runs": [
{
"text": "(Ký, ghi rõ họ tên và đóng dấu)",
"italic": true,
"size": 9
}
]
},
{
"type": "page_break"
},
{
"type": "heading",
"level": 2,
"text": "Phụ lục: Cách đếm kết nối"
},
{
"type": "list",
"style": "bullet",
"items": [
"Một người mở ba tài liệu ở chế độ sửa được tính là ba kết nối.",
"Phiên chỉ đọc và các yêu cầu tới DocBuilder không được tính.",
"Khi chạm trần, phiên đang mở không bị ngắt và thao tác lưu không bị chặn."
]
}
],
"save": [
{
"format": "docx",
"filename": "bao-gia.docx"
}
]
}curl -sS http://localhost:8080/v1/build \
-H "Authorization: Bearer O3O_DEMO_KEY" \
-H "Content-Type: application/json" \
--data-binary @bao-gia.json \
-o bao-gia.docx -w "HTTP %{http_code}\n"import { readFile, writeFile } from "node:fs/promises";
const BASE_URL = "http://localhost:8080";
const HEADERS = { Authorization: "Bearer O3O_DEMO_KEY" };
const script = JSON.parse(await readFile("bao-gia.json", "utf8"));
const res = await fetch(`${BASE_URL}/v1/build`, {
method: "POST",
headers: { ...HEADERS, "Content-Type": "application/json" },
body: JSON.stringify(script),
signal: AbortSignal.timeout(90_000),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
await writeFile("bao-gia.docx", Buffer.from(await res.arrayBuffer()));import json
import requests
BASE_URL = "http://localhost:8080"
HEADERS = {"Authorization": "Bearer O3O_DEMO_KEY"}
with open("bao-gia.json", encoding="utf-8") as f:
script = json.load(f)
r = requests.post(f"{BASE_URL}/v1/build", headers=HEADERS, json=script, timeout=90)
if not r.ok:
raise RuntimeError(f"{r.status_code}: {r.text}")
with open("bao-gia.docx", "wb") as fh:
fh.write(r.content)<?php
$payload = file_get_contents("bao-gia.json");
$ch = curl_init("http://localhost:8080/v1/build");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer O3O_DEMO_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 90,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
throw new RuntimeException("HTTP $status: " . ($body === false ? curl_error($ch) : $body));
}
file_put_contents("bao-gia.docx", $body);using System.Net.Http.Headers;
using System.Text;
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(90) };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "O3O_DEMO_KEY");
var json = await File.ReadAllTextAsync("bao-gia.json");
using var res = await http.PostAsync("http://localhost:8080/v1/build",
new StringContent(json, Encoding.UTF8, "application/json"));
if (!res.IsSuccessStatusCode)
throw new HttpRequestException($"{(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}");
await File.WriteAllBytesAsync("bao-gia.docx", await res.Content.ReadAsByteArrayAsync());