跳至主要内容

固定版面表單 PDF

狀態:實驗性 API;排版警示政策由不可變版本 appfuse-server 4.0.0-alpha.34 提供,前端警示交付由 @appfuse/appfuse-web 19.0.0-alpha.134 提供。2026-09-30 使用者確認將固定表單及其需求規格納入框架;下游提供的樣張只作能力核對,不在框架實作其業務報表。

與流式報表的邊界​

ADR-029 的模型分離仍有效。 io.leandev.appfuse.pdf.FixedDocument 明確建立每張頁面及元素座標;FixedPdfRenderer 輸出 PDF。 它不使用 ReportDocument、ReportArtifact 或 flow nodes 作公開固定表單模型;可交付 caller OutputStream 或中立的 PdfArtifact。 共用 ReportFonts 字型登錄、PDFBox runtime、內部字型量測與影像嵌入,避免維護兩套中文字型行為。 內部 renderer 暫置於既有 report.internal.pdf 以共用 package-private 基礎設施,該路徑不是消費端 API。

需求以 RPT 報表規格 描述,layout_mode: fixed。 規格可獨立實作與驗證;只有產品操作、資料授權與交付流程才由 US 承接。

公開契約​

API契約
FixedDocument(title, pages)不可變文件,至少一頁;資料綁定與續頁已由應用範本完成
Page(widthMm, heightMm, elements)每頁可有不同尺寸;元素清單順序即疊放順序,同頁 ID 唯一
Frame(x, y, width, height)mm,左上原點、x 向右、y 向下;有限數,寬高大於零
Text字型家族、pt 字級與基線行距、粗體、RGB、水平對齊、單行或框內換行、pt 字距(letterSpacingPt,預設 0)
Rectangle/Ellipse/Line框、橢圓、任意方向線;填色、線寬;Line 可自訂 pt 虛線(dashPatternPt/dashPhasePt),舊 dashed=true 為 3pt/3pt
ImagePNG/JPEG 等比置中,防禦複製資料;20MB/25M pixels 上限
PdfBackground指定 1-based PDF 頁面作 STATIC 底稿;保留頁面外觀,不匯入註解、AcroForm、動作
BarcodeCode 39、Code 128、QR 黑白向量;實體模組寬、條高、靜區;不自動縮放
FixedTextMeasurer以同一 ReportFonts 與 renderer 規則量測單行寬度、ascent/descent 及實際使用字型;供應用自行定位
Options全表單/只印 VARIABLE 層、mm 平移校準、Clock、取消回呼、完成頁數回呼 onPage、layoutPolicy(預設 OUTPUT_WITH_WARNINGS)
Result完成頁數、實際寫出 bytes、diagnostics
FixedLayoutException可定位的 code/location;嚴格模式的幾何異常及所有模式的非排版錯誤不產生可交付 PDF

STATIC 層用於底圖、印刷框線與標籤;VARIABLE 層用於資料。overlayOnly=true 完全略過 STATIC 元素。 嚴格模式要求框線筆畫及校準後的幾何落在紙張內;座標允許正負 5000 mm,尺寸最大 5000 mm,校準限正負 100 mm。 空資料是否仍出單、留幾列及如何呈現由 RPT 定義;renderer 不代替業務判斷。

文字框以字型 ascent 對齊頂部、以 descent 核對底部,行距是基線間距。 可選嚴格模式的 SINGLE_LINE 遇換行或超寬拒絕;WRAP 按 Unicode code point 換行,超高、單字元超寬或行距造成字型高度重疊時拒絕。 不自動截斷、縮字、改框高或移動後續欄位;缺字及未知字型也拒絕。換行目前不提供英文保詞、禁則排版、雙向文字、複雜 shaping 或拉丁字距微調(kerning)。

虛線、字距與文字量測​

Line 的虛線以 pt 指定交替的線段與空白長度,整條線以單一路徑繪製,不需拆成多段:

import io.leandev.appfuse.pdf.FixedDocument.Layer;
import io.leandev.appfuse.pdf.FixedDocument.Line;
import java.util.List;

