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

preflight เวอร์ชัน PDF ใน Delphi: กฎ Measure RL กับ GEO

PDFlibPas (PDF Library for Delphi) เช็กทุก object เทียบกับตารางกฎเวอร์ชัน PDF ก่อนเขียนไฟล์ และไม่นานมานี้ preflight เวอร์ชัน PDF ตัวนั้นยังเผลอเอา dictionary วัดขนาดของ CAD ธรรมดาไปเข้าใจว่าเป็นแบบ geospatial ภาพ CAD หน้าเดียวโหลดผ่านสบาย ๆ แล้ว SaveToFile คืน 0 พร้อม LastErrorCode 602 และทวงว่าต้องมี 1.7 ExtensionLevel 3 กฎที่แก้แล้วถือว่า dictionary /Measure แบบ rectilinear (/Subtype /RL) เป็น PDF 1.6 ธรรมดา และสงวนประตู extension ไว้กับสัญญาณ geospatial ที่แท้จริง

ไฟล์นี้เข้ามาผ่านการรับของ corpus: หนึ่งหน้า หนึ่ง optional-content group สอง viewport วัดขนาดแบบ rectilinear ชนิด output ที่แพ็กเกจ CAD สถาปัตยกรรมเขียนออกมาเพื่อให้ viewer อ่านระยะจากแปลนบ้านได้ ไม่มีอะไรในมันแปลกใหม่เลย ซึ่งเป็นเหตุผลพอดีว่าทำไมการปฏิเสธถึงน่าสนใจ preflight ที่ปิดกั้นไฟล์ที่ถูกต้องแย่กว่าตัวที่ช้า เพราะ caller ได้รับ diagnostic ที่ดูมีอำนาจชี้ไปที่ฟีเจอร์ที่เอกสารไม่ได้มี การแก้มีสองส่วน: การอ่านสเปกที่อยู่หลังกฎหนึ่งข้อ และการตระหนักว่ากฎนั้นแยก dictionary สองชนิดไม่ออกอยู่แล้วที่ระดับที่มันมอง

preflight เวอร์ชันตอนเซฟของ PDFlibPas ทำงานอย่างไร

ประตูตอนเซฟคือ PrepareAndCheckSaveVersion เทียบทุก indirect object กับ PDFFeatureRules และพังที่กฎแรกที่ทั้งตรงและต้องการมากกว่าที่ target ยอม target คือเวอร์ชันเอกสาร (หรือเวอร์ชันที่ล็อกไว้ด้วย LockSaveVersion) บวกกับ extension level ของ Adobe ที่ประกาศใต้ /Extensions /ADBE record TPDFFeatureRule แต่ละตัวแบก MinVersion, MinExtensionLevel, MatchKind อย่าง fmkDictKey หรือ fmkDictSubtype, สตริง Match, ชื่อ Feature ที่มนุษย์อ่านรู้เรื่องและ callback ที่ใส่ก็ได้ AddRule ลงทะเบียนกฎเวอร์ชันธรรมดา AddExtensionRule ล็อก MinVersion ไว้ที่ 17 เสมอแล้วเพิ่ม extension level ทับ กฎแบบ extension จึงพอใจได้ด้วย PDF 1.7 บวก entry /Extensions ที่ถูกต้องเท่านั้น เมื่อประตูกระแทก เวอร์ชันที่ต้องการกับชื่อฟีเจอร์ถูกเก็บไว้ให้ caller และคีย์ 311, 312 กับ 313 ของ GetInformation เปิดให้ดู

