Bỏ qua, tới nội dung
Tài liệu APIv1

O3O DocBuilder và chuyển đổi

Gọi API từ mã của bạn Beta

SDK chính thức đang được chuẩn bị; trong lúc chờ, trang này đưa hàm bọc ngắn cho curl, JavaScript, Python, PHP và C#, chép vào dự án là gọi được mọi endpoint DocBuilder v1.

Trang này mô tả tính năng đang ở giai đoạn beta: đã chạy được nhưng có thể còn thay đổi.

Trong trang này
Sắp có

SDK chính thức cho npm, PyPI, NuGet và Go. Khi phát hành, gói sẽ được liệt kê tại trang này.

Trong lúc chờ, mỗi hàm bọc dưới đây gói trọn trong một tệp, không cần gói ngoài ngoài những thứ ghi ở đầu tệp: Python cần requests; JavaScript cần Node.js 18 trở lên; PHP cần 8.1 trở lên và phần mở rộng cURL; C# cần .NET 8; bản bash cần curl 7.76 trở lên. Hàm bọc chỉ gọi đúng các endpoint trong đặc tả v1 và ném lỗi mang code, detail, request_id của máy chủ.

Hàm bọc#

o3o_docbuilder.py · docbuilder.mjs · DocBuilder.php · DocBuilder.cs · o3o-docbuilder.sh
"""o3o_docbuilder.py: hàm bọc tối giản cho O3O DocBuilder API v1, chỉ cần gói requests."""
import json
import os
import time

import requests


class DocBuilderError(Exception):
    def __init__(self, status: int, error: dict, retry_after=None):
        super().__init__(f"{status} {error.get('code')}: {error.get('message')}")
        self.status = status
        self.code = error.get("code")
        self.detail = error.get("detail") or {}
        self.request_id = error.get("request_id")
        self.retry_after = int(retry_after) if retry_after else None


class DocBuilder:
    def __init__(self, base_url="http://localhost:8080", api_key="O3O_DEMO_KEY", timeout=90):
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self.session = requests.Session()
        self.session.headers["Authorization"] = f"Bearer {api_key}"

    def _send(self, method, path, **kwargs):
        r = self.session.request(method, self.base_url + path, timeout=self.timeout, **kwargs)
        if r.status_code >= 400:
            try:
                error = r.json()["error"]
            except (ValueError, KeyError):
                error = {"code": "http_error", "message": r.text[:200]}
            raise DocBuilderError(r.status_code, error, r.headers.get("Retry-After"))
        return r

    @staticmethod
    def _result(r):
        return r.json() if r.status_code == 202 else r.content

    def _upload(self, path, field, endpoint, fields):
        data = {}
        for key, value in fields.items():
            if value is None:
                continue
            if isinstance(value, (dict, list)):
                data[key] = json.dumps(value, ensure_ascii=False)
            elif isinstance(value, bool):
                data[key] = "true" if value else "false"
            else:
                data[key] = str(value)
        with open(path, "rb") as fh:
            return self._send("POST", endpoint, files={field: (os.path.basename(path), fh)}, data=data)

    def limits(self):
        return self._send("GET", "/v1/limits").json()

    def formats(self):
        return self._send("GET", "/v1/formats").json()

    def convert(self, path, to, options=None, async_job=False):
        """Trả bytes của tệp kết quả, hoặc dict job khi async_job=True."""
        fields = {"to": to, "options": options, "async": async_job}
        return self._result(self._upload(path, "file", "/v1/convert", fields))

    def convert_url(self, url, to, options=None, async_job=False):
        body = {"url": url, "to": to, "async": async_job}
        if options:
            body["options"] = options
        return self._result(self._send("POST", "/v1/convert", json=body))

    def build(self, script, async_job=False):
        return self._result(self._send("POST", "/v1/build", json={"script": script, "async": async_job}))

    def render_template(self, path, data, to=None, options=None, async_job=False):
        """Chỉ bản doanh nghiệp."""
        fields = {"data": data, "to": to, "options": options, "async": async_job}
        return self._result(self._upload(path, "template", "/v1/template/render", fields))

    def extract_text(self, path, max_chars=None):
        return self._upload(path, "file", "/v1/extract/text", {"max_chars": max_chars}).json()

    def extract_meta(self, path):
        return self._upload(path, "file", "/v1/extract/meta", {}).json()

    def thumbnail(self, path, page=1, width=320, fmt="png"):
        fields = {"page": page, "width": width, "format": fmt}
        return self._upload(path, "file", "/v1/extract/thumbnail", fields).content

    def job(self, job_id):
        return self._send("GET", f"/v1/jobs/{job_id}").json()

    def wait(self, job_id, timeout=600):
        deadline, delay = time.monotonic() + timeout, 1.0
        while True:
            job = self.job(job_id)
            if job["status"] == "done":
                return job
            if job["status"] == "failed":
                raise DocBuilderError(0, job["error"])  # 0 = job thất bại
            if time.monotonic() > deadline:
                raise TimeoutError(job_id)
            time.sleep(delay)
            delay = min(delay * 1.5, 5.0)

    def download(self, file_id):
        return self._send("GET", f"/v1/files/{file_id}").content