var dotted = new Line("cut", Layer.STATIC, 15, 150, 195, 150, 0.4, 0x000000, false)
.withDash(List.of(1.0, 2.0), 0); // 1pt 線段、2pt 空白

空清單為實線;奇數個元素由 PDF 重複形成週期,phase 可大於週期。長度須為有限非負值、不可全為零,實線的 phase 必須為 0。 保留的 dashed 建構子等同 Line.DEFAULT_DASH(3pt/3pt),既有文件的繪圖指令不變。

Text.letterSpacingPt 是同一行相鄰 code point 之間額外加的距離,行尾不加;置中、靠右、換行與超框檢查都使用含字距的寬度,換行後新行首字仍貼齊框緣。 字距 0 時不寫出 PDF 字元間距指令。中文標籤的「姓 名」式拉開可直接以字距表達,不需逐字建立元素:

var label = new Text("name-label", Layer.STATIC, frame, "姓名", "sans", 11, 17, false, 0,
Align.LEFT, TextMode.SINGLE_LINE).withLetterSpacing(11);

應用需要自行計算座標時使用 FixedTextMeasurer,不要直接以 PDFBox 量測——後者不會套用框架的 fallback 鏈、字距與缺字拒絕規則:

import io.leandev.appfuse.pdf.FixedTextMeasurer;

try (var measurer = FixedTextMeasurer.create(fonts)) {
var m = measurer.measure("合計 NT$", "sans", 11, false, 0.5);
double widthMm = m.widthPt() * 25.4 / 72; // 與 renderer 對齊、超框檢查使用的寬度相同
}

量測結果含行寬、所用字型的最大 ascent/descent,以及依實際字型切分的片段(Run.font() 為 fallback 鏈中實際使用的 FontSource)。 只量單行:Tab 與 Text 相同展開為四個空格,換行或其他控制字元拒絕。未知家族、缺粗體字面及任何字型都不含的字元,與 renderer 一樣以 pdf.fixed.font 拒絕。 實例持有已解析字型,非 thread-safe;同一次產生內重用同一實例,用畢 close()。 不提供 kerning:PDFBox 不對內嵌 TrueType 套用 GPOS 字距調整,量測與繪製都以各字 advance 加總,兩者保持一致。

底稿限 20MB,須未加密、無旋轉、CropBox 原點為 0、UserUnit 為 1,尺寸與目標頁相同(0.01pt 比較容差);不偷偷縮放。 嚴格模式的全頁底稿配非零校準會越頁而拒絕;警示模式則照校準平移並回報可能裁切;預印紙套印模式略過底稿後可平移可變欄位。 只有底稿頁面內容會匯入;若原表單顯示內容來自註解/AcroForm appearance,須先用正式模板製作流程整理成純頁面內容。

排版異常仍輸出(預設)​

此政策只改變固定版面的幾何異常處理;流式報表維持原契約。舊建構子與 Options.defaults() 都使用 OUTPUT_WITH_WARNINGS。幾何異常不再使預設生成失敗;需要拒絕的建立者須明確選用 STRICT。

import io.leandev.appfuse.pdf.FixedPdfRenderer;

var options = FixedPdfRenderer.Options.defaults()
.layoutPolicy(FixedPdfRenderer.LayoutPolicy.OUTPUT_WITH_WARNINGS); // 可省略:這是預設政策。
try (var artifact = renderer.render(document, options, "form.pdf")) {
var warnings = artifact.layoutDiagnostics();
artifact.transferTo(out);
}

OUTPUT_WITH_WARNINGS 保留字型、字級、基線行距、對齊、座標、內容及元素繪製順序,允許重疊;不縮字、不刪資料、不增頁或移動後續欄位。 WRAP 仍按既定框寬換行,全部行都繪製;SINGLE_LINE 遇原始換行時逐行繪製,保留原基線間距。 框內有效高度及單字元寬度超限以 pdf.fixed.text-overflow 回報。 框、線、圖片、條碼、底稿的筆畫及校準越頁以 pdf.fixed.out-of-page 回報;文字實際繪製範圍即使框在頁內,超出紙張也另回報此代碼。 完整內容會寫入 PDF,但紙張之外的部分可能無法顯示或列印;warning 明確說明可能裁切,不將文字存在於 PDF 當成全部可見的保證。

