REP-04:Figure 與統計圖表契約設計
狀態:已通過設計審閱(3 輪,2026-09-29)與程式碼審閱(3 輪,2026-09-30);已實作。實作細節見「實作註記」,程式碼審閱的裁決見「程式碼審閱裁決」 依據:ADR-029(含 REP-01~03 實作註記)、報表產生開發計畫 REP-04、REP-02 契約、REP-03 契約、REP-01 spike、FU-105 日期:2026-09-29
範圍
| 納入 | 不納入(所屬工作包) |
|---|---|
Figure 的受限 SVG 子集:語法、繪製語意、安全政策與複雜度上限 | 漸層、圖樣、遮罩、濾鏡、<use>、內嵌圖片等 SVG 進階功能 |
點陣 Figure 內容(PNG/JPEG),供外部圖表函式庫的輸出使用 | 以點陣圖取代向量輸出的自動降級 |
| SVG 內文字以已登錄字型輸出為可抽取的 PDF 文字 | tagged PDF、圖表的結構化無障礙標記 |
長條圖與折線圖的 ChartSpec -> Figure adapter:座標軸、刻度、圖例、標籤、無資料狀態 | 堆疊圖、圓餅圖、雙軸、資料標籤、互動圖表 |
| Figure 在文件流、表格儲存格與跨頁前後的排版(沿用 REP-03 的不可分割規則) | ReportArtifact、下載與 ETag(REP-05) |
移除 REP-03 的替代文字框與 report.figure.not-rendered | — |
已確定的方向
以下兩項已由框架維護者於 2026-09-29 決定,本文據此設計:
- 自寫受限 SVG 子集,不採用 Batik+pdfbox-graphics2d。 REP-01 以 Batik 繪製時,圖內文字變成向量外框、無法抽取;
Batik 以 AWT 解析字型,函式庫必須在 JVM 全域註冊字型;還要新增 pdfbox-graphics2d 相依,並承擔 Batik 較大的安全面。
自寫子集只解析白名單內的元素,直接繪製成 PDF,圖內文字用
ReportFonts的字型輸出為真正的文字。代價是自行實作 path 與 transform 解析,並放棄 CSS 樣式表、漸層與濾鏡等功能。 - 圖表 adapter 提供長條圖與折線圖。 涵蓋 corpus 統計報表的兩張圖;兩者共用座標軸、刻度、圖例、標籤與無資料狀態。
設計原則
- SVG 子集是模型契約,不是某個 renderer 的實作細節。 子集的語法與上限由
ReportDocument.Builder#build()驗證 (延遲列來源在讀取時逐列驗證),所以不合法的 SVG 在建構時就失敗,任何 renderer 看到的都是同一個子集。 - 白名單,而非黑名單。 只接受列出的元素、屬性與值的形式;其他一律拒絕,不靜默略過。
- 不存取外部世界。 XML 解析停用 DOCTYPE、外部實體與 XInclude;子集沒有任何引用外部資源的元素或屬性。
- 先排版、後繪製(沿用 REP-03):SVG 在排版時解析成與後端無關的繪圖清單,文字在此時量測;繪製器只讀清單。
- 核心不懂圖表語意(ADR-029):
ChartSpec只是產生 SVG 的 adapter;應用也可以自己產生符合子集的 SVG。
公開 API
Figure 內容(io.leandev.appfuse.report.node,擴充 REP-02)
public sealed interface FigureContent permits FigureContent.Svg, FigureContent.Raster {
/// 受限 SVG;子集與上限見 REP-04 契約
record Svg(String markup) implements FigureContent { }
/// 點陣圖(PNG/JPEG),在 Figure 的框內等比置中
record Raster(ImageData image) implements FigureContent { }
static FigureContent svg(String markup) { ... }
static FigureContent raster(ImageData image) { ... }
}
Figure 的欄位不變。altText 與含文字的 caption 仍然必填:SVG 內文字雖然可以抽取,點陣內容不行,且替代文字是
螢幕報讀器唯一能讀的整體描述。
圖表 adapter(新套件 io.leandev.appfuse.report.chart,@ExperimentalApi)
Figure quarterly = BarChart.builder()
.categories("Q1", "Q2", "Q3", "Q4")
.series("台北信義門市", List.of(2_805_500d, 2_734_700d, 2_270_600d, 2_626_800d))
.series("新北板橋門市", List.of(2_137_800d, 1_805_400d, 1_630_400d, 1_998_400d))
.valueAxis(a -> a.title("新台幣元").format(v -> NUMBER.format(v)))
.noDataText("本季沒有營收資料")
.toFigure(FigureFrame.of("quarterly", Length.ofPoint(480), Length.ofPoint(260))
.altText("三家門市四個季度營業額的分組長條圖")
.caption("圖 1 各門市季度營業額"));
| 型別 | 說明 |
|---|---|
ChartSpec | sealed:BarChart、LineChart;toFigure(FigureFrame) 產生含 SVG 的 Figure |
BarChart、LineChart | 不可變;以 builder 建立:categories、series(名稱、數值、選填顏色)、valueAxis、legend、title、noDataText、textStyle |
ChartSeries | name、values(List<Double>,null 表示該類別無資料)、color(ReportColor,null 用預設色盤) |
ValueAxis | title(單位等)、format(DoubleFunction<String>,預設 #,##0.##、Locale.ROOT)、min/max(選填,須為有限值;預設自動並包含 0;只給一端時另一端自動,規則見「數值軸範圍」) |
FigureFrame | Figure 的 id、寬高、altText、caption;圖表以寬高為座標系(1 單位 = 1pt),所以字級就是 pt |
受限 SVG 子集
文件與座標
| 項目 | 規則 |
|---|---|
| 根元素 | <svg>,命名空間 http://www.w3.org/2000/svg;不得巢狀 <svg> |
| 座標系 | 有 viewBox 時以其為使用者座標;沒有時以根元素的 width/height 為 viewBox(0 0 w h)。兩者皆無為錯誤 |
| 置入 Figure | viewBox 依 preserveAspectRatio(預設 xMidYMid meet;支援九種對齊與 meet/slice、none)對應到 Figure 的寬高;超出框的內容裁切 |
| 長度 | 無單位數字或 px,皆為使用者單位;%、em、pt、mm 等單位為錯誤 |
| 數值 | 屬性中的每個數字必須是有限值,絕對值 ≤ 10⁷ |
| 衍生數值 | 見「安全政策與複雜度上限」的衍生數值範圍:單一數字合法不代表組合後合法 |
元素與屬性
| 元素 | 可用屬性(另加下方「共通屬性」) |
|---|---|
svg(根) | width、height、viewBox、preserveAspectRatio、version |
g | — |
rect | x、y、width、height、rx、ry |
circle | cx、cy、r |
ellipse | cx、cy、rx、ry |
line | x1、y1、x2、y2 |
polyline、polygon | points |
path | d:M L H V C S Q T A Z 及小寫相對形式;弧線轉為三次貝茲曲線 |
text | x、y、dx、dy(單一數值);內容為文字與 tspan |
tspan | x、y、dx、dy(單一數值);不可再巢狀 |
title、desc、metadata | 內容忽略(不繪製);metadata 內不檢查子元素以外的內容 |
共通屬性:id、class(忽略)、transform(matrix、translate、scale、rotate(含中心點)、skewX、skewY)、
xml:space、下列樣式屬性,以及以相同屬性清單解析的 style="…" 屬性。
樣式屬性:fill、fill-opacity、fill-rule、stroke、stroke-width、stroke-opacity、stroke-linecap、
stroke-linejoin、stroke-miterlimit、stroke-dasharray(≤ 32 個值)、stroke-dashoffset、opacity、color、display、visibility、
font-family、font-size、font-weight、font-style、text-anchor、dominant-baseline(auto、middle、central、
hanging、alphabetic)。樣式依 SVG 規則由祖先繼承。
顏色:#rgb、#rrggbb、rgb(r, g, b)、CSS 具名顏色、none、currentColor。color 屬性接受相同格式(none 與
currentColor 除外),由祖先繼承、根元素預設黑色;fill/stroke 為 currentColor 時取該元素計算後的 color。
url(...) 參照一律拒絕(子集沒有漸層與圖樣)。
g 的 opacity:PDF 的群組透明需要 transparency group;子集以「乘到每個子元素的 fill/stroke 不透明度」近似。
子元素彼此重疊時,重疊處的顏色與瀏覽器不同(見「已裁決的決策」第 4 項)。
文字
| 項目 | 規則 |
|---|---|
| 字型家族 | font-family 依序比對 ReportFonts 的邏輯家族名稱,取第一個已登錄者;sans-serif、serif、monospace、system-ui 等通用名稱直接對應預設家族;都不符時用預設家族並回報 report.font.unknown-family(WARNING,每個 Figure 一次) |
| 字面與缺字 | 與內文相同:font-weight ≥ 600 或 bold 為粗體、italic/oblique 為斜體;fallback、字面替代與 □ 替代沿用 REP-03 |
| 對齊 | text-anchor 的 middle/end 以實際量測的字寬計算,因此置中與靠右精確 |
| 空白 | 預設合併連續空白並去除首尾;xml:space="preserve" 時保留 |
| 不支援 | 多值的 x/y/dx/dy、rotate、textLength、textPath、直排 |
| 輸出 | 以 PDF 文字繪製(可抽取、可搜尋),套用 Figure 的座標轉換;fill 與 stroke 依設定以填色、描邊或兩者輸出(PDF 文字繪製模式),各自套用不透明度;文字不換行、不裁切到 Figure 以外 |
安全政策與複雜度上限(FU-105)
XML 解析器設定:禁止 DOCTYPE 宣告(disallow-doctype-decl)、FEATURE_SECURE_PROCESSING、停用外部一般/參數實體與外部
DTD 載入、停用 XInclude、不展開實體參照、不設定 EntityResolver、命名空間感知。
| 拒絕的內容 | 原因 |
|---|---|
| DOCTYPE、實體宣告、處理指令以外的非 SVG 命名空間元素 | XXE 與實體膨脹 |
script、foreignObject、a、image、use、style 元素、animate*/set、iframe 等白名單外元素 | 可執行內容、外部資源、CSS 解析面 |
任何 href/xlink:href、on* 事件屬性、白名單外屬性 | 外部參照與事件 |
url(...)、@import、expression( 等值 | 外部資源與 CSS 注入 |
| 上限 | 值 |
|---|---|
| 原始碼長度 | 1,000,000 字元 |
| 元素數 | 10,000 |
巢狀深度(含 title、desc、metadata 等不繪製的子樹與 tspan) | 32 |
| path 指令與 polyline/polygon 點數總和 | 100,000 |
| 文字總字元數 | 20,000 |
衍生數值範圍:transform 串接與 viewBox 映射會把合法的輸入組合成溢位或退化的數值,因此驗證時以 Figure 的寬高 計算每個元素的累積矩陣(viewBox 映射 × 祖先 transform × 自身 transform),並檢查:
| 衍生數值 | 範圍 |
|---|---|
| viewBox 寬高 | 介於 10⁻³ 與 10⁶ 之間 |
viewBox 映射比例(Figure 寬高 ÷ viewBox 寬高,含 slice 放大) | 介於 10⁻⁶ 與 10⁶ 之間 |
| 累積矩陣的每個係數 | 有限值,絕對值 ≤ 10⁶ |
| 轉換到 Figure 座標(pt)後的每個點:path 端點與控制點、圓與橢圓的外接框 | 有限值,絕對值 ≤ 10⁶ |
文字位置:依 SVG 規則累積(x/y 重設,dx/dy 與字寬跨 tspan 累加)。驗證時沒有字型,每段字寬只知道在 0 到每字 2 倍字級之間,因此水平位置以區間追蹤並檢查兩端;起點下界再減去 text-anchor 最多往回移的區塊寬 | 轉換到 Figure 座標後為有限值,絕對值 ≤ 10⁶ |
| 轉換後的線寬與字級(係數以矩陣的最大縮放計) | 有限值;字級 ≤ 10,000pt |
超出任一範圍回報 report.model.figure.svg-out-of-range(ERROR)。檢查在 build() 完成,renderer 不會收到無法繪製的數值。
違反上限或子集為 ERROR(見「Diagnostic 代碼」),build() 丟出 ReportValidationException,不產生產物。
圖表 adapter
| 項目 | 規則 |
|---|---|
| 版面 | 由上而下:標題(選填)、繪圖區、類別軸標籤、圖例;數值軸在左。邊距依標籤長度估算(見下方「文字寬度估算」) |
| 數值軸 | 自動刻度取 1、2、5 × 10ⁿ 的間距,約 5 個刻度,範圍包含 0;負值時 0 基線在繪圖區中間。刻度文字以 format 格式化,靠右對齊 |
| 類別軸 | 長條圖每個類別一個區間;折線圖為等距點。標籤放不下時每 k 個顯示一個(k 取最小可行值);單一標籤仍過寬時以「…」截斷 |
| 長條圖 | 同一類別內的系列並排(grouped),區間寬度 70% 給長條,長條間留白最多 1pt、最多占分配寬度的 15%;密集資料時長條變窄但不互相覆蓋(無最小寬度);負值往下長 |
| 折線圖 | 每個系列一條折線與資料點;null 使線段中斷 |
| 圖例 | 多於一個系列時顯示(可關閉),色塊+名稱,放不下時換行 |
| 顏色 | 預設 Okabe–Ito 色盤(8 色,色覺差異友善),可逐系列指定 |
| 無資料 | 沒有系列、沒有類別或所有數值皆為 null 時,不畫座標軸,在繪圖區置中顯示 noDataText(預設「無資料」) |
| 文字 | 字型家族預設為 ReportFonts 的預設家族名稱(由 renderer 對應),字級預設 9pt、標題 11pt,可由 textStyle 覆寫 |
| 輸出 | 只使用本子集的元素;數字以 Locale.ROOT、固定小數位數輸出,相同輸入產生相同 SVG |
輸入驗證
圖表資料由應用程式組成,錯誤屬於程式錯誤:BarChart/LineChart 的 builder 在 build() 時驗證資料,toFigure(FigureFrame)
驗證版面;違反時丟出 IllegalArgumentException,訊息指出系列名稱、類別索引與原因。產生的 SVG 另外照常經過模型驗證。
| 輸入 | 處置 |
|---|---|
| 系列的數值個數與類別個數不同 | 拒絕 |
| 數值為 NaN 或 ±Infinity | 拒絕;「無資料」只以 null 表示 |
| 系列名稱空白 | 拒絕(圖例與替代說明需要名稱) |
| 類別超過 500 個,或類別 × 系列超過 2,000 個資料點 | 拒絕(確保產生的 SVG 遠低於複雜度上限) |
min 或 max 為 NaN 或 ±Infinity | 拒絕 |
min ≥ max(兩者皆指定時,含相等) | 拒絕 |
| 數值超出最終範圍 | 照常繪製並裁切在繪圖區邊界:長條截在邊界、折線以繪圖區裁切;應用明確指定範圍即視為接受裁切 |
| 折線圖只有一個類別 | 該點置於繪圖區水平中央 |
數值軸範圍
最終範圍 [lower, upper] 依下列步驟決定,build() 時計算並驗證:
- 資料範圍:
dataMin= min(0, 所有非null數值)、dataMax= max(0, 所有非null數值);全為null時屬「無資料」,不畫座標軸。 - 自動端:
lower未指定時為dataMin向下取整到刻度間距、upper未指定時為dataMax向上取整到刻度間距 (間距取 1、2、5 × 10ⁿ,使範圍約分成 5 格)。 - 只指定一端且自動端沒有越過它(例如
min = 10、資料全為 1,自動得到upper = 1):自動端改為指定端加(或減) 一個刻度間距,間距以max(|指定端|, 1)為範圍計算。此時資料全部在範圍外,依上表裁切。 - 兩端皆自動且
dataMin = dataMax = 0(全零):範圍取 0~1。 - 驗證最終範圍:
lower、upper皆為有限值且lower < upper;刻度數不超過 20。任一不成立即拒絕(例如極端值使間距下溢)。
版面不足
版面依下列順序讓步,全部讓步後仍放不下才拒絕:
- 類別標籤:每 k 個顯示一個,單一標籤仍過寬時以「…」截斷(上表「類別軸」)。
- 數值軸:左邊距上限為 Figure 寬度的 40%,刻度文字超出時以「…」截斷。
- 標題:只佔一行,超出繪圖寬度時以「…」截斷。
- 圖例:最多兩行;放不下的項目以「+N」彙總(N 為未顯示的系列數)。
- 扣除以上後,繪圖區小於 40pt × 30pt 時,
toFigure拒絕並指出 Figure 尺寸與可用的繪圖區尺寸。
文字寬度估算:adapter 在模型層執行,沒有字型可量測,版面決策(左邊距、標籤抽樣與截斷、標題截斷、圖例換行)以
字寬上界估算:一般字面的數字與 .,:-/+() 及空白為 0.65em,其他所有字元(含 %、拉丁字母與 CJK)為 1.0em;
粗體(圖表標題)一律 1.0em。以 REP-03 測試字型量測:Noto Sans 一般字面的數字 0.572em、W 0.93em、M 0.907em、% 0.831em,
粗體 % 0.902em,皆在上界內。所以在符合下述條件的字型上,估算只會讓留白偏寬、標籤抽樣偏保守,不會重疊或超出 Figure;
代價是拉丁文字標籤會比實際需要更早被抽樣或截斷。
適用條件與限制:保證只在「一般字面的數字與上述標點 ≤ 0.65em,且所有字形 ≤ 1.0em」的字型上成立,並以測試字型驗證。
其他字型(例如數字較寬或含超過 1em 的符號)仍可能讓標籤重疊或超出 Figure;renderer 不偵測、不重新排版。
實際繪製時 renderer 以真實字寬處理 text-anchor,所以置中與靠右在上界內仍精確(見「已裁決的決策」第 3 項)。
排版與繪製
| 項目 | 規則 |
|---|---|
| 文件流與儲存格 | 沿用 REP-03 的 Image/Figure 規則:不可分割、放不下移到下一頁、高於可用高度時等比縮小並回報 report.layout.figure-scaled、圖說放不下回報 report.layout.no-space |
| 縮放 | Figure 縮小時整個框(含 SVG 內文字)等比縮小;字級隨之變小 |
| SVG 解析 | 在排版階段解析成繪圖清單(路徑、填色、線條、已量測的文字片段);每個 Figure 每次 render 解析一次 |
| 繪製 | 以 Figure 框裁切,套用 viewBox 與 transform 後把路徑與文字寫入頁面內容;不透明度以 ExtGState 設定 |
| 點陣內容 | 在框內等比置中(等同 xMidYMid meet),格式與解碼錯誤沿用 report.image.unsupported |
| REP-03 替代文字框 | 移除,report.figure.not-rendered 不再產生 |
新增 report.internal.svg 套件(解析、政策、繪圖清單),不相依 PDFBox 與 AWT;以 ArchUnit 規則確保。
Diagnostic 代碼
| 代碼 | 嚴重度 | 說明 |
|---|---|---|
report.model.figure.svg-invalid | ERROR | 不是格式正確的 XML、含 DOCTYPE、根元素不是 <svg>、缺少 viewBox 與寬高 |
report.model.figure.svg-unsupported | ERROR | 子集以外的元素、屬性或值(含外部參照、script、事件屬性、url()、不支援的單位) |
report.model.figure.svg-too-complex | ERROR | 超過任一複雜度上限(長度、元素數、深度、path 項目、文字長度、虛線值數) |
report.model.figure.svg-out-of-range | ERROR | 屬性數字、viewBox 寬高、累積矩陣、viewBox 映射比例、轉換後座標或文字位置、線寬或字級超出數值範圍 |
report.font.unknown-family | WARNING | SVG 的 font-family 都未登錄(沿用 REP-03 代碼,location 為該 Figure) |
report.figure.not-rendered | — | 移除 |
ERROR 的 location 是 Figure 的節點路徑(例如 body/3[figure:monthly]),訊息含 SVG 內的元素路徑(例如 svg/g[2]/path[5])
與原因。
驗收方式(對應計畫的完成條件)
| 完成條件 | 驗證方式 |
|---|---|
| 同一 Figure 可放在段落間、table cell 與跨頁前後,且不破壞 flow pagination | 排版測試:段落間、儲存格內(含重複表頭與續接列)、移到下一頁、等比縮小;corpus 統計報表 golden |
| 圖表的文字、顏色、單位與 alt text 可由應用控制 | adapter 測試:系列顏色、數值格式、軸標題、textStyle、altText/caption 反映在輸出;PDF 文字抽取含軸標籤與圖例 |
| 惡意 SVG、過大 viewBox、極端 path 數量與外部資源都有拒絕或限制測試 | 政策測試逐項:DOCTYPE/XXE(含 file: 與 http: 外部實體,斷言解析器從未開啟連線或檔案)、實體膨脹、script、事件屬性、href、url()、foreignObject、style 元素、巢狀 svg、每一項上限的邊界值(上限內通過、超過一個單位即拒絕) |
| SVG 繪製語意 | 子集單元測試:每種元素、path 指令(含相對形式、平滑曲線、各種弧線旗標)、transform 組合、樣式繼承、style 屬性、顏色格式、preserveAspectRatio、text-anchor、tspan;以點陣 golden 比對代表性圖形 |
| 圖內文字可抽取 | 統計報表的 PDF 文字抽取含刻度、類別與圖例文字;缺字以 □ 替代並回報 |
| 圖表 adapter | 刻度演算法(含負值、全零、極大極小值)、類別抽樣與截斷、null 中斷折線、無資料狀態、單一系列不顯示圖例;相同輸入產生相同 SVG |
| 圖表輸入驗證 | 「輸入驗證」表每一列:數量不符、NaN/Infinity、空白系列名稱、類別與資料點上限的邊界、min ≥ max(含相等)、非有限的 min/max 被拒絕;超出最終範圍的長條與折線被裁切在繪圖區內 |
| 數值軸範圍 | 「數值軸範圍」每一步:只指定 min(資料高於、低於、等於 min)、只指定 max(同上,含負值資料)、全零、全負、單一數值;斷言最終範圍有限且 lower < upper、刻度落在 1-2-5 間距上 |
| 版面不足與寬字元 | 以實際登錄的字型 render 後檢查每個文字 box:由 W/M 組成的類別標籤、長 CJK 標籤、長刻度文字、一般與粗體的百分比標籤(如 100% 的類別、刻度格式與標題)、10 個長名稱系列的圖例與長標題,皆不互相重疊且位於 Figure 內;圖例超過兩行時出現「+N」;繪圖區不足 40pt × 30pt 時 toFigure 拒絕 |
| 衍生數值範圍 | 45 次 scale(10000000) 串接、極小 viewBox(如 0 0 1e-9 1e-9)、slice 放大、轉換後座標或字級超界皆回報 svg-out-of-range;各範圍的邊界值(恰在範圍內通過、超出即拒絕) |
| 決定性與跨平台 | 現行 flow renderer 的固定時間測試核對完整位元組(含 /ID),見 PDF renderer;REP-03 歷史跨平台結果為排除 /ID 後一致,本次未重跑跨平台驗證 |
| 封裝 | ArchUnit:report.internal.svg 不相依 PDFBox 與 AWT;report.chart 只相依公開模型 |
已裁決的決策
以下 5 項於設計審閱(2026-09-29,3 輪)由審閱者裁決,皆採作者方案。
| # | 議題 | 裁決 | 捨棄的方案與理由 |
|---|---|---|---|
| 1 | SVG 在何時驗證 | ReportDocument.Builder#build()(延遲列在讀取時),子集與衍生數值範圍屬模型契約 | 只在 render 時驗證:錯誤延到 render,且子集變成 renderer 私有規則 |
| 2 | 不合法 SVG 的處置 | ERROR,不產生產物(與不支援的圖片一致) | WARNING 並畫替代文字框:報表仍能產生,但可能靜默遺失圖表內容 |
| 3 | 圖表文字的版面 | adapter 在模型層產生 SVG,以字寬上界估算版面;renderer 以真實字寬處理對齊;接受「適用條件與限制」所列的字型限制 | 新增 FigureContent.Chart 由 renderer 以真實字型排版:版面精確,但圖表語意進入 renderer 核心,違反 ADR-029 的 adapter 邊界 |
| 4 | g 的 opacity | 乘到子元素的不透明度(近似),接受子元素重疊處與瀏覽器的顏色差異 | 拒絕 g 上的 opacity:常見圖表工具的輸出會被拒;PDF transparency group 精確實作:成本較高 |
| 5 | 複雜度上限 | v1 固定為「安全政策與複雜度上限」所列常數 | 由應用經 RenderOptions 或模型設定調整:v1 無需求,且擴大攻擊面 |
程式碼審閱裁決
程式碼審閱(2026-09-30,3 輪)修正 5 項 major 意見(不繪製子樹的深度上限、文字描邊、文字位置的累積與區間檢查、 密集長條互相覆蓋;皆見「實作註記」),並裁決以下待決點:
| # | 議題 | 裁決 |
|---|---|---|
| 1 | viewBox 寬高超界的代碼 | 維持 svg-out-of-range,並把 viewBox 寬高從「複雜度上限」表移到「衍生數值範圍」表,統一契約歸類 |
| 2 | 類別標籤收進 Figure 後相撞即略過 | 接受 |
| 3 | SVG 在 build() 與 render 各解析一次 | v1 接受,不做快取 |
| 4 | FU-105 結案時機 | 政策缺口修正、審閱通過後結案 |
實作註記
| 項目 | 內容 |
|---|---|
| 套件 | report.internal.svg(解析、政策、viewBox 映射與衍生數值檢查;不相依 PDF、排版與 AWT,ArchUnit 規則確保)、report.internal.layout.FigureScenes(套用映射、解析字型家族、量測文字)、PdfPageWriter(裁切、座標轉換、路徑、ExtGState 透明度、文字)、report.chart(只相依公開模型) |
| viewBox 尺寸的代碼 | viewBox 寬高超出 10⁻³~10⁶ 回報 svg-out-of-range(數值範圍),與驗收表「極小 viewBox」一致;程式碼審閱裁決維持此代碼,並把 viewBox 寬高從「複雜度上限」表移到「衍生數值範圍」表 |
| 外部解析 | 除禁止 DOCTYPE 等設定外,另設定一律拒絕的 EntityResolver;測試以本機 socket 確認外部 DTD/參數實體不會連線 |
| path | 第一個指令必須是 moveto(PDF 不接受沒有起點的路徑);0 線寬視為不畫線(PDF 的 0 線寬是最細的線) |
| 文字 | dominant-baseline 以字級比例近似:middle 0.25em、central 0.3em、hanging 0.8em;斜體在沒有斜體字面時以傾斜合成並回報 INFO(沿用 REP-03) |
| 圖表:刻度標籤 | 繪圖區矮時每 m 個刻度顯示一個標籤(間距至少 1.4 倍字級),格線照畫 |
| 圖表:類別標籤 | 置中的標籤比區間寬時可能超出 Figure,因此以估算寬度收進 Figure 內;收進後與前一個標籤相撞時略過該標籤。標籤基線距繪圖區底一個字級加間距,與置中對齊的「0」刻度錯開 |
| 程式碼審閱修正 | 不繪製的子樹也受深度上限約束;文字位置以累積區間檢查(字寬上界不當作實際前進量,負向位移不會被掩蓋);文字描邊(fill="none" 或填色+描邊)以 PDF 文字繪製模式輸出,不再靜默消失;密集長條不再以最小寬度互相覆蓋,圖表 SVG 數字改為三位小數以免捨入後重疊 |
| 圖表:預設 | 字型家族 sans-serif(對應預設家族)、標籤 9pt、標題 11pt 粗體;數值格式 #,##0.##、Locale.ROOT |
| 跨平台 | scripts/report-linux.sh 在 arm64 與 amd64(Rosetta)皆通過(報表測試 259 項+相依任務);三份 corpus、SVG 範例與點陣 Figure 的輸出與 macOS 除 /ID 外位元組相同。圖表文字的真實字寬在兩平台一致 |
| 測試 | SvgParserTest(子集、安全政策、每項上限的邊界、衍生數值範圍、無連線)、ChartTest(輸入驗證、數值軸範圍、輸出)、ChartTextLayoutTest(以測試字型的真實字寬驗證文字不重疊且在 Figure 內:W/M、長中文、百分比一般與粗體、長圖例、長標題、矮繪圖區)、LayoutEngineTest(向量場景、preserveAspectRatio、未登錄字型、點陣內容、儲存格內 Figure)、PdfFigureTest(代表性 SVG 的點陣 golden 與點陣內容)、ReportModelValidationTest(建構時與讀取時的四種代碼)、corpus 統計報表改用實際圖表並更新 golden |
相關文檔
- REP-02 ReportDocument 契約設計:
Figure欄位、不可分割內容的超頁處置 - REP-03 PDF renderer 契約設計:排版與繪製架構、字型與 diagnostic
- REP-01 spike:Batik 路線的實測與圖內文字外框問題