ดึงอีโมจิหรือชื่อทะเบียนครอบครัวญี่ปุ่นออกจาก PDF เป็นข้อความ แล้วเอาต์พุตจะแสดงกล่อง เครื่องหมายคำถาม หรือไม่มีอะไรเลยตรงที่ควรมีตัวอักษร property Character[] ของ PDFium Component มักเป็นสาเหตุ มันอ่านแต่ละ glyph ผ่าน FPDFText_GetUnicode ซึ่งคืนค่า Unicode code point เต็มเป็นค่า unsigned 32 บิต แล้วเปิดมันให้ Delphi เห็นเป็น WideChar 16 บิตตัวเดียว code point ใดก็ตามที่เกิน U+FFFF ไม่สามารถเดินทางนั้นได้ในชิ้นเดียว และความเสียหายไม่เคยปรากฏขึ้นในขณะที่คุณกำลังดูหน้าที่ render อยู่ เพราะการ render กับการดึงข้อความวิ่งผ่าน code path ที่แยกกันใน PDFium เอกสารสามารถแสดงอีโมจิของมันได้อย่างสมบูรณ์แบบ และยังคงส่งขยะให้คุณทันทีที่คุณอ่าน Character[] ใน loop แล้วสร้าง string จากมัน
Basic Multilingual Plane และทำไม WideChar ถึงหยุดที่ U+FFFF
WideChar ของ Delphi เป็น type 16 บิตที่ถือได้แค่หนึ่ง UTF-16 code unit เท่านั้น Basic Multilingual Plane ของ Unicode ซึ่งเป็นช่วง U+0000 ถึง U+FFFF พอดีกับตรงนั้นเป๊ะ ซึ่งเป็นเหตุผลที่ละติน, ซีริลลิก, กรีก และ CJK Unified Ideographs block ทั่วไปล้วนไปกลับผ่าน WideChar ตัวเดียวได้โดยไม่มีเหตุการณ์อะไร ตัวอักษรสองตระกูลมักตกอยู่นอกเหนือมันในเอกสารจริง คืออีโมจิ หลายตัวอยู่ใน Emoticons block เริ่มที่ U+1F600 และอักษรจีน-ญี่ปุ่น-เกาหลีหายากจาก CJK Unified Ideographs Extension B ซึ่งเป็นช่วง U+20000 ถึง U+2A6DF ที่สงวนไว้สำหรับตัวอักษรจีน, ญี่ปุ่น และเกาหลีที่พบน้อยกว่า รวมถึงชื่อบุคคลและสถานที่จำนวนมาก UTF-16 จัดการอะไรก็ตามที่เหนือ U+FFFF ด้วย surrogate pair คือ code unit 16 บิตสองตัว high surrogate ในช่วง $D800 ถึง $DBFF ตามด้วย low surrogate ใน $DC00 ถึง $DFFF ที่รวมกันเข้ารหัส code point เดียว และคณิตศาสตร์เบื้องหลังการจับคู่นั้นแน่นอนพอที่จะสาธิตตรงๆ ใน Pascal ได้
function ToSurrogatePair(CodePoint: LongWord; out Hi, Lo: WideChar): Boolean;
var
V: LongWord;
begin
Result := CodePoint > $FFFF;
if Result then
begin
V := CodePoint - $10000;
Hi := WideChar($D800 + (V shr 10));
Lo := WideChar($DC00 + (V and $3FF));
end;
end;
ป้อน U+1F600 อีโมจิหน้ายิ้มกว้าง ผ่านฟังก์ชันนั้น แล้วผลลัพธ์คือ high surrogate เป็น $D83D และ low surrogate เป็น $DE00 สองค่า 16 บิต ไม่ใช่ค่าเดียว ไม่มีครึ่งไหนมีความหมายอะไรด้วยตัวเองเลย $D83D เดี่ยวๆ ที่นั่งอยู่ใน string โดยไม่มี $DE00 ตามหลังคือ dangling surrogate และโค้ดจัดการข้อความส่วนใหญ่ที่เจอมันจะทิ้งมัน แทนที่ด้วย glyph สำรอง หรือยก error
ทำไม FPDFText_GetUnicode ถึงคืนค่าที่ Character[] ถือไว้ไม่ได้
FPDFText_GetUnicode คืนค่า LongWord ซึ่งเป็นค่า 32 บิตเต็ม เพราะการเข้ารหัสข้อความ PDF พก scalar value ของ Unicode ที่สมบูรณ์อยู่แล้วสำหรับทุก glyph CMap ToUnicode ของ PDF แม็ปรหัสตัวอักษรไปเป็นข้อความ Unicode และเมื่อ glyph แสดงสิ่งที่เรียกกันอย่างไม่เป็นทางการว่าตัวอักษรระดับ astral-plane อะไรก็ตามที่เหนือ Basic Multilingual Plane การแม็ปนั้นเป็น code point เต็ม ไม่ใช่ชิ้นส่วน 16 บิต PDFium ถอดรหัสมันกลับเป็น scalar value ภายในและคืนมันข้ามขอบเขต DLL ผ่าน FPDFText_GetUnicode และขอบเขตนั้นคือจุดที่ค่า 32 บิตต้องกลายเป็นบางอย่างที่ property ของ Delphi ส่งกลับให้โค้ดของคุณได้พอดี
implementation ที่ดูชัดเจนคือ WideChar(FPDFText_GetUnicode(TextPage, Index)) และมันก็เป็นตัวที่ผิดด้วย การ cast แบบแข็งจากค่า 32 บิตเข้า type 16 บิตเก็บไว้แค่ 16 บิตล่างสุดและทิ้งที่เหลือไปเงียบๆ โดยไม่มี exception และไม่มีการตรวจสอบช่วง สำหรับ U+1F600 นั่นหมายถึงการเก็บ $F600 ไว้และสูญเสียข้อเท็จจริงที่ว่าค่าจริงเคยอยู่เหนือ U+FFFF ไปเลย ซึ่งสร้าง code unit ที่ไม่ใช่แม้แต่ dangling surrogate ที่ถูกต้อง แค่เป็นตัวอักษร Basic Multilingual Plane ที่ไม่เกี่ยวข้องกันซึ่งบังเอิญมีบิตล่างเหล่านั้นร่วมกัน ต่อกันหลายพันตัวเข้าเป็น string แล้วโค้ดที่อยู่ปลายทางก็ไม่มีทางเหลือให้แยกแยะตัวอักษรที่เสียหายจากตัวที่ถูกต้องได้เลย
Character[] และ Charcode[] คืนอะไรสำหรับ code point ระดับ astral-plane ตอนนี้
property Character[] และ Charcode[] ของ PDFium Component คืน U+FFFD ซึ่งเป็นตัวอักษรทดแทนของ Unicode ทุกครั้งที่ code point ข้างใต้เกิน U+FFFF แทนที่จะตัดทอนมันเงียบๆ การเฝ้านั้นอยู่ตรงภายใน property getter เบื้องหลัง Character[] เลย
function TPdf.GetCharacter(Index: Integer): WideChar;
var
Code: LongWord;
begin
LoadTextPage;
Code := FPDFText_GetUnicode(FTextPage, Index);
if Code > $FFFF then
Result := #$FFFD // astral-plane code point: cannot fit in one WideChar
else
Result := WideChar(Code);
end;
การคืน U+FFFD แทนชิ้นส่วนที่ถูกตัดทอน เป็นการแก้ไขที่จงใจและแคบ ไม่ใช่การออกแบบใหม่ Character[] และ Charcode[] ถูก type เป็น WideChar ทั้งบน TPdf และ TPdfView และการขยาย return type นั้นให้พก code point เต็มจะทำลายผู้เรียกที่มีอยู่แล้วทุกตัวที่คาดหวังว่าหนึ่ง glyph ต่อดัชนีหมายถึงค่า 16 บิตหนึ่งค่า U+FFFD เป็นตัวยึดตำแหน่งที่มาตรฐาน Unicode กำหนดไว้เองสำหรับสถานการณ์นี้พอดี ดังนั้นผู้เรียกที่ตรวจสอบมันจะได้สัญญาณที่นิยามไว้และมีเอกสารประกอบ แทนที่จะเป็นข้อมูลที่ผิดเงียบๆ ขอบเขตหนึ่งที่ควรรู้ไว้ U+FFFD ก็เป็นตัวอักษรที่ถูกต้องในสิทธิ์ของตัวเองด้วยเช่นกัน ดังนั้นในเอกสารหายากที่มี glyph ตัวอักษรทดแทนจริงอยู่แล้ว ดัชนีนั้นก็แยกไม่ออกจากตัวอักษร astral ที่ถูกตัดทอนด้วยค่าเพียงอย่างเดียว
คุณดึงข้อความอีโมจิและ CJK Extension B ได้อย่างถูกต้องใน Delphi ได้อย่างไร
เรียก Text แทนการเดินผ่าน Character[] ทุกครั้งที่เนื้อหาข้อความจริงสำคัญ เพราะ Text อ่านผ่าน FPDFText_GetText และคืน WString เต็มรูปแบบพร้อม surrogate pair ที่ถูกต้องสำหรับทุกตัวอักษร astral-plane ในช่วง แทนที่จะเป็นค่าความกว้างคงที่หนึ่งค่าต่อดัชนี Pdf.Text(0, MaxInt) หรือทางลัด Pdf.Text ดึงทั้งหน้าอย่างถูกต้องในการเรียกครั้งเดียว และ Pdf.Text(StartIndex, Count) ดึงช่วงที่เล็กกว่าแบบเดียวกัน Character[] ยังคงมีที่ทางของมันเมื่อคุณต้องการแค่ข้อมูลตำแหน่ง, ฟอนต์ หรือ flag ที่ดัชนีหนึ่งเท่านั้น และไม่แตะ code point เอง CharacterOrigin[], FontSize[] และ CharacterMapError[] ไม่สนใจเลยว่า glyph ข้างใต้เป็น astral หรือไม่
function ExtractLineSafely(Pdf: TPdf): WString;
var
I: Integer;
begin
Result := '';
for I := 0 to Pdf.CharacterCount - 1 do
if not (Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I]) then
Result := Result + Pdf.Text(I, 1); // full code point, never a truncated WideChar
end;
การตรวจสอบข้าม-สร้างขึ้น-และ-ไม่ได้แม็ปใน loop นั้นเป็นรูปแบบเดียวกับที่ใช้สำหรับการดึงข้อความธรรมดาในการดึงข้อความจากเอกสาร PDF ด้วย PDFium Component การเปลี่ยนแปลงเดียวคือบรรทัดสุดท้าย ซึ่งแลกการต่อท้าย Character[I] ตรงๆ ด้วยการเรียกดัชนีเดียวเข้า Text เพื่อให้ตัวอักษร astral มาถึงเป็น surrogate pair ที่สมบูรณ์แทนที่จะเป็นตัวยึดตำแหน่งทดแทน
จุดที่สิ่งนี้กัดจริงๆ: การ export แชท, ชื่อบุคคล และฟอนต์ CJK ที่ฝังไว้
อีโมจิปรากฏทุกที่ที่ PDF จับการสื่อสารแบบไม่เป็นทางการ ล็อกแชทที่ export ออกมา, ข้อมูลรีวิวจาก app store, transcript ระบบ ticketing ที่บันทึกเป็น PDF สำหรับ archive การปฏิบัติตามข้อกำหนด CJK Extension B ปรากฏในที่ที่แคบกว่าแต่มีเดิมพันสูงกว่า คือชื่อบุคคลและสถานที่ เพราะทะเบียนครอบครัวญี่ปุ่น, บันทึกทะเบียนบ้านของจีน และเอกสารประจำตัวไต้หวัน เป็นแหล่งคลาสสิกของตัวอักษรที่ไม่เคยเข้าไปอยู่ใน CJK block ทั่วไปเลย pipeline การจ่ายเงินเดือนหรือการยืนยันตัวตนที่ดึงชื่อจากเอกสารราชการที่สแกนมา เป็นภาระงานประเภทที่ตัวอักษรที่เสียหายอย่างเงียบๆ กลายเป็นการจับคู่ที่ล้มเหลว ไม่ใช่แค่ความผิดพลาดทางสายตาพอดี
อักษรจีน-ญี่ปุ่น-เกาหลีหายากมักมาพร้อมปัญหาด้านฟอนต์ด้วย ไม่ใช่แค่ปัญหาการเข้ารหัสเท่านั้น เพราะฟอนต์ต้องพก glyph สำหรับ code point ในช่วง U+20000 ก่อนที่อะไรจะ render ได้เลย และฟอนต์ระบบที่ติดตั้งไว้ไม่กี่ตัวมี ใครก็ตามที่เดินผ่าน FontIsEmbedded[] ต่อตัวอักษรอยู่แล้วแบบที่การอ่าน property ฟอนต์ PDF ด้วย PDFium Componentอธิบายไว้ ควรตรวจสอบดัชนีเดียวกันสำหรับทั้งสองปัญหาพร้อมกัน ดัชนีที่คืน U+FFFD จาก Character[] และรายงานฟอนต์ที่ไม่ได้ฝังไว้ เป็นเอกสารที่จะไม่ดึงหรือพิมพ์ตัวอักษรนั้นได้อย่างถูกต้องเลย และการแก้ไขอยู่ต้นทางในวิธีที่ PDF ถูกสร้างขึ้น ไม่ใช่ในโค้ดการดึงข้อมูลของคุณ
property Character[], Charcode[] และ Text ที่อธิบายในบทความนี้เป็นส่วนหนึ่งของPDFium Componentรุ่นมาตรฐานสำหรับ Delphi และ C++Builder