建立者將排版判斷加入 FixedDocument(title, pages, layoutIssues),例如:

import io.leandev.appfuse.pdf.FixedDocument;

var issue = new FixedDocument.LayoutIssue("app.form.block-overlap", 2,
"customer-name", "details", "Customer name overlaps the details block");
var document = new FixedDocument("Form", pages, java.util.List.of(issue));

此接點只供幾何排版問題;頁碼須存在,指定的元素 ID 須位於該頁,也可只提供區域識別。建立者自行判定業務區塊重疊,renderer 不將底圖與文字等合法疊放自動視為異常。 Options.defaults().layoutPolicy(LayoutPolicy.STRICT) 的嚴格政策拒絕建立者宣告的問題,警示政策將其保留為 WARNING。應用自訂穩定 code,不把權限、資料缺漏或版本衝突塞入此清單;這些檢查仍在建立文件之前依原應用政策拒絕。

診斷欄位契約
severity成功產物為 WARNING;PdfDiagnostic.Severity 另提供 ERROR 表達拒絕診斷
code內建 pdf.fixed.text-overflow/pdf.fixed.out-of-page;建立者自訂應用命名空間
pageNumber原始 PDF 的 1-based 實體頁碼,不因選頁列印重新編號
elementId/regionId至少一項;內建診斷提供元素 ID,應用可提供區域
message英文可顯示訊息,不含原始欄位資料

串流輸出的 Result.diagnostics()、PdfArtifact.layoutDiagnostics() 與保存後的 StoredPdf.diagnostics() 都提供不可變清單。 PdfArtifact 的中性接點刻意名為 layoutDiagnostics(),以保留既有流式 ReportArtifact.diagnostics() 的型別相容性。 StoredPdfs.save 保留診斷;應用將保存結果的 diagnostics 或串流結果映射至前端 ReportArtifact.diagnostics,PDF bytes 與診斷分開交付。 兩個參考 server 的固定 job 同樣預設警示政策;ReportJobWork.Fixed 可明確指定 STRICT。 保存時將完整清單與 READY 原子寫入 ReportJob.layoutDiagnostics,GET /api/v1/reports/{id} 及列表的 ReportJobResponse.diagnostics 會回傳 warning;PDF 的 content/print 端點仍可使用。 狀態回應的 severity 依應用 enum wire 規則為小寫 warning;ReportViewer 同時接受中立 Java serializer 的 WARNING。 舊 DB 列的 null 視為空診斷;已有 report_job 表的環境需在部署新來源前加入可空的 layout_diagnostics 大型文字欄,沿既有人工 Entity Diff/migration 流程執行,這次不改已接受 snapshot 或執行環境 migration。 非 READY 的狀態不投影成功產物診斷;既有 owner/tenant 限制同時保護 PDF 與 warnings。

只有明確的幾何檢查使用此政策,沒有攔截整個 renderer 的例外。缺字、未知字型、無效底稿或圖片、文字/資產容量超限、無法序列化或寫出有效 PDF 仍失敗;取消也仍取消。 完整 PDF 生成成功後才交付;遇非排版錯誤不交付前面已完成的頁面。

前端採用方式見 appfuse-web/lib/components/data-display/report-viewer/README.md:保持 ready,持續顯示「已產生,但有排版問題」及原始位置;預覽、下載、列印仍可使用。 共用列印 adapter 在直接列印前顯示問題,使用者選擇列印才繼續;有 warnings 時 viewer 的選頁對話框不自動關閉。自訂 adapter 也須在直接列印入口提供可見警示,onPrintComplete 不應自動卸載唯一的警示介面。

FixedLayoutPolicyTest 以實際 PDF 逐字與座標驗證嚴格拒絕/警示完整輸出、重疊、超框與越頁位置,並核對缺字、無效資產、容量與 I/O 仍失敗;前端單元及 Storybook browser 測試另核對交付操作與警示可見性。

相依與輸出​