preflight เวอร์ชันตอนเซฟใน PDFlibPas: PrepareAndCheckSaveVersion เทียบทุก object กับ record PDFFeatureRules ที่แบก MinVersion, extension level และชนิดการจับคู่ AddExtensionRule ล็อกข้อกำหนดไว้ที่ PDF 1.7 บวก extension level และการจับคู่แรกที่ target พอใจไม่ได้จะหยุดการเซฟด้วย error 602
คีย์ 311, 312 กับ 313 ของ GetInformation เปลี่ยนการปฏิเสธให้เป็นการวินิจฉัย รายงานเวอร์ชันที่ต้องการ ฟีเจอร์ที่กระตุ้นมัน และ target ที่ถูกล็อก caller จึงแก้ไฟล์หรือแก้กฎได้แทนที่จะเดา
var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('floor-plan.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    if Pdf.SaveToFile('floor-plan-out.pdf') <> 1 then
      if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
        // 311: เวอร์ชันที่ต้องการ, 312: ฟีเจอร์ที่กระตุ้นมัน,
        // 313: เวอร์ชันที่ save target ถูกล็อกอยู่ ('' เมื่อปลดล็อก)
        Writeln('Needs ', Pdf.GetInformation(311),
          ' for ', Pdf.GetInformation(312),
          ', locked at [', Pdf.GetInformation(313), ']');
  finally
    Pdf.Free;
  end;
end;

ทำไมภาพ CAD ธรรมดาพังด้วย error 602

ตารางกฎมี AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil) อยู่ ซึ่งยิงกับ dictionary ใดก็ตามที่แค่มีคีย์ /Measure และ viewport วัดขนาดทุกตัวก็มี array /VP ของหน้าถือ viewport dictionary ไว้ แต่ละ viewport ชี้ไปที่ measure dictionary ของตัวเองผ่าน /Measure และการจับคู่แบบเช็กว่ามีคีย์หยุดตรงนั้นโดยไม่สนว่า measure dictionary นั้นจริง ๆ คืออะไร การสแกนฟีเจอร์ตอนโหลดเลยพาเลขเวอร์ชันเอกสารขึ้นไปเป็น 1.7 ได้ แต่มันไม่เคยเขียนประกาศ /Extensions แทนไฟล์ input เลย ประตูเซฟจึงเห็น PDF 1.7 ที่ extension level 0 แล้วรายงาน 1.7 ExtensionLevel 3 การปฏิเสธที่จะมุมประกาศ extension ขึ้นมาเองนี้ตั้งใจมา: library ไม่เงียบ ๆ ยกไฟล์ input ขึ้นเพื่อปูเรื่องกฎที่ผิด

สเปกพูดเรื่องเคส rectilinear ไว้ชัดเจน Measure dictionary เข้ามาใน PDF 1.6 และ ISO 32000-1 §12.9 ให้ /Subtype default เป็น RL ระบบพิกัด rectilinear ที่อธิบายด้วยชุด entry ของตัวเอง: อัตราส่วนสเกล รูปแบบตัวเลข X กับ Y ระยะทางและพื้นที่ ส่วนการวัดเชิง geospatial เป็นของเพิ่มทีหลังจาก Adobe Extension Level 3 บนพื้น PDF 1.7 ระบุด้วย /Subtype /GEO และแบก array จุดภูมิศาสตร์, dictionary ระบบพิกัดและหน่วยการแสดงผล ซึ่งเป็นโครงสร้างที่เดินไว้แล้วในการอ่าน viewport ของ GeoPDF กับ array GPTS และ LPTS ใน Delphi dictionary ทั้งสองแขวนกับคีย์ /Measure เดียวกัน กฎใดที่หยุดที่คีย์จึงถูกไม่ได้กับทั้งคู่ ข้อมูลที่แยกของมันอยู่ลงไปหนึ่งระดับ ในตัว measure dictionary เอง

คีย์ /Measure เดียว สอง dictionary ใน PDFlibPas: การวัดแบบ rectilinear ที่มี /RL หรือละ subtype ไว้ ต้องการแค่ PDF 1.6 ขณะที่ dictionary แบบ geospatial ต้องการ 1.7 ExtensionLevel 3 CB_GeospatialDictionary จึงตัดสินจากเนื้อหา ตรงที่กฎแบบคีย์เดิมแยกไม่ออก
preflight ที่ปิดกั้นไฟล์ที่ถูกต้องแย่กว่าตัวที่ช้า เพราะ caller ได้รับ diagnostic ที่ดูมีอำนาจเรื่องฟีเจอร์ที่เอกสารไม่เคยมี การเช็กที่แยกของจึงถูกย้ายลงไปหนึ่งระดับ

กฎชุดที่แก้แล้วยังบังคับอะไรอยู่

การแก้ลบกฎแบบคีย์ที่ไม่มีเงื่อนไขออกแล้วเหลือประตูที่อธิบายข้อกำหนดเวอร์ชันที่แท้จริง หน้าที่แบก /VP หรือ /UserUnit ยังต้องการ PDF 1.6 ผ่าน CB_PagePDF16Entries คีย์ /PtData ยังต้องการ extension level 3 และ CB_GeospatialDictionary ตัดสินว่า measure dictionary เป็น geospatial ไหมจากเนื้อหา ไม่ใช่จากคีย์ที่พามันมา