// docbuilder.mjs: hàm bọc tối giản cho O3O DocBuilder API v1 (Node.js 18+, không cần gói ngoài).
import { readFile } from "node:fs/promises";
import { basename } from "node:path";
import { setTimeout as sleep } from "node:timers/promises";

export class DocBuilderError extends Error {
  constructor(status, error, retryAfter = null) {
    super(`${status} ${error.code}: ${error.message}`);
    this.status = status;
    this.code = error.code;
    this.detail = error.detail ?? {};
    this.requestId = error.request_id;
    this.retryAfter = retryAfter ? Number(retryAfter) : null;
  }
}

export class DocBuilder {
  constructor(baseUrl = "http://localhost:8080", apiKey = "O3O_DEMO_KEY", timeoutMs = 90_000) {
    this.baseUrl = baseUrl.replace(/\/+$/, "");
    this.apiKey = apiKey;
    this.timeoutMs = timeoutMs;
  }

  async #send(method, path, body, headers = {}) {
    const res = await fetch(this.baseUrl + path, {
      method,
      body,
      headers: { Authorization: `Bearer ${this.apiKey}`, ...headers },
      signal: AbortSignal.timeout(this.timeoutMs),
    });
    if (res.status >= 400) {
      let error;
      try {
        error = (await res.json()).error;
      } catch {
        error = { code: "http_error", message: res.statusText };
      }
      throw new DocBuilderError(res.status, error, res.headers.get("Retry-After"));
    }
    return res;
  }

  #json(path, payload) {
    return this.#send("POST", path, JSON.stringify(payload), { "Content-Type": "application/json" });
  }

  async #upload(path, field, endpoint, fields) {
    const form = new FormData();
    form.append(field, new Blob([await readFile(path)]), basename(path));
    for (const [key, value] of Object.entries(fields)) {
      if (value === undefined || value === null) continue;
      form.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
    }
    return this.#send("POST", endpoint, form);
  }

  static async #result(res) {
    return res.status === 202 ? res.json() : Buffer.from(await res.arrayBuffer());
  }

  async limits() {
    return (await this.#send("GET", "/v1/limits")).json();
  }

  async formats() {
    return (await this.#send("GET", "/v1/formats")).json();
  }

  async convert(path, to, { options, asyncJob = false } = {}) {
    return DocBuilder.#result(await this.#upload(path, "file", "/v1/convert", { to, options, async: asyncJob }));
  }

  async convertUrl(url, to, { options, asyncJob = false } = {}) {
    return DocBuilder.#result(await this.#json("/v1/convert", { url, to, options, async: asyncJob }));
  }

  async build(script, { asyncJob = false } = {}) {
    return DocBuilder.#result(await this.#json("/v1/build", { script, async: asyncJob }));
  }

  // Chỉ bản doanh nghiệp.
  async renderTemplate(path, data, { to, options, asyncJob = false } = {}) {
    const fields = { data, to, options, async: asyncJob };
    return DocBuilder.#result(await this.#upload(path, "template", "/v1/template/render", fields));
  }

  async extractText(path, { maxChars } = {}) {
    return (await this.#upload(path, "file", "/v1/extract/text", { max_chars: maxChars })).json();
  }

  async extractMeta(path) {
    return (await this.#upload(path, "file", "/v1/extract/meta", {})).json();
  }

  async thumbnail(path, { page = 1, width = 320, format = "png" } = {}) {
    const res = await this.#upload(path, "file", "/v1/extract/thumbnail", { page, width, format });
    return Buffer.from(await res.arrayBuffer());
  }

  async job(jobId) {
    return (await this.#send("GET", `/v1/jobs/${jobId}`)).json();
  }

  async wait(jobId, { timeoutMs = 600_000 } = {}) {
    const deadline = Date.now() + timeoutMs;
    for (let delay = 1000; ; delay = Math.min(delay * 1.5, 5000)) {
      const job = await this.job(jobId);
      if (job.status === "done") return job;
      if (job.status === "failed") throw new DocBuilderError(0, job.error); // 0 = job thất bại
      if (Date.now() > deadline) throw new Error(`Quá thời gian chờ job ${jobId}`);
      await sleep(delay);
    }
  }

  async download(fileId) {
    const res = await this.#send("GET", `/v1/files/${fileId}`);
    return Buffer.from(await res.arrayBuffer());
  }
}
<?php
// DocBuilder.php: hàm bọc tối giản cho O3O DocBuilder API v1 (PHP 8.1+, phần mở rộng cURL).

final class DocBuilderException extends RuntimeException
{
    public function __construct(
        public readonly int $status,
        public readonly string $errorCode,
        string $message,
        public readonly array $detail = [],
        public readonly ?string $requestId = null,
        public readonly ?int $retryAfter = null,
    ) {
        parent::__construct("$status $errorCode: $message");
    }
}

final class DocBuilder
{
    public function __construct(
        private string $baseUrl = 'http://localhost:8080',
        private string $apiKey = 'O3O_DEMO_KEY',
        private int $timeout = 90,
    ) {
        $this->baseUrl = rtrim($baseUrl, '/');
    }

    public function limits(): array
    {
        return json_decode($this->send('GET', '/v1/limits')[1], true);
    }

    public function formats(): array
    {
        return json_decode($this->send('GET', '/v1/formats')[1], true);
    }

    /** Trả nội dung tệp, hoặc mảng job khi $async = true. */
    public function convert(string $path, string $to, array $options = [], bool $async = false): string|array
    {
        $fields = ['to' => $to, 'options' => $options ?: null, 'async' => $async];
        return $this->result($this->upload($path, 'file', '/v1/convert', $fields));
    }

    public function convertUrl(string $url, string $to, array $options = [], bool $async = false): string|array
    {
        $body = ['url' => $url, 'to' => $to, 'async' => $async] + ($options ? ['options' => $options] : []);
        return $this->result($this->sendJson('/v1/convert', $body));
    }

    public function build(array $script, bool $async = false): string|array
    {
        return $this->result($this->sendJson('/v1/build', ['script' => $script, 'async' => $async]));
    }

    /** Chỉ bản doanh nghiệp. */
    public function renderTemplate(string $path, array $data, ?string $to = null, array $options = [], bool $async = false): string|array
    {
        $fields = ['data' => $data, 'to' => $to, 'options' => $options ?: null, 'async' => $async];
        return $this->result($this->upload($path, 'template', '/v1/template/render', $fields));
    }

    public function extractText(string $path, ?int $maxChars = null): array
    {
        return json_decode($this->upload($path, 'file', '/v1/extract/text', ['max_chars' => $maxChars])[1], true);
    }

    public function extractMeta(string $path): array
    {
        return json_decode($this->upload($path, 'file', '/v1/extract/meta', [])[1], true);
    }

    public function thumbnail(string $path, int $page = 1, int $width = 320, string $format = 'png'): string
    {
        $fields = ['page' => $page, 'width' => $width, 'format' => $format];
        return $this->upload($path, 'file', '/v1/extract/thumbnail', $fields)[1];
    }

    public function job(string $jobId): array
    {
        return json_decode($this->send('GET', "/v1/jobs/$jobId")[1], true);
    }

    public function wait(string $jobId, int $timeout = 600): array
    {
        $deadline = time() + $timeout;
        for ($delay = 1.0; ; $delay = min($delay * 1.5, 5.0)) {
            $job = $this->job($jobId);
            if ($job['status'] === 'done') {
                return $job;
            }
            if ($job['status'] === 'failed') {
                $e = $job['error'];
                throw new DocBuilderException(0, $e['code'], $e['message'], $e['detail'] ?? [], $e['request_id'] ?? null);
            }
            if (time() > $deadline) {
                throw new RuntimeException("Quá thời gian chờ job $jobId");
            }
            usleep((int) ($delay * 1_000_000));
        }
    }

    public function download(string $fileId): string
    {
        return $this->send('GET', "/v1/files/$fileId")[1];
    }

    private function upload(string $path, string $field, string $endpoint, array $fields): array
    {
        $post = [$field => new CURLFile($path, '', basename($path))];
        foreach ($fields as $key => $value) {
            if ($value === null) {
                continue;
            }
            if (is_array($value)) {
                $post[$key] = json_encode($value, JSON_UNESCAPED_UNICODE);
            } elseif (is_bool($value)) {
                $post[$key] = $value ? 'true' : 'false';
            } else {
                $post[$key] = (string) $value;
            }
        }
        return $this->send('POST', $endpoint, $post);
    }

    private function sendJson(string $endpoint, array $payload): array
    {
        $body = json_encode($payload, JSON_UNESCAPED_UNICODE);
        return $this->send('POST', $endpoint, $body, ['Content-Type: application/json']);
    }

    private function result(array $response): string|array
    {
        [$status, $body] = $response;
        return $status === 202 ? json_decode($body, true) : $body;
    }

    private function send(string $method, string $path, string|array|null $body = null, array $headers = []): array
    {
        $responseHeaders = [];
        $ch = curl_init($this->baseUrl . $path);
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_HTTPHEADER => array_merge(['Authorization: Bearer ' . $this->apiKey], $headers),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => $this->timeout,
            CURLOPT_HEADERFUNCTION => function ($ch, string $line) use (&$responseHeaders): int {
                $parts = explode(':', $line, 2);
                if (count($parts) === 2) {
                    $responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]);
                }
                return strlen($line);
            },
        ]);
        if ($body !== null) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
        }
        $raw = curl_exec($ch);
        if ($raw === false) {
            throw new RuntimeException(curl_error($ch));
        }
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        if ($status >= 400) {
            $error = json_decode($raw, true)['error'] ?? ['code' => 'http_error', 'message' => substr($raw, 0, 200)];
            $retry = isset($responseHeaders['retry-after']) ? (int) $responseHeaders['retry-after'] : null;
            throw new DocBuilderException(
                $status, $error['code'], $error['message'], $error['detail'] ?? [], $error['request_id'] ?? null, $retry
            );
        }
        return [$status, $raw];
    }
}
// DocBuilder.cs: hàm bọc tối giản cho O3O DocBuilder API v1 (.NET 8, không cần gói ngoài).
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;

