Bài viết kỹ thuật

Trạng thái content-stream của PDFlibPas: theo dõi CTM và clip

PDFlibPas, thư viện component PDF VCL gốc cho Delphi và C++Builder, phát lại content stream của một trang thông qua lớp TPDFContentStateTracker của nó mà hoàn toàn không đụng đến một canvas render nào. Đưa cho tracker từng toán tử đã phân tích một lần giữ một bản ghi trạng thái đồ họa đang chạy — ma trận biến đổi hiện tại, ma trận văn bản, phạm vi clip, và stack lưu q/Q — sẵn sàng để chụp ảnh nhanh trước hoặc sau khi mỗi toán tử thực thi

Hỏi một đoạn văn bản thực sự đáp xuống đâu trên trang in ra, và chỉ riêng các con số thô của content stream sẽ đánh lừa bạn mỗi lần. TPDFContentProgram.GetTextRuns đã báo cáo điểm neo của mỗi lệnh hiển thị văn bản thông qua các trường OriginX và OriginY trên TPDFTextRun, và comment trường nói rõ ràng rằng điểm này nằm trong không gian văn bản, đã gấp qua Tm, Td, TD, và T*. Điều vẫn còn thiếu, và điều comment nói caller phải tự cung cấp, là CTM đang hoạt động tại đúng lệnh đó — tích của mọi cm đã ghép nối cho đến lúc đó, lồng bên trong bất kể bao nhiêu cặp q/Q tình cờ đang mở tại điểm đó trong stream

Vì sao phát lại một content stream thay vì render nó?

PDFlibPas giữ hai khái niệm riêng biệt về trạng thái đồ họa cho hai công việc riêng biệt, và sự tách bạch này có chủ đích. Bản ghi trạng thái nội bộ của bộ render mang một handle canvas thiết bị sống, một handle vùng clip, và các cache raster hóa font — các tài nguyên thật gắn với bất cứ bề mặt nào đang được vẽ, và vô nghĩa một khi bề mặt đó biến mất. TPDFContentGraphicsState không mang bất cứ điều gì trong số đó: nó là một bản ghi thuần túy giới hạn ở các giá trị mà ISO 32000-1 §8.4 định nghĩa là có thể tiếp cận được chỉ từ các toán tử content-stream — CTM, kiểu đường, màu sắc, trạng thái văn bản, và phạm vi clip và path suy ra. Vì bản ghi không giữ tham chiếu canvas nào và không giữ handle file mở nào, một caller có thể phân tích một content stream, duyệt nó bằng TPDFContentStateTracker, và tiếp tục dùng các bản chụp nhanh kết quả rất lâu sau khi bất cứ thứ gì tạo ra các byte đó đã biến mất

TPDFContentStateTracker dựng CTM như thế nào

TPDFContentStateTracker.Apply ghép sáu toán hạng của một toán tử cm vào CTM của tracker dùng đúng phép nhân trước mà chính PDF chỉ định: ma trận mới M2 kết hợp với CTM hiện tại như M2 × CTM, theo quy ước row-vector nơi một điểm biến đổi thành P′ = P × M (ISO 32000-1 §8.4). Phần dễ làm sai nằm ở số hạng tịnh tiến, không phải phần tuyến tính: chính bản dịch chuyển của M2 phải đi qua thành phần xoay-và-tỉ-lệ của CTM hiện tại trước khi bản dịch chuyển của CTM hiện tại được cộng lên trên. Bỏ qua bước đó và hardcode một tổ hợp theo-từng-thành-phần ngây thơ thay vào đó, và cm cô lập đầu tiên bạn test sẽ trông đúng trong khi mọi tọa độ ở hạ nguồn của một cm lồng thứ hai hay thứ ba âm thầm trôi dạt, chính xác là loại lỗi sống sót qua code review vì unit test bắt được nó cần ít nhất hai phép biến đổi được nối chuỗi để thất bại

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;

Vòng lặp ở trên trả lời điểm đau từ phần mở đầu: TPDFContentProgram.GetTextRuns trả về OriginX và OriginY đã gấp qua Tm, Td, TD, và T*, và TraceGraphicsStates(nil, False) cung cấp mảnh còn thiếu duy nhất, CTM trước-lệnh tại đúng chỉ số mỗi lượt chạy được chụp, trong một lượt duyệt tuyến tính duy nhất trên toàn bộ chương trình. Truyền nil để phương thức sở hữu một tracker riêng cho lệnh gọi và tự giải phóng nó nội bộ, lựa chọn đúng cho một lượt quét một-lần; truyền một instance TPDFContentStateTracker hiện có thay vào đó là điều giữ trạng thái liên tục qua một trang được lắp ráp từ nhiều hơn một content stream, vì ISO 32000-1 coi mảng /Contents của một trang là một luồng logic duy nhất và stack q/Q phải đồng thuận

