Việc sắp chữ in ấn tài liệu toán học chất lượng cao trong lịch sử luôn phụ thuộc vào chuỗi công cụ (toolchain) cài đặt cục bộ trên máy trạm hoặc các trang trại máy chủ biên dịch từ xa. Giờ đây, các target biên dịch WebAssembly hiện đại cho phép những công cụ sắp chữ hàng chục năm tuổi viết bằng C và Pascal thực thi trực tiếp ngay bên trong trình duyệt web của người dùng cuối.
Để đạt được tốc độ biên dịch LaTeX nhanh, ổn định và an toàn bộ nhớ trong một tab trình duyệt độc lập, các nhà phát triển phải giải quyết những bất tương thích kiến trúc cốt lõi. Thiết kế TeX nguyên khối ban đầu của Donald Knuth vốn dựa trên cấu trúc bộ nhớ tĩnh, cơ chế vào/ra (I/O) tệp tin đồng bộ trên ổ đĩa và thư viện macro đồ sộ. Việc đưa hệ thống này lên trình duyệt đòi hỏi phải đánh chặn các lệnh gọi hệ thống POSIX, xây dựng hệ thống tệp ảo hóa trong bộ nhớ (in-memory file system), truyền tải font metrics theo nhu cầu (on-demand streaming), và bắc cầu bộ đệm đầu ra thô sang các bộ dựng đồ họa canvas được tăng tốc phần cứng.
1. Kiến trúc nguyên khối của TeX từ Donald Knuth
Để hiểu rõ thách thức khi chạy TeX trong trình duyệt web hiện đại, cần phân tích kiến trúc phần mềm mang tính lịch sử của nó. TeX ban đầu được Donald Knuth viết bằng hệ thống lập trình văn chương (literate programming) WEB, một phương pháp kết hợp mã Pascal với tài liệu giải thích. Sau đó, mã nguồn được dịch sang C thông qua WEB2C trước khi biên dịch bằng các chuỗi công cụ hiện đại.
+-----------------------------------------------------------------------+
| Traditional TeX Pipeline |
+-----------------------------------------------------------------------+
| Raw Input (.tex) |
| | |
| v |
| [ Tokenizer & Macro Expander ] ---> State tables, Hash table (\def) |
| | |
| v |
| [ Semantic Evaluator ] ----------> Font Metrics (.tfm) |
| | |
| v |
| [ Horizontal / Vertical Lists ] -> Nodes: glyph, kern, glue, penalty |
| | |
| v |
| [ Knuth-Plass Line Breaking ] ---> Dynamic programming badness |
| | |
| v |
| [ Page Builder & Shipout ] ------> Synchronous write to DVI / PDF |
+-----------------------------------------------------------------------+Cấp phát tĩnh và các cấu trúc dữ liệu cố định
Không giống như các trình biên dịch hiện đại cấp phát bộ nhớ động thông qua các bộ cấp phát heap tiêu chuẩn, TeX cổ điển tự quản lý không gian địa chỉ nội bộ bằng các mảng tĩnh kích thước cố định lớn, được xác định trong cấu hình bản dựng (build configuration):
mem[]: Mảng lưu trữ chính chứa danh sách token, các nút danh sách ngang (horizontal list nodes), nút danh sách dọc (vertical list nodes), và các đặc tả khoảng dãn (glue specifications).hash[]: Cấu trúc bảng băm nguyên thủy quản lý tên lệnh, các chuỗi macro đang hoạt động và mã phân loại ký tự (catcodes).eqtb[]: Bảng tương đương (equivalents table) theo dõi giá trị biến hiện tại, cấp độ phạm vi cục bộ (scoping levels), và các định nghĩa phông chữ.str_pool[]: Vùng gom chuỗi (string pool) lưu giữ tất cả định danh và chuỗi ký tự gặp phải trong suốt quá trình thực thi.
Do TeX xử lý bộ nhớ dưới dạng một khối liên tục các chỉ mục mảng thay vì các con trỏ liên kết động, bố cục bộ nhớ luôn mang tính tất định tuyệt đối. Tuy nhiên, nếu vượt quá bất kỳ giới hạn cố định nào (ví dụ: khi các hình vẽ TikZ lồng nhau phức tạp làm cạn kiệt mem[]), TeX sẽ lập tức dừng thực thi với lỗi dung lượng (capacity error) không thể phục hồi.
Thuật toán ngắt dòng tối ưu Knuth-Plass
Sức mạnh nghệ thuật sắp chữ cốt lõi của TeX nằm ở thuật toán ngắt dòng Knuth-Plass. Thay vì áp dụng phương pháp căn lề tham lam từng dòng (greedy line-by-line), Knuth-Plass tiếp cận việc định dạng đoạn văn như một bài toán tối ưu hóa toàn cục được giải quyết thông qua quy hoạch động.
Một đoạn văn được mô hình hóa thành một chuỗi có thứ tự gồm ba thành phần nguyên thủy cơ bản:
- Boxes: Các thực thể hình chữ nhật cố định có chiều rộng, chiều cao và độ sâu xác định (chẳng hạn như một glyph ký tự hoặc một công thức con được đóng gói).
- Glue: Khoảng cách linh hoạt có chiều rộng tự nhiên (), khả năng dãn (), và khả năng co ().
- Penalties: Chi phí phạt tường minh () gắn liền với việc ngắt dòng tại một ranh giới cụ thể, trong đó cấm ngắt dòng và bắt buộc ngắt dòng.
Thuật toán định nghĩa tỷ lệ điều chỉnh cho một dòng ứng viên kéo dài từ điểm ngắt đến :
Ở đây, đại diện cho tổng chiều rộng thực tế của các box và glue tự nhiên, trong khi đại diện cho chiều rộng dòng mục tiêu. Nếu , dòng không thể co đủ mức mà không làm chồng lấn các ký tự lên nhau và bị coi là không hợp lệ.
Điểm trừ thẩm mỹ (demerit score) cho một dòng ứng viên là sự kết hợp giữa độ xấu với các điểm phạt:
Bằng cách đánh giá tất cả các chuỗi ngắt dòng khả dĩ trên đồ thị đoạn văn, Knuth-Plass tìm ra tổng điểm trừ nhỏ nhất trên toàn cục. Việc chuyển mã thuật toán này lên web giúp bảo toàn tính chuẩn xác cao về mặt đồ họa chữ, đảm bảo văn bản hiển thị trong trình duyệt khớp chính xác với bản in vật lý đến từng phân số của point.
2. Port các target biên dịch C sang WebAssembly qua Emscripten
Việc triển khai pdfTeX, XeTeX, hoặc LuaTeX lên môi trường runtime của web đòi hỏi phải biên dịch các tầng chuyển đổi C của chúng sang WebAssembly (.wasm) bằng backend LLVM và Emscripten.
+-----------------------------------------------------------------------+
| Browser WebAssembly Architecture |
+-----------------------------------------------------------------------+
| Main Browser UI Thread (Monaco / CodeMirror, DOM, React) |
| | |
| | [ Web Worker Message Channel: SharedArrayBuffer / Transfers ] |
| v |
| Dedicated Dedicated Worker Runtime |
| +---------------------------------------------------------------+ |
| | Emscripten JavaScript Glue Layer (Syscall Dispatcher) | |
| | +---------------------------------------------------------+ | |
| | | MEMFS In-Memory POSIX File System Hierarchy | | |
| | | - /working/document.tex | | |
| | | - /usr/local/share/texmf/fonts/tfm/... | | |
| | +---------------------------------------------------------+ | |
| | ^ | |
| | | POSIX Read/Write/Open/Seek | |
| | v | |
| | WebAssembly Instance (pdftex.wasm) | |
| | +---------------------------------------------------------+ | |
| | | WebAssembly.Memory (32-bit linear address space) | | |
| | | - Static Data / Equiv Tables / String Pool | | |
| | | - TeX Execution Stack & Heap Buffers | | |
| | +---------------------------------------------------------+ | |
| +---------------------------------------------------------------+ |
+-----------------------------------------------------------------------+Giả lập các lệnh gọi hệ thống của hệ điều hành POSIX
TeX cổ điển giả định môi trường thực thi chuẩn POSIX. Nó liên tục gọi các hàm open(), read(), write(), lseek(), và stat() tương tác với bộ lưu trữ khối bên dưới. Sandbox của trình duyệt hoàn toàn không cấp quyền truy cập đĩa trực tiếp như vậy.
Emscripten giải quyết khoảng cách này bằng cách bọc các import của WebAssembly trong các bộ điều phối syscall bằng JavaScript kết nối với MEMFS, một hệ thống tệp ảo nằm hoàn toàn trên bộ nhớ RAM:
// Worker initialization: Configuring the virtual POSIX file system
import createTeXEngine from './pdftex.js';
async function initializeTeXWorker() {
const engine = await createTeXEngine({
noInitialRun: true,
print: (text) => postMessage({ type: 'stdout', payload: text }),
printErr: (text) => postMessage({ type: 'stderr', payload: text }),
});
// Create standard TeX directory hierarchy in virtual RAM
engine.FS.mkdir('/working');
engine.FS.mkdirTree('/usr/local/share/texmf/fonts');
engine.FS.mkdirTree('/usr/local/share/texmf/tex/latex/base');
return engine;
}Mỗi phiên biên dịch tài liệu sẽ gắn (mount) tệp nguồn đầu vào của người dùng trực tiếp vào /working/document.tex. Khi module WebAssembly kích hoạt thao tác đọc dữ liệu, MEMFS sẽ phân giải độ lệch byte (byte offset) bên trong một Uint8Array của JavaScript, trả các khối tệp đồng bộ vào bộ nhớ tuyến tính của engine.
Ràng buộc bộ nhớ tuyến tính và không gian địa chỉ
WebAssembly sử dụng mô hình bộ nhớ tuyến tính (linear memory) được hiển thị cho JavaScript dưới dạng một bộ đệm WebAssembly.Memory có thể thay đổi kích thước. Do WebAssembly tiêu chuẩn (Wasm32) dùng con trỏ 32-bit, tổng bộ nhớ có thể đánh địa chỉ bị giới hạn ở mức 4 GB. Đối với một TeX runtime nguyên khối, cấu hình bộ nhớ phải được tính toán cẩn thận:
- Initial Memory: Các trang bộ nhớ cấp phát trước (mỗi trang bằng 64 KB) cần thiết để khởi tạo binary.
- Maximum Memory: Giới hạn cấp phát trên. pdfTeX thường hoạt động trơn tru trong khoảng từ 128 MB đến 512 MB.
- Stack Size: Sự đệ quy macro sâu trong các gói phức tạp có thể làm tràn call stack trừ khi độ sâu ngăn xếp được cấu hình tối thiểu là 8 MB.
# Compilation flags for building pdfTeX with Emscripten
emcc -O3 \
-s WASM=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s INITIAL_MEMORY=134217728 \
-s STACK_SIZE=8388608 \
-s FORCE_FILESYSTEM=1 \
-s EXPORTED_FUNCTIONS="['_run_pdftex', '_main']" \
-s EXPORTED_RUNTIME_METHODS="['FS', 'callMain']" \
pdftex.c -o pdftex.js3. Phân giải gói và truyền tải tài nguyên ảo động
Một bản phân phối TeX Live đầy đủ cục bộ chiếm hơn 8 gigabyte dung lượng lưu trữ trên hàng trăm nghìn tệp tin, bao gồm macro, lớp tài liệu (.cls), gói kiểu định dạng (.sty), mẫu ngắt từ và bảng font metrics (.tfm).
Việc tải toàn bộ bản phân phối TeX qua mạng trước khi người dùng có thể biên dịch công thức đầu tiên là điều bất khả thi. Đường ống biên dịch phải phân giải và nạp tài nguyên theo nhu cầu thực tế (dynamic on-demand streaming).
+-----------------------------------------------------------------------+
| On-Demand Asset Virtualization Architecture |
+-----------------------------------------------------------------------+
| pdfTeX wasm requests file: "article.cls" |
| | |
| v |
| MEMFS Intercept: Is "/usr/local/share/texmf/.../article.cls" cached? |
| | |
| +--> YES: Return Uint8Array slice to WebAssembly linear memory |
| | |
| +--> NO: Suspend WebAssembly engine execution |
| Trigger Asset Fetch Worker |
| Fetch range slice: GET /packages/latex-base.tar.zst |
| Decompress chunk via WebAssembly Zstandard unpacker |
| Mount extracted file into MEMFS |
| Resume WebAssembly execution state |
+-----------------------------------------------------------------------+Đánh chặn tệp động và kỹ thuật Emscripten Asyncify
Khi pdfTeX cố gắng mở một tệp package chưa tồn tại trong MEMFS, lỗi thiếu tệp cần phải được can thiệp xử lý. Có hai mô hình kiến trúc để giải quyết bài toán này:
- Phân tích trước đồ thị phụ thuộc (Pre-Parsing Dependency Graphing): Một bộ quét AST tĩnh kiểm tra mã nguồn LaTeX đầu vào bằng biểu thức chính quy (regex) hoặc tokenizer nhẹ, trích xuất tất cả các chỉ thị
\documentclass{...},\usepackage{...}, và\input{...}trước khi tiến hành biên dịch. Các gói yêu cầu cùng các phần phụ thuộc bắc cầu của chúng được tải song song và mount vàoMEMFStrước khi gọi điểm nhập (entry point) của WebAssembly. - Tạm dừng thực thi đồng bộ qua Asyncify: Khi xảy ra tình trạng thiếu tài nguyên ngay giữa quá trình biên dịch, cơ chế Asyncify của Emscripten sẽ tháo gỡ (unwind) ngăn xếp thực thi của WebAssembly, bảo toàn các thanh ghi khung cuộc gọi (call frame registers) vào bộ nhớ, nhường quyền điều khiển cho vòng lặp sự kiện (event loop) của JavaScript để thực hiện lệnh
fetch()bất đồng bộ, mount dữ liệu nhận được vàoMEMFS, và khôi phục lại call stack để tiếp tục thực thi một cách minh bạch.
Phương pháp phân tích trước mang lại hiệu năng vượt trội và hiển thị tiến trình xác định rõ ràng hơn. Triển khai sau minh họa cách chủ động phân giải phụ thuộc:
// Dependency scanner mapping TeX directives to package bundles
export class TeXDependencyResolver {
private packageRegistry = new Map<string, string>([
['amsmath', '/bundles/amsmath.pkg.bin'],
['graphicx', '/bundles/graphics.pkg.bin'],
['hyperref', '/bundles/hyperref.pkg.bin'],
['tikz', '/bundles/pgf-tikz.pkg.bin'],
]);
public extractDependencies(texSource: string): string[] {
const packageRegex = /\\usepackage(?:\s*\[.*?\])?\s*\{([a-zA-Z0-9,\s_-]+)\}/g;
const dependencies = new Set<string>();
let match: RegExpExecArray | null;
while ((match = packageRegex.exec(texSource)) !== null) {
const packageList = match[1].split(',').map(pkg => pkg.trim());
for (const pkg of packageList) {
if (this.packageRegistry.has(pkg)) {
dependencies.add(pkg);
}
}
}
return Array.from(dependencies);
}
public async preloadPackages(packages: string[], fs: any): Promise<void> {
await Promise.all(packages.map(async (pkg) => {
const bundleUrl = this.packageRegistry.get(pkg)!;
const response = await fetch(bundleUrl);
const buffer = new Uint8Array(await response.arrayBuffer());
// Mount package directly into TeX search tree
fs.writeFile(`/usr/local/share/texmf/tex/latex/${pkg}.sty`, buffer);
}));
}
}4. Tạo kết quả đầu ra: Từ luồng bộ nhớ đến Canvas tương tác
Khi quá trình biên dịch hoàn tất, pdfTeX ghi kết quả đầu ra vào MEMFS dưới dạng một tệp PDF trong bộ nhớ. Thay vì ghi vào ổ đĩa vật lý liên tục, luồng byte sinh ra được trích xuất trực tiếp từ bộ nhớ trình duyệt:
// Extracting compiled PDF bytes from virtual filesystem
function extractCompilationArtifacts(fs: any): Uint8Array {
const pdfPath = '/working/document.pdf';
if (!fs.analyzePath(pdfPath).exists) {
throw new Error('Compilation halted: No output PDF generated.');
}
const rawBytes = fs.readFile(pdfPath, { encoding: 'binary' });
return rawBytes;
}+-----------------------------------------------------------------------+
| Zero-Copy Worker-to-Main Pipeline |
+-----------------------------------------------------------------------+
| Web Worker: pdftex.wasm |
| | |
| v |
| Virtual MEMFS: /working/document.pdf (Uint8Array) |
| | |
| +--> postMessage({ type: 'COMPILED_PDF', buffer: rawBytes.buffer }, |
| [rawBytes.buffer]) // Transferable Object |
| |
| Main UI Thread |
| | |
| v |
| PDF.js Vector Rendering Worker |
| | |
| v |
| HTML5 Canvas / WebGL Surface (Sub-pixel hardware accelerated render) |
| | |
| +--> SyncTeX Coordinate Overlay Engine (Active Source Syncing) |
+-----------------------------------------------------------------------+Truyền tải bộ nhớ Zero-Copy qua Transferable Objects
Các tệp PDF thô cho bài báo khoa học chứa đồ họa vector nhúng có thể lên đến hàng chục megabyte. Việc sao chép dữ liệu này giữa các luồng worker và luồng UI chính thông qua thuật toán bản sao có cấu trúc (structured clone) sẽ gây áp lực lớn lên bộ nhớ và tụt khung hình (frame drops).
Thay vào đó, worker chuyển quyền sở hữu của ArrayBuffer cơ sở trực tiếp thông qua Transferable Objects:
// Web Worker thread emitting output without memory cloning
const pdfData: Uint8Array = extractCompilationArtifacts(engine.FS);
// Transfer underlying buffer directly, emptying the worker's reference instantly
self.postMessage(
{
type: 'RENDER_OUTPUT',
payload: pdfData.buffer,
log: engine.FS.readFile('/working/document.log', { encoding: 'utf8' })
},
[pdfData.buffer]
);Ánh xạ tọa độ nguồn - đích qua SyncTeX
Một tính năng thiết yếu của kỹ thuật xử lý tài liệu chuyên nghiệp là khả năng đồng bộ hóa nguồn hai chiều. SyncTeX phát ra một tệp phụ trợ .synctex.gz trong quá trình biên dịch, ánh xạ vị trí dòng và cột từ mã nguồn .tex tới ranh giới tọa độ hình học trên từng trang PDF cụ thể.
Bằng cách phân tích các bản ghi SyncTeX ngay trong trình duyệt, thao tác nhấp vào một từ trong PDF đã kết xuất sẽ lập tức điều hướng con trỏ trình biên tập mã về đúng dòng tương ứng, và ngược lại việc điều hướng con trỏ trong trình soạn thảo sẽ làm nổi bật các khung bao tương ứng trên canvas.
5. Trải nghiệm sắp chữ thực tế trong trình duyệt: Kiến trúc trình soạn thảo LaTeX của PetexSpace
Triển khai biên dịch LaTeX trong trình duyệt của PetexSpace chứng minh cách các tầng kiến trúc này kết hợp với nhau nhằm đem lại khả năng soạn thảo tài liệu tương đương máy tính để bàn mà không cần đến máy chủ. Bạn cũng có thể kiểm tra các tài liệu PDF được tạo ra để đánh giá độ chuẩn xác của đồ họa vector và số liệu phông chữ.
+-----------------------------------------------------------------------+
| PetexSpace LaTeX Editor Architecture |
+-----------------------------------------------------------------------+
| User Interface Layer |
| [ Monaco Code Editor ] <-------------> [ Interactive PDF Canvas ] |
| | ^ |
| | Source Changes | Render Paint |
| v | |
| Compilation Orchestrator (Debounce & State Machine) |
| | | |
| | Worker Job RPC | Transferable |
| v | ArrayBuffer |
| Dedicated TeX Web Worker | |
| +---------------------------------------------------+-------------+ |
| | WebAssembly Engine (pdfTeX 3.141592653 / WebAssembly core) | |
| | MEMFS Virtual Filesystem Cache | |
| | Dynamic Font & Package Micro-Bundle Fetcher | |
| +-----------------------------------------------------------------+ |
+-----------------------------------------------------------------------+Cách ly đa luồng và đảm bảo hiệu năng UI 60 FPS
Việc thực thi các tác vụ tiêu tốn nhiều năng lượng tính toán như thuật toán ngắt dòng Knuth-Plass, đánh giá macro và nhúng phông chữ trực tiếp trên luồng chính của trình duyệt sẽ làm đóng băng giao diện người dùng. Phím gõ bị trễ, nhấp nháy con trỏ bị giật và thao tác cuộn bị khóa.
PetexSpace cô lập toàn bộ engine biên dịch WebAssembly bên trong một Web Worker riêng biệt. Mọi giao tiếp giữa giao diện người dùng và worker biên dịch diễn ra hoàn toàn qua các thông điệp bất đồng bộ. Nhờ đó, luồng chính tiếp tục duy trì tốc độ 60 FPS mượt mà, đảm bảo khả năng tô sáng cú pháp, tự động hoàn thành mã và điều hướng tài liệu phản hồi tức thì trong khi quá trình biên dịch diễn ra ngầm phía dưới.
// Compilation Orchestrator managing background worker lifecycles
export class TeXCompilationService {
private worker: Worker;
private isCompiling = false;
private pendingJob: string | null = null;
constructor() {
this.worker = new Worker(new URL('./tex.worker.ts', import.meta.url), {
type: 'module'
});
this.worker.onmessage = this.handleWorkerMessage.bind(this);
}
public requestCompilation(sourceCode: string): void {
if (this.isCompiling) {
// Coalesce rapid edits into single pending job
this.pendingJob = sourceCode;
return;
}
this.isCompiling = true;
this.worker.postMessage({
command: 'COMPILE',
source: sourceCode,
timestamp: performance.now()
});
}
private handleWorkerMessage(event: MessageEvent): void {
const { type, payload, log } = event.data;
if (type === 'SUCCESS') {
this.renderPdfOutput(payload); // payload is an ArrayBuffer
} else {
this.displayDiagnosticLog(log);
}
this.isCompiling = false;
// Execute queued compilation if edits arrived during execution
if (this.pendingJob !== null) {
const nextSource = this.pendingJob;
this.pendingJob = null;
this.requestCompilation(nextSource);
}
}
private renderPdfOutput(buffer: ArrayBuffer): void {
// Dispatched to PDF.js rendering pipeline on canvas
}
private displayDiagnosticLog(log: string): void {
// Surface parse errors and line number warnings
}
}Chủ quyền dữ liệu và thực thi ở phía client
Các nền tảng sắp chữ đám mây truyền thống yêu cầu người dùng gửi mã LaTeX chưa biên dịch, dữ liệu nghiên cứu bảo mật, các công thức độc quyền và phát hiện học thuật chưa công bố qua mạng lưới bên thứ ba tới máy chủ từ xa. Điều này tạo ra rủi ro an ninh và trở ngại pháp lý về bảo vệ sở hữu trí tuệ nhạy cảm.
Nhờ việc biên dịch LaTeX trực tiếp bên trong WebAssembly, PetexSpace thiết lập chủ quyền dữ liệu trọn vẹn. Tài liệu, đồ họa và các tệp nhị phân PDF sinh ra chỉ tồn tại duy nhất trong bộ nhớ trình duyệt phía client. Quá trình xử lý diễn ra hoàn toàn nội bộ, loại bỏ triệt để nguy cơ lộ lọt dữ liệu cũng như độ trễ vận chuyển qua mạng.
Thông qua sự giao thoa giữa WebAssembly, hệ thống tệp ảo hóa và công nghệ render tăng tốc phần cứng, các tác vụ sắp chữ in ấn với độ chuẩn xác tuyệt đối giờ đây có thể vận hành ở bất kỳ đâu có trình duyệt web hiện đại.