跳至主要内容

流式報表版面擴充契約

狀態:已完成審閱(3 輪),於2026-10-04隨 appfuse-server 4.0.0-alpha.23 發布。完整表頭/逐列樣式、每頁摘要及每頁邏輯紀錄上限三項能力已納入此不可變版本。 四邊框線、跨列與首頁表頭擴充狀態:於2026-10-05經兩輪人審通過,隨4.0.0-alpha.25釋出。 以下兩節的新 API 不在 alpha.24;下游須升級至alpha.25或更新版本。 分節最後頁摘要覆寫:於2026-10-06經兩輪人審通過,隨 4.0.0-alpha.26 釋出;alpha.24/alpha.25 均不含此 API。 基線:appfuse-server 4.0.0-alpha.22。以下新增 API 不存在於該已發布版本。 依據:ADR-029、核心模型、PDF renderer。

問題與範圍​

HTML+CSS 用來確認報表格式,Java 範本以公開 authoring API 產生 PDF。已確認的版面需要每頁完整表頭、 表頭外緣線、雙行紀錄之後的分隔線,以及跟在每頁內容下方的頁次/最後頁合計。 alpha.22 的自動欄標題不能關閉,額外表頭只能追加;全表共享內距及每列底線;PageRegion 固定在紙張上/下方, 不能在內文使用 PageNumber/PageCount,也沒有每頁邏輯紀錄上限。 本次新增完整表頭/逐列樣式、每頁摘要及紀錄上限三項可選能力,不改 HTML 或在應用後製 PDF。

不包含 HTML importer、固定座標 authoring、正式資料查詢與授權、job、其他下游升級。

表格表頭與逐列樣式​

TableBuilder.columnTitles(false) 關閉自動欄標題列,headerRows 成為按指定順序排列的完整表頭。 這些列可使用既有 colSpan、Paragraph、Spacer,包含跨全欄的標題、左右日期及兩行欄名。 repeatHeader 仍控制續頁重複;沒有 headerRows 時代表沒有表頭,空資料也不另造欄標題。

TableRow.withStyle(TableRowStyle) 指定:

欄位null有值
cellPadding繼承表格有限非負 Length,四邊相同
topRule不畫上線線寬零不畫;正值畫上線
bottomRule繼承表格的每列底線線寬零不畫;正值覆寫

TableRowStyle.inherit() 全部繼承;noRules() 明確關閉上下線。線條在列邊緣繪製,不增加列高; 需額外距離時用 Spacer 或內距。可拆列的每個實體片段使用相同樣式。記憶體列及延遲來源共享同一驗證規則。 未啟用紀錄上限時,超高不可拆列、keepWithNext、有限預看、群組續接與取消維持既有契約。 雙行紀錄以第一列 keepWithNext、兩列皆不可拆、第二列底線組合。超高整組仍沿既有 keep hint 的診斷; RPT 要求不可拆整組時,應用須將此診斷視為 layout-invalid,不接受降級輸出。 群組跨頁合成的續接列沒有來源列的 TableRowStyle,沿用表格預設內距、群組文字/底色及每列底線。 來源列的 noRules 或自訂內距不傳給續接列;需一致外觀時設定表格預設,再由來源列覆寫。 目前兩份 nbsos 試點沒有群組續接列。此行為由跨頁群組+逐列樣式測試驗證,不新增續接列樣式 API。

var plain = TableRowStyle.noRules().withCellPadding(Length.ofPoint(1.5));
var rule = new LineStyle(Length.ofPoint(0.35), ReportColor.rgb(0x333333));
var first = TableRow.detail("第一列").withStyle(plain)
.withKeepWithNext(true).withSplittable(false);
var second = TableRow.detail("第二列").withStyle(plain.withBottomRule(rule))
.withSplittable(false);
var document = ReportDocument.builder()
.table(t -> t.column("id", "", 1).columnTitles(false)
.headerRow(TableRow.detail("指定表頭"))
.rows(List.of(first, second)))
.build();

隨內容的每頁摘要​

ReportDocument.Builder.pageSummary(Consumer<PageSummary.Builder>) 建立 PageSummary。 它與固定 footer 可以並存,兩者都從可用內文高度扣除預留空間;預設無摘要。

