مقاله فنی

پیشوند در برابر پسوند ماتریس در PDFium Delphi: چرخش محوری

ماتریس‌های آفین PDF از قرارداد بردار سطری ISO 32000-1 §8.3.3 استفاده می‌کنند، جایی که یک نقطه ماتریس را از چپ ضرب می‌کند: point' = point * M. در PDFium Component برای Delphi و C++Builder آن یک واقعیت کل سطح API TPdfMatrix را ثابت می‌کند: Multiply پسوند می‌کند، پس M := M * Op، در حالی که PreMultiply پیشوند می‌کند، پس M := Op * M

هر باگ تبدیل کلاسیک به این جمله برمی‌گردد که برعکس به‌خاطر سپرده شده. واترمارکی که در فایل آزمون شما تمیز می‌چرخد و در فایل مشتری نیمی بیرون از صفحه فرود می‌آید. تصویر کوچکی که چرخیده-دوبار درمی‌آید چون صفحه از قبل یک ربع‌چرخش داشته. مهری که آفستش روی A4 کامل است و روی Letter منحرف می‌شود. هیچ‌کدام از این‌ها باگ رندرینگ نیستند؛ باگ‌های ترتیب ضرب‌اند، و همه‌شان قابل‌رفع‌اند وقتی بتوانید بلندبگویید هر عملیات در کدام فضا نوشته شده

قرارداد بردار سطری که قواعد را تعیین می‌کند

TPdfMatrix شش عنصر نام‌گذاری‌شده در مشخصات را ذخیره می‌کند و دقیقاً همان‌طور که فرمت تعریفشان می‌کند اعمالشان می‌کند، پس خود تبدیل جایی است که استدلال شروع می‌شود. TPdfMatrix.TransformPoint x' = x*a + y*c + e و y' = x*b + y*d + f را محاسبه می‌کند، که همان فرم شش‌عنصری است که ISO 32000-1 §8.3.4 برای عملگر cm که ماتریسی را روی ماتریس تبدیل جاری ترکیب می‌کند تعریف می‌کند. جفت (a, b) ردیف اول است، (c, d) ردیف دوم، و (e, f) ردیف انتقال. عادات بردار-ستونی گرفته‌شده از OpenGL یا یک درس جبر خطی شما را اینجا گمراه می‌کنند، و بی‌صدا گمراه می‌کنند، چون یک ماتریس با ترتیب اشتباه همچنان یک ماتریس کاملاً معتبر است. یک ترکیب را در قرارداد سطری چپ‌به‌راست بخوانید و ترتیب اعمال رایگان بیرون می‌آید: چون point * (M * Op) برابر (point * M) * Op است، یک عملیات پسونددار روی مختصاتی عمل می‌کند که ماتریس موجود از قبل تولید کرده، یعنی در فضای صفحه، در حالی که یک عملیات پیشونددار پیش از اجرای ماتریس موجود عمل می‌کند، در فضای ورودی خودِ آبجکت

var
  M: TPdfMatrix;
  Pt: FS_POINTF;
begin
  M := TPdfMatrix.Create;                // identity
  try
    // Append order: each call acts on what the previous calls produced.
    M.Scale(0.5, 0.5);                   // M := M * S   half size
    M.Rotate(90);                        // M := M * R   clockwise, degrees
    M.Translate(300, 400);               // M := M * T   then move on the page

    Pt := M.TransformPoint(0, 0);        // x*a + y*c + e, x*b + y*d + f
  finally
    M.Free;
  end;
end;

TPdfMatrix.Rotate به‌طور پیش‌فرض ساعتگرد و بر‌حسب درجه است، با ACounterClockwise و AAngleInRadians در دسترس وقتی داده منبعتان علامت متفاوتی دارد. ویژگی‌های فقط-خواندنی a تا f و ویژگی Handle همان شش عدد خام FS_MATRIX را به شما پس می‌دهند، که چیزی است که FPDFPageObj_SetMatrix می‌خواهد. هیچ‌چیز در کلاس شش عدد را از شما پنهان نمی‌کند، و این عمدی است: وقتی یک تبدیل بدرفتاری می‌کند، چاپ‌کردن a تا f سریع‌ترین تشخیصی است که دارید