應用加入 org.apache.pdfbox:pdfbox 3.0.8 以上、4.0 以前;只有使用條碼才需另加入 com.google.zxing:core:3.5.3。 兩者皆為框架 compileOnly,未用固定 PDF 的應用不必引入;缺依賴時給出明確指引。 ZXing 3.5.3 是本次驗證的條碼編碼器版本。

renderer 可重用,每次產生各自的 PDF 文件、字型與暫存檔。 完整 PDF 先序列化至暫存,再寫入 caller OutputStream;模型/排版失敗或產生階段取消不寫出任何 bytes。 輸出串流由 caller 關閉;寫出階段 I/O 失敗可能已送出部分 bytes,caller 必須捨棄部分輸出。 預設紙張輸出沒有自動安全邊界:不可列印區由 RPT 的元件配置約束,實紙另驗證。

條碼責任​

Code 39 使用基本字元集,不接受手動 start/stop 星號;Code 128 限 printable ASCII。 QR 使用 UTF-8、M 錯誤更正;byte mode 可能包含 UTF-8 ECI 標頭,正式掃描器必須以實際設備驗證,不保證代收相容。QR 高度由模組數決定,heightMm 只控制一維碼條高。 一維碼左右靜區至少 10 modules,QR 四邊至少 4;編碼後若超紙張,嚴格模式拒絕,警示模式完整繪製並回報可能裁切。 payload 不在例外中回顯。可讀文字以獨立 Text 放置,應用須保證與 payload 對應。

銀行、郵局、超商等帳號組合、補零、業務檢查碼及代收規範由下游 RPT/adapter 定義; 通用條碼可以解碼,不等於正式繳款單已經代收機構認證。

合成驗證規格與證據​

測試來源為 appfuse-server/src/test/java/io/leandev/appfuse/report/FixedPdfTest.java,不帶入下游樣張的個資。 樣張使用 A4 210×297mm、sans(Noto Sans/Noto Sans TC fallback)、11pt、17pt 行距、固定產生時間 2026-09-30T04:00:00Z。

情境明確預期證據
訂購單 7 筆每頁 5 個固定槽,兩頁;7 個品項各出現一次;第 2 頁整份總計 700.00;空槽不產生假資料orderWithExplicitContinuation、order-fixed.pdf
簽收單指定簽名區留白,標題/框線為 STATIC、單號/收件資料為 VARIABLEreceiptFullAndCalibratedOverlay、receipt-fixed.pdf
同簽收單套印只有 VARIABLE;x +1mm、y +2mm;紙張仍為 A4receipt-overlay.pdf;獨立座標測試驗證 dx/dy
三種條碼DEMO payload 在 PDF raster 300dpi 解碼後逐字相同barcodeRasterRoundTripAndPaymentLikeSample、barcode-fixed.pdf
長中文框內完整換行;後續 END 欄位不移動PDF 文字與位置斷言
超容量明確選用 STRICT 時,文字超框或線寬/校準越頁明確失敗;即使前頁成功也不交付半份 PDFatomic failure 測試
底稿/圖形PDF 底稿與新文字可共存;套印省略底稿;尺寸/頁碼錯誤拒絕;橢圓外觀底稿文字及 raster 斷言
圖片bytes 不可被外部改寫,等比置中、不拉伸;損壞圖片拒絕raster 幾何斷言
執行期取消、thread interrupt、輸出失敗、caller 串流所有權、並行重用lifecycle 測試
選用相依缺 PDFBox 時模型可建、factory 有指引;缺 ZXing 時一般 PDF 可輸出、Barcode 有指引且零 bytes隔離 classpath 測試

明細的 5 筆容量與續頁是合成範本規則,不是 renderer 內建業務上限;各下游須在 RPT 定義自身 N/N+1、總計與聯別。 重跑:在 appfuse-server/ 執行 ./gradlew test --tests 'io.leandev.appfuse.report.FixedPdfTest' --console=plain。 產物在 build/reports/fixed-forms/;供檢視副本與摘要在 workspace output/pdf/fixed-forms/。

固定訂購單樣張的資料與定位契約​

