مقاله فنی

structure treeهای tagged PDF در Delphi با PDF Library for Delphi

یک PDF قابل‌دسترس بر یک structure استوار است که page تصویری هرگز نشان نمی‌دهد: structure tree تعریف‌شده در ISO 32000-1 §14.7. این یک hierarchy منطقی از headingها، paragraphها، tableها و figureها است، لایه‌بندی‌شده روی content رنگ‌شده و نگاشت‌شده به roleهای standard از طریق یک role map. یک screen reader آن tree را می‌خواند، نه markهای روی page. بدون آن، یک invoice تولیدشده که بی‌نقص به‌نظر می‌رسد از نظر semantics خالی است، چون content stream تنها ترتیب drawing را ضبط می‌کند و هیچ چیز دیگر. total می‌تواند قبل از line itemها اعلام شود، footer می‌تواند به یک paragraph برود، items table می‌تواند به یک run تمایزناپذیر از wordها collapse شود. هزینهٔ جلوگیری از آن به نفع شما نامتوازن است. emit کردن structure حین drawing چند دقیقه code است؛ retrofit کردن آن در documentهای تمام‌شده یک project remediation است. losLab PDF Library (PDF Library for Delphi) آن tree را به Delphi و C++Builder از طریق یک set کوچک از callها expose می‌کند که هر operation drawing را در role منطقی‌اش wrap می‌کنند

چگونگی bind شدن marked content به structure tree

دو لایه همکاری می‌کنند. در content stream، operationهای drawing در sequenceهای marked-content bracket می‌شوند، که هر کدام یک MCID عدد صحیح حمل می‌کنند. در document catalog، structure tree آن MCIDها را به یک hierarchy از elementهای typed (H1، P، Table، Figure) با attributeهایی مانند alternate text و language نگاشت می‌کند. نوع‌های element سفارشی legal هستند، اما هر کدام باید از طریق role map به یک role standard resolve شود (ISO 32000-1 §14.8.4). contentای که اصلاً معنیی حمل نمی‌کند، مانند ruleها، backgroundها و page furniture تکرارشده، به‌عنوان artifact علامت‌گذاری می‌شود تا assistive technology به‌جای خواندن آن وسط جمله، skipاش کند

PDF Library for Delphi هر دو لایه را پشت یک جفت bracket نگه می‌دارد. BeginTag یک structure element را باز می‌کند و sequence marked-content را آغاز می‌کند، callهای drawing درون آن land می‌شوند، و EndTag هر دو را می‌بندد. bookkeepingای که tagging دست‌ساز را به‌هم می‌ریزد، MCIDها و parent tree و page referenceها، در داخل اتفاق می‌افتد آنجا که نمی‌توانید اشتباهشان کنید

نمودار PDF Library for Delphi که گذرهای marked content با MCID عددی را از طریق role map به درخت ساختار H1 و P و Figure می‌بندد، با artifactهایی که از ترتیب خواندن مستثنی‌اند
MCIDهای عددی رشته‌های محتوای نشان‌دار را به درخت ساختار نوع‌دار گره می‌زنند، در حالی که نقشه نقش، نقش‌های سفارشی را حل می‌کند و artifactها بیرون از ترتیب خواندن می‌مانند

دو switch در سطح document کار را قبل از باز شدن هر tagی قاب می‌کنند. SetMarkInfo flag در catalog را می‌نویسد که اعلام می‌کند document tagged است، و IsTaggedPDF آن را بازخوانی می‌کند، که اولین probe ارزان است هنگام تصمیم‌گیری اینکه آیا یک file inbound اصلاً structureای ارزش حفظ دارد. language دو entry point دارد. SetDocumentLanguage پیش‌فرض document را به‌تنهایی set می‌کند، در حالی که SetPDFUAMode آن را به‌عنوان بخشی از enable کردن output کامل PDF/UA set می‌کند. یک file می‌تواند به‌شکل مفید tagged باشد بدون ادعای conformance در PDF/UA، و یک rollout فازدار اغلب دقیقاً از آنجا شروع می‌شود

tagging حین drawing، نه بعداً

pattern generationای که کار می‌کند این است که bracket تگ را به‌عنوان بخشی از signature هر draw call در نظر بگیرید، هرگز به‌عنوان یک pass بعدی:

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);                          // مبدأ بالا-چپ
    Lib.SetPDFUAMode('en-US');                 // نسخهٔ save را به PDF 1.7 ارتقا می‌دهد
    Lib.SetInformation(1, 'Service Manual');   // /Title برای PDF/UA اجباری است
    Lib.AddRoleMap('ManualTitle', 'H1');       // نوع سفارشی -> role استاندارد
    Lib.AddStandardFont(4);
    Lib.SetTextSize(18);
    Lib.BeginTagEx2('ManualTitle', '', '', 'en-US', '', 'h1-cover', '');
    Lib.DrawText(72, 96, 'Service Manual');
    Lib.EndTag;
    Lib.BeginTag('Figure', 'Exploded view of the gearbox assembly', '');
    Lib.AddImageFromFile('gearbox.png', 0);
    Lib.EndTag;
    Lib.BeginArtifact('Layout');               // تزئین page: از خواندن مستثنی است
    // ... rule ها و tint پس‌زمینه را بکش ...
    Lib.EndArtifact;
    Lib.SaveToFile('manual.pdf');
  finally
    Lib.Free;
  end;