public sealed class DocBuilderException : Exception
{
    public DocBuilderException(int status, string code, string message, JsonNode? detail = null,
        string? requestId = null, int? retryAfter = null) : base($"{status} {code}: {message}")
    {
        Status = status;
        Code = code;
        Detail = detail;
        RequestId = requestId;
        RetryAfter = retryAfter;
    }

    public int Status { get; }
    public string Code { get; }
    public JsonNode? Detail { get; }
    public string? RequestId { get; }
    public int? RetryAfter { get; }
}

public sealed class DocBuilder : IDisposable
{
    private readonly HttpClient _http;

    public DocBuilder(string baseUrl = "http://localhost:8080", string apiKey = "O3O_DEMO_KEY")
    {
        _http = new HttpClient { BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/"), Timeout = TimeSpan.FromSeconds(90) };
        _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
    }

    public void Dispose() => _http.Dispose();

    public Task<JsonNode> LimitsAsync() => GetJsonAsync("v1/limits");

    public Task<JsonNode> FormatsAsync() => GetJsonAsync("v1/formats");

    /// <summary>Đồng bộ: trả nội dung tệp kết quả.</summary>
    public async Task<byte[]> ConvertAsync(string path, string to, object? options = null)
    {
        using var res = await UploadAsync(path, "file", "v1/convert", new() { ["to"] = to, ["options"] = Json(options) });
        return await res.Content.ReadAsByteArrayAsync();
    }

    /// <summary>Bất đồng bộ: trả đối tượng job.</summary>
    public async Task<JsonNode> SubmitConvertAsync(string path, string to, object? options = null)
    {
        using var res = await UploadAsync(path, "file", "v1/convert",
            new() { ["to"] = to, ["options"] = Json(options), ["async"] = "true" });
        return JsonNode.Parse(await res.Content.ReadAsStringAsync())!;
    }

    public async Task<byte[]> BuildAsync(string scriptJson)
    {
        using var res = await SendAsync(HttpMethod.Post, "v1/build",
            new StringContent(scriptJson, Encoding.UTF8, "application/json"));
        return await res.Content.ReadAsByteArrayAsync();
    }

    /// <summary>Chỉ bản doanh nghiệp.</summary>
    public async Task<byte[]> RenderTemplateAsync(string path, object data, string? to = null)
    {
        using var res = await UploadAsync(path, "template", "v1/template/render", new() { ["data"] = Json(data), ["to"] = to });
        return await res.Content.ReadAsByteArrayAsync();
    }

    public async Task<JsonNode> ExtractTextAsync(string path)
    {
        using var res = await UploadAsync(path, "file", "v1/extract/text", new());
        return JsonNode.Parse(await res.Content.ReadAsStringAsync())!;
    }

    public async Task<JsonNode> ExtractMetaAsync(string path)
    {
        using var res = await UploadAsync(path, "file", "v1/extract/meta", new());
        return JsonNode.Parse(await res.Content.ReadAsStringAsync())!;
    }

    public async Task<byte[]> ThumbnailAsync(string path, int page = 1, int width = 320, string format = "png")
    {
        using var res = await UploadAsync(path, "file", "v1/extract/thumbnail",
            new() { ["page"] = page.ToString(), ["width"] = width.ToString(), ["format"] = format });
        return await res.Content.ReadAsByteArrayAsync();
    }

    public Task<JsonNode> JobAsync(string jobId) => GetJsonAsync($"v1/jobs/{jobId}");

    public async Task<JsonNode> WaitAsync(string jobId, TimeSpan? timeout = null)
    {
        var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromMinutes(10));
        for (var delay = 1.0; ; delay = Math.Min(delay * 1.5, 5))
        {
            var job = await JobAsync(jobId);
            var status = (string?)job["status"];
            if (status == "done") return job;
            if (status == "failed")
            {
                var error = job["error"];
                throw new DocBuilderException(0, (string?)error?["code"] ?? "failed", (string?)error?["message"] ?? "", error?["detail"]);
            }
            if (DateTime.UtcNow > deadline) throw new TimeoutException(jobId);
            await Task.Delay(TimeSpan.FromSeconds(delay));
        }
    }