設定預設/行為
left/right非最後頁左右內容,預設空;可用 PageNumber/PageCount,頂層分頁分節內可用 SectionPageNumber/SectionPageCount(alpha.38)
lastPageLeft/lastPageRight最後頁覆寫;未呼叫時沿用相同側,空 varargs 可明確清空
style沿用文件段落文字與行高;段距與縮排不適用
gap內容結束後的距離,預設0pt
padding(top,bottom)上下內距,預設0pt;不改變左右內距
padding(top,right,bottom,left)上右下左內距(CSS 順序,alpha.38),預設皆0pt;左右只縮排文字,上線仍畫滿內容區
reserveHeight每頁最小預留高度,預設0pt
rule上線;null/零寬不畫,正值畫在 gap 之後,線不增加高度

實際預留取 max(reserveHeight, gap + paddingTop + max(一般頁高, 最後頁高, 各分節覆寫合併後高度) + paddingBottom)。 沒有分節覆寫時維持原本兩種摘要的量測與繪製路徑。 摘要在當頁內文游標之後排列,而非紙張底部;游標包括作者設定的 Spacer 及區塊間距。 左右同字級的單行使用相同 baseline,不做自動換行;LineBreak 明確換行。超出各自半寬(扣除左右內距後的內容區寬度之半)回報 report.layout.no-space ERROR,不能截斷或縮字。

PageSummary 的公開 record 使用兩個 PageRegion 承載文字,限制為左右格、region.rule=false; 線條只由 PageSummary.rule 設定。此為值模型重用,不改變固定 PageRegion 的位置或截斷政策。

var document = ReportDocument.builder()
.pageSummary(s -> s.right(Text.of("頁次:"), new PageNumber(), Text.of("/"), new PageCount())
.lastPageLeft(Text.of("合計筆數:17"))
.style(ParagraphStyle.inherit()
.withText(TextStyle.inherit().withSize(Length.ofPoint(9)))
.withLineHeight(1.4))
.padding(Length.ofPoint(2.25), Length.ofPoint(2.25))
.reserveHeight(Length.ofPoint(24)))
.paragraph("合成內容")
.build();

已知最後頁時用其專用內容,合計值由應用提供全份實際計算結果,不由 renderer 累計。 需要總頁數的非最後頁只保存頁號與內容結束座標,完成全部頁面後逐頁 append;不保存全文件 box 或重新讀資料。 新摘要模式將同頁的頁首、固定頁尾與摘要回填合併為一次 append,避免 backend 為每個區域另建 content stream。 記憶體為原有單頁 box、有限列預看,加上 O(頁數) 的數值位置紀錄,與既有頁首頁尾回填策略一致。 回填期間檢查取消;回填或超寬失敗不構成成功產物。頁數/產物限制仍由原有 render/呼叫端契約承接。

所有長度及新增線寬必須有限且非負。Length 本身拒絕負值及 NaN/Infinity,模型亦檢查自訂 Length 的點值; 摘要導致內容區少於一行時以既有 report.layout.page-regions-too-tall 停止,不無限換頁。 PageNumber/PageCount 仍不允許出現在任意內文,只新增指定的摘要區域。

左右內距與分節頁次(4.0.0-alpha.38)​

padding(top,right,bottom,left) 讓左側內容自 frame.x + left 起排、右側內容右緣止於 frame.x + frame.width − right, 可與表格儲存格文字的左右內距精確對齊;rule 仍畫滿內容區寬度。未設定或設為0時繪製指令與既有 bytes 相同。 左右內距總和須小於內容區寬度,否則建構時回報 report.model.styles.incomplete(location pageSummary)。

everyPage、lastPage 及 summaryLastPage 可使用 SectionPageNumber/SectionPageCount,解析為該頁所在頂層 pageBreakBefore=true Section 的頁碼與排版完成後回填的實際總頁數(與分節頁首尾相同計數;節內 PageBreak 的空白頁也計入)。 一份文件可每個收件日一個 Section,各頁印「頁次:段內頁/段內總頁」、各段最後頁以 summaryLastPage 印總筆數,不需分段排版再串接 PDF。 使用分節頁次的摘要在全部頁面完成後才回填;不使用者維持原本的立即繪製路徑。

分節頁次不會靜默改用全份頁碼:

