مقاله فنی

وضعیت جریان محتوا در PDFlibPas: پیگیری CTM و کلیپ

PDFlibPas، کتابخانه‌ی بومی کامپوننت VCL برای PDF در Delphi و C++Builder، جریان محتوای یک صفحه را از طریق کلاس TPDFContentStateTracker بازپخش می‌کند بدون اینکه اصلاً یک canvas رندر را لمس کند. تغذیه‌ی تراکر با یک عملگر تجزیه‌شده در یک زمان، یک رکورد وضعیت گرافیکی در حال اجرا — ماتریس تبدیل فعلی، ماتریس متن، حدود کلیپ، و پشته‌ی ذخیره‌ی q/Q — را برای عکس‌گیری پیش یا پس از اجرای هر عملگر در دسترس نگه می‌دارد

بپرسید یک run از متن واقعاً کجای صفحه‌ی چاپ‌شده فرود می‌آید، و اعداد خام جریان محتوا به‌تنهایی همیشه شما را گمراه می‌کنند. TPDFContentProgram.GetTextRuns از پیش نقطه‌ی لنگر هر دستورالعمل نمایش-متن را از طریق فیلدهای OriginX و OriginY روی TPDFTextRun گزارش می‌دهد، و کامنت‌های فیلد صریح هستند که این نقطه در فضای متن می‌نشیند، از پیش از طریق Tm، Td، TD، و T* تا شده. آنچه هنوز گم است، و آنچه آن کامنت‌ها می‌گویند یک فراخواننده باید تأمین کند، CTM فعال در همان دستورالعمل دقیق است — حاصل‌ضرب هر cm که تا کنون به‌هم پیوسته، تودرتو شده درون هر تعداد جفت q/Q که اتفاقاً در آن نقطه از جریان باز هستند

چرا یک جریان محتوا را به‌جای رندرکردنش بازپخش کنیم؟

PDFlibPas دو مفهوم جداگانه از وضعیت گرافیکی را برای دو کار جداگانه نگه می‌دارد، و این تفکیک عمدی است. رکورد وضعیت داخلی رندرر یک handle canvas دستگاه زنده، یک handle ناحیه‌ی کلیپ، و کش‌های رستری‌سازی فونت حمل می‌کند — منابع واقعی گره‌خورده به هر سطحی که در حال حاضر رنگ‌آمیزی می‌شود، و بی‌معنا به‌محض اینکه آن سطح از بین برود. TPDFContentGraphicsState هیچ‌کدام از آن‌ها را ندارد: یک رکورد ساده است محدود به مقادیری که ISO 32000-1 §8.4 آن‌ها را به‌عنوان قابل‌دسترس فقط از عملگرهای جریان-محتوا تعریف می‌کند — CTM، سبک خط، رنگ، وضعیت متن، و حدود کلیپ و مسیر مشتق‌شده. چون رکورد هیچ ارجاع canvasای و هیچ handle فایل بازی ندارد، یک فراخواننده می‌تواند یک جریان محتوا را تجزیه کند، آن را با TPDFContentStateTracker بپیماید، و مدت‌ها پس از اینکه هر چیزی که آن بایت‌ها را تولید کرده از بین رفته همچنان از عکس‌های حاصل استفاده کند

TPDFContentStateTracker چطور CTM را می‌سازد

TPDFContentStateTracker.Apply شش عملوند یک عملگر cm را در CTM تراکر با همان premultiply‌ای که خودِ PDF مشخص می‌کند به‌هم می‌پیوندد: ماتریس جدید M2 با CTM فعلی به‌صورت M2 × CTM ترکیب می‌شود، در قرارداد بردار-ردیفی که در آن یک نقطه به‌صورت P′ = P × M تبدیل می‌شود (ISO 32000-1 §8.4). بخشی که آسان است اشتباه گرفته شود در جمله‌ی انتقال می‌نشیند، نه بخش خطی: انتقال خودِ M2 باید از طریق مؤلفه‌ی چرخش-و-مقیاس CTM فعلی عبور کند پیش از اینکه انتقال CTM فعلی رویش افزوده شود. آن گام را رد کنید و به‌جای آن یک ترکیب مؤلفه‌به‌مؤلفه‌ی ساده‌لوحانه را hardcode کنید، و اولین cm منزوی‌ای که تست می‌کنید درست به‌نظر می‌رسد درحالی‌که هر مختصات پایین‌دست یک cm تودرتوی دوم یا سوم بی‌سروصدا منحرف می‌شود، که دقیقاً همان نوع باگی است که از بازبینی کد جان سالم به‌در می‌برد چون تستی که آن را می‌گیرد به حداقل دو تبدیل زنجیره‌ای برای شکست‌خوردن نیاز دارد