    public async Task<byte[]> DownloadAsync(string fileId)
    {
        using var res = await SendAsync(HttpMethod.Get, $"v1/files/{fileId}");
        return await res.Content.ReadAsByteArrayAsync();
    }

    private static string? Json(object? value) => value is null ? null : JsonSerializer.Serialize(value);

    private async Task<JsonNode> GetJsonAsync(string path)
    {
        using var res = await SendAsync(HttpMethod.Get, path);
        return JsonNode.Parse(await res.Content.ReadAsStringAsync())!;
    }

    private async Task<HttpResponseMessage> UploadAsync(string path, string field, string endpoint, Dictionary<string, string?> fields)
    {
        var form = new MultipartFormDataContent();
        form.Add(new ByteArrayContent(await File.ReadAllBytesAsync(path)), field, Path.GetFileName(path));
        foreach (var (key, value) in fields)
            if (value is not null) form.Add(new StringContent(value), key);
        return await SendAsync(HttpMethod.Post, endpoint, form);
    }

    private async Task<HttpResponseMessage> SendAsync(HttpMethod method, string path, HttpContent? content = null)
    {
        var res = await _http.SendAsync(new HttpRequestMessage(method, path) { Content = content });
        var status = (int)res.StatusCode;
        if (status < 400) return res;
        var raw = await res.Content.ReadAsStringAsync();
        int? retryAfter = res.Headers.RetryAfter?.Delta is TimeSpan d ? (int)d.TotalSeconds : null;
        res.Dispose();
        JsonNode? error = null;
        try { error = JsonNode.Parse(raw)?["error"]; } catch (JsonException) { }
        throw new DocBuilderException(status, (string?)error?["code"] ?? "http_error", (string?)error?["message"] ?? raw,
            error?["detail"], (string?)error?["request_id"], retryAfter);
    }
}
# o3o-docbuilder.sh: hàm bọc curl cho O3O DocBuilder API v1 (curl 7.76+). Nạp bằng: source o3o-docbuilder.sh
O3O_URL="${O3O_URL:-http://localhost:8080}"
O3O_KEY="${O3O_KEY:-O3O_DEMO_KEY}"