使用位置建構時要求(違反時 report.model.inline.page-field-outside-region)
everyPage(lastPage 未覆寫時亦沿用)body 非空且每個頂層區塊都是 pageBreakBefore Section
只有 lastPage最後一個頂層區塊是 pageBreakBefore Section
Section.summaryLastPage該節 pageBreakBefore=true(覆寫原本即限頂層)

排版時若某頁仍無法歸屬單一頂層分頁分節(例如同頁混有節外內容),以 report.layout.section.summary-outside-section 停止,不輸出部分 PDF。 文件固定頁首尾與正文仍不允許分節頁次。

var builder = ReportDocument.builder()
.styles(s -> s.table(TableStyle.inherit().withCellPadding(Length.ofPoint(5.4))))
.pageSummary(s -> s.right(Text.of("頁次:"), new SectionPageNumber(), Text.of("/"), new SectionPageCount())
.padding(Length.ofPoint(0), Length.ofPoint(5.4), Length.ofPoint(0), Length.ofPoint(5.4)));
for (var day : days) {
builder.section(s -> s.pageBreakBefore(true)
.summaryLastPage(p -> p.left(Text.of("總筆數:" + day.count())))
.table(t -> t.column("name", "姓名", 1).rows(day.rows())));
}

分節最後內容頁覆寫(4.0.0-alpha.26)​

ReportDocument.Builder.section(Consumer<SectionBuilder>) 建立 Section, SectionBuilder.summaryLastPage(Consumer<PageSummaryOverride.Builder>) 宣告本節最後內容頁的摘要。 完整值模型為 Section(id, title, blocks, pageBreakBefore, summaryLastPage); 舊四參數建構器保留且新欄位為 null。PageSummaryOverride(left, right) 的各側為 Inline 清單, 未設定/null 繼承、空清單或空 varargs 明確清空;不以空清單表示繼承。

頁面每側的優先順序
一般頁everyPage
文件最後頁lastPage(Builder 未設定該側時已沿用 everyPage)
分節最後內容頁分節 summaryLastPage 的該側 → 該頁原本的文件摘要

最後一段的總筆數可覆蓋文件最後頁的總筆數,未覆寫的右側仍印全文件頁次。 PageNumber/PageCount 採全文件 physical page number/total page count,不因分節重新起算;需要分節頁次時改用下節的 SectionPageNumber/SectionPageCount。 左右允許 Text、PageNumber、PageCount、LineBreak,以及(本節為 pageBreakBefore 時)SectionPageNumber/SectionPageCount;共用文件 PageSummary 的樣式、上線、gap 及上下內距。 分節覆寫及繼承的另一側按每行共用 baseline(不同字級亦對齊),高度採合併後的行高;不提供分節自己的樣式/線配置。 此對齊僅作用於有分節覆寫的頁面。一般頁(含沒有分節覆寫的文件最後頁)保留既有左右各自的 baseline; 左右字級不同時,較小字的右側頁次在覆寫頁可能隨較大字的左側摘要下移,與一般頁的垂直位置不同。 這是維持既有輸出 bytes 並讓覆寫頁左右同 baseline 的取捨;需要跨頁一致時,左右使用相同字級/行高。

var document = ReportDocument.builder()
.pageSummary(p -> p.right(Text.of("頁次:"), new PageNumber(), Text.of("/"), new PageCount())
.padding(Length.ofPoint(2), Length.ofPoint(2))
.rule(new LineStyle(Length.ofPoint(0.5), ReportColor.rgb(0))))
.section(s -> s.id("receipt-2026-02-26").pageBreakBefore(true)
.summaryLastPage(p -> p.left(Text.of("總筆數:60")))
.table(t -> t.column("id", "編號", 1)
.rows(RowSource.lazy(() -> java.util.stream.Stream.of(TableRow.detail("合成資料"))))))
.build();

「最後內容頁」依實際放置的內文判定,來源 EOF 前不猜頁數、不重新讀 lazy 資料。 空表格有表頭時表頭視為內容;無任何已放置內容(例如空節、只有 Spacer 或 PageBreak)的覆寫節在 render 時 回報 report.layout.section.summary-empty。若節尾有 PageBreak,覆寫仍綁定換頁前的最後內容頁,空白頁照常保留。 摘要跟在該實體頁整體內文游標後,包括 Spacer/區塊間距;同頁後續其他內容仍使用同一列摘要。