var
  Prog: TPDFContentProgram;
  Runs: TPDFTextRunArray;
  States: TPDFContentGraphicsStateArray;
  DeviceX, DeviceY: Double;
  I: Integer;
begin
  Prog := TPDFContentProgram.Create;
  try
    Prog.Parse(ContentBytes);
    Runs := Prog.GetTextRuns;
    // One before-instruction snapshot per operator, computed in a single pass
    States := Prog.TraceGraphicsStates(nil, False);
    for I := 0 to High(Runs) do
    begin
      // OriginX/OriginY already fold in Tm/Td/TD/T*; only the CTM active
      // at this instruction is still missing (ISO 32000-1 8.4)
      with States[Runs[I].InstructionIndex].CTM do
      begin
        DeviceX := Runs[I].OriginX * M11 + Runs[I].OriginY * M21 + DX;
        DeviceY := Runs[I].OriginX * M12 + Runs[I].OriginY * M22 + DY;
      end;
      LogTextOrigin(Runs[I].Text, DeviceX, DeviceY); // caller-supplied handler
    end;
  finally
    Prog.Free;
  end;
end;

حلقه‌ی بالا به مسئله‌ی ابتدای مقاله پاسخ می‌دهد: TPDFContentProgram.GetTextRuns، OriginX و OriginY را از پیش از طریق Tm، Td، TD، و T* تاشده پس می‌دهد، و TraceGraphicsStates(nil, False) تکه‌ی باقی‌مانده را تأمین می‌کند، CTM پیش-از-دستورالعمل در همان اندیس دقیقی که هر run در آن گرفته شده، در یک پاس خطی تکی روی کل برنامه. سپردن nil اجازه می‌دهد متد یک تراکر خصوصی را برای فراخوانی در اختیار داشته باشد و آن را داخلاً آزاد کند، که برای یک اسکن یک‌باره انتخاب درستی است؛ سپردن یک نمونه‌ی موجود TPDFContentStateTracker به‌جای آن چیزی است که وضعیت را در سراسر یک صفحه‌ی مونتاژشده از بیش از یک جریان محتوا پیوسته نگه می‌دارد، چون ISO 32000-1 آرایه‌ی /Contents یک صفحه را یک جریان منطقی تکی در نظر می‌گیرد و پشته‌ی q/Q باید موافق باشد

ماتریس متن از Q جان سالم به‌در می‌برد؛ وضعیت گرافیکی نه

ISO 32000-1 §9.4.2، Td، TD، Tm، و T* را به‌عنوان عملگرهایی که ماتریس متن و ماتریس خط متن را درون یک بلوک BT/ET می‌سازند تعریف می‌کند، و PDFlibPas آن تمایز را تیز نگه می‌دارد: Td و TD یک انتقال خالص را روی ماتریس خط متن به‌هم می‌پیوندند، T* همان کار را با استفاده از منفی leading فعلی انجام می‌دهد، و فقط Tm هر دو ماتریس را کاملاً با شش عددی که به آن داده شده جایگزین می‌کند. BT هر دو ماتریس را به identity بازنشانی می‌کند، دقیقاً یک‌بار، در ابتدای شیء متن — اما q و Q اصلاً آن‌ها را لمس نمی‌کنند. TPDFContentStateTracker.Apply دقیقاً به همین دلیل coRestoreState را حالت خاص می‌کند: پیش از اینکه وضعیت ذخیره‌شده را از پشته pop کند، ماتریس متن فعلی، ماتریس خط متن، و پرچم BT/ET را می‌گیرد و آن‌ها را روی هر آنچه وضعیت pop‌شده اتفاقاً نگه داشته دوباره اعمال می‌کند، چون یک جفت q/Q که دور یک run متن پیچیده شده قرار نیست موقعیت متن را به عقب حرکت دهد