end;

سه call در آن sequence وزن compliance حمل می‌کنند. SetPDFUAMode output PDF/UA را enable می‌کند و بی‌صدا نسخهٔ document را به PDF 1.7 bump می‌کند، که با version pinning تصادف می‌کند. یک document locked به PDF 1.4 با LockSaveVersion یک‌بار UA mode active باشد از save کردن امتناع می‌کند و error code 602 برمی‌گرداند، تصادفی که تمایل دارد وقتی profileهای archival و requirementهای accessibility توسط تیم‌های متفاوتی پیکربندی می‌شوند ظاهر شود. SetInformation(1, ...) عنوان document را می‌نویسد، که ISO 14289 انتظار دارد viewerها به‌جای filename آن را نشان دهند؛ غیبتش یکی از رایج‌ترین findingهای PDF/UA در طبیعت است. AddRoleMap نوع سفارشی ManualTitle را به‌عنوان یک H1 ثبت می‌کند، و skip کردنش، diagnosticهای توضیح‌داده‌شده در زیر را روی یک role unmapped رها می‌کند

levelهای heading یک policy عمدی می‌طلبند، نه انتخاب‌های ad-hoc که برای look یک page ساخته می‌شوند. کاربران screen-reader با shortcut heading بین sectionها می‌پرند، پس یک template که از H1 به H3 می‌رود چون level میانی در design تصویری بیش‌ازحد بزرگ به‌نظر می‌رسید، بی‌صدا آن navigation را می‌شکند، و هیچ review تصویری هرگز آن را نمی‌گیرد. این دقیقاً defectای است که diagnostic HEADING-LEVEL-SKIP برای نام‌بردنش وجود دارد. styleهای تصویری هر template را یک‌بار، در یک جا، به یک ladder heading ثابت نگاشت کنید، و drift هرگز شروع نمی‌شود

tableهایی که یک screen reader واقعاً می‌تواند navigate کند

grid lineهای کشیده‌شده off screen هیچ معنیی ندارند. آنچه screen readerها navigate می‌کنند relationshipهای ساختاری هستند: کدام cellها header هستند، هر header چه را governance می‌کند، و data cellها چگونه در layoutهای نامنظم به headerها bind می‌شوند. callهای attribute structure-element هر سه را handle می‌کنند:

Lib.BeginTag('Table', '', '');
Lib.BeginTag('TR', '', '');
Lib.BeginTagEx2('TH', '', '', '', '', 'col-part', '');
Lib.SetStructElemScope('Column');          // فقط تا وقتی این TH باز است معتبر است
Lib.DrawText(72, 120, 'Part');
Lib.EndTag;
Lib.BeginTagEx2('TH', '', '', '', '', 'col-torque', '');
Lib.SetStructElemScope('Column');
Lib.SetStructElemColSpan(2);               // header ستون‌های مقدار و واحد را span می‌کند
Lib.DrawText(200, 120, 'Tightening torque');
Lib.EndTag;
Lib.EndTag;
Lib.BeginTag('TR', '', '');
Lib.BeginTag('TD', '', '');
Lib.SetStructElemHeaders('col-part');      // binding صریح برای tableهای نامنظم
Lib.DrawText(72, 140, 'M8 flange bolt');
Lib.EndTag;
Lib.EndTag;
Lib.EndTag; // Table

rule ترتیب‌دهی سخت و در سکوت enforce می‌شود. هر call SetStructElem* به tagای اعمال می‌شود که در آن لحظه باز است، بین BeginTag و EndTag آن، و وقتی هیچ tagای باز نیست یا attribute به tag فعلی اعمال نمی‌شود، بدون raise کردن چیزی ۰ برمی‌گرداند. یک call اشتباه‌جا ساده‌اند ناپدید می‌شود. wrap کردن return valueها در assertionها در طول development، drift را می‌گیرد وقتی هنوز می‌توانید ببینیدش؛ رهاش کنید، یک scope مفقود فقط وقتی ظاهر می‌شود که یک audit accessibility یک screen reader واقعی را روی table اجرا کند. IDهای element پاس‌شده از طریق BeginTagEx2، tree در ID (ISO 32000-1 §14.7.4) را تغذیه می‌کنند، و آن است که binding SetStructElemHeaders را در درجهٔ اول قابل‌resolve می‌کند

همان خانوادهٔ attribute بقیهٔ آنچه assistive technology به آن تکیه می‌کند را پوشش می‌دهد. SetStructElemListNumbering اعلام می‌کند itemهای list چگونه label می‌شوند، تا یک screen reader position درون list را به‌جای recite کردن bullet glyphها اعلام کند. SetStructElemBBox bounding box از figureها و tableها را ضبط می‌کند، که viewهای reflow برای قرار دادن content از آن استفاده می‌کنند. SetStructElemActualText text جایگزین برای runهایی که glyphهایشان به characterهای قابل‌خواندن نگاشت نمی‌شوند تأمین می‌کند، مانند یک drop cap از vector art ساخته‌شده. هر کدام از همان rule پیروی می‌کند: به tag باز bind می‌شود، یا ناپدید می‌شود