此表把合成測試的規格寫成可核對的定位資料,示範下游 RPT 應有的精度;不是正式訂購單的業務需求。 文件 ID 固定 DEMO-ORDER-001,客戶與地址為合成固定文字;明細序號 1–7,名稱依序「合成品項-01」至「合成品項-07」, 每筆數量 1、保存金額 100.00 TWD,整份總計 700.00。模板不得因續頁重複加總。 本樣張不讀 DB;真實資料的查詢、快照、稅額與授權不在此合成案例驗證範圍。

共同樣式為 11pt/17pt 行距、左對齊、框內換行,金額另採單行靠右;此合成範本測試明確選用 STRICT,所有超框皆拒絕,不移動其他欄位。 幾何單位 mm、定位容差 0.01pt(PDF 數值量測);這不是實紙套印容差。

元件/欄位圖層xywh資料/規則
標題STATIC151518012訂購單 — 框架驗證樣張
單號及頁次VARIABLE153018010單號、實體頁次/2
客戶及地址VARIABLE154418016兩行固定合成文字
表格外框STATIC1562180900.6pt 黑線;直線 x=35、115、145
表頭四欄STATIC18/40/120/1586514/65/20/328項次、商品名稱、數量、金額
每頁序號VARIABLE1879 + 15r1412r=0..4;續頁接續第 6 筆
每頁品名VARIABLE4079 + 15r6512綁定當頁第 r 筆名稱
每頁數量VARIABLE12079 + 15r2012綁定數量
每頁金額VARIABLE15079 + 15r4012兩位小數,靠右,單行
總計區VARIABLE1201607512第 1 頁「尚有續頁」,第 2 頁整份總計
簽名區STATIC1520518012訂購人及審核留白
樣張提示STATIC1526518010非正式單據、100% 列印

表格橫線 y=77、92、107、122、137,0.4pt 黑線;頁 2 未使用的三個槽保留空白,不造出零金額品項。 只印 VARIABLE 模式會省略框線及提示,應與已印好的同版次表單配對;不得拿空白紙套印輸出當成完整單據。

下游能力核對與發布界線​

參考版面判定與仍需驗證
月結對帳單適合既有 flow;下游補來源、分組與金額規格
收到檢體登記表適合 flow 表格;下游定義計數、日期格式與空值
初檢基本資料紀錄表flow 多段文字與表格;需合成密集多行及跨頁組合驗證
固定繳款單本 API 提供座標、底稿、圖層、橢圓、條碼;還需下游正式欄位座標、payload 規則與實紙掃讀/套印驗收

工作區合成驗證不計為 REP-09 的兩個實際 consuming reports;未發布前下游不能宣稱已可從 registry 使用此 API。 人工版面接受、實紙套印、設備掃讀、真實資料與產品授權均另列證據,不能沿用 RPT-001 流式樣張的接受結果。

目前不含 AcroForm 編輯、數位簽章、自動表格分頁、縮字/截斷、自動印表機校準、silent print、任意 HTML 模板或 JasperReports 引擎。 兩個 server 參考實作已將固定表單接入 ReportJob;下游仍須採用該參考實作並接上自己的業務資料與權限。固定產物不包裝成流式 ReportArtifact。

本次驗證結果​

2026-09-30:固定表單 16 項、缺 ZXing 2 項、缺 PDFBox 5 項測試通過;既有流式 corpus/diagnostics 回歸及記憶體 gates 通過。 Javadoc、文檔站建置與 llms 索引檢查通過。四份樣張共五頁完成 AI 視覺檢視;人工版面、實紙套印與設備掃讀尚未驗收。 結果與來源/PDF 摘要保存在 workspace output/pdf/fixed-forms/verification.json。仍為未發布的工作區成果。

共用交付管線(2026-10-01)​

FixedPdfRenderer.render(document, options, filename) 回傳 PdfArtifact,呼叫端負責關閉。 它提供檔名、頁數、大小、產生時間、layoutDiagnostics 及可重複開啟的串流;關閉產物後禁止開新串流,既有串流關閉後才刪除暫存檔。 流式 ReportArtifact 繼承同一中立介面,另外保留 flow metadata 與 diagnostics。 StoredPdfs.save(storage, partition, artifact, clock) 接管產物並在成功或失敗後關閉,回傳 StoredPdf。