// ลบแล้ว: dictionary ทุกตัวที่มีคีย์ /Measure ถูกนับเป็น geospatial
// AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil);

AddRule(16, fmkCustom, '', 'Page PDF 1.6 entry /UserUnit /VP', CB_PagePDF16Entries);
AddExtensionRule(3, fmkDictKey, 'PtData', '/PtData geospatial dictionary', Nil);
AddExtensionRule(3, fmkCustom, '', 'geospatial measure dictionary', CB_GeospatialDictionary);

function CB_GeospatialDictionary(Obj: TPDFObject; const Ctx: TPDFRuleContext): Boolean;
var
  Dict: TPDFDictionary;
begin
  Result := False;
  if not (Obj is TPDFDictionary) then
    Exit;
  Dict := TPDFDictionary(Obj);
  Result := (Dict.StringValue('Subtype') = 'GEO') or
    (Dict.FindIndexByKeyName('GCS') >= 0) or (Dict.FindIndexByKeyName('DCS') >= 0) or
    (Dict.FindIndexByKeyName('GPTS') >= 0) or (Dict.FindIndexByKeyName('LPTS') >= 0) or
    (Dict.FindIndexByKeyName('PDU') >= 0);
end;

regression ร่วมของ Delphi กับ FPC ตอกหมุดเขตแดนนั้นจากสองฝั่ง viewport ที่ measure dictionary ละ /Subtype ไว้กับตัวที่สะกด /RL ออกมาชัด ๆ ผ่านที่ PDF 1.6 ทั้งคู่ หน้าเดิมยังถูกปฏิเสธที่ PDF 1.5 และการตรวจจับฟีเจอร์ไม่รายงาน extension ให้มันอีกต่อไป ใส่ array /GPTS เข้าไปคำตัดสินพลิกกลับเป็น 1.7 ExtensionLevel 3 ซึ่งผ่านทันทีที่ประกาศ extension level และ dictionary เปล่า ๆ ที่มีแค่ /Subtype /GEO ถูกปฏิเสธเมื่อไม่มีมัน callback นี้อนุรักษ์นิยมโดยตั้งใจ: dictionary rectilinear ที่แถมคีย์เร่ร่อน /GCS หรือ /PDU มาด้วยถูกถือเป็น geospatial เพราะคีย์พวกนั้นไร้ความหมายในโมเดล RL

LockSaveVersion คือจุดที่การเปลี่ยนนี้มองเห็นได้จากฝั่ง caller TPDFlib.LockSaveVersion รับ '1.0' ถึง '1.7' คืน 0 กับอย่างอื่น ล็อกเวอร์ชันเอกสารไว้ และห้าม call ฝั่ง writer ยกมันขึ้นเงียบ ๆ แต่ประตูเซฟยังรันเทียบกับค่าที่ล็อกไว้ เมื่อใช้กฎที่แก้แล้ว ไฟล์ CAD ที่ล็อกไว้ที่ 1.6 เซฟได้สะอาด GeoPDF แท้ที่ล็อกไว้ที่ 1.6 ยังได้ 602 ซึ่งเป็นคำตอบที่ถูก และ call กลุ่มเขียน geospatial อย่าง SetMeasureDictCoordinateSystem ประกาศ extension level 3 ให้เองเมื่อคุณสร้างเนื้อหาพวกนั้นผ่าน API