以下情境直接失敗,不選擇某一段或輸出半份 PDF:

  • 沒有文件 PageSummary,或巢狀 Section 設定覆寫(即使外節沒覆寫):建構時 report.model.section.summary-invalid。
  • 同一頁有兩個以上設定覆寫的 Section 結束(即使左右各覆寫不同側):排版時 report.layout.section.summary-conflict。
  • 覆寫內容超出半頁寬:report.layout.no-space,包括總頁數回填後才超寬的情況;不截斷或縮字。
  • 文件/分節摘要預留後不足一行內文:report.layout.page-regions-too-tall。

只有設定覆寫的節參與同頁衝突判定。為處理結尾換頁,啟用分節覆寫後全部頁摘要在文件完成時統一回填; 每頁只保存頁號、最後頁旗標及內容結束座標,各節只保存兩個合併候選與最後內容頁綁定, 記憶體為 O(頁數+分節數) 加上原單頁 boxes/有界列預看,不保存全文件 boxes/列資料。 回填檢查取消,失敗/取消沿用 renderer 的兩階段交付及暫存清理契約。

Section 新增 record component 的反射、toString、equals/hashCode 會改變; 舊四元件 record deconstruction pattern 須調整後重編,舊建構器的 binary signature 保留。 Java 實驗模型不保證 wire schema。未啟用功能的完整 PDF bytes 以已發布 alpha.24 的八個固定案例回歸, 並另核對依賴 alpha.25 的 nbsos RPT-001~004 原接受產物;版本、字型、metadata 與 PDFBox 必須固定。 這些框架驗證不代表 RPT-005 的 HTML/PDF 或產品流程已驗收。

完整 PDF bytes 回歸的環境與更新程序​

PageSummaryCompatibilityTest 的八個 golden 僅適用於資源 legacy-summary-alpha24.sha256 記錄的環境: PDFBox 3.0.8、JDK runtime version/vendor、OS/architecture,以及固定 Deflater 探針的 SHA-256。 測試的 PDFBox 依賴仍採動態範圍;執行時任何一個前提不符,以 JUnit assumption 略過這八項, XML 會記錄實際/預期值與原因。略過不代表 bytes 相容性通過;其他模型、排版及 PDF 內容/座標測試照常執行。 JDK 版本號相同不能單獨證明 zlib 相同,壓縮探針亦非所有輸入的相等證明;不得宣稱未執行的 CI 與本機 zlib 一致。

需要在另一個 CI/JDK/PDFBox 環境證明 bytes 時:

  1. 固定字型 SHA-256、metadata、PDFBox 精確版本與同一 JDK;以 PageSummaryCompatibilityFixtures 分別啟動兩個 JVM。 第一個的 classpath 把已發布 alpha.24 JAR 放最前面,不包含候選 main classes;第二個使用候選框架。
  2. 核對第一個 JVM 的 class origin 確為 alpha.24,記錄 JAR SHA-256、兩次 byte-environment.txt,並逐 byte 比對全部八份 PDF。 兩次環境前提必須吻合;任何 PDF 不同,先調查,不能用候選輸出覆蓋 golden。
  3. 若需替換常駐 golden,僅以已發布 alpha.24 的 PDF 更新八個 SHA-256 與 # environment.* 記錄, 保留重產/比較證據並經 peer review;不由 -PupdateGolden 自動更新這份基準。
  4. 升級到 alpha.24 無法支援的 PDFBox 時,既有八項維持略過,改做新版環境的內容/座標驗證;不得把新版 bytes 誤稱 alpha.24 相容。

分節最後內容頁摘要的審閱裁決(2026-10-06)​

本擴充經兩輪人審通過,3條意見均已處置,第2輪無新增意見。審閱接受獨立 nullable 側覆寫模型、無內容回報 summary-empty、要求文件 PageSummary 並共用配置的取捨;理由見ADR-029實作註記。

僅覆寫頁共用 baseline,混合字級的跨頁垂直差異已明定。完整 bytes 測試採保守環境前提,不符時明確略過;審閱者接受此取捨,略過不視為相容性通過。缺少文件摘要與巢狀覆寫使用不同診斷訊息,錯誤碼仍為 summary-invalid。

全量1655項(9 skipped、零失敗)、Javadoc與文件建置通過,本機8項 bytes 實際執行。此框架擴充已通過審閱,由 4.0.0-alpha.26 提供;RPT-005 的HTML/PDF與產品驗收仍由下游承接。