# In thân JSON; lỗi HTTP thì in JSON lỗi và trả mã khác 0.
o3o_json() {
  curl -sS --fail-with-body -H "Authorization: Bearer $O3O_KEY" "$@"
}

# Lưu thân vào tệp $1; lỗi thì in JSON lỗi ra stderr và xoá tệp.
o3o_file() {
  local out="$1" code
  shift
  code=$(curl -sS -o "$out" -w '%{http_code}' -H "Authorization: Bearer $O3O_KEY" "$@") || return 1
  if [ "$code" -ge 400 ]; then cat "$out" >&2; rm -f "$out"; return 1; fi
}

o3o_convert()  { o3o_file "$3" -F "file=@$1" -F "to=$2" "$O3O_URL/v1/convert"; }
o3o_build()    { o3o_file "$2" -H "Content-Type: application/json" --data-binary "@$1" "$O3O_URL/v1/build"; }
o3o_render()   { o3o_file "$4" -F "template=@$1" -F "data=<$2" -F "to=$3" "$O3O_URL/v1/template/render"; }
o3o_text()     { o3o_json -F "file=@$1" "$O3O_URL/v1/extract/text"; }
o3o_meta()     { o3o_json -F "file=@$1" "$O3O_URL/v1/extract/meta"; }
o3o_thumb()    { o3o_file "$2" -F "file=@$1" -F "width=${3:-320}" "$O3O_URL/v1/extract/thumbnail"; }
o3o_job()      { o3o_json "$O3O_URL/v1/jobs/$1"; }
o3o_download() { o3o_file "$2" "$O3O_URL/v1/files/$1"; }
o3o_limits()   { o3o_json "$O3O_URL/v1/limits"; }