چرا پیشونددادن یک انتقال به بخش خطی نیاز دارد؟

چون یک شیفت پیشونددار در فضای ورودی ماتریس نوشته می‌شود، و باید از راه بخش خطی جاری عبور کند پیش از آنکه بتواند به ردیف انتقال بپیوندد. TPdfMatrix.PreTranslate بنابراین e := dx*a + dy*c + e و f := dx*b + dy*d + f را محاسبه می‌کند. پسونددادن جهت آسان است: TPdfMatrix.Translate در فضای صفحه نوشته می‌شود، جایی که چیزی نیاز به تبدیل ندارد، پس فقط dx را به e و dy را به f اضافه می‌کند. هرکسی که PreTranslate را به دو جمع «بهینه‌سازی» کند تازه چرخش و مقیاس را از شیفت حذف کرده

M := TPdfMatrix.Create;
try
  M.Rotate(90);              // a=0, b=-1, c=1, d=0

  M.Translate(10, 0);        // append: e := e + 10
                             // -> 10 points to the right on the page

  M.Reset;
  M.Rotate(90);
  M.PreTranslate(10, 0);     // prepend: e := 10*a + 0*c + e  (unchanged)
                             //          f := 10*b + 0*d + f  (f - 10)
                             // -> 10 points along the stamp own x axis,
                             //    which after the turn points down the page
finally
  M.Free;
end;

همان عدم‌تقارن در سراسر جفت مقیاس جاری است، و ارزش دانستن دارد که هرکدام کدام عناصر را لمس می‌کند پیش از آنکه ساعت سه بامداد یکی را دیباگ کنید. TPdfMatrix.PreScale ردیف‌ها را ضرب می‌کند، a و b را در scaleX و c و d را در scaleY مقیاس می‌کند، و انتقال را دست‌نخورده می‌گذارد چون شیفت از قبل پایین‌دست رخ داده. TPdfMatrix.Scale پسونددار به‌جایش ستون‌ها را ضرب می‌کند، a، c، e را در scaleX و b، d، f را در scaleY می‌گیرد، پس آفست موجود همراه با هرچیز دیگر مقیاس می‌شود. هر دو مسیرهای تک‌منظوره‌ای هستند که ضرب شش‌عنصری عمومی را رد می‌کنند، و هر دو معنای ترکیب فرم عمومی را دقیقاً حفظ می‌کنند

دو انتقال در یک چرخش محوری کجا می‌روند؟

پیرامون عملیات، نه پیرامون کل ماتریس، و به همان ترتیب. TPdfMatrix.RotateAt Translate(-pivot)، سپس چرخش، سپس Translate(+pivot) را ضمیمه می‌کند، که تحت قرارداد بردار سطری به‌صورت Translate(-pivot) * Op * Translate(pivot) ترکیب می‌شود. آن دنباله چیزی است که محور را ثابت نگه می‌دارد زیر عملیات جدید در حالی که همچنان اجازه می‌دهد ماتریس موجود مختصاتش را اول تولید و منتقل کند. جفت را برعکس بنویسید، همان‌طور که در یک کتابخانه بردار-ستونی درست می‌بود، و آبجکت به‌جای چرخیدن سرجایش دور مبدأ می‌چرخد، که دقیقاً چگونگی فرودآمدن یک واترمارک مرکزشده بیرون از crop box است

procedure RotateStampAboutPageCenter(AObj: FPDF_PAGEOBJECT;
  const AAngleDegrees, APageWidth, APageHeight: Single);
var
  M: TPdfMatrix;
  Raw: FS_MATRIX;
begin
  if not FPDFPageObj_GetMatrix(AObj, Raw) then
    raise Exception.Create('Page object carries no matrix');
  M := TPdfMatrix.Create(Raw);
  try
    // Appends Translate(-pivot) * Rotate * Translate(+pivot) in one call.
    M.RotateAt(AAngleDegrees, APageWidth / 2, APageHeight / 2);
    Raw := M.Handle;
    FPDFPageObj_SetMatrix(AObj, Raw);
  finally
    M.Free;
  end;
