บทความเทคนิค

การอ่านและเขียน PDF Marked Content ใน Delphi

marked content คือกลไกที่ ISO 32000-1 §14.6 กำหนดสำหรับการติดแท็กเนื้อหาหน้า และทั้ง tagged PDF และ PDF/UA สร้างบนมัน PDFium Component เปิดเผยมันโดยตรง PageObjectMarks อ่านแท็ก BDC ทุกตัวและ property list ของมันจาก page object AddPageObjectMark เขียนหนึ่งตัว RemovePageObjectMark ลบหนึ่งตัว และ PageObjectMarkedContentID รายงาน MCID ที่เชื่อมเนื้อหาไปยัง structure tree

จนกว่า structure tree จะสามารถเชื่อมกลับไปยังเนื้อหาที่มันบรรยายได้ เครื่องมือการเข้าถึงจึงเป็นการเดา structure tree บอกว่า "นี่คือหัวเรื่อง" MCID บอกว่า mark ใดบนหน้าใดที่หัวเรื่องนั้นเป็นจริง ๆ ทั้งสองส่วนต้องอ่านได้ก่อนที่แอปพลิเคชันจะตรวจสอบ ซ่อมแซม หรือรายงานการติดแท็ก

mark ในไบต์คืออะไร

operator BDC พร้อมชื่อแท็กและ property list ที่เป็นทางเลือก ปิดด้วย EMC ใน content stream มันดูเหมือน /P <</MCID 3>> BDC ... EMC แท็ก /P ตั้งชื่อบทบาท dictionary พกคุณสมบัติ และทุกสิ่งระหว่าง operator คือ marked content page object ที่อยู่ในช่วงนั้นพก mark ซึ่งเป็นสิ่งที่ PDFium คืนและสิ่งที่ PDFium Component เปลี่ยนเป็น record

TPdfContentMark ถือ handle แท็ก Name และอาร์เรย์ของ TPdfContentMarkParam พารามิเตอร์แต่ละตัวมี Key, Kind และฟิลด์ค่าที่มีความหมายหนึ่งฟิลด์ที่เลือกโดย kind นั้น pmpInt, pmpFloat, pmpString หรือ pmpBlob kind มาจากรายงานประเภทของ PDFium เอง ไม่ใช่จาก getter ที่บังเอิญสำเร็จ ซึ่งเป็นความแตกต่างระหว่างการอ่าน property list กับการเดามัน

var
  Marks: TPdfContentMarks;
  M: TPdfContentMark;
  P: TPdfContentMarkParam;
  I: Integer;
begin
  Pdf.PageNumber := 1;                    // PageNumber is 1-based
  for I := 0 to Pdf.ObjectCount - 1 do    // page object indexes are 0-based
  begin
    Marks := Pdf.PageObjectMarks(I);
    for M in Marks do
    begin
      Memo1.Lines.Add('mark ' + M.Name +
        ' (MCID ' + IntToStr(Pdf.PageObjectMarkedContentID(I)) + ')');
      for P in M.Params do
        case P.Kind of
          pmpInt:    Memo1.Lines.Add('  ' + P.Key + ' = ' + IntToStr(P.IntValue));
          pmpString: Memo1.Lines.Add('  ' + P.Key + ' = ' + P.StringValue);
          pmpFloat:  Memo1.Lines.Add('  ' + P.Key + ' = ' + FloatToStr(P.FloatValue));
          pmpBlob:   Memo1.Lines.Add('  ' + P.Key + ' = ' +
                       IntToStr(Length(P.BlobValue)) + ' bytes');
        end;
    end;
  end;
end;

ทำไม pmpUnknown จึงหมายถึงสองสิ่งที่ต่างกัน

pmpUnknown ถูกคืนเมื่อ PDFium รายงาน FPDF_OBJECT_UNKNOWN และ PDFium ยังคืนมันสำหรับคีย์ที่ไม่มีอยู่ ทั้งสองกรณีแยกไม่ออกที่ชั้นนี้ และการแกล้งทำเป็นอย่างอื่นจะแย่ไปกว่าการบอกตรง ๆ

ผลลัพธ์เชิงปฏิบัติต่อโค้ดของคุณ คือปฏิบัติต่อ pmpUnknown เป็น "ไม่มีค่าที่ใช้ได้ที่นี่" แทนที่จะเป็นประเภทที่คุณอาจถอดรหัสได้อยู่ดี หากคุณสมบัติหนึ่งสำคัญต่อเวิร์กโฟลว์ของคุณ ให้ตรวจสอบว่ามันอยู่ด้วย kind ที่คุณรู้จัก และอย่าอนุมานการไม่อยู่จาก unknown — mark ที่ property list ของมันคุณอ่านไม่ได้คือ mark ที่คุณควรรายงาน ไม่ใช่ mark ที่คุณควรยอมรับอย่างเงียบ ๆ