Dùng hàm bọc#

Chuyển đổi đồng bộ, bất đồng bộ, trích văn bản, xử lý lỗi theo mã
import os

from o3o_docbuilder import DocBuilder, DocBuilderError

db = DocBuilder("http://localhost:8080", "O3O_DEMO_KEY")

# Đồng bộ: docx sang PDF/A
with open("bao-cao.pdf", "wb") as fh:
    fh.write(db.convert("bao-cao.docx", "pdf", options={"pdf": {"pdfa": True}}))

# Bất đồng bộ: gửi job, chờ, tải mọi tệp
job = db.convert("bao-cao.docx", "pdf", async_job=True)
done = db.wait(job["id"])
for output in done["outputs"]:
    with open(os.path.basename(output["filename"]), "wb") as fh:
        fh.write(db.download(output["file_id"]))

print(db.extract_text("bao-cao.docx")["page_count"])

try:
    db.render_template("hop-dong-mau.docx", {"so_hop_dong": "HD-2026-091"}, to="pdf")
except DocBuilderError as e:
    if e.code != "forbidden_feature":
        raise
    print("Điền mẫu cần bản doanh nghiệp")
import { writeFile } from "node:fs/promises";
import { basename } from "node:path";
import { DocBuilder, DocBuilderError } from "./docbuilder.mjs";

const db = new DocBuilder("http://localhost:8080", "O3O_DEMO_KEY");

// Đồng bộ: docx sang PDF/A
await writeFile("bao-cao.pdf", await db.convert("bao-cao.docx", "pdf", { options: { pdf: { pdfa: true } } }));

// Bất đồng bộ: gửi job, chờ, tải mọi tệp
const job = await db.convert("bao-cao.docx", "pdf", { asyncJob: true });
const done = await db.wait(job.id);
for (const output of done.outputs) {
  await writeFile(basename(output.filename), await db.download(output.file_id));
}

console.log((await db.extractText("bao-cao.docx")).page_count);

try {
  await db.renderTemplate("hop-dong-mau.docx", { so_hop_dong: "HD-2026-091" }, { to: "pdf" });
} catch (e) {
  if (!(e instanceof DocBuilderError) || e.code !== "forbidden_feature") throw e;
  console.log("Điền mẫu cần bản doanh nghiệp");
}
<?php
require __DIR__ . '/DocBuilder.php';

$db = new DocBuilder('http://localhost:8080', 'O3O_DEMO_KEY');

// Đồng bộ: docx sang PDF/A
file_put_contents('bao-cao.pdf', $db->convert('bao-cao.docx', 'pdf', ['pdf' => ['pdfa' => true]]));

// Bất đồng bộ: gửi job, chờ, tải mọi tệp
$job = $db->convert('bao-cao.docx', 'pdf', [], true);
$done = $db->wait($job['id']);
foreach ($done['outputs'] as $output) {
    file_put_contents(basename($output['filename']), $db->download($output['file_id']));
}

