Flow 報表:分節每頁頁首尾與頁碼
適用版本:io.leandev.appfuse.webapp:appfuse-server:4.0.0-alpha.29(Inline 模式自 alpha.28 起)。這是 opt-in Java authoring API,使用既有 flow 排版。
公開 API 查核
新增能力前,於 2026-10-10 查核 Maven releases metadata 與已發布 4.0.0-alpha.27 sources JAR,最新已發布版本為 alpha.27。
該版 SectionBuilder 只有 summaryLastPage;Table.repeatHeader 僅重印該表;PageNumber/PageCount 為全文計數。
固定 PageRegion 不自動換行、超寬會截斷,因此既有公開組合不能提供逐案跨表格及備註的頁首尾。
本能力新增獨立分節區域與頁碼節點,保留已發布既有 API 的行為。
alpha.28 已提供逐案計數,但只有等寬左中右 Inline,不能組合全寬抬頭與獨立段落樣式;alpha.29 新增 Block 模式。
快速開始
下游固定依賴新版本,並由消費端加入 PDFBox:
repositories {
mavenCentral()
maven { url = uri("https://devops.leandev.io/repository/maven-releases/") }
}
dependencies {
implementation(platform("org.springframework.boot:spring-boot-dependencies:4.0.1"))
implementation("io.leandev.appfuse.webapp:appfuse-server:4.0.0-alpha.29")
implementation("org.apache.pdfbox:pdfbox:3.0.8")
}
每個個案使用一個頂層 Section;頁首包括機構、報告名稱與個案資訊,頁尾使用分節頁碼:
import io.leandev.appfuse.measure.Length;
import io.leandev.appfuse.report.ReportDocument;
import io.leandev.appfuse.report.node.LineBreak;
import io.leandev.appfuse.report.node.ParagraphStyle;
import io.leandev.appfuse.report.node.SectionPageNumber;
import io.leandev.appfuse.report.node.SectionPageCount;
import io.leandev.appfuse.report.node.TableRow;
import io.leandev.appfuse.report.node.Text;
import java.util.List;
var document = ReportDocument.builder()
.section(s -> s.pageBreakBefore(true)
.header(h -> h.left(Text.of("姓名:合成個案"), new LineBreak(), Text.of("出生:合成"))
.center(Text.of("合成醫療機構"), new LineBreak(), Text.of("初檢報告"))
.right(Text.of("篩檢:A"))
.style(ParagraphStyle.inherit().withLineHeight(1.2))
.gap(Length.ofPoint(8)))
.footer(f -> f.right(Text.of("A 第 "), new SectionPageNumber(), Text.of("/"),
new SectionPageCount(), Text.of(" 頁"))
.pageNumberDigits(4).reserveHeight(Length.ofPoint(18))
.style(ParagraphStyle.inherit().withLineHeight(1.2)).gap(Length.ofPoint(8)))
.table(t -> t.column("disease", "指定疾病", 1).column("result", "結果", 1)
.repeatHeader(true).rows(List.of(TableRow.detail("疾病甲", "陰性"))))
.table(t -> t.column("disease", "自選疾病", 1).column("result", "結果", 1)
.repeatHeader(true).rows(List.of(TableRow.detail("疾病乙", "陰性"))))
.paragraph("備註內容".repeat(2000)))
.build();
完整可執行範例:下載範例 ZIP,原始碼位於框架
appfuse-server/examples/section-pages/。解壓後用 JDK 25 與既有 Gradle 9.2.1 wrapper 執行:
/path/to/server/gradlew -p /path/to/section-pages run --args='/path/to/fonts /path/to/output.pdf'
範例含九項資訊、A案8,000字備註與B案短備註;全部合成值。字型目錄、完整執行說明在ZIP內README。 範例不是 RPT-014 revision 7 的樣式替代品;nbsos 應映射該版已確認的完整樣式與字型,接續相同案例的PDF比對及接受。
全寬 Block 頁首(alpha.29)
SectionPageRegion.Builder.blocks(Block...) 或 blocks(List<Block>) 接受全寬、不可變區塊清單,與 left/center/right 行內內容互斥。
各 Paragraph 使用自己的字級、行高、段前/段後距及對齊;Table 使用自己的欄寬、儲存格內距、列樣式及組內段落對齊。
region 的 style 僅適用 Inline 模式;Block 樣式繼承文件,再由各區塊覆寫。
段距沿用正文 flow:首段段前距省略,區域頂部留白使用 padding(top,bottom)。
範例以 A4 左右10mm邊界取得190mm正文與頁首寬度。抬頭使用全寬10pt Paragraph,報告名稱使用全寬14pt Paragraph,
下方無框線 Table 的五欄權重為55/12.5/55/12.5/55:三個55mm組分別靠左、居中、靠右,兩個12.5mm欄是組間留白。
每個資訊格含三個 Align.LEFT Paragraph,所以中/右組的每一列也從同一左緣開始;長字串在55mm內續行。
機構與標題的行高及間距各自設定;這些數值為公開能力範例,下游依已確認 RPT 尺寸調整。
import io.leandev.appfuse.measure.Length;
import io.leandev.appfuse.measure.Paper;
import io.leandev.appfuse.report.Margins;
import io.leandev.appfuse.report.PageSetup;
import io.leandev.appfuse.report.ReportDocument;
import io.leandev.appfuse.report.node.Align;
import io.leandev.appfuse.report.node.ParagraphStyle;
import io.leandev.appfuse.report.node.TextStyle;
var institutionStyle = ParagraphStyle.inherit()
.withText(TextStyle.inherit().withSize(Length.ofPoint(10)))
.withAlign(Align.CENTER).withLineHeight(1.2).withSpaceAfter(Length.ofPoint(3));
var titleStyle = ParagraphStyle.inherit()
.withText(TextStyle.inherit().withSize(Length.ofPoint(14)))
.withAlign(Align.CENTER).withLineHeight(1.4).withSpaceBefore(Length.ofPoint(0))
.withSpaceAfter(Length.ofPoint(6));
var header = ReportDocument.builder()
.paragraph("完整機構抬頭", institutionStyle)
.paragraph("初檢報告", titleStyle)
.build().body(); // 完整範例另加三組資訊 Table;見 ZIP 的 header(CaseData)
var document = ReportDocument.builder()
.page(PageSetup.of(Paper.A4).margins(Margins.of(Length.ofMillimeter(10))))
.section(s -> s.pageBreakBefore(true).header(h -> h.blocks(header).gap(Length.ofPoint(8)))
.paragraph("正文"))
.build();
支援 Paragraph、Heading、Spacer、KeepTogether 與 materialized Table(包括跨欄)。 不允許 Section、PageBreak、lazy RowSource、每頁紀錄數限制、Image/Figure 或任何頁碼欄位;建構時拒絕,lazy source 不會開啟。 動態全文/分節頁碼仍放在既有 Inline 頁尾;Block 模式亦可用於固定文字的分節頁尾。
renderer 以同一 flow 排版器量測並保留單一區域的繪製結果,加入 padding 與 reserveHeight 後預留正文空間; 每頁只平移並重印一次,不在每頁重新讀取表格。Block 自己不跨頁;區域過高、字形超寬、表格需要裁切或 段距超過可用高度即失敗,不截斷、不縮字、不產出半份 PDF。表格與備註的跨頁方式及分節計數維持 alpha.28。 除頁數/節數引用,額外記憶體為各節頁首尾的已量測 boxes,與正文列數及備註續頁數無關。
配置與排版
Inline 模式的 node.SectionPageRegion 內容為不可變左、中、右行內節點清單。各端水平對齊固定左/中/右;
中央有內容時各占正文寬度的三分之一,中央為空時左右各占二分之一。
支援 Text 的個別樣式及 LineBreak,完整文字依實際字型寬度換行;不截斷、不縮字、標點不懸掛到鄰欄。
單一字形比欄寬還寬會停止產生;不默默裁切。三端高度取最高者,不要求逐列共用基線。
| 配置 | 語意/預設 |
|---|---|
style(ParagraphStyle) | 文字與行高覆寫;null繼承文件,段距/縮排/段落align不適用 |
padding(top,bottom) | 上下內距,預設0 |
reserveHeight(Length) | 最小區域高度,預設0;實際取換行高度+內距與此值的較大者 |
gap(Length) | 區域與正文距離,預設6pt,獨立於最小高度 |
rule(LineStyle) | 頁首下線/頁尾上線,位於gap中央;null或零線寬不畫 |
pageNumberDigits(int) | 頁碼最大預留位數1~10,預設10;1,000頁可設4,這不是頁數上限 |
頁首從上邊界開始;頁尾固定在下邊界之上。正文可用高度扣除兩者實際預留及gap;
PageSummary 額外保留其原有空間,在正文末端繪製。
頁碼在排版前以字型中最寬數字與指定最大位數量測換行,避免總頁數增加後侵入正文;
實際短頁碼可能比保留區域少行,保留高度不因此縮小。超過位數即停止,不能輸出縮字的頁尾。
配置分節頁首尾須為文件body直接包含的頂層Section且pageBreakBefore=true。
每節獨占頁面;節後下一區塊也起頁,最後一節不多生空白頁。節內一般巢狀Section可用,不能再配置分節區域。
未設定的頁首或頁尾各自沿用文件區域,設定者取代同端文件區域;空內容配置可明確清除該端。
同頁表格切換不觸發個案頁首重印;表格自己的 repeatHeader 持續在該表續頁印類別欄名。
頁碼與錯誤
| 行內節點 | 範圍 |
|---|---|
SectionPageNumber | 本節目前實體頁,從1起 |
SectionPageCount | 本節實際排版總頁數,完成後回填 |
PageNumber/PageCount | 全文目前頁/總頁數,所有配置中都保持原意 |
分節頁碼允許用於分節每頁頁首尾;自 alpha.38 起也可用於頂層分頁分節內頁面的 PageSummary(含 summaryLastPage),
規則見隨內容的每頁摘要。不能用在任意正文、表格或文件固定區域。
A兩頁+B一頁得到A 1/2、2/2、B 1/1;同時使用全文欄位仍是 1/3、2/3、3/3。
節內明確PageBreak產生的空白頁亦屬本節實體頁並計數;沒有任何已放置內容的節則拒絕。
| 錯誤碼 | 條件 |
|---|---|
report.model.section.region-block-invalid | Inline/Block混用、換頁、巢狀Section、lazy來源、圖形或每頁紀錄數限制 |
report.model.section.region-invalid | 非頂層或沒有pageBreakBefore |
report.model.inline.page-field-outside-region | 分節頁碼出現在不允許的位置(沿用頁碼驗證代碼) |
report.layout.section.region-empty | 分節沒有已放置內容,只有Spacer/PageBreak亦屬此條件 |
report.layout.section.summary-outside-section | PageSummary 使用分節頁碼,但該頁不屬於單一頂層分頁分節 |
report.layout.page-regions-too-tall | 頁首尾與摘要後不足一行正文 |
report.layout.no-space | 字形超過欄寬、回填高度不足或頁碼超過預留位數 |
空表有可見表頭視為內容。排版/回填失敗及取消遵守原renderer暫存清理與兩階段交付,不回傳成功產物、不輸出部分PDF。 來源只讀一次,無需猜頁數或在應用PDF後製;框架只保留逐頁分節引用與節的起訖,O(頁數+節數),不累積全文件boxes。
100案與1,000頁
1~100案、每案備註最多8,000 UTF-16 code units為下游參數限制,範例的純值模型驗證它們。
框架沒有全域1,000頁預設上限;以既有公開 RenderOptions.onPage 實作RPT的產生上限:
import io.leandev.appfuse.report.RenderOptions;
var options = RenderOptions.defaults().onPage(page -> {
if (page > 1000) throw new IllegalArgumentException("Report page limit exceeded: 1000");
});
第1,001頁完成繪製時回呼中止,但尚在產生階段,沒有對外輸出。使用artifact overload先產生完成產物, 再開最終輸出串流或交給HTTP adapter。下游將此例外映射至RPT既有容量錯誤契約;不切成多份PDF、不轉背景job。
相容性與驗證
SectionPageRegion 保留 alpha.28 十參數建構器;新增 blocks component 的反射/解構限制與其他 record 擴充相同。 Section舊四/五參數建構器保留;新增record components會改變反射、toString、equals/hashCode及解構模式。 Inline sealed permits新增兩型別,下游 exhaustive switch須補case或適當default;未使用新能力的作者API、排版與全文頁碼不變。 Java實驗模型不保證wire schema。
SectionPagesPdfTest 實際解析PDF文字/座標:A2+B1、長姓名機構、兩表同頁及類別續頁、8,000字不縮字與完整末字、
100案、1,000/1,001頁零輸出、文件區域繼承、lazy來源單次讀取/關閉與非法配置。
PdfRenderMemoryTest.hundredCasesWithLongNotesAndSectionRegions 以獨立128MB heap驗100案各8,000字單份PDF。
alpha.28 發布時完整測試共1,656項,失敗0、環境條件略過9項;另有7項獨立128MB渲染測試及7項新增排版邊界測試通過。
既有LayoutEngine、模型、PDF、架構、corpus及8項alpha.24完整bytes回歸均通過;bytes環境前提不符時會明確略過。
SectionBlockPagesPdfTest 驗證190mm完整抬頭、獨立10pt/14pt字級、三組組內左對齊、長姓名/機構、8,000字續頁、兩表同頁只印一次、逐案頁數與非法模式零輸出。
獨立128MB測試另涵蓋100案各8,000字與Block頁首。
alpha.29 完整測試為1,667項、失敗0、環境條件略過9項;8項獨立128MB渲染測試、Javadoc及8項既有PDF bytes回歸均通過。
框架能力驗證不代表nbsos RPT-014 revision 7的HTML/PDF、正式資料adapter或產品流程已接受。 維護端詳細背景見flow版面擴充。
人工文字與長姓名需要保留空白、Tab、正常斷點優先拆詞時,alpha.30可在Block的ParagraphStyle配置 TextFlow。未配置時保留alpha.29的原有文字行為。