var
  Tracker: TPDFContentStateTracker;
  Prog: TPDFContentProgram;
  I: Integer;
begin
  Prog := TPDFContentProgram.Create;
  Tracker := TPDFContentStateTracker.Create;
  try
    Prog.Parse('BT 100 700 Td q 2 0 0 2 0 0 cm (A) Tj Q (B) Tj ET');
    for I := 0 to Prog.Count - 1 do
    begin
      Tracker.Apply(Prog[I]);
      if Prog[I].Op in [coShowText, coRestoreState] then
        LogState(Prog[I].OpName, Tracker.Snapshot); // caller-supplied handler
    end;
  finally
    Tracker.Free;
    Prog.Free;
  end;
end;

آن دنباله را اجرا کنید و CTM گزارش‌شده در دومین Tj به همان مقیاس identityای که پیش از q داشت برمی‌گردد — 2 0 0 2 0 0 cm درون جفت save/restore رفته، همان‌طور که q/Q الزام می‌کند. با این‌حال TextMatrix.DX در همان دستورالعمل همچنان ۱۰۰ است: Tdای که آن را تنظیم کرد پیش از q اجرا شد، پس این وضعیت گرافیکی‌ای نیست که Q هرگز حق لمس‌کردنش را داشت، و ابزاری که خلافش را فرض می‌کرد گزارش می‌داد run گلیف دومی از موقعیت افقی اشتباهی روی صفحه شروع می‌شود

وقتی یک عملگر مسیر کلیپ اجرا می‌شود چه اتفاقی می‌افتد؟

یک عملگر W یا W* بلافاصله کلیپ را کوچک نمی‌کند؛ فقط ثبت می‌کند کدام قاعده‌ی پرشدگی استفاده شود، و تقاطع واقعی منتظر هر عملگر رسم-مسیری می‌ماند که به‌دنبالش می‌آید، از جمله رسام no-op به نام n که نویسندگان PDF به‌طور معمول دقیقاً برای کلیپ‌کردن بدون رسم‌کردن هیچ‌چیزی استفاده می‌کنند. TPDFContentStateTracker آن زمان‌بندی دومرحله‌ای را دقیقاً آینه می‌کند: coClip و coClipEvenOdd فقط یک پرچم قاعده-کلیپ در انتظار تنظیم می‌کنند، و EndCurrentPath — فراخوانی‌شده توسط هر عملگر رسم-مسیری — واقعاً حدود مسیر در انتظار را در ClipMinX، ClipMinY، ClipMaxX، و ClipMaxY تقاطع می‌دهد. درست‌گرفتن این مرحله‌بندی برای خودِ قرارداد عکس پیش/پس اهمیت دارد: یک عکس-پیش که دقیقاً در دستورالعمل W گرفته شده همچنان باید کلیپ قدیمی و گسترده‌تر را نشان دهد، چون کلیپ هنوز در آن نقطه از جریان اعمال نشده، و فروکاستن دو گام به یک بی‌سروصدا هر فراخواننده‌ای را که به این تکیه می‌کند که وضعیت-پیش دقیقاً همان چیزی است که می‌گوید می‌شکست