echo $db->extractText('bao-cao.docx')['page_count'], "\n";

try {
    $db->renderTemplate('hop-dong-mau.docx', ['so_hop_dong' => 'HD-2026-091'], 'pdf');
} catch (DocBuilderException $e) {
    if ($e->errorCode !== 'forbidden_feature') {
        throw $e;
    }
    echo "Điền mẫu cần bản doanh nghiệp\n";
}
using var db = new DocBuilder("http://localhost:8080", "O3O_DEMO_KEY");

// Đồng bộ: docx sang PDF/A
await File.WriteAllBytesAsync("bao-cao.pdf", await db.ConvertAsync("bao-cao.docx", "pdf", new { pdf = new { pdfa = true } }));

// Bất đồng bộ: gửi job, chờ, tải mọi tệp
var job = await db.SubmitConvertAsync("bao-cao.docx", "pdf");
var done = await db.WaitAsync((string)job["id"]!);
foreach (var output in done["outputs"]!.AsArray())
{
    var name = Path.GetFileName((string)output!["filename"]!);
    await File.WriteAllBytesAsync(name, await db.DownloadAsync((string)output["file_id"]!));
}

Console.WriteLine((await db.ExtractTextAsync("bao-cao.docx"))["page_count"]);

try
{
    await db.RenderTemplateAsync("hop-dong-mau.docx", new { so_hop_dong = "HD-2026-091" }, "pdf");
}
catch (DocBuilderException e) when (e.Code == "forbidden_feature")
{
    Console.WriteLine("Điền mẫu cần bản doanh nghiệp");
}
source ./o3o-docbuilder.sh

o3o_convert bao-cao.docx pdf bao-cao.pdf
o3o_text bao-cao.docx
o3o_thumb bao-cao.docx trang-1.png 480
o3o_build bao-gia.json bao-gia.docx
o3o_limits

Bảng ánh xạ#

PythonJavaScriptPHPC#Endpoint
limits()limits()limits()LimitsAsync()GET /v1/limits
formats()formats()formats()FormatsAsync()GET /v1/formats
convert(), convert_url()convert(), convertUrl()convert(), convertUrl()ConvertAsync(), SubmitConvertAsync()POST /v1/convert
build()build()build()BuildAsync()POST /v1/build
render_template()renderTemplate()renderTemplate()RenderTemplateAsync()POST /v1/template/render (bản doanh nghiệp)
extract_text()extractText()extractText()ExtractTextAsync()POST /v1/extract/text
extract_meta()extractMeta()extractMeta()ExtractMetaAsync()POST /v1/extract/meta
thumbnail()thumbnail()thumbnail()ThumbnailAsync()POST /v1/extract/thumbnail
job(), wait()job(), wait()job(), wait()JobAsync(), WaitAsync()GET /v1/jobs/{id}
download()download()download()DownloadAsync()GET /v1/files/{id}

Thử lại khi bị giới hạn#

Chỉ thử lại với 429 rate_limited503 queue_full hoặc 503 pool_unavailable, sau đúng số giây trong Retry-After. Các lỗi 4xx khác là lỗi của yêu cầu: gửi lại y nguyên sẽ lỗi y nguyên.

Hàm thử lại dùng chung
import time

from o3o_docbuilder import DocBuilder, DocBuilderError

db = DocBuilder("http://localhost:8080", "O3O_DEMO_KEY")


def with_retry(call, attempts=5):
    for attempt in range(attempts):
        try:
            return call()
        except DocBuilderError as e:
            if e.status not in (429, 503) or attempt == attempts - 1:
                raise
            time.sleep(e.retry_after or 5)


pdf = with_retry(lambda: db.convert("bao-cao.docx", "pdf"))
import { setTimeout as sleep } from "node:timers/promises";
import { DocBuilder, DocBuilderError } from "./docbuilder.mjs";

const db = new DocBuilder("http://localhost:8080", "O3O_DEMO_KEY");

async function withRetry(call, attempts = 5) {
  for (let attempt = 0; ; attempt++) {
    try {
      return await call();
    } catch (e) {
      const retryable = e instanceof DocBuilderError && (e.status === 429 || e.status === 503);
      if (!retryable || attempt === attempts - 1) throw e;
      await sleep(1000 * (e.retryAfter ?? 5));
    }
  }
}

const pdf = await withRetry(() => db.convert("bao-cao.docx", "pdf"));