PDFium Component لایههای optional content در PDF (OCG) را در Delphi از طریق دو متد TPdf کنترل میکند: InspectOptionalContent هر لایه را همراه وضعیت دیداریای که PDFium واقعاً رندر میکند فهرست میکند، و SaveAsOptionalContentConfigured یک کپی راستیآزماییشده مینویسد که در آن لایههایی که انتخاب میکنید روشن یا خاموش شدهاند. متد دوم قواعد Usage و /AS را هم خنثی میکند که وگرنه بیسروصدا ویرایش شما را خنثی میکردند. هر دو روی سندی که از قبل در TPdf باز است کار میکنند، پس پارسر دومی وجود ندارد که لازم باشد با چیزی که viewer نشان میدهد sync بماند
درخواست معمولاً از یک شرکت CAD یا GIS میرسد: مجموعه نقشهها با ابعاد و حاشیهنویسیها و بلوک عنوان روی لایههای جدا ارسال میشود، و مشتری قبل از رفتنش به تأمینکننده یک کپی با ابعاد مخفی میخواهد. PDFium محتوای اختیاری را درست رندر میکند، ولی ABI عمومیاش هیچ تابعی برای فهرست کردن OCGها یا انتخاب یک configuration یا فلیپ کردن وضعیت یک لایه ندارد. پس به سطح آبجکت میروید، /OCProperties را ویرایش میکنید، ذخیره میکنید، دوباره لود میکنید، و لایه هنوز سر جایش است. دلیلش منطق دیداری PDFium است، و ارزش دارد قبل از دست زدن به هر بایتی بفهمیدش
چرا ویرایش /ON و /OFF چیزی را که PDFium رندر میکند عوض نمیکند؟
ویرایش آرایههای /ON و /OFF دیکشنری configuration کافی نیست، چون PDFium میگذارد وضعیت صریحی که داخل دیکشنری /Usage مال خود OCG است بر آن آرایهها پیروز شود، و یک قاعدهٔ auto-state از نوع /AS بعد میتواند هر دو را override کند. ISO 32000-1 §8.11.4 configurationها و usage dictionaryها را دو مکانیسم جدا توصیف میکند؛ رندرکنندهٔ PDFium آنها را در یک تصمیم واحد میپیچد، و InspectOptionalContent آن را به این ترتیب بازتولید میکند:
- از
/BaseStateمربوط به configuration شروع کنید، جایی که/ONو/Unchangedهر دو دیدنی حساب میشوند و فقط/OFFپنهان میکند - آرایهٔ
/ONمربوط به configuration را اعمال کنید، بعد آرایهٔ/OFFآن را، پس گروهی که در هر دو لیست شده در نهایت پنهان میشود - وضعیت Usage صریح گروه را برای usage درخواستی اعمال کنید، مثل
/Usage << /View << /ViewState /OFF >> >>که بر همهٔ بالاییها اولویت دارد - گروهی که
/Intentآن نه/Viewدارد و نه/Allرا دیدنی بگیرید، چون در دیداریبودن با view-intent سهمی ندارد - در آخر آرایهٔ
/ASمربوط به configuration انتخابشده را اجرا کنید که مدخلهایش برای رخداد مچشده وضعیت گروههایی که لیست کردهاند را ست میکند
مرحلهٔ سوم همان است که آدمها را میسوزاند. فایلی که یک ابزار page layout ذخیره کرده اغلب روی هر OCG یک /ViewState /ON دارد، و PDFium بعد آرایهٔ /OFF دقیق ویرایششدهٔ شما را نادیده میگیرد: ذخیره موفق است، فایل تمیز دوباره باز میشود، و لایه هنوز رنگ میشود. برای Print و Export، OcExplicitUsageState اول PrintState یا ExportState را میخواند و وقتی مدخل مخصوص غایب است به ViewState برمیگردد، پس یک ViewState /ON تنها لایه را برای چاپ هم پین میکند. marked contentی که به یک OCMD ارجاع میدهد (§8.11.2.2) بعد بر اساس این نتایج به-ازای-هر-گروه حل میشود، از طریق سیاست /P یا در صورت وجود عبارت دیداری /VE
چطور لایههایی را که PDFium واقعاً نشان میدهد فهرست کنید؟
TPdf.InspectOptionalContent یک TPdfOptionalContentInventory برمیگرداند که آرایهٔ Groups آن شمارهٔ آبجکت و نام و intentها و سه وضعیت Usage و زبان و بازهٔ zoom و پرچم Locked و اندیس radio-group و EffectiveVisible محاسبهشدهٔ هر OCG را حمل میکند. متد اول به PDFium میگوید سند درون-حافظهٔ فعلی را ذخیره کند، object streamها را باز میکند و نتیجه را اسکن میکند، پس ویرایشهایی که زودتر در همین session انجام شدهاند منعکس میشوند. اندیس configuration با 0 همیشه دیکشنری پیشفرض /D است و مدخلهای /Configs از اندیس 1 دنبال میشوند؛ آرگومان پیشفرض یعنی -1 اندیس 0 را برمیدارد. سندی بدون /OCProperties باعث میشود متد False برگرداند با دلیلش در ErrorMessage، بهجای استثنا دادن
procedure TFormMain.ListLayers;
var
Inv: TPdfOptionalContentInventory;
G: TPdfOptionalContentGroup;
begin
// Usage پیشفرضش ocuView است؛ -1 یعنی configuration 0، همان دیکشنری /D
if not Pdf.InspectOptionalContent(Inv) then
begin
Memo1.Lines.Add('No usable layers: ' + Inv.ErrorMessage);
Exit;
end;
Memo1.Lines.Add(Format('Configuration %d: %s',
[Inv.SelectedConfigurationIndex,
string(Inv.Configurations[Inv.SelectedConfigurationIndex].Name)]));
for G in Inv.Groups do
Memo1.Lines.Add(Format('obj %d %s visible=%s locked=%s radio=%d',
[G.ObjectNumber, string(G.Name),
BoolToStr(G.EffectiveVisible, True),
BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;
آرایهٔ Memberships هر OCMD را با Policy آن (ocmpAnyOn و ocmpAllOn و ocmpAnyOff و ocmpAllOff) و متن خام VisibilityExpression و EffectiveVisible خودش گزارش میکند. چند قاعدهٔ لبه عامدانهاند. /P پیشفرضش /AnyOn است، و OCMDی بدون گروه دیدنی حساب میشود. ارجاعی به شمارهٔ آبجکتی که OCG شناختهشدهای نیست دیدنی گرفته میشود بهجای اینکه کل عبارت را بیندازد. ارزیابی /VE در عمق تودرتویی 32 متوقف میشود و هر چه عمیقتر باشد پنهان گرفته میشود، تا یک عبارت خصمانه یا خود-ارجاع بررسی را به stack overflow نکشاند
نوشتن وضعیت جدید لایه با SaveAsOptionalContentConfigured
TPdf.SaveAsOptionalContentConfigured آرایهای از رکوردهای TPdfOptionalContentStateChange میگیرد (شمارهٔ آبجکت گروه بهعلاوهٔ Visible) و سندی مینویسد که configuration انتخابشده دقیقاً همان وضعیت را تولید میکند. configuration انتخابشده /BaseState /ON میگیرد بهعلاوهٔ آرایههای کامل /ON و /OFF که همهٔ گروهها را پوشش میدهند، و هر OCGی که از قبل دیکشنری Usage دارد یک ViewState صریح (یا PrintState / ExportState، پیرو Options.Usage) میگیرد که با وضعیت جدیدش جور است. با TPdfOptionalContentConfigureOptions.Default کلید /AS مربوط به configuration انتخابشده حذف میشود تا یک رخداد open یا print یا export نتواند لایهها را برگرداند
procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer);
var
Changes: TPdfOptionalContentStateChanges;
Options: TPdfOptionalContentConfigureOptions;
Report: TPdfOptionalContentConfigureReport;
begin
SetLength(Changes, 2);
Changes[0].GroupObjectNumber := DimensionsObj;
Changes[0].Visible := False;
Changes[1].GroupObjectNumber := NotesObj;
Changes[1].Visible := True;
// configuration 0، ocuView، DisableAutomaticState و EnforceRadioGroups روی True
Options := TPdfOptionalContentConfigureOptions.Default;
if not Pdf.SaveAsOptionalContentConfigured('C:\Out\Drawing-NoDims.pdf',
Changes, Options, Report) then
raise Exception.Create('Layer update rejected: ' + Report.ErrorMessage);
Log(Format('%d of %d groups changed, %d Usage states rewritten, /AS removed: %s',
[Report.ChangedGroupCount, Report.GroupCount,
Report.UpdatedUsageStateCount,
BoolToStr(Report.RemovedAutomaticState, True)]));
end;
مسیر نوشتن خروجی ذخیرهٔ خود PDFium را بهعنوان پیشوند بایتبه-بایت نگه میدارد و فقط مالک configuration بازنویسیشده و آبجکتهای OCGی که دیکشنری Usage دارند را append میکند، با دنبال کردن یک سکشن xref و trailer جدید. قبل از اینکه حتی یک بایت به مقصد شما برسد، نتیجه در یک TPdf جداگانه زیر سیاست لود سختگیرانه دوباره باز میشود، و متد شکست میخورد اگر جدول cross-reference راستیآزمایی نشود. overload فایلی یک قدم جلوتر میرود: در فایلی موقت کنار مقصد مینویسد و مقصد را فقط بعد از موفقیت راستیآزمایی جایگزین میکند، پس یک آپدیت ردشده هرگز نیمی از یک نقشهٔ نوشتهشده جا نمیگذارد. همین رویکرد revision افزایشی راستیآزماییشدهای است که ویرایشگر name tree و number tree در PDFium Component هم استفاده میکند
ذخیرهٔ configured از انجام چه کارهایی سر باز میزند؟
ذخیرهٔ configured از هر تغییری که خود سند منعش کرده یا نمیتواند امن نمایشش دهد سر باز میزند، و هر رد کردن قبل از دست زدن به مقصد اتفاق میافتد. شمارهٔ آبجکتی که در /OCGs نباشد یکسره شکست میخورد. عوض کردن گروهی که در آرایهٔ /Locked مربوط به configuration لیست شده شکست میخورد، هرچند بازگویی مقدار فعلیاش مجاز است. با EnforceRadioGroups روشن، هر مجموعهٔ /RBGroups که سرانجام بیش از یک عضو دیدنی داشته باشد رد میشود بهجای اینکه بقیه بیسروصدا خاموش شوند. سندهای رمزشده رد میشوند چون آبجکتهای افزایشی آشکار نمیتوانند هندلر امنیتی فعال را حمل کنند. سندهای امضاشده EPdfError میدهند مگر اینکه AllowSignedDocument = True بدهید، چون عوض کردن چیزی که یک صفحه نشان میدهد میتواند پوشش امضا یا یک سیاست certification را بشکند
function TFormMain.SavePrintPreset(Target: TStream;
const Changes: TPdfOptionalContentStateChanges): Boolean;
var
Options: TPdfOptionalContentConfigureOptions;
Report: TPdfOptionalContentConfigureReport;
begin
Options := TPdfOptionalContentConfigureOptions.Default;
Options.Usage := ocuPrint; // مینویسد /Print << /PrintState ... >>
Options.ConfigurationIndex := 1; // اولین مدخل /Configs، نه /D
try
Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
Report); // AllowSignedDocument روی False میماند
if not Result then
ShowMessage(Report.ErrorMessage);
except
on E: EPdfError do
begin
ShowMessage(E.Message); // فایل امضاشده: هیچ چیزی در Target نوشته نمیشود
Result := False;
end;
end;
end;
قبل از سیمکشی این به یک batch job معاملاتش را بدانید. revision appendشده روی ریسیو کامل PDFium مینشیند نه روی بایتهای فایل اصلی شما، و دقیقاً به همین دلیل ورودی امضاشده رضایت صریح میخواهد. بازنویسی configuration انتخابشده را هم به /BaseState /ON نرمال میکند، پس baseline از نوع /Unchanged یا /OFF نویسنده با آرایههای صریحی جایگزین میشود که دیداری حاصلشان یکی است. حذف /AS ترفندهای مخصوص-چاپ مثل لایهٔ واترمارکی که فقط روی کاغذ ظاهر میشود را برمیدارد؛ برای نگه داشتن آن قواعد DisableAutomaticState را روی False بگذارید و بپذیرید که ممکن است برای آن رخداد بر وضعیت درخواستی شما غلبه کنند. از سوی دیگر، PDF/A-2 (ISO 19005-2 بند 6.9) و PDF/UA (ISO 14289-1 بند 7.10) هر دو /AS را در دیکشنریهای configuration منع میکنند، پس خروجی پیشفرض یک مورد را حذف میکند که وگرنه preflight اعتبارسنجی PDF/A با PDFium Component گزارشش میکرد
کنترل لایه در یک PDF viewer دلفی کجا مینشیند؟
در یک viewer، کنترل لایه یک چکلیست است که از inventory و یک reload از نتیجهٔ ذخیرهشده تغذیه میشود. چکلیست را از Groups پر کنید، مدخلهای Locked را غیرفعال کنید، اعضایی که RadioGroupIndex مشترک دارند را دوسویه-انحصاری بگیرید، و موقع apply در یک TMemoryStream بنویسید و همان stream را داخل TPdf لود کنید تا view وضعیت جدید را رنگ کند. سیمکشی بین TPdf و TPdfView در ساختن یک PDF viewer پرمایه با PDFium VCL در Delphi پوشش داده شده. مجوز و دانلود trial و بقیهٔ مجموعه فیچرها در صفحهٔ محصول PDFium Component برای Delphi است