每頁邏輯紀錄上限與高度分頁​

TableBuilder.maxRecordsPerPage(int) 預設0,表示沿用既有排版;正值啟用紀錄模式,負值被模型拒絕。 TableRow.recordStart 預設true,每個來源列各為一筆;後續列 withRecordStart(false) 接到前一筆,直到下一個true或來源結束。 第一個來源列必須為true,建構時與延遲來源讀取時共用 report.model.table.record-structure 驗證。 標題、指定表頭及合成群組續接列不計入上限;來源的任何角色仍依recordStart計數,與業務groupPath無關。

紀錄模式以整筆列高總和及當頁紀錄數共同判斷。高度不足時提前換頁並歸零當頁計數,達上限也在下一筆之前換頁; 每次換頁沿用重複表頭及群組續接規則。不預切每15筆的表格、不猜高度、不重新取數。 一筆的各列完整同頁;此模式由recordStart定義硬性邊界,不以keepWithNext跨筆串接,也不拆分splittable列。 來源的 GROUP_HEADER 也依 recordStart 計數。群組標題若獨立為一筆,即使 keepWithNext=true,仍可能成為當頁最後一筆; 首筆明細換頁時會依 groupPath 補群組續接列,原標題與明細因此分離,合成續接仍不占紀錄配額。 需要標題與首筆明細同頁時,將首筆明細設為 withRecordStart(false),與標題合為一筆;若該明細有多列,其後續列也設為false。 此時配額按合併後的邏輯紀錄計算,標題加首筆明細共占一筆,整筆高度仍須容納。後續明細照常以true開始新筆。 GroupPath只決定群組續接,不自動合併紀錄;不要僅依賴 GROUP_HEADER 預設的 keepWithNext。 紀錄含表頭/續接後超出整頁,或一筆超過256個來源列,回報 report.layout.record-too-large ERROR,不裁切或降級。 最多保存256列及一列邊界預看,並有一頁高度限制;讀取及換頁仍檢查取消,結束或失敗均關閉來源。 上限屬每張Table在各實體頁的紀錄數,多張Table不共用配額;KeepTogether內不允許有限額表格,以免相矛盾。

var first = TableRow.detail("初檢基本資料");
var second = TableRow.detail("聯絡電話與次行資料").withRecordStart(false);
var document = ReportDocument.builder()
.table(t -> t.column("id", "欄名", 1).maxRecordsPerPage(15).rows(List.of(first, second)))
.build();

nbsos RPT-002 將每筆雙行作為同一紀錄,單張Table上限15;15筆短資料一頁、16筆為15+1。 長文以整筆高度提早換頁後,新頁仍可容納15筆;例如單筆高度較大的首頁只容納2筆,後續頁可以接著放滿3筆(上限3的驗證案例)。 RPT-001 保持0,依高度正常續頁且不印頁碼。此處只定義框架承接方式;nbsos 的字型、1mm幾何及所有HTML案例仍需另行驗證。

相容性與替代方案​

Table 已發布的六參數建構器與 TableRow 已發布的五參數建構器保留,另提供包含全部元件的八/七參數建構器。 未發布的過渡七/六參數建構器已移除;選用設定可透過Builder及with方法組合,避免累積伸縮建構器。 Builder 預設自動欄標題仍為true,列樣式為null、紀錄上限0、recordStart=true。 既有 report 的欄標題、表頭追加順序、padding、每列線、固定頁尾及 corpus 輸出須保持。 新增 public record component 會改變反射/toString 結構,equals/hashCode 亦納入新欄位; 使用舊元件數的 Java record deconstruction pattern 必須修改後重新編譯,保留舊建構器不涵蓋此語法相容性。 本模型是實驗性 Java API,原契約沒有 wire schema 保證。

其他方案:以固定紙底頁尾取代內容下方頁次會改變已確認版面;應用猜頁數、預先量測/分頁或 PDFBox 後製 會承擔框架排版責任;使用 Figure 畫表格線還需要圖說,不適合表格樣式。因此新增 opt-in 模型與排版設定。

驗證與交付​