record ของ mark คือ snapshot ไม่ใช่ handle ที่คุณเป็นเจ้าของ

ฟิลด์ Handle เป็นของไลบรารี มันเสียทันทีที่ mark ถูกเอาออก page object ถูกทำลาย หรือหน้าถูกยกเลิกโหลด record จึงเป็น snapshot แบบอ่านอย่างเดียวที่มีอายุสั้น แคชมันข้ามการสลับหน้าแล้วคุณกำลังถือพอยน์เตอร์เข้าไปในหน่วยความจำที่เอนจินคืนไปแล้ว

นี่คือวินัยเดียวกันที่ใช้กับ page object handle โดยทั่วไปใน PDFium และจับคนในจุดเดียวกัน ตัวควบคุมรายการที่เติมด้วย mark record ผู้ใช้นำทางไปยังหน้าอื่น และการล้มเหลวที่ดูไม่เกี่ยวกับการนำทาง คัดลอกค่าที่คุณต้องการออกมา — ชื่อ คีย์ ตัวเลข — แล้วปล่อย handle ไป บันทึกของ page object handle ที่เสียไปหลังการแปลง ครอบคลุมกฎทั่วไปและวิธีที่มันกัดในที่อื่น

การเพิ่ม mark และขั้นตอนบันทึกที่ง่ายต่อการพลาด

AddPageObjectMark รับ page object index ชื่อแท็ก และเซ็ตพารามิเตอร์ที่สมบูรณ์ พารามิเตอร์ถูกเขียนเป็นเซ็ตแทนที่จะแก้ทีละคีย์ ซึ่งเป็นเหตุผลว่าทำไม TPdfContentMarkParam ไม่มี sentinel Has* — กรณี "อัปเดตหนึ่งฟิลด์ของ record ที่มีอยู่" ที่สิ่งเหล่านั้นจะป้องกันจึงไม่เกิดขึ้น

ส่วนที่ควรกล่าวให้ชัดเจน การเพิ่ม mark สร้าง page content stream ใหม่เพื่อให้แท็กรอดผ่านการบันทึก สิ่งนี้ต้องทำให้ชัดเพราะ SaveAs ไม่สร้างเนื้อหาใหม่ด้วยตัวมันเอง — การเปลี่ยนแปลงที่อยู่ใน object model เพียงอย่างเดียวจะถูกทิ้ง และไฟล์ที่บันทึกจะดูเหมือนต้นฉบับที่คุณเริ่มต้นพอดี หากคุณเคยเพิ่มบางสิ่งลงในหน้า PDFium แล้วพบว่ามันหายไปจากเอาต์พุต นี่มักเป็นเหตุผล

var
  Params: TPdfContentMarkParams;
begin
  SetLength(Params, 1);
  Params[0].Key := 'MCID';
  Params[0].Kind := pmpInt;
  Params[0].IntValue := NextMcid;
  Pdf.AddPageObjectMark(ObjectIndex, 'P', Params);   // rebuilds the content stream
  Pdf.UpdatePage;
  Pdf.SaveAs('tagged-out.pdf');
end;

สิ่งที่นี่ทำและไม่ทำให้เอกสารเป็น

mark เพียงอย่างเดียวไม่ทำให้ tagged PDF เอกสาร tagged ที่สอดคล้องต้องการ structure tree ที่องค์ประกอบอ้าง MCID เหล่านี้ entry /MarkInfo ที่ประกาศเอกสารเป็น marked และชื่อบทบาทที่หมายถึงสิ่งที่มาตรฐานบอก การเขียน mark /P พร้อม MCID ที่ไม่มีองค์ประกอบโครงสร้างใดชี้ไป ให้เนื้อหาที่อ้างว่า tagged และ structure tree ที่ไม่เคยพูดถึงมัน

จุดที่ marked content สมควรได้รับคุณค่าจริง ๆ ที่ระดับนี้คือการตรวจสอบและซ่อมแซม การตรวจสอบว่า page object ใดถูกติดแท็ก การค้นหา artifact ที่ควรถูกทำเครื่องหมายเช่นนั้น หรือการจับคู่ MCID กับ structure tree เพื่อหาเด็กกำพร้า สำหรับครึ่ง structure tree ของงานนั้น ดูคู่มือ การตรวจสอบ structure tree ของ PDF/UA และสำหรับประสบการณ์การอ่านที่แท็กตั้งใจให้เป็นในท้ายที่สุด บันทึกของการสร้าง ตัวอ่าน PDF ที่เข้าถึงได้ใน Delphi

PDFium Component ให้แอปพลิเคชัน Delphi, C++Builder และ Lazarus API VCL ระดับสูงเหนือเอนจิน PDFium โดยที่ marked content structure tree และการตรวจสอบการเข้าถึงเข้าถึงได้จากโค้ด Pascal ธรรมดา — ดู หน้าผลิตภัณฑ์ PDFium Component สำหรับพื้นผิว API ฉบับสมบูรณ์