兩個 server 的 ReportJobRequest.fixed(...) 接受 FixedReportJobTask,由 worker 在原有唯讀交易與身分脈絡中建立文件。 worker 統一管理 Clock、取消與頁數進度,再走相同保存、owner 授權下載、頁面選取、刪除及到期清理管線。 校準或套印可透過 ReportJobWork.Fixed 明確指定;舊 flow request 的五參數建構子保留。 FixedLayoutException 對外映射為不含 payload 的 report.invalid,取消映射為 report.cancelled。

這些 API 尚未發布;參考實作目前以 -PuseLocalFramework=true 驗證,正式採用須先發布框架並更新相依版本。 前節 2026-09-30 的視覺證據是當時快照,來源修改後不能當成目前版本的驗收證據;人工接受狀態不自動繼承。

審閱補充契約(2026-10-01)​

中立 PdfArtifact/StoredPdf/StoredPdfs 遵循一般 API 相容政策;只有 Fixed* 為實驗性。 使用新版 worker 即須註冊 FixedPdfRenderer bean,純 flow 也相同;沿用既有 PDFBox/字型相依,不要求 ZXing,除非輸出條碼。 ZXing 支援範圍為 3.5.3 以上、3.6.0 以前;runtime 不作版本探測,下游須依此範圍鎖版,本次測試使用 3.5.3。

CRLF/CR 正規化為 LF,Tab 展開為四個空格(不提供 tab stops);其他控制字元及不合法條碼 payload 丟 FixedLayoutException, 對外映射 report.invalid。幾何/樣式程式設定錯誤仍為 IllegalArgumentException,不把所有程式錯誤當資料錯誤。 文字行高使用整個文字框各字型的最大 ascent 加最大 descent,並非逐字形可見輪廓。多行有效下限是該值(pt),隨字型與 fallback 改變; 不能保證 1.2 倍字級可用,本次 sans 合成樣張採 11pt/17pt,模板換字型須重驗。 WRAP 以 code point 分行,可能拆開 combining marks/ZWJ 序列;行尾空白計入量測而影響 CENTER/RIGHT 的視覺對齊。 本次接受此限制,應用輸入需符合 RPT 的文字範圍與空白規則;框架不擅自 trim 業務文字。 下載檔名替換 Unicode 控制/格式字元與路徑分隔字元,最多保留 180 code points,再補 .pdf。

覆蓋界線:job 取消測試驗 save 階段,invalid 文件實際經 renderer 驗證溢位及失敗分類,故障注入仍為 flow;FixedPdfTest 另驗 renderer 的取消與排版拒絕。 內部 TempStorage 測試接點可直接斷言序列化暫存檔於排版失敗、頁面完成後取消及產物關閉後刪除;不涵蓋 PDFBox scratch 或每一種 I/O 故障。新補粗體/對齊/緊行距、旋轉/CropBox 底稿、超長 Code128、UTF-8 QR raster 掃讀測試。 原 output/pdf/fixed-forms/verification.json 是舊來源快照,已過期;本輪不重新宣稱該批樣張的 AI 視覺檢視有效。

圖像資料與診斷邊界​

目前 Image/PdfBackground 的空 bytes、超過 20MB、mediaType 不符等建構期契約檢查仍丟 IllegalArgumentException, job 會映射 report.failed;通過建構後的解碼/底稿內容錯誤才丟 FixedLayoutException,映射 report.invalid。 此分類尚未統一,呼叫端應在 adapter 先檢查素材契約;不能假設所有資料錯誤都會回 report.invalid。 建構期尚未組入頁面,例外 location 只有元件 ID;render 期才含 page/N/element。 直接呼叫 renderer/範本的程式可讀取例外的 code/location,ReportJob 對外狀態刻意只傳通用錯誤碼以免洩露內部識別; 失敗的逐欄錯誤資訊仍需另定授權與脫敏契約;成功固定產物的排版 warnings 已由新 job API 的 diagnostics 提供,與失敗訊息分開。