命題驗證
完整表頭無自動欄標題、按順序跨頁重複、逐列線與內距LayoutEngineTest.AdoptedLayouts
長列分割保留內距、文字完整,其他既有分頁不變新排版測試、既有完整 report 回歸
摘要隨短/長內容、總頁數回填、最後頁合計同線、空資料、固定頁尾並存排版位置測試及 FlowLayoutPdfTest 實際PDF文字座標
摘要字級/行高覆寫、忽略段距/縮排、回填途中取消LayoutEngineTest.AdoptedLayouts
群組續接使用表格預設而非來源列覆寫跨頁群組+逐列樣式位置/線條測試
15/16筆雙行、長文高度先換頁並重設配額、不可拆紀錄及有限預看LayoutEngineTest.AdoptedLayouts、FlowLayoutPdfTest實際PDF
群組標題獨立計數時可能孤立,與首筆明細合為一筆可避免;續接不占配額LayoutEngineTest.AdoptedLayouts群組+紀錄上限案例
總頁數位數增加才超寬時,零輸出/無成功產物/暫存清理PdfRenderLifecycleTest控制1/1成功、兩位總頁數回填失敗,兩個render overload皆檢查
不合法設定及任意內文頁碼仍被拒絕ReportModelValidationTest,共用讀取時 RowValidator
舊 API、字型、架構與 corpus 無意外改變既有完整 test、Javadoc、memory gates及 corpus golden
新摘要與固定頁尾同時回填仍可流式完成PdfRenderMemoryTest:50,000 列、獨立 128MB heap,包含舊模式與新摘要模式
邏輯紀錄與摘要的串流記憶體/來源釋放PdfRenderMemoryTest:10,000筆雙行、每頁15筆、667頁,獨立128MB heap

此處定義驗證命題,結果與指紋寫入審閱證據;不宣稱下游 HTML/PDF 接受。 框架設計及實作經三輪 peer review 通過後提交,於2026-10-04發布不可變 pre-release 4.0.0-alpha.23;既有 alpha.22 套件未覆寫。 下游需 pin 新發布版本,使用有效已接受 HTML 的相同合成案例逐頁比對,再建立新 PDF 帳本與真人接受。 Maven releases repository 已成功發布 4.0.0-alpha.23;本頁及核心模型/PDF renderer 引言同步登錄版號與發布日。框架發布不代表下游 HTML/PDF 版面或產品流程已驗收。

儲存格四邊框線(4.0.0-alpha.25)​

新增 CellBorders(top, right, bottom, left)、TableStyle.withCellBorders 與 Cell.withBorders。 API 的繼承順序為文件表格樣式 → 節點表格樣式 → 個別儲存格,各邊獨立合併:

值行為
null繼承上一層;沒有上一層則不畫
線寬0明確不畫,不產生 PDF hairline
有限正線寬在實際儲存格外緣畫線,線寬以邊緣為中心

CellBorders.inherit() 全部繼承;all(line) 四邊相同;none() 明確關閉四邊。 欄的合併只畫合併後外緣,不畫跨欄內線;可拆列的每個跨頁實體片段重複指定的四邊, 以該片段的實際高度收框。表頭重複與群組合成續接列使用同一張表的框線預設。 框線不改文字量測、內距、列高或記錄分頁規則;需要避開粗線時由作者增加內距。

相鄰儲存格各自繪製邊緣,不做 CSS border-collapse 的衝突決策。順序為每列由左到右、 各格上→右→下→左、列由上到下。不同寬度/色碼可能疊線;共享邊指定相同樣式, 或只保留其中一邊,可取得一致外觀。整筆底色先於所有文字與框線繪製;跨列格內容與邊框於其他列完成後,依起始列及欄位順序繪製。 此版本不提供 HTML importer 或任意 CSS 解譯。

既有逐列 topRule/bottomRule 與四邊框線獨立;跨列格內部的列界線略過該格佔用區段, 其餘區段及合併外緣仍使用原列線樣式。要純格線可用 columnTitles(false) 建立明確表頭、各列 TableRowStyle.noRules();只把表格 rule 設零仍保留舊 backend hairline 語意。 未設定四邊框線時不增加線條;保留 Cell 四參數/TableStyle 七參數建構器。 新增 record component 會改變反射、toString、equals/hashCode;舊 record deconstruction pattern 須重編調整。