Ma trận văn bản sống sót qua Q; trạng thái đồ họa thì không

ISO 32000-1 §9.4.2 định nghĩa Td, TD, Tm, và T* là các toán tử dựng ma trận văn bản và ma trận dòng văn bản bên trong một khối BT/ET, và PDFlibPas giữ sự phân biệt đó rõ nét: Td và TD ghép một phép tịnh tiến thuần túy lên ma trận dòng văn bản, T* làm điều tương tự dùng số âm của leading hiện tại, và chỉ Tm thay thế hoàn toàn cả hai ma trận bằng sáu con số nó được cho. BT đặt lại cả hai ma trận về identity, đúng một lần, tại đầu đối tượng văn bản — nhưng q và Q hoàn toàn không đụng đến chúng. TPDFContentStateTracker.Apply xử lý riêng trường hợp coRestoreState chính vì lý do này: trước khi nó pop trạng thái đã lưu ra khỏi stack, nó chụp ma trận văn bản hiện tại, ma trận dòng văn bản, và cờ BT/ET, và áp dụng lại chúng lên trên bất cứ thứ gì trạng thái đã pop tình cờ mang, vì một cặp q/Q bọc quanh một lượt chạy văn bản không được cho là di chuyển vị trí văn bản trở lại

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;

Chạy chuỗi đó và CTM được báo cáo tại Tj thứ hai quay lại tỉ lệ identity nó có trước q — 2 0 0 2 0 0 cm bên trong cặp save/restore đã biến mất, đúng như q/Q yêu cầu. TextMatrix.DX tại chính lệnh đó, tuy nhiên, vẫn là 100: Td đặt nó chạy trước q, nên nó không phải trạng thái đồ họa mà Q từng có quyền đụng tới, và một công cụ giả định ngược lại sẽ báo cáo lượt chạy glyph thứ hai bắt đầu từ sai vị trí ngang trên trang

Điều gì xảy ra khi một toán tử đường clip chạy?

Một toán tử W hay W* không thu nhỏ clip ngay lập tức; nó chỉ ghi lại quy tắc fill nào sẽ dùng, và giao điểm thực sự chờ bất kỳ toán tử vẽ path nào theo sau nó, kể cả toán tử vẽ không-làm-gì n mà các tác giả PDF thường xuyên dùng chính để clip mà không vẽ gì cả. TPDFContentStateTracker phản chiếu chính xác thời điểm hai bước đó: coClip và coClipEvenOdd chỉ đặt một cờ quy tắc clip đang chờ, và EndCurrentPath — được gọi bởi mọi toán tử vẽ path — là thứ thực sự giao phạm vi path đang chờ vào ClipMinX, ClipMinY, ClipMaxX, và ClipMaxY. Làm đúng việc phân giai đoạn này quan trọng đối với chính hợp đồng chụp-nhanh-trước/sau: một bản chụp trước lấy đúng tại lệnh W vẫn phải hiển thị clip cũ, rộng hơn, vì clip chưa có hiệu lực tại điểm đó trong stream, và việc gộp hai bước thành một sẽ âm thầm phá vỡ mọi caller dựa vào trạng thái-trước để có nghĩa như nó nói

ClipBoundsExact nói cho một caller biết nó đang nhìn vào tình huống nào trong hai tình huống, và nó chỉ bao giờ là True cho một hình chữ nhật căn trục duy nhất được xây dựng bởi re trên một path trống về mặt khác — hình dạng duy nhất PDFlibPas có thể biểu diễn chính xác như bốn con số. Mọi thứ khác — một hình chữ nhật đã xoay, một đường viền cong, một path phức hợp với nhiều subpath, hay một clip được xây dựng từ một chế độ render văn bản — vẫn tạo ra ClipMinX đến ClipMaxY, nhưng với ClipBoundsExact bị xóa thành False, một tín hiệu trung thực rằng bốn con số là một cận ngoài an toàn chứ không phải hình dạng clip thật; các caller chỉ cần cận đó, chẳng hạn cô lập một vùng con hình chữ nhật trước khi hạ cấp halftone GDI được mô tả trong render trang PDF thành 1-bit đơn sắc, có thể đọc nó trực tiếp thay vì suy ra lại nó từ hình học trang

Đường cong Bézier: một cận chính xác hoặc một cận an toàn