if Pdf.LockSaveVersion('1.6') <> 1 then
  raise Exception.Create('unsupported version string');
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
begin
  if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
    // เนื้อหาจริงที่สูงกว่า 1.6 เช่น measure dictionary แบบ GEO
    raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
      [string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;

ทำไมการ scan กฎเวอร์ชันช้ากว่าที่ควร

การ scan คัดลอก TPDFFeatureRule ทุกตัวลง record ท้องถิ่นก่อนทดสอบ และเพราะ record ถือฟิลด์ AnsiString สองตัว การคัดลอกแต่ละครั้งปรับ reference count สองครั้งและปล่อยค่าเดิม preflight เยี่ยมทุก node ของ object tree ทุกต้น สเกลาร์รวมอยู่ด้วย ต้นทุนจึงถูกคูณจำนวน object ด้วยจำนวนกฎ และกฎที่แม้ไม่เกี่ยวกับเวอร์ชัน target ก็ถูกคัดลอกก่อนแล้วค่อยข้าม เมื่อ PDFFeatureRules ถูกเติมครั้งเดียวตอน unit initialization และถือว่า read-only v3.539.17 จึงส่ง entry จากตารางตรงเข้า MatchSingleRule กับ RuleExceedsTarget ที่พารามิเตอร์ const Rule รับ reference โดยไม่แตะสตริง

การเร่ง scan กฎใน PDFlibPas: preflight เดิมคัดลอก record TPDFFeatureRule ทุกตัวก่อนทดสอบ ปรับ reference count ของ AnsiString กับทุก object ที่เยี่ยม ขณะที่พารามิเตอร์ const อ่านตาราง read-only ในที่ตอนนี้ ตัด median ของรอบจับคู่กฎจาก 0.711 วินาทีเหลือ 0.203 วินาที
กำไรนี้จริงแต่แคบ: การเซฟเต็มยังต้องจ่ายค่า deferred feature detection, การ decode object และ serialization ratio ที่วัดได้จึงเป็นของเส้นทางจับคู่กฎ ไม่ใช่ของเวลาเซฟรวม
// ก่อน: คัดลอก record แบบ managed ต่อกฎ ต่อ object ที่เยี่ยม
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
  Continue;

// หลัง: พารามิเตอร์ const อ่าน entry ของตาราง immutable ในที่
if not RuleExceedsTarget(PDFFeatureRules[X], TargetVersion, TargetExtensionLevel) then
  Continue;
if MatchSingleRule(Obj, Ctx, PDFFeatureRules[X]) then
begin
  RequiredVersion := RequiredVersionString(PDFFeatureRules[X]);
  FeatureName := PDFFeatureRules[X].Feature;
  Result := False;
  Exit;
end;

ผลที่วัดได้แคบและควรอ้างอิงในแง่นั้น benchmark เช็ก array ของ object ตัวเลข 20,000 ตัวเทียบกับ target PDF 1.4 สิบรอบต่อหนึ่งรอบใหญ่ build ด้วย FPC Win64 ที่ -O2 median ของห้ารอบตกจาก 0.711 วินาทีเหลือ 0.203 วินาที และการรันสองบิลด์สลับลำดับกันได้ 0.459 วินาทีเทียบกับ 0.150 วินาที คือกำไรประมาณสามเท่าบนเส้นทางจับคู่กฎเพียงอย่างเดียว การเซฟจริงยังต้องจ่ายค่า deferred feature detection, การ decode object และ serialization ratio จึงย้ายไปใช้กับเวลาเซฟรวมไม่ได้ ลำดับกฎ, callback, เพดานเวอร์ชันและ diagnostic ของความล้มเหลวแรกเหมือนเดิมทุกอย่าง และไม่มีกฎไหนถูกแคชข้ามการเซฟหรือถูกข้ามเพื่อให้ได้ตัวเลขนี้

ควรเช็กอะไรเมื่อ PDF ที่โหลดมาพังที่ preflight เวอร์ชัน

อ่านคีย์ 311 กับ 312 ก่อนแตะเวอร์ชัน ถ้าฟีเจอร์ที่รายงานเป็น dictionary แบบ geospatial แต่ไฟล์วาดแต่การวัด rectilinear นั่นคือ false positive ตัวนี้ และบิลด์ปัจจุบันเซฟไฟล์เดิมได้ ถ้าฟีเจอร์เป็นของจริง จะประกาศ extension หรือล็อกไปที่เวอร์ชันที่บรรจุเนื้อหาอย่างตรงไปตรงมาก็ได้ การยกเวอร์ชันเพียงเพื่อหุบประตูซ่อนคำถามว่าผู้บริโภคปลายน้ำอ่านสิ่งที่คุณส่งออกได้ไหม หลักการเดียวกันของการเช็กที่มีขอบเขตและอาศัยหลักฐานก็เป็นแกนของpreflight โหมดผู้เขียนของ PDF/E-1 สำหรับเอกสารวิศวกรรม ที่ภาพ CAD ต้องเจอมาตรฐาน conformance ไม่ใช่แค่เลขเวอร์ชัน

การเช็กความสอดคล้องเวอร์ชัน, dictionary การวัดขนาดกับ geospatial และการล็อกเวอร์ชันการเซฟ ทั้งหมดเป็นส่วนหนึ่งของ PDF Library for Delphi, toolkit PDFlibPas สำหรับนักพัฒนา Delphi, C++Builder และ Lazarus