var line = new LineStyle(Length.ofPoint(0.5), ReportColor.rgb(0));
var grid = TableStyle.inherit().withCellBorders(CellBorders.all(line));
var mainCell = Cell.of("資料").withBorders(CellBorders.inherit()
.withBottom(CellBorders.none().bottom()));
var noteCell = Cell.of("補充").withColSpan(2).withBorders(CellBorders.inherit()
.withTop(CellBorders.none().top()));
var document = ReportDocument.builder()
.table(t -> t.column("a", "", 1).column("b", "", 1).columnTitles(false).style(grid)
.headerRow(TableRow.detail("欄一", "欄二").withStyle(TableRowStyle.noRules()))
.rows(List.of(TableRow.detail(mainCell, Cell.of("值")).withStyle(TableRowStyle.noRules()),
TableRow.detail(noteCell).withStyle(TableRowStyle.noRules()))))
.build();

測試涵蓋繼承/零寬/跨欄幾何、跨頁片段與表頭、完整多列紀錄、建構與延遲列驗證, 並以實際 PDF raster 檢查垂直線和跨欄內部留白。下游樣張與驗收仍由該專案 RPT 承接。

跨列與首頁表頭(4.0.0-alpha.25)​

Cell.withRowSpan(n) 預設1,接受1~256。限 maxRecordsPerPage > 0 的紀錄模式, 同筆全部使用不可拆 DETAIL 列;第二列起省略已被前列跨列格佔用的欄,依左至右填空位。 colSpan 可同時合併欄位,但不得越過已佔用欄位;跨紀錄、懸空、超界、表頭/群組列或可拆列 回報 report.model.table.row-span(欄數超界仍使用 column-mismatch)。建構與延遲列讀取共用驗證, 來源結束也檢查未完成跨度。全列均被跨列佔用時使用空 cells 的 TableRow。

每列先取非跨列格的高度;跨列格內容高度超過涵蓋列總高時,增加最後涵蓋列高度。 TOP/MIDDLE/BOTTOM 使用合併後完整高度,邊框只畫合併外緣。整筆一次判斷能否留在同頁, 不能拆跨列格到續頁;超過頁面高度或256列預看上限仍回報 ERROR。預看含下一筆邊界列, 只保留當前有界紀錄及欄位佔用計數;不為 rowspan 建立全文件座標/Box 陣列。

var rows = List.of(
TableRow.detail(Cell.of("主碼"), Cell.of("V").withRowSpan(2).withVerticalAlign(VerticalAlign.MIDDLE))
.withSplittable(false).withStyle(TableRowStyle.noRules()),
TableRow.detail(Cell.of("原碼")).withRecordStart(false).withSplittable(false)
.withStyle(TableRowStyle.noRules()));
var doc = ReportDocument.builder().table(t -> t.column("id", "", 1).column("result", "", 1)
.columnTitles(false).maxRecordsPerPage(10000)
.firstHeaderRow(TableRow.detail("日期與簽核", "欄名"))
.headerRow(TableRow.detail("日期", "欄名"))
.rows(rows)).build();

firstHeaderRow 可指定多列,完全取代首張表格頁的 headerRows;若開啟自動 columnTitles, 仍先印自動欄標題。未指定時沿用 headerRows;續頁僅在 repeatHeader=true 時印一般表頭。 首次放置以首頁表頭及首筆高度共同判定,避免簽核頁沒有首筆;連首筆最小內容都放不下即 ERROR。 此處「首頁」是該表格首次出現的頁,非文件頁碼1;空表仍印首頁表頭。

未新增 CSS border-collapse、任意可拆 rowspan 或表頭 rowspan。已發布的 Cell 四參數、 Table 六/八參數建構器均保留;未發布的 Cell 五參數過渡建構器不納入 API, 四邊與跨列設定使用 with 方法或完整六參數建構器。record 反射/equals/hashCode/toString/解構模式須依新組件核對。 測試包括合併欄與跨列共同幾何、內容增高、完整紀錄跨頁、首頁/續頁/空表、延遲來源 EOF 及舊行為回歸;下游 RPT-004 以同版框架 composite build 產生已接受 HTML 的合成案例再逐頁比對。

四邊框線、跨列與首頁表頭的審閱裁決(2026-10-05)​

