PDFlibPas ถอดรหัส TIFF ด้วย parser Object Pascal ที่เขียนขึ้นเองทั้งหมด ไม่ใช่การ bind เข้ากับ libtiff และเวอร์ชัน 3.534.1 ได้กระชับจุดที่ parser ปฏิเสธอินพุตให้เข้มงวดยิ่งขึ้น ตอนนี้ BigTIFF magic 43 ถูกปฏิเสธแบบระบุชื่อ TileOffsets และ TileByteCounts ถูกปฏิเสธตั้งแต่ขั้นแยกวิเคราะห์แท็ก และบัฟเฟอร์ทุกตัวคำนวณขนาดด้วยเลขคณิต Int64 ภายใต้เพดานการถอดรหัส 256 MiB
ข้อบกพร่องที่ถูกปิดลงนี้ไม่เคยปรากฏในห้องแล็บ มันปรากฏเป็นเกตเวย์สแกนเนอร์ที่ทำงานเงียบ ๆ มาสามปี จนกระทั่งลูกค้าส่งไฟล์ geospatial archive หรือภาพสไลด์พยาธิวิทยาทั้งใบผ่านเข้ามา ไฟล์นั้นมี TIFF header ที่ถูกต้อง มัน parse ผ่าน สิ่งที่ออกมาคือหน้าภาพเป็นแถบสัญญาณรบกวน หรือการจองหน่วยความจำหลายกิกะไบต์ที่ลากเซอร์วิสลงไปด้วย และตลอดทางไม่มีอะไรประกาศว่าอินพุตไม่ถูกต้องเลย นั่นคือรูปแบบความล้มเหลวที่ควรออกแบบรับมือ ไม่ใช่การ crash แต่เป็นคำตอบที่ผิดถูกส่งมอบอย่างมั่นใจ
ทำไม II หรือ MM จึงไม่พิสูจน์ว่าคุณมี classic TIFF
เพราะ byte-order marker เป็นสิ่งที่ทั้งสองภาษาย่อยใช้ร่วมกัน classic TIFF และ BigTIFF ต่างเปิดด้วย II หรือ MM เหมือนกัน และฟิลด์ที่แยกทั้งสองอย่างจริง ๆ คือ magic ขนาด 16 บิตที่อยู่ถัดไปทันที นั่นคือ 42 สำหรับ classic TIFF ตามข้อกำหนด TIFF 6.0 และ 43 สำหรับ BigTIFF ที่ใช้ offset 64 บิต loader ที่เขียนเป็น FValidTIFF := PopWord = 42 ไม่ได้ผิดกับ classic TIFF แต่มันยุบการปฏิเสธสองแบบที่ต่างกันมากให้เหลือ boolean เงียบ ๆ ตัวเดียว ผลคือ BigTIFF กลายเป็นสิ่งที่แยกไม่ออกจาก JPEG ที่ถูกตัดขาดแล้วถูกเปลี่ยนชื่อ PDFlibPas ตอนนี้แยกเคสเหล่านี้และบันทึกแต่ละเคสใน TPDFTIFF.LastError ทั้ง header ที่สั้นกว่าสี่ไบต์, byte-order marker ที่ไม่ถูกต้อง, magic 43 และค่า magic อื่น ๆ ล้วนให้ข้อความต่างกัน ไลบรารียังคงไม่ถอดรหัส BigTIFF และการพูดตรง ๆ แบบนั้นคือประเด็น ผู้เรียกได้รับความต่างระหว่าง "นี่ไม่ใช่ TIFF" กับ "นี่เป็น TIFF ที่เลย์เอาต์ offset 64 บิตซึ่ง decoder ในตัวไม่รองรับ" ซึ่งเป็นความต่างระหว่างตั๋วสนับสนุนที่ตอบจบในหนึ่งฉบับกับตั๋วที่กลายเป็นหนึ่งสัปดาห์ของการเดา
var
Tiff: TPDFTIFF;
Page: Integer;
begin
Tiff := TPDFTIFF.Create;
try
Tiff.LoadFromFile('inbox\scan-0417.tif');
if not Tiff.ValidTIFF then
raise Exception.Create('TIFF rejected: ' + Tiff.LastError);
if Tiff.PageCount < 1 then
raise Exception.Create('TIFF carries no decodable page');
for Page := 1 to Tiff.PageCount do
Writeln(Format('page %d: %dx%d, %d spp',
[Page,
Tiff.PageInfo[Page].Width,
Tiff.PageInfo[Page].Height,
Tiff.PageInfo[Page].SamplesPerPixel]));
finally
Tiff.Free;
end;
end;
Tile คือเรขาคณิตที่ต่างออกไป ไม่ใช่อาร์เรย์ offsets อีกชุดหนึ่ง
PDFlibPas ปฏิเสธ TIFF แบบ tiled ตั้งแต่ระหว่างแยกวิเคราะห์แท็ก ก่อนข้อมูลพิกเซลใดจะถูกแตะ ทางลัดที่เชิญชวนบั๊กมองเห็นได้ง่าย: แท็ก 324 (TileOffsets) กับแท็ก 325 (TileByteCounts) เป็นอาร์เรย์ของ file offset กับจำนวนไบต์ มีโครงสร้างเหมือนอาร์เรย์ strip ทุกประการ การชี้ฟิลด์ strip เดิมไปที่มันใช้แค่สองบรรทัดและคอมไพล์ผ่านสวยงาม แต่มันก็ผิด Tile ประกอบเป็นกริดสองมิติที่มีบล็อกขอบชนขอบ มี row stride ของตัวเองภายในแต่ละ tile และไม่มีความหมายของ RowsPerStrip อยู่เลย ตามที่หมวดภาพแบบ tiled ของข้อกำหนด TIFF 6.0 ระบุไว้ การป้อนเพย์โหลด tile ให้ strip decoder จึงไม่ล้มเหลวอย่างโฉ่งฉ่าง SimpleExtract กับ CompDecode จะเดินข้อมูลด้วย stride ที่ผิดและปล่อยภาพที่มิติถูกแต่พิกเซลผิดออกมา โค้ดรุ่นเก่ายิ่งซ้ำเติมด้วยการเก็บ StripsAreTiles, ColumnsPerTile และ RowsPerTile ไว้ใน TTIFFPage นั่นคือเรขาคณิตของ tile ที่ถูกบันทึกโดย decoder ที่ไม่มีตัวประกอบ tile อยู่เบื้องหลังเลย ใน 3.534.1 ตัวจัดการแท็ก 324 และ 325 จะ raise error เรื่อง tile และละทิ้ง IFD ทันที การปฏิเสธจึงพูดคำว่า "tiled" ตรง ๆ แทนที่จะผุดขึ้นหลังจากนั้นอีกหลายสัปดาห์ในรูปของข้อร้องเรียนเรื่องการแสดงผล
การ clamp มิติเดียวไม่ใช่งบหน่วยความจำ
การ clamp ความกว้างกับความสูงไว้ที่ 65,535 ต่อด้านเป็นสิ่งจำเป็นแต่ห่างไกลความเพียงพอ เพราะปริมาณที่กำหนดการจองหน่วยความจำคือผลคูณ RowsPerStrip * Width * SamplesPerPixel ล้นเลขคณิต 32 บิตได้นานก่อนที่แต่ละด้านจะแตะขีดจำกัดของตัวเอง และแม้ไม่ล้นก็ยังอาจระบุการจองหน่วยความจำที่ไม่มีเซอร์วิสใดควรลอง PDFlibPas คำนวณ row bytes ด้วย Int64 และบังคับใช้เพดานสามชั้นพร้อมกัน คือ 65,535 ต่อมิติ, 32 ส่วนประกอบสี และ 256 MiB ของไบต์ที่ถอดรหัสแล้ว
const
PDFLIB_TIFF_MAX_IMAGE_DIM = 65535;
PDFLIB_TIFF_MAX_COLOR_COMPONENTS = 32;
PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES = 256 * 1024 * 1024;
// ภายใน TPDFTIFF.ValidatePageForDecode
BitsPerPixel := Int64(P.BitsPerSample) * P.SamplesPerPixel;
RowBytes := (Int64(P.Width) * BitsPerPixel + 7) div 8;
if (RowBytes < 1) or
(RowBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
(Int64(P.Height) > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES div RowBytes) then
Exit(False);
DecodedBytes := RowBytes * P.RowsPerStrip;
if (DecodedBytes < 1) or
(DecodedBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
(DecodedBytes > MaxInt) then
Exit(False);
รายละเอียดสามจุดในนั้นสำคัญกว่าค่าคงที่เสียอีก การทดสอบความสูงถูกเขียนเป็นการหาร ไม่ใช่การคูณ ผลคูณที่เกินขนาดจึงไม่เคยถูกคำนวณขึ้นเลย RowsPerStrip ที่ต่ำกว่า 1 หรือสูงกว่าความสูงภาพจะถูกปรับเป็นความสูงภาพก่อน ซึ่งเป็นการอ่านแบบ single-strip ที่ TIFF 6.0 บอกไว้แล้ว และยังกันแท็กป่วนไม่ให้บัฟเฟอร์ strip พองโต และรูทีนนี้ถูกใช้ร่วมกัน ValidatePageForDecode ทำงานท้ายการแยกวิเคราะห์แท็กและอีกครั้งที่จุดเข้าของทั้ง SimpleExtract และ CompDecode โค้ดที่พุ่งเข้าหา decoder โดยตรงจึงอ้อมงบไม่ได้ นั่นเป็นกฎเดียวกับที่ PDFlibPas ใช้เวลา แยกวิเคราะห์กราฟออบเจกต์ PDF ที่ไม่น่าเชื่อถือ เพราะขีดจำกัดที่บังคับที่ประตูเดียวจากสามประตูไม่ใช่ขีดจำกัด
ผู้เรียกต้องตรวจอะไรก่อนอ่าน PageInfo
ตรวจ ValidTIFF ก่อน ตามด้วย PageCount แล้วจึง index เข้า PageInfo เท่านั้น ไฟล์ที่ถูกปฏิเสธอาจทิ้ง PageCount ไว้ที่ศูนย์ และ GetPageInfo ตอบ index ที่เลยช่วงด้วยเรกคอร์ด TTIFFPage ที่ยังไม่ได้เริ่มค่า เส้นทาง error ที่อ่านค่า resolution หรือจำนวน sample ระหว่างทางไปรายงานความล้มเหลวจึงกลายเป็นการอ่านสัญญาณรบกวนแทน เวอร์ชัน 3.534.1 แก้ทั้งสองจุดเรียกภายในไลบรารี: เส้นทาง import ภาพอ่าน XRes กับ YRes เฉพาะในสาขา valid เท่านั้น และ TPDFlib.GetImagePageCount บังคับว่าต้องผ่าน ValidTIFF แทนที่จะเชื่อจำนวนหน้าที่ไม่เป็นศูนย์ของมันเอง ฝั่งปลายทาง อาร์กิวเมนต์ Options ของ AddImageFromFile คือเลขหน้าแบบ 1-based สำหรับ multipage TIFF GetImagePageCount จึงต้องเชื่อถือได้ก่อนลูปเริ่ม ไม่ใช่หลังจากนั้น หน้าศูนย์ตอนนี้คือคำตอบจริงที่หมายถึง "ไม่มีอะไรในนี้ถอดรหัสได้" ไม่ใช่อุบัติเหตุของ early return ซึ่งสำคัญที่สุดตอนที่คุณกำลัง จัดเรียงและสลับชุดสแกนสองด้าน และแผ่นที่ถอดรหัสผิดอย่างเงียบ ๆ หนึ่งแผ่นจะไปตกอยู่ผิดตำแหน่ง
var
Pdf: TPDFlib;
Pages, I, ImageID: Integer;
begin
Pdf := TPDFlib.Create;
try
Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
if Pages < 1 then
Exit; // header เสีย, BigTIFF, เลย์เอาต์ tiled หรือเกินงบ
Pdf.NewDocument;
for I := 1 to Pages do
begin
Pdf.NewPage;
ImageID := Pdf.AddImageFromFile('inbox\scan-0417.tif', I);
if ImageID > 0 then
begin
Pdf.SelectImage(ImageID);
Pdf.DrawImage(0, 0, 595, 842);
end;
end;
Pdf.SaveToFile('scan-0417.pdf');
finally
Pdf.Free;
end;
end;
เขียน decoder เองหรือลิงก์ libtiff
PDFlibPas ยังใช้ decoder ในตัวต่อไป และปัจจัยตัดสินคือรัศมีแพลตฟอร์ม ไม่ใช่ว่าใครเขียนมัน Object Pascal ราว 1,873 บรรทัดคอมไพล์ได้ทุกที่ที่คอมไพเลอร์ไปถึง ทั้ง Win32, Win64, macOS, iOS, Android และ FPC บน Linux ส่วน libtiff 4.7.1 คือโค้ด C ราว 30,000 บรรทัดกระจายใน 34 translation units ของ tif_*.c และออบเจกต์ไฟล์สำเร็จรูปที่มีอยู่จริงวันนี้ครอบคลุมเฉพาะ Windows การรับมันมาจึงแลกความครอบคลุม TIFF ทั้งเส้นกับรายการแพลตฟอร์มที่รองรับซึ่งหดเหลือเท่าเครื่องที่รัน C toolchain ได้ บวกกับ linker pass ที่ยังไม่มีใครเดินผ่านเลย
ราคาที่จ่ายควรพูดกันตรง ๆ โดยไม่ต้องประโลม decoder ในตัวรับมือกับสิ่งที่งานสแกนเอกสารผลิตออกมาจริง ทั้ง CCITT Group 3 หนึ่งและสองมิติ, Group 4, LZW, Deflate, PackBits และ JPEG-in-TIFF ครอบคลุม photometric แบบ WhiteIsZero, BlackIsZero, RGB, palette และ CMYK พร้อม Predictor 1 และ 2 เพย์โหลดเหล่านี้ตรงกับ PDF filter ใน ISO 32000-1 §7.4.4 และ §7.4.6 นั่นคือเหตุที่ TIFF front-end แบกน้ำหนักมากในไปป์ไลน์สแกน สิ่งที่ไม่รับมือคือ BigTIFF, tiles, floating-point Predictor 3, PixarLog กับ SGILog, JPEG compression 6 สไตล์เก่า และพีระมิด sub-IFD นับตั้งแต่ 3.534.1 ทุกเคสเหล่านี้กลายเป็นการปฏิเสธที่ระบุชื่อ ไม่ใช่ภาพที่ผิด และไลบรารีเก็บรายการทริกเกอร์ที่เขียนไว้เป็นลายลักษณ์อักษรสำหรับเปิดการตัดสินใจ libtiff ขึ้นใหม่:
- ลูกค้ารายงานไฟล์ BigTIFF และต้องการการรองรับแบบ native ไม่ใช่ขั้นตอนแปลงไฟล์
- ลูกค้ารายงาน TIFF แบบ tiled จากแหล่งทางการแพทย์, GIS หรืออุตสาหกรรม และต้องการถอดรหัสในที่
- ลูกค้ารายงาน TIFF แบบ floating-point Predictor 3
- ช่องโหว่ที่เผยแพร่แล้วตกลงบนเส้นทางถอดรหัส CCITT หรือ LZW ในตัว
- ข้อโต้แย้งเรื่องข้ามแพลตฟอร์มหมดที่ยืน ไม่ว่าเพราะการรองรับ macOS, iOS และ Android ถูกถอดออก หรือเพราะการ integrate libtiff แบบใช้ซ้ำได้ครอบคลุม macOS กับ Linux ไปแล้ว
ตัวการย้ายถิ่นเองถูกกำหนดขอบเขตไว้แล้ว ไม่ใช่แค่สมมติ: conditional USE_LIBTIFF จะคงผิวสาธารณะของ TPDFTIFF ไว้ครบ หน้า LoadFromStream ผ่าน TIFFClientOpen ด้วย stream callback และให้ parser Pascal เป็น fallback สำหรับ non-Windows จนกว่าทริกเกอร์เหล่านั้นจุดจริง การดูแล decoder สองตัวพร้อม test matrix ที่โตขึ้นเท่าตัวไม่ได้ซื้ออะไรที่ลูกค้าสัมผัสได้เลย การเลื่อนต้นทุนไว้โดยที่ทางหนีได้ถูกเขียนลงกระดาษแล้ว ต่างจากการมองข้ามมัน
ไปป์ไลน์สแกนเอกสารควรวาง decoder ไว้ตรงไหน
ใช้ TPDFTIFF เป็นประตู ไม่ใช่ตัวแปลงไฟล์ โหลดไฟล์ อ่าน ValidTIFF และ log LastError ตรงตามตัวอักษรทุกครั้งที่เป็นเท็จ เพราะสตริงนั้นตอนนี้คือเส้นทางสั้นที่สุดจากรายงานหน้างานไปสู่การวินิจฉัย ไฟล์ที่ตกประตูยังกู้ได้ด้วยการแปลงที่ต้นน้ำ ซึ่งเป็นคำตอบเชิงปฏิบัติสำหรับแหล่ง BigTIFF กับ tiled ในวันนี้ ส่วนอินพุตที่ไม่ใช่ TIFF เลย PDFlibPas เลือกเส้นทางแยกผ่านช่องทาง อินพุตภาพ AVIF, HEIF และ JPEG XL เพื่อให้คำถามว่า decoder ตัวไหนเป็นเจ้าของฟอร์แมตไหนยังชัดเจนแบบประกาศไว้ ไม่ใช่เกิดขึ้นเอง
ทั้งหมดนี้ซ่อนอยู่หลัง image API ปกติ ไปป์ไลน์เอกสารจึงได้เขตแดนที่แน่นขึ้นโดยไม่ต้องแก้โค้ดเรียกแม้แต่บรรทัดเดียว นอกจากการตรวจจำนวนหน้าที่ควรตรวจอยู่แล้ว ถ้ากำลังชั่งใจเรื่องเส้นทาง TIFF เป็น PDF แบบ native สำหรับ Delphi หรือ C++Builder คอมโพเนนต์เต็มพร้อมการจัดการภาพมีเอกสารอยู่ที่ หน้าผลิตภัณฑ์ PDF Library สำหรับ Delphi