ทุกตัวอักษรที่มองเห็นได้ในเอกสาร PDF จะพาตัวอ้างอิงของฟอนต์ที่เป็นตัววาดมันมาด้วย และ PDFium Component ทำให้คุณสามารถแกะรอยตามตัวอ้างอิงนั้นกลับไปยังออบเจกต์ของฟอนต์แล้วอ่านค่าที่มันเก็บไว้ได้ หน่วยของการเข้าถึงคือตัวอักษร ไม่ใช่ทั้งเอกสาร: คุณเลือกตัวอักษรหนึ่งจากค่าดัชนี (index) ในหน้าข้อความ แล้วเรียกถามชื่อตระกูลฟอนต์ (family name), ชื่อพื้นฐาน (base name), น้ำหนัก (weight), มุมเอียง (italic angle) และดูว่ามีตัวอักษรนั้นฝัง (embedded) ไว้ในไฟล์จริง ๆ หรือไม่ คุณสมบัติตัวสุดท้ายนั้นคือสิ่งที่ระบบประเมินค่าส่วนใหญ่ต้องการคำตอบอย่างแท้จริง เพราะฟอนต์ที่ฝังไว้จะเดินทางไปพร้อมกับเอกสาร ในขณะที่ฟอนต์ที่ไม่ฝังเป็นเพียงคำมั่นสัญญาว่าเครื่องของผู้อ่านบังเอิญจะมีรูปแบบตัวอักษรเดียวกันนั้นติดตั้งอยู่
อ่านฟอนต์ที่อยู่เบื้องหลังตัวอักษรหนึ่งตัว
การทำงานที่มีประโยชน์ที่เล็กที่สุดคือการนำดัชนีตัวอักษรมาแล้วให้ PDFium แจกแจงทุกอย่างที่มันบอกได้เกี่ยวกับฟอนต์นั้น คุณสมบัติฟอนต์ทุกตัวใน TPdf และ TPdfView จะถูกจัดทำดัชนีตามตำแหน่งของตัวอักษร ดังนั้นตัวดัชนีจึงเชื่อมต่อพวกมันเข้าไว้ด้วยกัน หน้านั้นจำเป็นจะต้องเป็นหน้าปัจจุบันที่ดัชนีจะเทียบค่าดึงข้อความที่ถูกต้องออกมา ซึ่งนี่จะสำคัญเมื่อคุณย้ายผ่านหน้าแรกไปแล้ว
procedure DescribeFontAt(Pdf: TPdf; CharIndex: Integer);
var
Report: TStringList;
PtSize: Single;
begin
Report := TStringList.Create;
try
PtSize := Pdf.FontSize[CharIndex];
Report.Add('Character : ' + Pdf.Character[CharIndex]);
Report.Add('Family : ' + Pdf.FontFamilyName[CharIndex]);
Report.Add('Base name : ' + Pdf.FontBaseName[CharIndex]);
Report.Add('Weight : ' + IntToStr(Pdf.FontWeight[CharIndex]));
Report.Add('Italic : ' + IntToStr(Pdf.FontItalicAngle[CharIndex]) + ' deg');
Report.Add('Size : ' + FormatFloat('0.0', PtSize) + ' pt');
Report.Add('Ascent : ' + FormatFloat('0.0', Pdf.FontAscent[CharIndex, PtSize]));
Report.Add('Descent : ' + FormatFloat('0.0', Pdf.FontDescent[CharIndex, PtSize]));
Report.Add('Embedded : ' + BoolToStr(Pdf.FontIsEmbedded[CharIndex], True));
ShowMessage(Report.Text);
finally
Report.Free;
end;
end;
รูปแบบคำสั่งบางตัวอาจทำให้คนที่มาจากไลบรารีอื่นประหลาดใจ FontAscent และ FontDescent รับสองอาร์กิวเมนต์คือ ดัชนีตัวอักษรและขนาดพอยต์ เพราะ PDFium รายงานค่าเมตริกเหล่านั้นในหน่วยของพื้นที่กลีฟ (glyph-space units) ที่จะกลายเป็นพิกเซลก็ต่อเมื่อคุณปรับสเกลมันตามขนาดที่ข้อความถูกตั้งค่าไว้ ให้ส่งค่าที่คุณอ่านมาจาก FontSize[CharIndex] แล้วคุณก็จะได้ค่าความสูง (ascent) และค่าความลึก (descent) ในหน่วยพอยต์แบบเดียวกันกับส่วนอื่น ๆ ของเลย์เอาต์ ค่า descent จะส่งกลับมาเป็นค่าติดลบ เนื่องจากมันวัดจากใต้เส้นฐานบรรทัด (baseline) ลงไป ชื่อตระกูลและชื่อพื้นฐานเป็นสตริงสองตัวที่แยกจากกันโดยตั้งใจ: ชื่อพื้นฐานคือ entry /BaseFont ดิบจาก PDF มักจะมีคำนำหน้าหมวดหมู่ย่อยเช่น ABCDEF+ นำมาด้วย ในขณะที่ชื่อตระกูลคือชื่อที่ทำความสะอาดแล้วและระบบเรนเดอร์สามารถวิเคราะห์เพื่อใช้งานได้
แปลงการคลิกเป็นดัชนีตัวอักษร
ในโปรแกรม viewer คุณแทบจะไม่ทราบค่าดัชนีล่วงหน้าเลย ผู้ใช้คลิกที่กลีฟหนึ่งตัว และคุณต้องแปลพิกัดพิกเซลไปเป็นตัวอักษรที่อยู่ข้างใต้นั้น CharacterIndexAtPos ทำหน้าที่นั้นพอดี โดยรับตำแหน่งเมาส์และค่าความคลาดเคลื่อน (tolerance) ที่รับได้ จากนั้นก็ส่งคืนดัชนีของตัวอักษรที่ใกล้ที่สุด หรือค่าลบเมื่อคลิกลงบนพื้นที่ว่างหรือหน้ากระดาษเปล่า
procedure TfrmMain.PdfViewMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
Index: Integer;
begin
if not PdfView.Active then
Exit;
// 4 px of slack in each direction so a near-miss still hits the glyph.
Index := PdfView.CharacterIndexAtPos(X, Y, 4.0, 4.0);
if Index < 0 then
Exit; // clicked between glyphs; leave the panel alone
PdfView.CurrentCharIndex := Index;
DescribeFontAt(PdfView.Pdf, Index);
end;
ค่าความคลาดเคลื่อนที่ยอมรับได้เป็นสิ่งที่คุ้มค่าที่จะปรับแต่ง ถ้ากำหนดแคบเกินไป ผู้ใช้จะรู้สึกว่าต้องคลิกให้โดนเส้นก้านของตัวอักษรพอดี; ถ้ากว้างเกินไป การคลิกในขอบกระดาษอาจจะกระโดดไปโดนตัวอักษรไกล ๆ ที่ไม่ได้เกี่ยวอะไรกับสิ่งที่พวกเขาตั้งใจเลย สามถึงห้าดีไวซ์พิกเซลเป็นจุดเริ่มต้นที่สมเหตุสมผลสำหรับการดูบนหน้าจอ ค่าดัชนีที่ได้กลับมาอยู่ในส่วนของข้อความที่ผ่านการพาร์สแล้วในหน้าปัจจุบัน ซึ่งเป็นพื้นที่ดัชนีเดียวกันกับที่คุณสมบัติฟอนต์ทุกตัวต้องการใช้งาน ดังนั้นคุณสามารถส่งมันเข้าสู่ฟังก์ชันรูทีนข้างบนได้ทันที การจัดเก็บมันไว้ใน CurrentCharIndex เป็นเรื่องทางเลือกแต่ให้ความสะดวก: โปรแกรม viewer จะเก็บมันไว้เป็นจุดกำหนดสถานะกลีฟที่กำลังถูกโฟกัสอยู่ ซึ่งเป็นประโยชน์ถ้าส่วนอื่น ๆ ของ UI ต้องการอ่านค่าที่เลือกไว้โดยไม่ต้องคำนวณสกัดมันใหม่
การฝังไฟล์คือคุณสมบัติที่สำคัญ
สำหรับงานจริงส่วนใหญ่ คำถามเดียวที่ควรตอบให้ได้คือฟอนต์แต่ละตัวถูกฝัง (embedded) ไว้หรือไม่ เอกสารที่ฝังฟอนต์ทั้งหมดไว้ข้างในจะเรนเดอร์ออกมาเหมือนกันไม่ว่าจะอยู่บน RIP ของโรงพิมพ์ แล็ปท็อปของเพื่อนร่วมงาน หรือบนเซิร์ฟเวอร์ที่ไม่มี GUI เลยก็ตาม ส่วนเอกสารที่พึ่งพาฟอนต์ Helvetica ที่ไม่ได้ฝังมาถือเป็นการเสี่ยงโชคที่ต้องลุ้นว่าเครื่องเหล่านี้ทุกเครื่องจะมีรูปแบบตัวอักษรตรงกัน และเมื่อโชคไม่เข้าข้าง โปรแกรมอ่านจะเลือกใช้อะไรที่ใกล้เคียงมาแทนที่ ค่าเมตริกจะคลาดเคลื่อน และแบบฟอร์มที่จัดวางอย่างประณีตจะถูกจัดรูปแบบใหม่มากพอที่จะทำให้มันพัง การวนลูปตรวจสอบข้อความในหน้าแล้วแยกกลุ่มฟอนต์ตามสถานะการฝังไฟล์เป็นวิธีที่จะให้คำตอบนี้อย่างประหยัดทรัพยากร
procedure ReportNonEmbeddedFonts(Pdf: TPdf);
var
Embedded, External: TStringList;
I: Integer;
Name: string;
begin
Embedded := TStringList.Create;
External := TStringList.Create;
try
Embedded.Sorted := True;
Embedded.Duplicates := dupIgnore;
External.Sorted := True;
External.Duplicates := dupIgnore;
for I := 0 to Pdf.CharacterCount - 1 do
begin
Name := Pdf.FontBaseName[I];
if Name = '' then
Continue; // generated spaces and the like have no font
if Pdf.FontIsEmbedded[I] then
Embedded.Add(Name)
else
External.Add(Name);
end;
if External.Count > 0 then
ShowMessage(IntToStr(External.Count) +
' non-embedded font(s):' + sLineBreak + External.Text)
else
ShowMessage('All ' + IntToStr(Embedded.Count) +
' font(s) on this page are embedded.');
finally
Embedded.Free;
External.Free;
end;
end;
มีสองรายละเอียดที่จะทำให้การทำงานนี้ถูกต้องเที่ยงตรง ประการแรก CharacterCount เป็นค่าแยกแต่ละหน้า ดังนั้นการตรวจสอบทั้งเอกสารหมายความว่าต้องตั้งค่า Pdf.PageNumber ไปยังแต่ละหน้าตามลำดับแล้วรันลูปใหม่อีกครั้ง พร้อมรวบรวมผลลัพธ์เข้าด้วยกัน ประการที่สอง เลเยอร์ข้อความมีตัวอักษรที่ระบบสร้างขึ้น เช่น ช่องว่างที่โปรแกรมอ่านแทรกระหว่างคำ และช่องว่างเหล่านี้ไม่มีออบเจกต์ฟอนต์อยู่เบื้องหลัง; การตรวจสอบชื่อพื้นฐานที่ว่างเปล่า (empty-base-name check) จะข้ามพวกมันไปแทนที่จะเก็บบันทึกความว่างเปล่านั้นไว้ ชื่อพื้นฐานเป็นกุญแจสำคัญที่ถูกต้องสำหรับการขจัดข้อมูลที่ซ้ำซ้อนออกไปที่นี่ เนื่องจากคำนำหน้าของเซตย่อย (subset prefix) ที่มันพกมาด้วยจะช่วยแยกแยะฟอนต์ย่อยสองเซตของตระกูลเดียวกันออกจากกัน ซึ่งมักจะเป็นสิ่งที่คุณต้องการรู้
การดึงอักษรที่ฝังออกมา
เมื่อฟอนต์ถูกฝังอยู่คุณสามารถอ่านค่าไบต์ของมันได้โดยตรง FontData จะส่งคืนโปรแกรมฟอนต์ดิบ เป็นข้อมูล TrueType หรือ CFF ตัวเดียวกันกับที่ไฟล์ PDF ใช้นำพา ซึ่งเพียงพอที่จะเขียนไฟล์ฟอนต์แบบสแตนด์อะโลนออกมาหรือนำไปสร้างลายนิ้วมือเพื่อตรวจสอบเปรียบเทียบใบหน้าอักษร (face) กับไลบรารีที่คุณรู้จัก มันจะส่งคืนค่าอาร์เรย์ว่างเมื่อฟอนต์นั้นไม่ได้ถูกฝังไว้ ดังนั้นการทำงานตรวจเช็กการฝังและตรวจเช็กความยาวตัวอักษรร่วมกันจะช่วยป้องกันตัวไฟล์จากการเขียนทับได้
procedure SaveEmbeddedFont(Pdf: TPdf; CharIndex: Integer;
const OutputFile: string);
var
Data: TBytes;
Stream: TFileStream;
begin
if not Pdf.FontIsEmbedded[CharIndex] then
begin
ShowMessage('That glyph''s font is not embedded; nothing to extract.');
Exit;
end;
Data := Pdf.FontData[CharIndex];
if Length(Data) = 0 then
Exit;
Stream := TFileStream.Create(OutputFile, fmCreate);
try
Stream.WriteBuffer(Data[0], Length(Data));
finally
Stream.Free;
end;
ShowMessage('Wrote ' + IntToStr(Length(Data)) + ' bytes.');
end;
ค่าข้อมูลไบต์นี้เป็นเพียงแค่ส่วนของซับเซตที่ถูกฝังไว้ ไม่ใช่ตัวเต็มอย่างในฉบับที่ขาย ดังนั้นสิ่งที่คุณจะได้กลับมามักครอบคลุมเพียงกลีฟที่เอกสารนั้นใช้งานจริง ๆ เท่านั้น ซึ่งเป็นสิ่งที่เหมาะสมแล้วสำหรับงานด้านตรวจสอบวิเคราะห์นิติวิทยาศาสตร์และการยืนยัน แต่ไม่เหมาะสำหรับการนำไปใช้งานต่อ (reuse); ซับเซตของ Times New Roman ที่มีแค่สามสิบกลีฟไม่ใช่ฟอนต์ที่คุณจะนำไปติดตั้งและใช้พิมพ์ได้ ให้มองว่าการดึงออกมานี้เป็นวิธีตรวจสอบสิ่งที่ถูกส่งมา ไม่ใช่เครื่องมือสำหรับกู้คืนฟอนต์ หากคุณต้องการให้มีชื่อพื้นฐานเพื่อใส่เป็นป้ายกำกับตอนนำออก ให้อ่าน FontBaseName[CharIndex] พร้อมกับตัวข้อมูล และตัดคำนำหน้าแท็กของซับเซตออกหากคุณต้องการแค่ชื่อตระกูลแบบเปล่า ๆ
ทำความเข้าใจตัวเลขน้ำหนัก
FontWeight จะส่งค่าคลาสตัวเลขน้ำหนัก กลับมา ซึ่งก็คือสเกลเดียวกันตั้งแต่ 100-ถึง-900 ตามการใช้งานบน CSS โดยที่ 400 หมายถึงตัวปกติ (regular) และ 700 หมายถึงตัวหนา (bold) PDFium จะรายงานตามที่ฟอนต์นั้น ๆ ทำการประกาศมา ซึ่งอาจจะไม่ได้ลงท้ายร้อยถ้วนเสมอไป; ใบหน้าตัวอักษรอาจรายงานว่ามีค่า 350 หรือ 650 การจะนำไปใช้งานให้ครอบคลุมที่ค่าตั้งแต่ 600 เป็น "ค่าที่หนาพอให้ถูกใช้งาน" นั้นจะส่งผลลัพธ์ที่ดีกว่าการมานั่งตั้งค่าตรวจสอบให้ตรง 700 ถ้วน และค่าตัวมุมความเอียงจะเป็นตัวสัญญาณที่ตีคู่มาด้วย: ค่าที่ไม่ใช่ศูนย์ ซึ่งมักจะเป็นลบ หมายความว่าใบหน้านั้นเป็นดีไซน์แบบตัวเอียงลู่ลง (oblique) หรือเป็นตัวเอียงจริง (italic design) และค่าศูนย์หมายความว่าตัวตั้งตรง การรวมทั้งสองสิ่งจะทำให้คุณสามารถบอกกลุ่มข้อความที่เป็นแบบ ตัวหนา-เอียง (bold-italic run) แยกจากกลุ่มปกติได้โดยไม่ต้องเรนเดอร์อะไรออกมาเลย ซึ่งเป็นประเภทการเช็กระบบตรวจสอบไฟล์แบบพรีไฟลต์ (preflight pass) หรือการเข้าตรวจสอบแบบเข้าถึงระบบทั้งหมด (accessibility audit) ที่ชอบเรียกใช้ในการทำทีละมาก ๆ (in bulk)
ไม่มีการอ่านไฟล์แบบใดเลยที่ต้องพึ่งพาภาพบิตแมปที่ต้องเรนเดอร์ออกมา มันมาจากเลเยอร์ข้อความที่ถูกพาร์สแล้ว ดังนั้นการเปิดเอกสารที่หน้าเพจที่ถูกต้องก็เป็นสิ่งเดียวที่คุณต้องตั้งค่าเพื่อเตรียมพร้อม ซึ่งทำให้การตรวจสอบสแกนฟอนต์ประหยัดทรัพยากรสำหรับการไล่ตรวจรันผ่านทั้งตัวเก็บเอกสาร (archive) หากคุณนำสิ่งนี้ไปจับคู่กับการสกัดข้อความ ดัชนีตัวอักษรเดียวกันเหล่านี้ก็จะเรียงตรงกันกับตัวข้อความที่คุณดึงออกมา ดังนั้นฟอนต์ของกลีฟและค่า Unicode ของมันคือการอ่านค่าสองตัวด้วยดัชนีเดียว บทความที่เกี่ยวข้องเกี่ยวกับเรื่อง การแยกข้อความออกจากเอกสาร PDF ด้วย PDFium Component จะครอบคลุมในด้านรายละเอียดของฝั่งเลเยอร์ข้อความได้ลึกซึ้งยิ่งขึ้น
คุณสมบัติฟอนต์ที่แสดงไว้นี้เป็นส่วนหนึ่งของ PDFium Delphi VCL Component