end;

همان ترکیب پشت ScaleAt، SkewAt، HorizontalFlipAt، VerticalFlipAt، و CentralFlipAt است، پس وقتی به الگو برای چرخش اعتماد کنید می‌توانید به بقیه نیز اعتماد کنید. TPdfMatrix.CentralFlip ارزش تک‌نام‌بردن دارد: هر شش عنصر را نگاتیو می‌کند تا یک چرخش ۱۸۰درجه بدون هیچ مثلثاتی به شما بدهد، که یعنی بدون cos از مقداری که باید دقیقاً صفر می‌بود و بدون رانش انباشته وقتی آن را در یک حلقه اعمال کنید. اگر در حال قراردادن نشانه‌های تکراری هستید نه چرخاندن یکی، مکانیک خود قرارگیری در مهرهای صفحه قابل‌استفاده‌مجدد با Form XObject پوشش داده شده، و کار ماتریسی اینجا مستقیم رویش می‌نشیند

TryDecompose درباره یک ماتریس چه چیزی به شما می‌گوید؟

TPdfMatrix.TryDecompose انتقال، مقیاس، چرخش، برشیدگی، دترمینان و یک پرچم بازتاب را زیر یک قرارداد مقیاس-سپس-چرخش گزارش می‌کند، و آن‌ها را به‌قدر کافی صادقانه گزارش می‌کند که برای تصمیم‌گیری مفید باشد نه فقط لاگ‌گیری. ScaleX از طول ردیف اول می‌آید، Sqrt(a*a + b*b)، پس همیشه مثبت است. ScaleY سپس Determinant / ScaleX است، که آن را علامت‌دار می‌کند. چرخش از ArcTan2(-b, a) بر‌حسب درجه می‌آید، و برشیدگی از حاصل‌ضرب داخلی دو ردیف نرمال‌شده با هر دو مقیاس

آن علامت روی ScaleY چیزی است که مردم حذف می‌کنند، و حذفش یک باگ واقعی است نه یک نقص آرایشی. یک دترمینان منفی یعنی ماتریس یک بازتاب دارد. هر دو عامل مقیاس را مثبت اجبار کنید تا اعداد مرتب‌تر به‌نظر برسند و بازتاب را دور ریخته‌اید، پس یک ماتریس بازساخته‌شده از تجزیه آینه‌ای برمی‌گردد: متن برعکس خوانده می‌شود، یک صفحه اسکن‌شده برمی‌گردد، یک لوگوی وارد‌شده رو به جهت اشتباه می‌کند. فیلد IsReflected وجود دارد تا هرگز مجبور نباشید آن را استنتاج کنید. این همچنین آزمونی است که چرخش دوگانه کلاسیک را جلوگیری می‌کند، جایی که کد یک چرخش نمایشی را به صفحه‌ای که از قبل یکی حمل می‌کند اضافه می‌کند؛ نسخه سمت-نمایشگر آن مسئله در جای‌گیری تصویر کوچک، زوم، و چرخش دوگانه کار شده

var
  D: TPdfMatrixDecomposition;
begin
  if M.TryDecompose(D) then
  begin
    // D.ScaleX is always positive; D.ScaleY carries the determinant sign.
    if D.IsReflected then
      Log('mirrored, ScaleY = %.3f', [D.ScaleY]);

    if Abs(D.RotationDegrees) > 0.5 then
      SkipDisplayRotation;      // the object already carries its own turn
  end
  else
    UseIdentityFallback;        // near-singular or non-finite: no answer
end;

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