Cách rẻ nhất để đóng khung một đoạn Bézier bậc ba là lấy hull lồi của bốn điểm điều khiển của nó, và điều đó luôn an toàn vì đường cong không bao giờ rời khỏi nó — nhưng một đường cong nông, rộng có thể báo cáo một hộp bao lớn hơn nhiều so với những gì đường cong thực sự chiếm, làm suy yếu lọc dựa-trên-clip đúng lúc nó quan trọng nhất, trên các đường path trang trí lớn. PDFlibPas giải quyết vấn đề chặt chẽ hơn thay vào đó: cho mỗi trục, nó giải đạo hàm của đường cong bậc ba tìm nghiệm bên trong khoảng mở (0, 1) và đánh giá đường cong tại bất kỳ nghiệm nào nó tìm thấy, cùng với cả hai điểm cuối, cách chuẩn dạng đóng để lấy phạm vi căn trục thật của một đường cong thay vì một ước tính quá cao. Độ chính xác theo-từng-đường-cong tuy nhiên không mang theo vào chính clip: một khi một đường viền cong trở thành một path clip, ClipBoundsExact vẫn rơi về False cho nó, vì một hộp bao, dù chặt chẽ đến đâu, vẫn không phải cùng hình dạng với đường cong nó bao quanh, và bộ theo dõi trạng thái thà nói vậy còn hơn để một caller giả định một hình chữ nhật ở nơi thực sự có một đường cong

Đọc trạng thái trước và sau mỗi toán tử

Việc một caller muốn trạng thái trước hay sau hoàn toàn phụ thuộc vào toán tử đó làm gì: một câu hỏi vẽ hay hit-test về một path hay lượt chạy văn bản muốn trạng thái như nó đứng ngay khoảnh khắc trước khi toán tử đó chạy, vì đó là điều thực sự quyết định toán tử vẽ như thế nào, trong khi một câu hỏi chẩn đoán về một toán tử đặt-trạng-thái như gs thường muốn thấy nó vừa thay đổi gì. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) phơi bày đúng lựa chọn đó như một Boolean duy nhất, tính một TPDFContentGraphicsState cho mỗi lệnh trong một lượt tuyến tính duy nhất trên toàn bộ chương trình bất kể khoảnh khắc nào được yêu cầu. GetGraphicsState(InstructionIndex, AfterInstruction, State) cung cấp cùng lựa chọn trước/sau cho một lệnh đơn lẻ thay vì toàn bộ chương trình, nhưng nó phát lại từ lệnh không ở mỗi lệnh gọi để đến đó, nên quét nhiều chỉ số bằng cách gọi nó trong một vòng lặp tốn O(n²) so với một lệnh gọi O(n) duy nhất đến TraceGraphicsStates trên cùng chương trình

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;

Sống chung với các content stream lỗi định dạng

Hai loại đầu vào lỗi định dạng đủ phổ biến trong các nhà sản xuất PDF thực tế đến mức TPDFContentStateTracker phải chịu đựng chúng thay vì thất bại trên chúng. Loại đầu tiên là một path trải dài qua một ranh giới q/Q: path hiện tại, điểm hiện tại, và số lượng subpath không phải các tham số trạng thái đồ họa — ISO 32000-1 §8.4 bao phủ những gì q và Q lưu và khôi phục, và path hiện tại đang được xây dựng không nằm trong đó — nên TPDFContentStateTracker theo dõi dữ liệu đó hoàn toàn bên ngoài trạng thái đã lưu, và một subpath bắt đầu trước một q vẫn còn đó, chưa vẽ, ngay sau Q khớp. Loại thứ hai là một Q trần trụi không có q khớp nào ở bất cứ đâu trước nó trong stream, không hiếm trong đầu ra từ các bộ tạo lắp ráp các mảnh content-stream bằng nối chuỗi và làm sai việc kế toán. TPDFContentStateTracker.RestoreUnderflowCount đếm mỗi sự kiện đó thay vì ném ra một ngoại lệ hay làm hỏng trạng thái: một Q không khớp chỉ để lại trạng thái đồ họa hiện tại đúng như nó vốn có, như thể lệnh đó là một no-op, nên phần còn lại của stream tiếp tục phát lại trên một trạng thái hợp lý và một caller vẫn có thể quyết định sau đó, từ số đếm, liệu đầu vào có đáng gắn cờ trở lại cho bất cứ ai đã tạo ra nó hay không

Việc ghép CTM, sự độc lập của ma trận văn bản khỏi q/Q, và việc hiện thực hóa theo giai đoạn của một path clip không phụ thuộc vào cách hay việc content stream có bao giờ được vẽ hay không, chính xác là điểm mấu chốt: cùng bản chụp nhanh TPDFContentStateTracker đúng dù trang không bao giờ được render hay sắp được giao cho bất kỳ back end nào PDFlibPas chọn cho file đó, bao gồm việc chuyển đổi engine lúc chạy được nói đến trong hướng dẫn về render PDF đa-engine trong PDFlibPas. Phân tích nội dung, ánh xạ tọa độ, và công cụ redaction đều có thể chạy hoàn toàn trên đầu ra của tracker, rất lâu trước hoặc hoàn toàn không bao giờ yêu cầu một bộ render tham gia

Việc phát lại content-stream thông qua TPDFContentStateTracker là một phần của khung chỉnh sửa nội dung có cấu trúc được xây dựng sẵn vào PDFlibPas, thư viện component PDF VCL gốc dành cho Delphi và C++Builder