本擴充經兩輪人審通過,8項意見均已處置,第2輪無新增意見。 審閱接受相鄰邊各自繪製、rowSpan限同筆不可拆紀錄、首次表頭不能與首筆同頁即 ERROR 的三項取捨; 理由與責任邊界記錄於ADR-029實作註記。 修正已涵蓋跨列內部列線裁切、底色先於框線、重複續列的末列增高及實際首頁表頭高度診斷, 並恢復非跨列資料的既有診斷數量與訊息;未發布的五參數過渡建構器不納入公開 API。

框架1632例(9 skipped)、nbsos報表/架構64例及revision10四案例六頁比對通過, 包括128MB heap下10,000筆跨列紀錄/667頁。人審通過不代表已發布; 下游revision11及真人PDF接受仍由RPT驗收承接,不能沿用revision10的比對結果。

分節每頁頁首尾與頁碼(alpha.28)​

為RPT-014新增opt-in的 SectionBuilder.header/footer 與node.SectionPageRegion, 逐節頁碼使用SectionPageNumber/SectionPageCount,PageNumber/PageCount保留全文語意。 完整公開配置、限制、1,000頁限制組合、相容性與可執行下載範例見 分節頁首尾使用指南。

alpha.27已發布sources與releases metadata已查核;最後頁摘要和表格repeatHeader無法涵蓋跨表及備註的每頁個案資訊, 固定PageRegion的超寬截斷亦不適用。擴充使用獨立node區域而不改既有RegionLayout,維持node為leaf package。 頁面只屬一節,要求body直接子Section與pageBreakBefore;節後下一正文自動起頁,最後節不另產空白頁。 起訖頁與區域只作O(節數+頁數)回填,不保存全部boxes,不重讀來源。 本能力沿用flow及兩階段交付;不涉及HTML importer或job。

本輪以合成PDF驗A2+B1、長文字與8,000字備註、兩表欄名續頁、100案、1,000頁邊界與1,001頁零輸出, 並擴充128MB heap的100案長備註回歸。發布與下游RPT真人接受為不同狀態,不能承接下游已確認HTML作為PDF接受。

alpha.29:完整分節 Block 頁首尾​

alpha.28 的 Inline 等寬配置保留;alpha.29 另增 SectionPageRegion.Builder.blocks, 由 Paragraph 的獨立段距/樣式與無框線 Table 承接全寬抬頭及三組左對齊資訊。 區域共用 flow 排版器,先完成單頁量測並快取區域 boxes,再按每頁位置平移重印。 區域禁止換頁與 lazy 列來源;正文仍保持串流,過高不截斷、交付仍為兩階段。 量測盒的記憶體按各節區域內容成長,不按正文列數或續頁數成長;既有分節頁碼及全文頁碼語意保持。 公開能力、限制及190mm範例見分節頁首尾指南。

alpha.30:人工文字空白與長字詞策略​

alpha.29已發布sources與Maven metadata已核對,沒有空白/Tab/斷詞配置點。新增node.TextFlow及ParagraphStyle的 textFlow、repeatSpaceBefore覆寫,保留九參數建構器。未opt-in仍走原LineLayout,不重寫既有字詞切分; 新FlowLineLayout跨Inline計算正常斷點、以grapheme作緊急拆詞、量測原空白glyph及Tab advance, 並允許CJK避頭/避尾及行尾懸掛配置。分節Fragment仍使用有界規則,無額外縮字或裁切。

末尾換行的額外空行高度與CSS末尾換行語意分開配置;本次需求明示選擇保留。續頁段前距可獨立opt-in, 防止RPT-014每頁2mm段前距消失。零可用行高會立即ERROR,不產生無限空頁。 段落分頁仍依orphans/widows,HTML逐行切分對照設定為1,頁數上限沿用既有onPage。

檢視範圍為report/node與internal樣式/排版/PDF寫入、相關report測試、兩份使用指南、report-flow-layout、 ADR-029及API/llms索引;不修改歷史REP與ADR的既有成果,不涉及job、HTML importer、權限或資料來源。 完整能力、限制與可執行回歸見文字流指南。

表格虛線與每頁表身末端線(alpha.31)​

公開API查核、dash/phase單位及合法值、頁末覆寫優先序與範例見 表格線型指南。保留alpha.30的實線預設與既有建構器。 新增能力只改繪製線條,不增加排版尺寸;原rowSpan與recordStart限制維持。 未提供CSS border-collapse、跨頁rowSpan或相鄰獨立邊的phase連續保證。