PDFlibPas เวอร์ชัน 3.539.22 decode ตาราง Huffman แบบกำหนดเองของ JBIG2 ได้เองในตัว: decoder ที่เขียนด้วย Pascal ล้วนใน PDFlibJBIG2.pas อ่าน Tables segment (type 53) กำหนด canonical prefix code ตามลำดับบรรทัดของตารางอย่างที่ ITU-T T.88 Annex B.3 กำหนด ใช้ตารางที่อ้างอิงแบบกำหนดเองตามลำดับ selector สำหรับ symbol dictionary และ text region และจำกัดทุกการอ่านด้วยความยาว segment ที่ประกาศไว้ ไม่ใช่ด้วยไบต์อะไรก็ตามที่ตามมาพอดี
ไฟล์ที่ทำให้ต้องลงมือทำเรื่องนี้ดูไม่มีอะไรพิเศษบนผิวหน้า สัญญาที่สแกนมาไฟล์หนึ่งบีบอัดด้วย JBIG2 แบบ Huffman symbol coding ไม่ใช่ arithmetic coding ที่พบมากกว่ามาก และ encoder ส่งตารางโค้ดของตัวเองมาแทนตารางมาตรฐาน B.1 ถึง B.15 decoder อิสระสองตัวให้พิกเซล refinement ไม่ตรงกัน และ decoder ของ PDFlibPas ในตอนนั้นให้ข้อความที่ดูเหมือนผ่านเครื่องย่อยกระดาษ: ชิ้นส่วน glyph เลื่อนไปไม่กี่พิกเซล ทุกตัวอักษรขาดไปหนึ่งคอลัมน์ ไม่มีอะไรโยน error ออกมา นั่นคือรูปร่างของ bug ที่อยู่รอดได้เป็นปี ๆ เพราะ decoder ที่ปฏิเสธไฟล์จะได้ support ticket ส่วน decoder ที่ render ผิดนิดหน่อยจะได้ลูกค้าที่คิดว่า scan ต้นฉบับไม่ดีเอง
Tables segment ของ JBIG2 บรรจุอะไรไว้จริง ๆ
Tables segment คือคำอธิบายแบบกระชับของตาราง Huffman หนึ่งตาราง: flags หนึ่งไบต์, ขอบเขต signed 32-bit สองค่า และจากนั้นชุดคู่ (ความยาว prefix, ความยาว range) ที่แบ่งช่วงระหว่างขอบเขตทั้งสอง ตามที่วางไว้ใน T.88 §7.4.13 และ Annex B.2 บิต 0 ของ flags คือ HTOOB บอกว่าตารางนี้มี out-of-band code หรือไม่ บิต 1 ถึง 3 บวกหนึ่งให้ HTPS คือจำนวนบิตที่ใช้เขียนความยาว prefix แต่ละตัว บิต 4 ถึง 6 บวกหนึ่งให้ HTRS คือความกว้างของฟิลด์ความยาว range แต่ละตัว บิต 7 สงวนไว้ และ PDFlibPas ปฏิเสธ segment นั้นถ้าบิตนี้ถูกตั้ง แทนที่จะเดาว่า revision ในอนาคตหมายถึงอะไร HTLOW กับ HTHIGH ตามมาเป็น signed 32-bit integer ซึ่งเป็นจุดแรกที่ decoder พลาดได้: อ่านเป็น unsigned ทำให้ตารางที่มีขอบล่างติดลบ ซึ่งเป็นเรื่องปกติมากสำหรับความกว้าง symbol ที่เข้ารหัสแบบ delta ดูเหมือนเริ่มที่สี่พันล้าน ทุกฟิลด์ผ่าน helper ReadField ภายในที่ตรวจคำขอกับตำแหน่งบิตที่ข้อมูล segment จบก่อนจะแตะ reader เพราะตารางที่อ่านเลย segment ของตัวเองจะกิน header ของ segment ถัดไปเป็นความยาว prefix
// TCodeTableSegment.readSegment ใน PDFlibJBIG2.pas
EndBit := (Int64(decoder.reader.bytePointer) +
segmentHeader.getSegmentDataLength) * 8;
Flags := ReadField(8);
if (Flags and $80) <> 0 then
raise EJBIG2DecodeError.Create('reserved custom Huffman table flag');
PrefixBits := ((Flags shr 1) and 7) + 1; // HTPS
RangeBits := ((Flags shr 4) and 7) + 1; // HTRS
LowValue := Integer(ReadField(32)); // HTLOW แบบ signed
HighValue := Integer(ReadField(32)); // HTHIGH แบบ signed
if LowValue >= HighValue then
raise EJBIG2DecodeError.Create('invalid custom Huffman range bounds');
CurrentValue := LowValue;
while CurrentValue < HighValue do
begin
PrefixLength := ReadField(PrefixBits);
RangeLength := ReadField(RangeBits);
if RangeLength > 32 then
raise EJBIG2DecodeError.Create('invalid custom Huffman range length');
AddLine(CurrentValue, PrefixLength, RangeLength);
Inc(CurrentValue, Int64(1) shl RangeLength);
end;
AddLine(LowValue - 1, ReadField(PrefixBits), jbig2HuffmanLOW);
AddLine(HighValue, ReadField(PrefixBits), 32);
if (Flags and 1) <> 0 then
AddLine(0, ReadField(PrefixBits), jbig2HuffmanOOB);
สองบรรทัดที่ต่อท้ายหลัง loop คือบรรทัด escape จาก Annex B.2: บรรทัดช่วงล่างเริ่มที่ HTLOW ลบหนึ่งแล้วนับลงไป บรรทัดช่วงบนเริ่มที่ HTHIGH ด้วย range 32 บิตคงที่ และบรรทัด OOB ซึ่งมีหรือไม่ก็ได้ไม่มีค่าอะไรเลย PDFlibPas ทำเครื่องหมายบรรทัดเหล่านี้ด้วยความยาว range พิเศษ jbig2HuffmanLOW ($FFFFFFFD) และ jbig2HuffmanOOB ($FFFFFFFE) ซึ่งเป็นธรรมเนียมเดียวกับที่ตารางมาตรฐานที่มีมาให้ทั้งสิบห้าตารางใช้ loop ที่ decode จึงไม่ต้องสนว่าตารางมาจากสเปกหรือมาจากไฟล์
ทำไม prefix code ต้องถูกกำหนดตามลำดับบรรทัดของตาราง
เพราะ encoder ไม่เคยเขียนโค้ดลงไป Tables segment ของ JBIG2 มีแค่ความยาว prefix และทั้งสองฝ่ายสร้างบิตแพตเทิร์นจริงขึ้นมาใหม่ด้วยขั้นตอน canonical ใน Annex B.3: นับว่ามีบรรทัดที่มีความยาวแต่ละค่าเท่าไร กำหนดโค้ดความยาวหนึ่งก่อน จากนั้นเลื่อนซ้ายแล้วทำต่อ และภายในความยาวเดียวกันแจกโค้ดตามลำดับที่บรรทัดปรากฏ เบี่ยงจากลำดับนั้นแม้นิดเดียวก็ได้ตารางอีกชุดเงียบ ๆ decoder จะไม่รู้ตัวเลย เพราะบิตแพตเทิร์นทุกตัวที่มันสร้างยังเป็น prefix code ที่ถูกต้อง เพียงแต่ไม่ใช่ตัวที่ encoder ใช้ และ output ก็เป็นบิตแมปที่ดูสมเหตุสมผลประกอบขึ้นจาก symbol ผิดตัว
// THuffmanDecoder.buildTable ใน PDFlibJBIG2.pas
FillChar(Counts, SizeOf(Counts), 0);
for I := 0 to length - 1 do
begin
if table[I].prefixLen > 32 then
raise EJBIG2DecodeError.Create(
'Huffman prefixes longer than 32 bits are not supported');
Inc(Counts[table[I].prefixLen]);
end;
Active := 0;
Code := 0;
for Bits := 1 to 32 do
begin
Starts[Bits] := Active;
Positions[Bits] := Active;
Inc(Active, Counts[Bits]);
if Code + UInt64(Counts[Bits]) > (UInt64(1) shl Bits) then
raise EJBIG2DecodeError.Create('oversubscribed Huffman prefix codes');
Code := (Code + UInt64(Counts[Bits])) shl 1;
end;
SetLength(Result, Active + 1);
for I := 0 to length - 1 do // stable: ลำดับเดิมยังอยู่
if table[I].prefixLen > 0 then // ภายในความยาว prefix เดียวกัน
begin
Result[Positions[table[I].prefixLen]] := table[I];
Inc(Positions[table[I].prefixLen]);
end;
Code := 0;
for Bits := 1 to 32 do
begin
for I := Starts[Bits] to Positions[Bits] - 1 do
begin
Result[I].prefix := Cardinal(Code);
Inc(Code);
end;
Code := Code shl 1;
end;
Result[Active].rangeLen := jbig2HuffmanEOT;
THuffmanDecoder.buildTable เป็น counting sort ไม่ใช่ comparison sort ด้วยเหตุผลเดียว: การนับรอบเดียวบน Counts, Starts และ Positions เป็น stable ตั้งแต่โครงสร้าง บรรทัดที่ความยาว prefix เท่ากันจึงลงในผลลัพธ์ตามลำดับที่ประกาศไว้ ซึ่งก็คือลำดับที่ Annex B.3 ใช้กำหนดโค้ดนั่นเอง บรรทัดที่ความยาว prefix เป็นศูนย์ถูกตัดออกก่อนการกำหนดโค้ด เพราะ B.3 นิยามว่าเป็น unused ไม่ใช่โค้ดหนึ่งบิต มี guard สองตัวอยู่ใน loop เดียวกัน การตรวจ oversubscription จับตารางที่ความยาวอ้างจำนวนโค้ดเกินกว่าที่ prefix code ความลึกนั้นจะรับได้ ซึ่งก็คืออสมการ Kraft ในรูปการเปรียบเทียบจำนวนเต็ม ถ้าไม่มี guard นี้ ตารางที่มุ่งร้ายจะสร้างโค้ดที่ตรงกับสองบรรทัด แล้ว decoder จะเลือกตัวที่สแกนเจอก่อน ส่วนเพดาน 32 บิตมีอยู่เพราะ prefix เป็น Cardinal และตัวจับคู่ใน decodeInt สะสมบิตเข้าไปในตัวเดียว T.88 ยอมให้ prefix ยาวกว่านั้นในทางทฤษฎี PDFlibPas ปฏิเสธโดยเรียกชื่อออกมา และยังไม่เคยเห็น encoder จริงตัวไหนปล่อยออกมา การคำนวณค่าก็ต้องระวังเท่ากับการคำนวณโค้ด: THuffmanTable.val เป็น Int64 และบรรทัดช่วงล่างถูก decode เป็น val - readBits(32) คือ offset unsigned 32 บิตที่ลบออกจาก HTLOW ลบหนึ่ง ถ้าใช้ Integer เป็นตัวกลาง การลบนั้นจะ wrap และค่าที่ wrap แล้วจะถูกยอมรับเป็นความกว้าง symbol ต่อ เส้นทาง 64 บิตคำนวณค่าจริง ตรวจกับช่วง signed 32 บิต และ raise ถ้าใส่ไม่ลง ซึ่งเปลี่ยนความเสียหายเงียบ ๆ ให้เป็นการปฏิเสธที่ชัดเจน
ทำไมตารางแบบกำหนดเองถึงไม่เคยทำงานก่อน 3.539.22
ข้อบกพร่องสองตัวซ่อนกันอยู่ ตัวแรกเป็น bug ในตัว setter บรรทัดเดียว: TTextRegionHuffmanFlags.setFlags รับ argument ด้วยชื่อเดียวกับฟิลด์ที่มันเก็บลงไป Self.flagsAsInt := flagsAsInt จึง assign ฟิลด์ที่ยังไม่ถูก initialize ให้ตัวมันเอง และ selector ทุกตัวอ่านกลับมาเป็นศูนย์ ซึ่งพา text region ที่ขอตารางแบบกำหนดเองไปใช้ตารางมาตรฐาน F, H และ K แทน ข้อบกพร่องตัวที่สองหมายความว่าแก้ตัวแรกอย่างเดียวก็ยังได้ symbol ที่เสียหาย เพราะเมื่อ Huffman symbol dictionary เก็บ symbol เป็น collective bitmap ที่ไม่บีบอัด ไบต์สุดท้ายของแต่ละแถวจะไม่เต็ม และ loop คัดลอกแบบเก่าถือ padding ซึ่งเก็บจำนวนบิตที่ใช้ได้ เป็นตำแหน่งของบิตที่ใช้ได้ต่ำสุด แถวที่กว้าง 63 พิกเซลจึงคัดลอกหนึ่งบิตจากไบต์สุดท้ายแทนที่จะเป็นเจ็ดบิต loop ที่แก้แล้วรัน for bitPointer := 7 downto ((8 - padding) and 7) และ fixture สังเคราะห์ที่ความกว้าง 7 บิตกับ 9 บิตล็อกทั้งสองข้างของขอบไบต์ไว้ เมื่อ selector อ่านถูกต้อง ตารางก็ถูกแจกตามลำดับที่สเปกไล่ไว้ ซึ่ง T.88 §7.4.3.1.2 กำหนดสำหรับ text region เป็น FS, DS, DT, RDW, RDH, RDX, RDY และ RSIZE และ §7.4.2.1.1 กำหนดสำหรับ symbol dictionary เป็น DH, DW, BMSIZE และ AGGINST selector สองบิตแต่ละตัวหมายถึงตารางมาตรฐาน 0 หรือ 1, สงวนไว้เมื่อเป็น 2 บนฟิลด์ที่มีตารางมาตรฐานแค่สองตัว และแบบกำหนดเองเมื่อเป็น 3 และการเลือกแบบกำหนดเองทุกครั้งกิน Tables segment ถัดไปในบรรดา segment ที่อ้างถึงตามลำดับการอ้าง NextCustomHuffmanTable เดินตามนั้นเป๊ะ ๆ และ raise missing custom Huffman table reference เมื่อ region อ้างตารางน้อยกว่าที่ selector ของมันต้องการ อีกบรรทัดหนึ่งเป็นของ fix ชุดเดียวกัน: Huffman symbol dictionary ที่ input กับ symbol ใหม่รวมกันได้หนึ่งจะคำนวณความยาว symbol code ออกมาเป็นศูนย์จากสูตร log2 ขณะที่ตัวแปร Huffman ของฟอร์แมตเขียน symbol ID ทุกตัวด้วยอย่างน้อยหนึ่งบิต ดังนั้น if sdHuffman and (symbolCodeLength = 0) then symbolCodeLength := 1 ใน TSymbolDictionarySegment จึงกันไม่ให้เส้นทาง refinement กับ aggregate อ่านศูนย์บิตต่อ symbol ID
ขอบเขตของ segment รับประกันอะไร
PDFlibPas ถือว่าความยาว segment data ในทุก header เป็น contract ที่ทั้งสองฝ่ายต้องรักษา: segment อ่านเลยจุดจบที่ประกาศไว้ไม่ได้ และจบสั้นเกินจนทิ้ง header ถัดไปไว้ที่ offset ที่เดาไม่ได้ก็ไม่ได้ กฎที่ตามมาจาก contract นี้แต่ละข้อเล็ก ๆ ทั้งนั้น ความยาวข้อมูลที่บิต 31 ถูกตั้งคือเครื่องหมาย unknown-length ของ T.88 §7.2.7 และ handleSegmentDataLength map มันเป็นค่าติดลบที่ readSegments ปฏิเสธทันที แทนที่จะสแกนหาตัวปิดถัดไป หมายเลข segment ที่อ้างถึงทุกตัวต้องน้อยกว่าหมายเลข segment ปัจจุบันและต้องมีอยู่แล้ว การอ้างไปข้างหน้าหรืออ้างตัวที่ไม่มีจึงล้มก่อนที่ region ใดจะพยายาม resolve มัน END_OF_PAGE กับ END_OF_FILE ต้องประกาศข้อมูลศูนย์ไบต์ ส่วน Profiles segment (type 52) บรรจุ count ขนาด 32 บิตตามด้วย identifier ขนาด 32 บิตจำนวนนั้นและไม่มีพิกเซลเลย จึงถูกตรวจเป็น 4 บวก 4 คูณ count เทียบกับความยาวที่ประกาศไว้ ข้ามไป และเก็บไว้ในรายการ segment เท่านั้นเพื่อให้ segment หลัง ๆ ยังอ้างถึงมันด้วยหมายเลขได้ profile identifier ที่ไม่รู้จักไม่ใช่ encoding ที่ไม่รู้จัก และการปฏิบัติกับมันเหมือนอย่างหลังจะปฏิเสธไฟล์ที่ decode ได้ดีอยู่แล้ว
// TJBIG2StreamDecoder.readSegments ใน PDFlibJBIG2.pas
DataLength := segmentHeader.getSegmentDataLength;
if DataLength < 0 then
raise EJBIG2DecodeError.Create(Context +
'unknown or oversized segment length is not supported');
if DataLength > Length(reader.Data) - reader.bytePointer then
raise EJBIG2DecodeError.Create(Context + 'truncated segment data');
DataEnd := reader.bytePointer + DataLength;
for I := 0 to noOfReferredToSegments - 1 do
if (referredToSegments[I] >= segmentHeader.getSegmentNumber) or
(findSegment(referredToSegments[I]) = nil) then
raise EJBIG2DecodeError.Create(Context + 'invalid segment reference');
// ... สร้าง segment object สำหรับ type นี้ ...
reader.SegmentEnd := DataEnd;
segment.readSegment;
if reader.bytePointer > DataEnd then
raise EJBIG2DecodeError.Create(Context +
'decoded data exceeds declared segment length');
if reader.bytePointer < DataEnd then
begin
reader.bytePointer := DataEnd; // MMR อาจทิ้งให้ EOFB ยังไม่ถูกอ่าน
reader.bitPointer := 7;
end;
ส่วนท้ายของ loop นั้นคือที่ที่ decoder เวอร์ชันก่อนหน้าพลาดกับ region ที่เข้ารหัสแบบ MMR decoder MMR รู้ว่าตัวเองจบแล้วเมื่อพิกเซลสุดท้ายของแถวสุดท้ายถูกผลิตออกมา ซึ่งเกิดขึ้นได้ก่อนที่มันจะกินตัวปิด EOFB ที่ T.88 §6.2.5.7 วางไว้ท้ายข้อมูล โค้ดเดิมสันนิษฐานว่า reader ยืนอยู่ที่ header ถัดไป ไบต์ตัวปิดที่เหลือจึงถูก parse เป็นหมายเลข segment แล้ว stream ก็ล้มเหลวอีกไม่กี่ไบต์ถัดมาด้วย error ที่ชวนให้เข้าใจผิด ตอนนี้จุดจบที่ประกาศไว้ชนะ: อ่านเลยมันคือ error จบสั้นกว่าเป็นเรื่องปกติ และ reader ถูกย้ายไปที่ DataEnd พร้อมรีเซ็ตบิต pointer เพื่อให้ header ถัดไปถูกอ่านจากที่ที่ไฟล์บอกว่ามันจะอยู่ วินัยเดียวกันนี้โผล่ทุกที่ที่ PDFlibPas parse โครงสร้าง PDF ที่ไม่น่าไว้ใจ: ความยาวที่ประกาศไว้คือขอบเขต และ decoder ไม่ไปหาขอบเขตที่ใจดีกว่าตัวอื่น
Huffman refinement อ่านขนาดบิตแมปจากที่ไหน
ก่อนที่ arithmetic decoder จะเริ่ม และจากฟิลด์ที่มีอยู่เฉพาะในโหมด Huffman เมื่อ instance ของ text region มี refinement (RI ไม่เป็นศูนย์) และ SBHUFF ถูกตั้ง T.88 §6.4.11 กำหนดให้ decoder อ่าน RDW, RDH, RDX และ RDY ด้วยตารางที่เลือกไว้ จากนั้นอ่าน BMSIZE ด้วยตาราง RSIZE แล้ว align ไปที่ขอบไบต์ และเพิ่งจากตรงนั้นจึงรันการ decode refinement แบบทั่วไปบนข้อมูลพอดี BMSIZE ไบต์ text region ในโหมด arithmetic ไม่มีฟิลด์แบบนี้ และ decoder ที่ใช้เส้นทางโค้ดเดียวกันกับทั้งสองโหมดจะข้ามมันไป เริ่ม arithmetic decoder เร็วไปสองไบต์หรือมากกว่า แล้ว refine symbol ทุกตัวเทียบกับขยะ เส้นทาง symbol dictionary ที่ใช้ REFAGG กับ refinement instance เดียว ตามที่อธิบายใน §6.5.8.2.2 มีฟิลด์ BMSIZE เดียวกันพร้อมผลลัพธ์เดียวกัน ใน PDFlibPas ขอบบนของขนาดนั้นคือ TStreamReader.SegmentEnd ซึ่งเป็นจุดจบของ segment ปัจจุบันตามที่ readSegments ตั้งไว้ ไม่ใช่จุดจบของ stream ทั้งก้อน เพราะ BMSIZE ที่จะพอได้ก็ต่อเมื่อยืมไบต์จาก segment ถัดไปนั้นเป็นไฟล์ผิดรูป และการตรวจมันเทียบกับความยาว stream จะเปิดทางให้ arithmetic decoder อ่านเข้าไปใน header ถัดไป ขอบล่างสองไบต์สะท้อนคู่ไบต์เริ่มต้นที่ arithmetic decoder กินเสมอ และหลัง refinement reader กระโดดไปที่ RefinementEnd ไม่ว่ามันอ่านล่วงหน้าไปไกลแค่ไหน เพราะตำแหน่งสุดท้ายของมันไม่ใช่ตำแหน่งของฟิลด์ที่เข้ารหัส Huffman ตัวถัดไป
// การ decode text region ของ TJBIG2Bitmap เส้นทาง Huffman refinement
RefinementSize := huffmanDecoder.decodeInt(huffmanRSizeTable).intResult;
huffmanDecoder.consumeRemainingBits;
if (RefinementSize < 2) or
(RefinementSize > huffmanDecoder.reader.SegmentEnd -
huffmanDecoder.reader.bytePointer) then
raise EJBIG2DecodeError.Create('invalid refinement bitmap size');
RefinementEnd := huffmanDecoder.reader.bytePointer + RefinementSize;
arithmeticDecoder.start;
// ... readGenericRefinementRegion ...
if huffmanDecoder.reader.bytePointer > RefinementEnd then
raise EJBIG2DecodeError.Create('refinement data exceeds declared size');
huffmanDecoder.reader.bytePointer := RefinementEnd;
huffmanDecoder.reader.bitPointer := 7;
อะไรถูกตรวจแล้ว และอะไรยังถูกปฏิเสธอยู่
ตัวอย่างที่ทำให้เรื่องนี้เริ่มต้นขึ้น ภาพ JBIG2 ขนาด 500 คูณ 473 พิกเซลที่มีตารางแบบกำหนดเองและ Huffman refinement ตอนนี้ decode ออกมาเป็นบิตแมปที่ต่างจาก decoder อิสระ zero พิกเซล และ fixture collective bitmap สังเคราะห์ที่ความกว้าง 7 บิตกับ 9 บิตให้แถวตามที่คาดไว้ทั้งคู่ decoder อิสระสองตัวที่เคยไม่ตรงกันบนตัวอย่างต้นฉบับก็ยังไม่ตรงกันเอง PDFlibPas ตรงกับหนึ่งในนั้น และคำกล่าวที่ซื่อสัตย์คือ output แบบ native ตรงกับ implementation อิสระตัวหนึ่งและตรงกับสเปกตามที่อ่าน ไม่ใช่ตรงกับ decoder ทุกตัวในโลกนี้ ส่วนด้านไฟล์ผิดรูปของชุดทดสอบครอบคลุม:
- บิต flag ที่สงวนไว้ หรือค่า selector ที่สงวนไว้
- ตารางที่ถูกตัดขาดกลางบรรทัด
- ความยาว prefix ที่ oversubscribed และ prefix ที่ยาวกว่า 32 บิต
- region ที่ selector ขอตารางแบบกำหนดเองมากกว่าจำนวนที่มันอ้างถึง
- การยืนยันว่า output เก่าถูกล้างหลังการ decode ที่ล้มเหลว แทนที่จะทิ้งไว้ให้ caller เข้าใจผิดว่าเป็นผลลัพธ์
ยังมีข้อจำกัดสามข้อที่ตั้งใจคงไว้ การจัด stream แบบ random access ซึ่ง header ของ segment ทั้งหมดมาก่อนข้อมูลของ segment ทั้งหมด จะ raise JBIG2 random-access organisation is not supported ทันทีที่อ่าน flags ของ file header เพราะไม่มีตัวอย่างที่เป็นตัวแทนให้ตรวจเทียบ และเส้นทางที่ทำไว้ครึ่ง ๆ กลาง ๆ แย่กว่าการปฏิเสธที่เรียกชื่อได้ ตารางแบบกำหนดเองถูกจำกัดที่ 65,536 บรรทัดกับ prefix 32 บิต และ entry การ decode สาธารณะอย่าง TPLJBIG2Decoder.LoadFromByteArray คืนบิตแมปของหน้าแรกตามลำดับใน stream ผ่าน getPageAsJBIG2Bitmap(0) ซึ่งเป็น page-information segment ตัวแรกที่เจอ แทนที่จะค้นหาด้วย page association หมายเลขศูนย์ stream ที่ฝังใน PDF มักใส่เลข 1 ให้หน้าอยู่หน้าเดียวของมัน และการขอหน้า 0 ด้วย association จะไม่เจออะไร ข้อความความล้มเหลวไปอยู่ใน TPLJBIG2Decoder.LastError ซึ่งเป็น diagnostic ภายในของ decoder ที่พาหมายเลข segment type และ offset ไบต์ของจุดที่พัง ไม่ใช่สิ่งเดียวกับ TPDFlib.LastErrorCode ระดับ library ทั้งหมดนี้ไม่แตะฝั่ง encode ซึ่งอยู่ในบันทึกเรื่อง backend ของ JBIG2 encoder และวิธีที่มันถูก link ฝั่งอ่านต้องยอมรับอะไรก็ตามที่ encoder ของคนอื่นตัดสินใจปล่อยออกมา และมันใช้กฎเดียวกันกับส่วนอื่นของกอง image stack รวมถึงdecoder TIFF ที่มีมาให้ในตัวพร้อมการปฏิเสธ BigTIFF กับเลย์เอาต์แบบ tiled: ปฏิเสธโดยเรียกชื่อ ไม่ยืมไบต์ข้ามขอบเขตที่ประกาศไว้ และคุมความกว้างของการคำนวณให้พอที่ค่ากลางที่ wrap แล้วจะผ่านเป็นคำตอบที่ถูกต้องไม่ได้ ถ้าคุณกำลังประเมินเส้นทางอ่าน JBIG2 แบบ native สำหรับ Delphi หรือ C++Builder decoder และการจัดการภาพส่วนที่เหลือมีเอกสารอยู่ที่ หน้า PDF Library for Delphi