一批掃描可以每頁就一張 JPEG、沒有字型、沒有附註、沒有書籤,卻仍然過不了 PDF/raster。ISO 23504-1 對檔案的要求超出純光柵頁面:它得用頁尾區的 %PDF-raster-1.0 註解自我識別、帶 XMP 中繼資料與頁尾 /ID,並把 catalog 與 Info 字典限制在短小的許可清單裡。所以檢查一批擷取檔需要兩種不同的讀法:一種讀原始位元組,一種讀每頁實際畫了什麼
同一份檔案的兩種讀法
PDF/R 驗證與 SaveAsPdfR 隨 PDFium Component 的 Small Business 與 Enterprise 版提供,驗證正是沿著這條線切分。FPdfPdfr 單元裡的 ValidatePdfRCompliance 讀一個串流、檢查位元組層規則:標頭版本、光柵標記、物件串流、catalog 與 Info 項目、XMP、頁尾 /ID、MediaBox 原點、字型資源、禁用的篩選器名稱,以及影像物件數是否等於頁數。ValidatePdfRPageFacts 則以 TPdfRPageFacts 記錄一次檢查一頁:頁面畫了多少文字、路徑、表單與漸層物件,那張條帶影像在哪裡、像素尺寸、位元深度、色彩空間與篩選器鏈
兩種讀法透過同一個列舉 TPdfRValidationIssue 回報,合併的進入點把它們融進單一 TPdfRValidationResult。它的 Conformance 欄位只有在找到光柵標記時才變成 prc1,IsCompliant 只在等級為 prc1 且問題集合為空時回傳 True。一份其他方面完美、只缺標記的檔案回報 prcNone——第一次拿乾淨的掃描去跑的人多半會被嚇一跳
問題在於頁面事實從哪裡來。TPdf.ValidatePdfR 從已載入文件收集它們,但會先把該文件序列化到暫存區、對重新存出的副本跑位元組檢查,所以標記與 catalog 的判定描述的是一次新存檔,可能與磁碟上的檔案不同。FPdfBusinessWorkflows 裡的 ValidatePdfRasterSource 對輸入串流只取一次快照,兩種讀法都對著它跑。對一份即將進封存的檔案,該用的是這個呼叫:判定針對的就是要被封存的那些位元組
uses
System.Classes, System.SysUtils, System.TypInfo,
FPdfPdfr, FPdfReport, FPdfBusinessWorkflows;
procedure CheckDeliveredScan(const FileName: string; Log: TStrings);
var
Src: TFileStream;
Options: TPdfRasterSourceValidationOptions;
Report: TPdfRasterSourceValidationReport;
Issue: TPdfRValidationIssue;
I: Integer;
begin
Options := TPdfRasterSourceValidationOptions.Default;
Options.MaxInputBytes := 512 * 1024 * 1024; // 預設預算是 64 MiB
Options.Validation.MinimumResolutionDpi := 200; // 我們的擷取合約
Src := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
if not ValidatePdfRasterSource(Src, Options, Report) then
begin
Log.Add(Format('Check stopped (%s): %s',
[GetEnumName(TypeInfo(TPdfRasterSourceStatus), Ord(Report.Status)),
Report.ErrorMessage]));
Exit;
end;
finally
Src.Free;
end;
if Report.IsCompliant then
Log.Add('PDF/R-1 conformant')
else
begin
// Validation.Issues 裝著位元組層問題加上每頁的問題
for Issue := Low(TPdfRValidationIssue) to High(TPdfRValidationIssue) do
if Issue in Report.Validation.Issues then
Log.Add('File: ' + GetEnumName(TypeInfo(TPdfRValidationIssue), Ord(Issue)));
// PageIssues 說明哪些頁要重掃
for I := 0 to Report.ProcessedPageCount - 1 do
for Issue := Low(TPdfRValidationIssue) to High(TPdfRValidationIssue) do
if Issue in Report.PageIssues[I] then
Log.Add(Format('Page %d: %s', [Report.Pages[I].PageNumber,
GetEnumName(TypeInfo(TPdfRValidationIssue), Ord(Issue))]));
end;
// 把機器可讀的判定放在封存檔旁邊
Log.Add(Report.ToJson(TPdfReportLimits.Default));
end;
TPdfRasterSourceValidationOptions 裡有兩個預設值對掃描批次要緊。MaxInputBytes 是 64 MiB、MaxPages 是 1000;一批長的彩色掃描可能超出位元組預算,檢查以狀態 prssBudgetExceeded 收場、Complete 為 False。MemoryLimitBytes 是 8 MiB,超過後快照溢寫到 TemporaryDirectory,報告設 SnapshotSpilled。Report.IsCompliant 還要求 Complete,所以中途停止的檢查絕不會被讀成通過
為什麼 OCR 文字層會讓 PDF/R 失敗?
因為光柵頁面畫的就是掃描影像,其他什麼也沒有。不可見的 OCR 層在 PDF/A 工作流程裡是正常加項,但它把兩樣 PDF/R 排除的東西放進檔案:一個字型資源——位元組讀法回報 pvriFontForbidden——以及頁面上的文字物件——頁面讀法回報 pvriNonRasterPageContent。向量路徑、漸層與表單 XObject 也觸發同一個頁面問題,所以蓋上去的頁碼或畫的邊框線,與隱形文字以同樣方式失敗
頁面與影像的關係也嚴格。每頁必須剛好帶一張條帶影像;沒有影像的頁面、掃描被寫成兩張影像並排的頁面,都觸發 pvriPageImageMismatch。位元組那一側對每個 /Subtype 為 /Image 的物件計數,所以任何多餘的影像物件——例如掛在條帶上的軟遮罩——都會把總數推過頁數
幾何有兩道檢查。pvriBadMediaBox 回報不從 [0 0 ...] 開始的 MediaBox;位元組讀法檢查行內數值陣列。頁面讀法接著比較條帶畫出的邊界與 MediaBox,任一邊偏差超過 GeometryTolerancePoints(預設 0.5 點)就回報 pvriStripGeometryInvalid。把影像縮到頁面 98% 而留下白邊的掃描器驅動,在這裡失敗——即使頁面在螢幕上看起來沒問題。內部檔案若可接受邊距,把 RequireFullPageStrip 設為 False 可關掉涵蓋測試
哪些影像編碼通得過
驗證器接受四種條帶篩選器:/CCITTFaxDecode、/DCTDecode、/JPXDecode 與 /FlateDecode。位元組讀法按名稱把其他標準篩選器標成 pvriForbiddenImageFilter:/LZWDecode、/RunLengthDecode、/ASCII85Decode、/ASCIIHexDecode、/JBIG2Decode 與 /Crypt
JBIG2 是擷取團隊最先撞上的拒收。它在 PDF/A 裡合法、對雙階頁面壓縮極好,所以為 PDF/A 建的擷取設定檔可能已經在產出它。它的有損樣式匹配模式也已知會用長得像的字形替換掃描字形——掃描表單上的數字有明文記載的這類問題——而光柵封存存在的意義,正是紙上是什麼、檔案裡就是什麼。把那些頁面重編碼成 CCITT Group 4,既保住雙階、又是無損
頁面讀法更進一步,把篩選器鏈與影像的位元深度、色彩空間一起看,不符合就回報 pvriImageCompressionParametersInvalid:
- 篩選器鏈超過一層就被拒,所以 ASCII85 包裝的 Flate 影像會失敗兩次——位元組那側一次、這裡一次
/CCITTFaxDecode必須坐在 1 位元 DeviceGray 條帶上/DCTDecode必須坐在 8 或 24 位元、DeviceGray 或 DeviceRGB 的條帶上/FlateDecode接受 1 位元條帶;24 位元的 Flate 頁面過不了這關/JPXDecode接受每像素 1、8 或 24 位元
與篩選器無關地,色彩空間不是 DeviceGray 也不是 DeviceRGB 的條帶拿到 pvriImageColorInvalid。所以 ICC 或 CMYK 掃描必須在包裝前先轉換,而最便宜的轉換點是掃描器設定檔——在任何 PDF 寫出之前
條帶解析度怎麼算出來
影像自帶解析度時,頁面讀法直接用它。沒有時,有效解析度由像素與頁面尺寸推導:像素寬乘 72 除以 MediaBox 寬的點數,高同理。假設掃描器把 2550×3300 像素的條帶寫到 612×792 點的 US Letter 頁面上,兩個方向剛好都是 300 dpi。同一頁若是 600×776 像素,水平方向約 70.6 dpi,低於預設下限 72,頁面就拿到 pvriImageResolutionInvalid
TPdfRValidationOptions 的預設範圍是 72 到 2400 dpi。這兩個界限只抓明顯壞掉的頁面。擷取合約通常承諾特定下限,常是 200 或 300 dpi,那個值該放進 MinimumResolutionDpi:150 dpi 的頁面過得了預設範圍,而它正是合約想排除的那種頁面。把任一界限設為 0 即關掉那一側的檢查
ValidatePdfRPageFacts 是公開的,所以同一套規則可以在任何 PDF 存在之前就跑——直接用掃描器對每張影像回報的資訊:
uses
FPdfPdfr;
// 從掃描器輸出建立頁面事實;這個輔助函式是您自己的,不是程式庫的一部分
function ScannerStripFacts(PixelW, PixelH: Integer;
PageWidthPt, PageHeightPt: Single; const Filter: string;
BitsPerPixel: LongWord; ColorSpace: Integer): TPdfRPageFacts;
begin
Result := Default(TPdfRPageFacts);
Result.PageNumber := 1;
Result.MediaWidth := PageWidthPt;
Result.MediaHeight := PageHeightPt;
Result.ImageCount := 1;
Result.ImageRight := PageWidthPt; // 條帶涵蓋整頁
Result.ImageTop := PageHeightPt;
Result.PixelWidth := PixelW; // HorizontalDpi 留 0:自行推導
Result.PixelHeight := PixelH;
Result.BitsPerPixel := BitsPerPixel;
Result.ColorSpace := ColorSpace; // 1 = DeviceGray,2 = DeviceRGB
SetLength(Result.Filters, 1);
Result.Filters[0] := Filter;
end;
var
Opts: TPdfRValidationOptions;
Issues: TPdfRValidationIssues;
begin
Opts := TPdfRValidationOptions.Default;
Opts.MinimumResolutionDpi := 300;
// Letter 上的 300 dpi 彩色 JPEG:空集合
Issues := ValidatePdfRPageFacts(
ScannerStripFacts(2550, 3300, 612, 792, 'DCTDecode', 24, 2), Opts);
// 同一頁用 Flate 壓 24 位元:
// [pvriImageCompressionParametersInvalid]
Issues := ValidatePdfRPageFacts(
ScannerStripFacts(2550, 3300, 612, 792, 'FlateDecode', 24, 2), Opts);
end;
Catalog、Info 字典與 XMP
除了必要的 /Type 與 /Pages,ISO 23504-1 只允許 catalog 裡出現 Version、ViewerPreferences、PageLayout、PageMode、AcroForm 與 Metadata。驗證器對照該集合之外的 21 個標準鍵檢查 catalog,任何一個出現就觸發 pvriProhibitedCatalogEntry,其中包括 Outlines、Names、PageLabels、OpenAction、StructTreeRoot、MarkInfo、Lang、OCProperties 與 OutputIntents。最後一個在實務上有影響:PDF/A 檔案必帶輸出意圖,所以一份檔案無法同時滿足兩個設定檔
Info 字典可以放 Creator、Producer、CreationDate 與 ModDate;Title、Author、Subject、Keywords 與 Trapped 會觸發 pvriProhibitedInfoEntry。catalog 缺 /Metadata 串流得到 pvriMissingXmpMetadata。串流在、但 XMP 缺 xmp:CreateDate、xmp:ModifyDate、xmp:MetadataDate 任一項,則改判 pvriMetadataInvalid。pvriInfoXmpMismatch 比較 Producer 對 pdf:Producer、Creator 對 xmp:CreatorTool。日期不參與比較,因為兩種編碼本來就可以不同;以十六進位字串存放的 Info 值則跳過。典型的觸發是後續工具改寫了 Info 的 Producer 卻沒動 XMP
其餘容器規則很短。標頭必須是 %PDF-1.4 到 %PDF-1.7 或 %PDF-2.0(pvriBadHeader);任何 /Type /ObjStm 串流觸發 pvriObjStmPresent;加密檔案必須用 %PDF-2.0 標頭(pvriEncryptVersionMismatch)。RequireEncryptedPdf20Profile 保持開啟時,加密字典還得宣告 /V 5 或 6 搭 /R 5 以上,否則檔案拿到 pvriEncryptedPdf20ProfileInvalid
寫入標記
TPdfRSaveOptions 帶 Creator、Producer、CreationDate、ModDate、DocumentId 與 InstanceId,沒有 Title、Author、Subject 或 Keywords 的欄位,因為設定檔禁止它們。StripInfoOptionalEntries 與 StripCatalogOptionalEntries 都預設 True,從來源移除被排除的項目;任一設為 False,項目就留下——代價是一致性。TPdf.SaveAsPdfR 從已載入文件自己的中繼資料與檔案識別碼填補空白選項欄位、移除安全性,然後寫出標記、修剪過的字典、XMP 封包與頁尾 /ID
uses
System.Classes, System.SysUtils, PDFium, FPdfPdfr;
procedure WrapScanAsPdfR(const InFile, OutFile: string; Log: TStrings);
var
Pdf: TPdf;
Opts: TPdfRSaveOptions;
Written: TFileStream;
Res: TPdfRValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := InFile;
Pdf.Active := True;
Opts := TPdfRSaveOptions.Default;
Opts.Creator := 'Capture Station 4'; // Producer、日期與 ID 來自來源
if not Pdf.SaveAsPdfR(OutFile, Opts) then
raise Exception.Create('PDF/R save failed: ' + InFile);
finally
Pdf.Free;
end;
// 檢查寫出的位元組,不是記憶體中的文件
Written := TFileStream.Create(OutFile, fmOpenRead or fmShareDenyWrite);
try
Res := ValidatePdfRCompliance(Written);
if pvriPageImageMismatch in Res.Issues then
Log.Add('A source page still has no single strip image');
if pvriFontForbidden in Res.Issues then
Log.Add('The source carries fonts; rescan or drop the text layer');
finally
Written.Free;
end;
end;
包裝只動中繼資料與識別。畫向量文字、或沒有條帶影像的來源頁面,出 SaveAsPdfR 時頁面內容照舊、也照樣過不了那些頁面檢查。頁面內容被刻意保留:悄悄重渲染掃描,會改掉封存想保存的那些像素。把第一個範例的 CheckDeliveredScan 跑在 OutFile 上,就拿到含頁面讀法的完整判定
PDF/R 在擷取管線中的定位
PDF/R 適合交付物本身就是紀錄、保真度就是整份合約的場合:證據影像、支票與匯款擷取、來自大尺寸掃描器的圖面封存。文件一旦需要可搜尋文字、標記、附件或書籤,就該歸 PDF/A,因為上述每個功能都落在上面的排除清單裡
很多團隊兩者都留。PDF/R 原件寫一次、永不改動,帶 OCR 層的 PDF/A 衍生檔負責檢索。兩個驗證器互相獨立,同一個批次作業可以按各檔宣稱的設定檔分別檢查。封存那一側在PDF/A 封存合規與PDF/A 預檢驗證涵蓋,列印輸出在驗證可列印 PDF/X 文件涵蓋
這裡展示的 PDF/R 檢查屬於 PDFium Component for Delphi、C++Builder 與 Lazarus 的 Small Business 與 Enterprise 版,與 PDF/A、PDF/X、PDF/E、PDF/UA 的驗證器並列;版本與支援的 IDE 見 PDFium Component 產品頁