建築 CAD 套件匯出的平面圖,首頁通常帶著一個有兩個 viewport 的 /VP 陣列,每個 viewport 又帶一個 /Measure 條目,讓檢視器能直接從圖面上讀出公尺數。GeoPDF 地圖圖幅有同樣的 /VP 陣列、同樣的 /Measure 鍵。平面圖只是普通的 PDF 1.6;地圖圖幅卻需要 PDF 1.7 加 Adobe Extension Level 3。只看鍵名的版本檢查會對兩者都要求擴充,平面圖便在存檔時被拒,理由是它從未有過的地理空間內容
一個 /Measure 鍵,兩種字典
鍵名只告訴你這個 viewport 可量測;只有它背後的字典才說得出怎麼量。量測字典始於 PDF 1.6,ISO 32000-1 §12.9 定義了直線式:比例字串在 /R,數字格式陣列在 /X 與 /Y,另有距離(/D)與面積(/A)的陣列。地理空間量測來得更晚,是 PDF 1.7 之上 Adobe Extension Level 3 的一部分。它以 /Subtype /GEO 標記,攜帶 /GCS 裡的地理座標系統、選填 /DCS 裡的顯示座標系統、/GPTS 與 /LPTS 的成對點陣列,以及 /PDU 裡偏好的顯示單位—這些結構的走讀見在 Delphi 中讀取 GeoPDF viewport、GPTS 與 LPTS 陣列一文
讓人栽跟頭的細節是預設值。§12.9 給 /Subtype 的預設值是 RL,很多 CAD 匯出器善用這一點,乾脆不寫這個條目。把缺失的子類型讀成「未知,往壞處想」的檢查器,會把每一張這樣的圖面都判成地理空間文件。而比對 /Measure 鍵的檢查器根本走不到子類型那一步,因為拿著鍵的 viewport 字典,在回答問題的量測字典上一層
PDF Library for Delphi 的存檔預檢如何做出裁定
PDF Library for Delphi(PDFlibPas)在 SaveToFile 裡、任何輸出寫出之前跑一次版本預檢。它走訪每個間接物件,深入巢狀字典與陣列,把每個節點對著一張功能規則表測試。每條規則標明最低 PDF 版本,擴充功能另標 Adobe 擴充層級;擴充規則只能由 PDF 1.7 加 catalog 裡相符的 /Extensions /ADBE 條目滿足。檢查的目標是文件版本,或你用 LockSaveVersion 釘住的版本,再加上 catalog 宣告的擴充層級。第一條既命中又超出目標的規則終止存檔:SaveToFile 回傳 0,LastErrorCode 變成 602,即 PDFLIB_ERROR_VERSION_COMPLIANCE
光有一個錯誤碼,你還是不知道要修什麼。三個 GetInformation 鍵補上其餘資訊:311 是出問題的功能所需版本(例如 1.7 ExtensionLevel 3),312 是該功能的名稱,313 是存檔目標鎖定的版本,沒鎖時為空
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('floor-plan.pdf', '') <> 1 then
raise Exception.Create('load failed');
if Pdf.SaveToFile('floor-plan-out.pdf') <> 1 then
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// 311:所需版本,312:觸發它的功能,
// 313:存檔目標鎖定的版本(未鎖定時為 '')
Writeln('Needs ', Pdf.GetInformation(311),
' for ', Pdf.GetInformation(312),
', locked at [', Pdf.GetInformation(313), ']');
finally
Pdf.Free;
end;
end;
對量測內容,規則表放了三條規則,並刻意不對裸的 /Measure 鍵設規則。帶 /VP 或 /UserUnit 的頁面需要 PDF 1.6。/PtData 字典需要擴充層級 3。量測字典在寫明 /Subtype /GEO、或含有 /GCS、/DCS、/GPTS、/LPTS、/PDU 任何一項時需要擴充層級 3。所以省略 /Subtype 的 viewport 與寫明 /RL 的 viewport 都能在 1.6 過關,同一頁若是 1.5 則因 /VP 被拒。加一個 /GPTS 陣列,答案就變成 1.7 ExtensionLevel 3
內容測試刻意保守。同時帶著遊離 /GCS 或 /PDU 的直線式字典,會被當成地理空間處理。我們寧可拒收那種檔案也不願放行:這些鍵在直線式模型裡沒有意義,寫下它們的要麼是地理空間工具,要麼是弄壞檔案的東西,而認得 GEO 的檢視器很可能會試著解讀它們。載入器在另一個方向上同樣謹慎:它的功能掃描可以把文件版本號升到 1.7,但絕不會替輸入檔代寫 /Extensions 宣告—宣告擴充是對內容的主張,該由你的程式碼或撰寫呼叫來做
存檔之前先檢查圖面的 viewport
當 312 指名地理空間功能,而你確定檔案只是普通圖面時,自己去看看那些量測字典。公開的 viewport 呼叫就夠用。GetMeasureDictCoordinateSystem 讀 /Subtype,RL 回傳 1,GEO 回傳 2,條目缺失回傳 0—按 §12.9,缺失同樣代表直線式。點陣列的數量則告訴你有沒有地理參照資料
// 回報已載入文件裡的每一個量測字典。
// 這裡的呼叫全部唯讀;沒有任何一個會往檔案加鍵。
procedure ReportMeasureDicts(Pdf: TPDFlib);
var
Page, VP, VPID, MD, Kind: Integer;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.SelectPage(Page);
for VP := 1 to Pdf.GetPageViewPortCount do
begin
VPID := Pdf.GetPageViewPortID(VP);
MD := Pdf.GetViewPortMeasureDict(VPID);
if MD = 0 then
Continue;
Kind := Pdf.GetMeasureDictCoordinateSystem(MD); // 0 = 省略(等同 RL)、1 = RL、2 = GEO
Writeln(Format('page %d viewport "%s": subtype %d, GPTS %d, LPTS %d',
[Page, string(Pdf.GetViewPortName(VPID)), Kind,
Pdf.GetMeasureDictGPTSCount(MD), Pdf.GetMeasureDictLPTSCount(MD)]));
end;
end;
end;
那個迴圈刻意少列了兩個呼叫:GetMeasureDictGCSDict 與 GetMeasureDictDCSDict。這兩個是為撰寫而建的 get-or-create 存取器。量測字典沒有 /GCS 條目時,GetMeasureDictGCSDict 會加一個空的,並為文件宣告擴充層級 3。把它當探測器用在直線式平面圖上,字典就真的多了 /GCS;隨後鎖在 1.6 的存檔會被拒——理直氣壯地,為的是你的診斷程式剛寫進去的內容。只想知道座標系統在不在,子類型與點陣列數量就能無副作用地回答
把輸出鎖在 PDF 1.6
當你的下游客戶有版本上限時,釘住版本能讓輸出保持誠實。TPDFlib.LockSaveVersion 接受 '1.0' 到 '1.7' 的字串,其他回傳 0。它固定文件版本,阻止寫入端呼叫自行抬高,但存檔預檢仍然對著鎖定值執行。鎖在 1.6 的 CAD 圖面乾淨存出。鎖在 1.6 的真 GeoPDF 拿到 602,而這正是你要的答案,因為另一個選項是一份檔頭承諾低於內容所需的檔案
if Pdf.LockSaveVersion('1.6') <> 1 then
raise Exception.Create('unsupported version string');
try
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
// 1.6 以上的真實內容,例如 GEO 量測字典
raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
[string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
finally
Pdf.UnlockSaveVersion;
end;
先讀 311 與 312,再動版本。如果功能是真實的地理空間內容,你有兩個誠實的選擇:讓文件不鎖在 1.7 並宣告擴充,或者從送給 1.6-only 客戶的那份副本裡拿掉地理空間資料。為了讓錯誤消失而抬高鎖,是要避免的選項;它把真正的問題藏起來—下游讀取器處理得了你要交付的東西嗎?透過 API 建地理空間內容時,SetMeasureDictCoordinateSystem、SetMeasureDictGPTSCount 這類 SetMeasureDict* 設定器會自行宣告擴充層級 3,所以撰寫出的檔案抵達預檢時已經貼好標籤。這種有界、有證據的檢查習慣同樣出現在工程文件的 PDF/E-1 撰寫模式預檢一文裡,CAD 圖面在那裡面對的是符合性標準,而不是版本號
規則掃描為什麼原地讀表
預檢的成本大約是物件節點數乘以規則數,而節點數把每個陣列裡的每個數字與名稱都算在內,所以每條規則的固定開銷比看上去更要緊。表裡每條規則是一個帶兩個 AnsiString 欄位的紀錄:比對字串與功能名稱。把這種紀錄複製進局部變數,Delphi 或 FPC 要動兩次參照計數,並釋放局部變數原本持有的東西。每節點每規則來一次,意味著連那些對目標版本不適用、下一行就被丟棄的規則,也要付字串簿記的成本
規則表在單元初始化時填一次,之後從不修改,所以掃描以 const record 參數把表格條目交給比對程式碼,原地讀取。const record 參數按參照傳遞,測試一條規則不碰任何字串欄位,參照計數維持原值。效果集中在規則比對路徑上。真正的存檔還要解碼物件、執行延遲的功能偵測、序列化輸出,那些階段不受規則傳遞方式的影響。規則順序、門檻與 311、312 的首個失敗診斷都與它無關,規則結果也不在兩次存檔之間快取,所以你編輯後存兩次的文件,兩次都做完整檢查
同樣的模式值得在自己的 Delphi 程式碼裡檢查一遍:凡是迴圈走訪的表格紀錄帶有字串、interface 或動態陣列,都適用:
type
TMyRule = record
Name: string; // 受管欄位:複製要付出參照計數的代價
Threshold: Integer;
end;
// 讀者自己的程式碼,示意用
function Applies(const Rule: TMyRule; Value: Integer): Boolean;
begin
Result := Value >= Rule.Threshold; // const:不複製、沒有參照計數的折騰
end;
// 在熱迴圈裡直接傳入表格條目,
// 而不是先指派給局部 record
for I := 0 to High(Rules) do
if Applies(Rules[I], V) then
Inc(Hits);
量測 viewport、地理空間字典、版本鎖定與存檔合規檢查,全部隨 PDF Library for Delphi(支援 Delphi、C++Builder 與 Lazarus 專案)提供