TPdfMatrix.TryCreateRectMapping ماتریس منبع-به-مقصد را برایتان می‌سازد و یک TPdfMatrixFitMode از pmfStretch، pmfContain، یا pmfCover می‌گیرد. این تابع هر دو مستطیل را ابتدا نرمال می‌کند، چون مستطیل‌های PDF ملزم نیستند با چپ زیر راست یا پایین زیر بالا برسند، سپس مقیاس‌های X و Y مستقل را استخراج می‌کند: pmfStretch آن‌ها را مستقل نگه می‌دارد، pmfContain کوچک‌تر را می‌گیرد و لتربکس را مرکز می‌کند، pmfCover بزرگ‌تر را می‌گیرد و برش را مرکز می‌کند. MapRectToRect همراه همان نگاشت را روی یک ماتریس موجود ضمیمه می‌کند، و NewRectMapping EPdfMatrixError پرتاب می‌کند جایی که فرم Try False برمی‌گرداند. این عنصر پایه زیر هر جای‌گذاری سلول در چیدمان N-up و بازترتیب صفحه است، جایی که هر صفحه منبع باید درون یک سلول محاسبه‌شده فرود بیاید بدون آنکه شما حساب را به‌ازای هر چیدمان دوباره استخراج کنید

ماتریس‌های منحط و مسیر شکست صادقانه

ورودی‌های متناهی یک نتیجه متناهی را تضمین نمی‌کنند، پس کد جای‌گیری در Double محاسبه می‌کند و سپس نامزد Single باریک‌شده را پیش از انتشار برای تناهی دوباره بررسی می‌کند؛ یک نگاشت حاوی یک بی‌نهایت هرگز انگار معتبر است پس داده نمی‌شود. همان نظم روی معکوس‌سازی حاکم است. TPdfMatrix.TryGetInverse یک ماتریس را با یک آستانه نسبی رد می‌کند، دترمینان را علیه اپسیلون ضرب‌در مربع بزرگ‌ترین عنصر خطی مقایسه می‌کند نه علیه یک ثابت ثابت، که چیزی است که آزمون را با‌معنا نگه می‌دارد چه واحدهای شما نقطه باشند چه میکرومتر. TryDecompose به همان شکل کناره‌گیری می‌کند، امتناع وقتی طول ردیف اول یا ScaleY استخراج‌شده در یا زیر اپسیلون بیفتد

سبک شکستی را انتخاب کنید که با نقطه فراخوانی جور باشد به‌جای پیچیدن هرچیزی در try-except از سر عادت. TryInvert، TryGetInverse، TryInverseTransformPoint، TryTransformBounds و TryCreateRectMapping False برمی‌گردانند و اهدافشان را دست‌نخورده می‌گذارند، که برای آزمون-برخورد و حلقه‌های به‌ازای هر آبجکت جایی که یک آبجکت منحط باید رد شود، نه فاجعه‌بار، مناسب است. Invert، InverseCopy، InverseTransformPoint، MapRectToRect و TransformBounds به‌جایش EPdfMatrixError پرتاب می‌کنند، که برای کد راه‌اندازی جایی که یک ماتریس منفرد یعنی فراخوان‌کننده چیزی اشتباه محاسبه کرده مناسب است. برای کار دسته‌ای، TransformPoints و TransformRects آرایه نتیجه‌شان را دقیقاً یک‌بار تخصیص می‌دهند، TransformPointsInPlace و TransformRectsInPlace ذخیره‌سازی شما را دوباره استفاده می‌کنند، و TryTransformBounds جعبه محدودکننده را در یک گذر واحد انباشته می‌کند به‌جای مادی‌سازی نقاط تبدیل‌شده ابتدا

هیچ‌کدام از این‌ها ریاضی عجیبی نیست. این یک قرارداد است، به‌طور پیوسته اعمال‌شده، با API نام‌گذاری‌شده پس قرارداد در نقطه فراخوانی مرئی است: Multiply و افعال ساده پسوند می‌کنند، خانواده Pre پیشوند می‌کند، خانواده At عملیات را با جفت محورش می‌بندد. ترتیب را در یک کامنت کنار هر ترکیبی که می‌سازید بنویسید، چون کدی که امروز درست خوانده می‌شود کدی است که کسی شش ماه دیگر معکوس می‌کند. مرجع کامل TPdfMatrix، در کنار APIهای آبجکت-صفحه و رندرینگ که این تبدیل‌ها به آن‌ها می‌رسند، با PDFium Component برای Delphi و C++Builder زندگی می‌کند