ClipBoundsExact به یک فراخواننده می‌گوید کدام‌یک از دو وضعیت را دارد نگاه می‌کند، و فقط تا به‌حال برای یک مستطیل تک‌محورهمسو ساخته‌شده با re روی یک مسیر در غیر این‌صورت خالی True است — تنها شکلی که PDFlibPas می‌تواند دقیقاً به‌عنوان چهار عدد نمایش دهد. هر چیز دیگر — یک مستطیل چرخیده، یک کانتور منحنی، یک مسیر ترکیبی با چندین زیرمسیر، یا یک کلیپ ساخته‌شده از یک حالت رندر متن — همچنان ClipMinX تا ClipMaxY تولید می‌کند، اما با ClipBoundsExact پاک‌شده به False، یک سیگنال صادقانه که آن چهار عدد یک کران بیرونی امن هستند نه شکل واقعی کلیپ؛ فراخوانندگانی که فقط به آن کران نیاز دارند، مثل جداکردن یک زیرناحیه‌ی مستطیلی پیش از تبدیل نزولی هافتون GDI توصیف‌شده در رندرکردن صفحات PDF به تک‌رنگ ۱-بیتی، می‌توانند مستقیم آن را بخوانند به‌جای اینکه از هندسه‌ی صفحه دوباره‌اش کنند

منحنی‌های بزیه: یک کران دقیق یا یک کران امن

ارزان‌ترین راه برای محدودکردن یک قطعه‌ی بزیه‌ی مکعبی گرفتن پوسته‌ی محدب چهار نقطه‌ی کنترلش است، و این همیشه امن است چون منحنی هرگز آن را ترک نمی‌کند — اما یک منحنی کم‌عمق و پهن می‌تواند یک جعبه‌ی محدودکننده گزارش دهد که به‌مراتب بزرگ‌تر از چیزی است که منحنی واقعاً اشغال می‌کند، که فیلترینگ مبتنی‌بر-کلیپ را دقیقاً وقتی بیشترین اهمیت را دارد، روی مسیرهای تزئینی بزرگ، ضعیف می‌کند. PDFlibPas به‌جای آن مسئله‌ی سخت‌تر را حل می‌کند: برای هر محور، مشتق منحنی مکعبی را برای ریشه‌ها درون بازه‌ی باز (۰، ۱) حل می‌کند و منحنی را در هر ریشه‌ای که پیدا کند ارزیابی می‌کند، همراه با هر دو نقطه‌ی انتهایی، که راه استاندارد فرم-بسته برای گرفتن گستره‌ی واقعی تک‌محورهمسوی یک منحنی است نه یک تخمین بالاتر. با این‌حال دقت به‌ازای هر منحنی به خودِ کلیپ منتقل نمی‌شود: به‌محض اینکه یک کانتور منحنی به یک مسیر کلیپ تبدیل شود، ClipBoundsExact همچنان برایش به False می‌افتد، چون یک جعبه‌ی محدودکننده، هرچند دقیق، همچنان همان شکلی نیست که محدودش می‌کند، و تراکر وضعیت ترجیح می‌دهد این را بگوید تا اینکه اجازه دهد یک فراخواننده یک مستطیل را فرض کند جایی که واقعاً یک منحنی است

خواندن وضعیت پیش و پس از هر عملگر

اینکه یک فراخواننده وضعیت پیش یا پس بخواهد کاملاً به اینکه عملگر چه کاری انجام می‌دهد بستگی دارد: یک سؤال رسم یا hit-testing درباره‌ی یک مسیر یا run متن، وضعیت را همان‌طور که لحظه‌ی پیش از اجرای آن عملگر بود می‌خواهد، چون آن همان چیزی است که واقعاً تعیین کرد آن عملگر چطور رنگ‌آمیزی کند، درحالی‌که یک سؤال تشخیصی درباره‌ی یک عملگر وضعیت-تنظیم‌کننده مثل gs معمولاً می‌خواهد ببیند همین الان چه چیزی را تغییر داده. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) دقیقاً همان انتخاب را به‌عنوان یک بولی تکی در معرض دید می‌گذارد، یک TPDFContentGraphicsState به‌ازای هر دستورالعمل در یک پاس خطی روی کل برنامه محاسبه می‌کند صرف‌نظر از اینکه کدام لحظه درخواست شده. GetGraphicsState(InstructionIndex, AfterInstruction, State) همان انتخاب پیش/پس را برای یک دستورالعمل تکی به‌جای کل برنامه ارائه می‌دهد، اما در هر فراخوانی از دستورالعمل صفر دوباره بازپخش می‌کند تا به آنجا برسد، پس اسکن‌کردن اندیس‌های زیاد با فراخوانی آن درون یک حلقه O(n²) هزینه دارد در برابر یک فراخوانی تکی O(n) به TraceGraphicsStates روی همان برنامه

