跳至主要内容

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 決定,本文據此設計:

  1. 自寫受限 SVG 子集,不採用 Batik+pdfbox-graphics2d。 REP-01 以 Batik 繪製時,圖內文字變成向量外框、無法抽取; Batik 以 AWT 解析字型,函式庫必須在 JVM 全域註冊字型;還要新增 pdfbox-graphics2d 相依,並承擔 Batik 較大的安全面。 自寫子集只解析白名單內的元素,直接繪製成 PDF,圖內文字用 ReportFonts 的字型輸出為真正的文字。代價是自行實作 path 與 transform 解析,並放棄 CSS 樣式表、漸層與濾鏡等功能。
  2. 圖表 adapter 提供長條圖與折線圖。 涵蓋 corpus 統計報表的兩張圖;兩者共用座標軸、刻度、圖例、標籤與無資料狀態。

設計原則​

  1. SVG 子集是模型契約,不是某個 renderer 的實作細節。 子集的語法與上限由 ReportDocument.Builder#build() 驗證 (延遲列來源在讀取時逐列驗證),所以不合法的 SVG 在建構時就失敗,任何 renderer 看到的都是同一個子集。
  2. 白名單,而非黑名單。 只接受列出的元素、屬性與值的形式;其他一律拒絕,不靜默略過。
  3. 不存取外部世界。 XML 解析停用 DOCTYPE、外部實體與 XInclude;子集沒有任何引用外部資源的元素或屬性。
  4. 先排版、後繪製(沿用 REP-03):SVG 在排版時解析成與後端無關的繪圖清單,文字在此時量測;繪製器只讀清單。
  5. 核心不懂圖表語意(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 各門市季度營業額"));
型別說明
ChartSpecsealed:BarChart、LineChart;toFigure(FigureFrame) 產生含 SVG 的 Figure
BarChart、LineChart不可變;以 builder 建立:categories、series(名稱、數值、選填顏色)、valueAxis、legend、title、noDataText、textStyle
ChartSeriesname、values(List<Double>,null 表示該類別無資料)、color(ReportColor,null 用預設色盤)
ValueAxistitle(單位等)、format(DoubleFunction<String>,預設 #,##0.##、Locale.ROOT)、min/max(選填,須為有限值;預設自動並包含 0;只給一端時另一端自動,規則見「數值軸範圍」)
FigureFrameFigure 的 id、寬高、altText、caption;圖表以寬高為座標系(1 單位 = 1pt),所以字級就是 pt

受限 SVG 子集​

文件與座標​

項目規則
根元素<svg>,命名空間 http://www.w3.org/2000/svg;不得巢狀 <svg>
座標系有 viewBox 時以其為使用者座標;沒有時以根元素的 width/height 為 viewBox(0 0 w h)。兩者皆無為錯誤
置入 FigureviewBox 依 preserveAspectRatio(預設 xMidYMid meet;支援九種對齊與 meet/slice、none)對應到 Figure 的寬高;超出框的內容裁切
長度無單位數字或 px,皆為使用者單位;%、em、pt、mm 等單位為錯誤
數值屬性中的每個數字必須是有限值,絕對值 ≤ 10⁷
衍生數值見「安全政策與複雜度上限」的衍生數值範圍:單一數字合法不代表組合後合法

元素與屬性​

元素可用屬性(另加下方「共通屬性」)
svg(根)width、height、viewBox、preserveAspectRatio、version
g—
rectx、y、width、height、rx、ry
circlecx、cy、r
ellipsecx、cy、rx、ry
linex1、y1、x2、y2
polyline、polygonpoints
pathd:M L H V C S Q T A Z 及小寫相對形式;弧線轉為三次貝茲曲線
textx、y、dx、dy(單一數值);內容為文字與 tspan
tspanx、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() 時計算並驗證:

  1. 資料範圍:dataMin = min(0, 所有非 null 數值)、dataMax = max(0, 所有非 null 數值);全為 null 時屬「無資料」,不畫座標軸。
  2. 自動端:lower 未指定時為 dataMin 向下取整到刻度間距、upper 未指定時為 dataMax 向上取整到刻度間距 (間距取 1、2、5 × 10ⁿ,使範圍約分成 5 格)。
  3. 只指定一端且自動端沒有越過它(例如 min = 10、資料全為 1,自動得到 upper = 1):自動端改為指定端加(或減) 一個刻度間距,間距以 max(|指定端|, 1) 為範圍計算。此時資料全部在範圍外,依上表裁切。
  4. 兩端皆自動且 dataMin = dataMax = 0(全零):範圍取 0~1。
  5. 驗證最終範圍:lower、upper 皆為有限值且 lower < upper;刻度數不超過 20。任一不成立即拒絕(例如極端值使間距下溢)。

版面不足​

版面依下列順序讓步,全部讓步後仍放不下才拒絕:

  1. 類別標籤:每 k 個顯示一個,單一標籤仍過寬時以「…」截斷(上表「類別軸」)。
  2. 數值軸:左邊距上限為 Figure 寬度的 40%,刻度文字超出時以「…」截斷。
  3. 標題:只佔一行,超出繪圖寬度時以「…」截斷。
  4. 圖例:最多兩行;放不下的項目以「+N」彙總(N 為未顯示的系列數)。
  5. 扣除以上後,繪圖區小於 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-invalidERROR不是格式正確的 XML、含 DOCTYPE、根元素不是 <svg>、缺少 viewBox 與寬高
report.model.figure.svg-unsupportedERROR子集以外的元素、屬性或值(含外部參照、script、事件屬性、url()、不支援的單位)
report.model.figure.svg-too-complexERROR超過任一複雜度上限(長度、元素數、深度、path 項目、文字長度、虛線值數)
report.model.figure.svg-out-of-rangeERROR屬性數字、viewBox 寬高、累積矩陣、viewBox 映射比例、轉換後座標或文字位置、線寬或字級超出數值範圍
report.font.unknown-familyWARNINGSVG 的 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 輪)由審閱者裁決,皆採作者方案。

#議題裁決捨棄的方案與理由
1SVG 在何時驗證ReportDocument.Builder#build()(延遲列在讀取時),子集與衍生數值範圍屬模型契約只在 render 時驗證:錯誤延到 render,且子集變成 renderer 私有規則
2不合法 SVG 的處置ERROR,不產生產物(與不支援的圖片一致)WARNING 並畫替代文字框:報表仍能產生,但可能靜默遺失圖表內容
3圖表文字的版面adapter 在模型層產生 SVG,以字寬上界估算版面;renderer 以真實字寬處理對齊;接受「適用條件與限制」所列的字型限制新增 FigureContent.Chart 由 renderer 以真實字型排版:版面精確,但圖表語意進入 renderer 核心,違反 ADR-029 的 adapter 邊界
4g 的 opacity乘到子元素的不透明度(近似),接受子元素重疊處與瀏覽器的顏色差異拒絕 g 上的 opacity:常見圖表工具的輸出會被拒;PDF transparency group 精確實作:成本較高
5複雜度上限v1 固定為「安全政策與複雜度上限」所列常數由應用經 RenderOptions 或模型設定調整:v1 無需求,且擴大攻擊面

程式碼審閱裁決​

程式碼審閱(2026-09-30,3 輪)修正 5 項 major 意見(不繪製子樹的深度上限、文字描邊、文字位置的累積與區間檢查、 密集長條互相覆蓋;皆見「實作註記」),並裁決以下待決點:

#議題裁決
1viewBox 寬高超界的代碼維持 svg-out-of-range,並把 viewBox 寬高從「複雜度上限」表移到「衍生數值範圍」表,統一契約歸類
2類別標籤收進 Figure 後相撞即略過接受
3SVG 在 build() 與 render 各解析一次v1 接受,不做快取
4FU-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

相關文檔​