نمودار جدول PDF Library for Delphi: scope در TH، colspan دو و اتصال ویژگی headers به سلول‌های داده، در کنار قاعده‌ای که فراخوانی‌های ویژگی فقط تا باز بودن tag متناظر اتصال می‌دهند
صفحه‌خوان‌ها دامنه TH و colspan و اتصال‌های headers را دنبال می‌کنند نه خطوط کشیده‌شده را، و فراخوانی‌های ویژگی فقط وقتی به اتصال می‌رسند که تگ‌شان باز باشد

artifactها، language و gate diagnostics پیش از save

page furniture تکرارشده، یعنی running headerها، fold markها، watermarkها و tintهای background، داخل bracketهای BeginArtifact و EndArtifact متعلق است تا هرگز وارد reading stream نشود. language قابل ارث‌بردن است. پیش‌فرض document از argument SetPDFUAMode می‌آید، و یک run به زبانی دیگر آن را per element از طریق BeginTagEx یا SetStructElemLang override می‌کند. آن است که یک quotation فرانسوی داخل یک manual انگلیسی را قابل تلفظ نگه می‌دارد

پیش از save، GetPDFUADiagnostics checkهای ساختاری کتابخانه را روی document در-memory اجرا می‌کند و findingها به‌شکل text برمی‌گرداند، که در آن یک string خالی یعنی چیزی پیدا نشد. codeها مستقیماً classic authoring mistakeها را name می‌برند: FIGURE-NO-ALT برای یک image بدون alternate text، HEADING-LEVEL-SKIP برای یک H3 بعد از یک H1، ROLEMAP-UNMAPPED برای یک نوع سفارشی که هرگز ثبت نشده. این را در build سیم‌کشی کنید (set document را تولید کنید، step را در diagnostics غیرخالی fail کنید) و regressionهای accessibility به failureهای به‌سبک compile-time به‌جای findingهای audit ماه‌ها بعد تبدیل می‌شوند. verdict کامل conformance همچنان به preflight روی file ذخیره‌شده تعلق دارد، که در preflight در PDF/A و PDF/UA در Delphi پوشش داده شده، چون برخی normalizationها فقط در طول serialization اعمال می‌شوند

navigation annotation knob خودش را دارد. PDF/UA انتظار دارد traversal با keyboard از form fieldها و linkها از ترتیب structure پیروی کند، و SetTabOrderMode entry tab-order در سطح page را می‌نویسد که viewerها honor می‌کنند، با GetTabOrderMode برای audit fileهای inbound در دسترس. این از نوع requirementهایی است که هیچ‌کس تا وقتی یک کاربر فقط-keyboardی bug را ثبت نکند متوجه نمی‌شود، و یک call per document هزینه دارد درستش کنید

structure treeها از هر mergeای جان سالم به در نمی‌برند

documentهای tagged تنها وقتی tagged می‌مانند که هر step پردازش بعدی tree را حفظ کند، و لبهٔ تیز داخل PDF Library for Delphi خانوادهٔ merge-list است. MergeFileListFast حفظ structure-tree را بهspeed معامله می‌کند. این trade درست برای batchهای image اسکن‌شده و trade اشتباه برای reportهای tagged است، چون output خوب باز می‌شود، یکسان render می‌شود، و بی‌صدا لایهٔ accessibility خود را گم کرده. هرگاه هر inputای tagged است از MergeFileList پیش‌فرض یا variant strict استفاده کنید، و IsTaggedPDF را بخشی از assertionهای post-assembly کنید تا یک batch flatten‌شده نتواند بدون اینکه کسی متوجه شود ship شود. pipelineهای assembly برای setهای بزرگ document trade-offهای بیشتری از این نوع حمل می‌کنند، که در merge، split و direct access بزرگ PDF بررسی شده‌اند

نمودار PDF Library for Delphi از GetPDFUADiagnostics که رشته خالی یا یافته‌های نام‌دار مثل FIGURE-NO-ALT را برمی‌گرداند و build را پیش از آنکه preflight فایل ذخیره‌شده را بسنجد مردود می‌کند
GetPDFUADiagnostics یافته‌هایی مثل FIGURE-NO-ALT را پیش از ذخیره گزارش می‌کند، و نتیجه غیرخالی متصل‌شده به بیلد همان لحظه مرحله را fail می‌کند

حلقهٔ verify کردن بیرون کتابخانه بسته می‌شود: output را در Acrobat باز کنید، panel tagها را inspect کنید، و حداقل یک document per template family را با یک screen reader واقعی بخوانید. diagnosticها mistakeهای ساختاری را می‌گیرند؛ تنها یک گوش انسانی یک reading order را می‌گیرد که از نظر فنی valid و از نظر عملی گیج‌کننده است. buildهای evaluation و مرجع کامل tagging API در page محصول losLab PDF Library برای Delphi هستند