var
  Before, After: TPDFContentGraphicsState;
begin
  // Same instruction index, two different instants: before vs. after it runs
  Prog.GetGraphicsState(CmIndex, False, Before);
  Prog.GetGraphicsState(CmIndex, True, After);
  // Before.CTM reflects every earlier cm; After.CTM already folds in
  // this instruction's own concatenation as well
end;

زیستن با جریان‌های محتوای بدشکل

دو نوع ورودی بدشکل در تولیدکنندگان واقعی PDF به‌اندازه‌ی کافی رایج هستند که TPDFContentStateTracker باید آن‌ها را تحمل کند نه اینکه رویشان شکست بخورد. اولی مسیری است که یک مرز q/Q را می‌پیماید: مسیر فعلی، نقطه‌ی فعلی، و شمار زیرمسیر پارامترهای وضعیت-گرافیکی نیستند — ISO 32000-1 §8.4 آنچه q و Q ذخیره و بازیابی می‌کنند را پوشش می‌دهد، و مسیر فعلی که در حال ساخته‌شدن است در میان آن‌ها نیست — پس TPDFContentStateTracker آن داده را کاملاً بیرون از وضعیت ذخیره‌شده پیگیری می‌کند، و یک زیرمسیری که پیش از یک q شروع شده، بلافاصله پس از Q متناظرش همچنان آنجاست، رنگ‌آمیزی‌نشده. دومی یک Q ساده بدون هیچ q متناظری جایی پیش از آن در جریان است، که در خروجی مولدهایی که قطعات جریان محتوا را با الحاق مونتاژ می‌کنند و حسابداری را اشتباه می‌گیرند نادر نیست. TPDFContentStateTracker.RestoreUnderflowCount هر یک از آن رخدادها را می‌شمارد به‌جای اینکه یک استثنا raise کند یا وضعیت را خراب کند: یک Q بدون‌جفت صرفاً وضعیت گرافیکی فعلی را دقیقاً همان‌طور که بود رها می‌کند، انگار آن دستورالعمل یک no-op بوده، پس بقیه‌ی جریان روی یک وضعیت سالم به بازپخش ادامه می‌دهد و یک فراخواننده همچنان می‌تواند بعداً، از آن شمارش، تصمیم بگیرد آیا ورودی ارزش پرچم‌گذاری به هر کسی که آن را تولید کرده دارد

ترکیب CTM، استقلال ماتریس متن از q/Q، و تحقق مرحله‌ای یک مسیر کلیپ به اینکه جریان محتوا چطور یا آیا اصلاً رنگ‌آمیزی می‌شود بستگی ندارند، که دقیقاً همان نکته است: همان عکس TPDFContentStateTracker درست است چه صفحه هرگز رندر نشود چه در شرف سپرده‌شدن به هر back-endی باشد که PDFlibPas برای آن فایل انتخاب می‌کند، از جمله سوییچ موتور زمان‌اجرا پوشش‌داده‌شده در راهنمای رندر چندموتوره‌ی PDF در PDFlibPas. تحلیل محتوا، نگاشت مختصات، و ابزار سیاه‌کردن همگی می‌توانند کاملاً روی خروجی تراکر اجرا شوند، مدت‌ها پیش از یا کاملاً بدون هرگز درخواست از یک رندرر برای درگیرشدن

بازپخش جریان محتوا از طریق TPDFContentStateTracker بخشی از چارچوب ویرایش محتوای ساختاریافته‌ای است که در PDFlibPas، کتابخانه‌ی بومی کامپوننت VCL برای PDF در Delphi و C++Builder، ساخته شده