HotPDF làm phẳng xoay trang PDF bằng THotPDF.FlattenLoadedPageRotation: phương thức bọc content của mỗi trang bị xoay trong một phép biến đổi cm chiều kim đồng hồ, viết lại mọi page box mà trang thực sự có, xoay hình học annotation, matrix appearance, explicit destination và hình học cấu trúc theo cùng một góc, rồi đặt /Rotate về 0. Trang nhìn trong một viewer vẫn y nguyên, nhưng hệ tọa độ của nó giờ đã đứng thẳng. Điều đó quan trọng ngay khi một tool hạ lưu, một print RIP hay đoạn code đóng dấu của bạn bỏ qua /Rotate và đặt mọi thứ vào user space thô
Kích hoạt điển hình là một máy scan hay một app capture trên di động ghi các trang ngang dưới dạng media dọc kèm /Rotate 90. Mọi viewer đều hiển thị đúng, nên chẳng ai để ý cho tới khi ai đó đóng số trang vào "góc dưới phải" và nó nằm ngang lạch theo cạnh trái, hay một bước imposition chỉ đọc /MediaBox sắp một ô dọc cho một trang ngang. Làm phẳng nghe như một việc matrix một dòng code. Trên thực tế nó chạm tới năm page box, ba loại hình học annotation, các đích liên kết của tài liệu và cây cấu trúc, và mỗi thứ có luật riêng của nó trong ISO 32000-1
/Rotate xoay một trang PDF theo hướng nào?
/Rotate xoay trang theo chiều kim đồng hồ để hiển thị và in, theo bội số của 90 độ (ISO 32000-1 §7.7.3.3, Bảng 30). Ở 90 độ, cạnh trái của media thành cạnh trên và cạnh trên thành bên phải, nên trong không gian thiết bị y-hướng-xuống, phép ánh xạ là X = (y - Bottom) * Scale và Y = (x - Left) * Scale. Ở 270 độ, cạnh phải thành cạnh trên. /Rotate cũng là một trong bốn thuộc tính trang được thừa kế, cùng /Resources, /MediaBox và /CropBox (§7.7.3.4), nên một dictionary trang không có /Rotate riêng vẫn có thể bị một tổ tiên /Pages xoay giúp. THotPDF.GetLoadedPageRotation đi theo chuỗi /Parent và chuẩn hóa kết quả về 0–359, đó mới là giá trị bạn muốn, chứ không phải khóa thô trên trang
Chiều xoay rất dễ sai theo kiểu vẫn sống sót qua test, và các bản HotPDF trước đây đã sai đúng kiểu đó. Matrix trang-sang-thiết bị cũ hoán đổi các thành phần y cho 90 và 270 độ, thứ cho ra một phép phản chiếu qua đường chéo thay vì một phép xoay: hướng của matrix lật ngược so với ca chưa xoay. Cả hai góc vẫn "trông có xoay", bitmap có bề rộng và bề cao hoán đổi, và một vòng đi từ trang ra view rồi quay lại trả về đúng điểm xuất phát, nên các phép kiểm kích thước lẫn test round-trip đều pass. Phép kiểm đáng tin duy nhất là xem mốc góc kết thúc ở đâu, so từng pixel với một renderer tham chiếu. Vì mô hình viewer, backend render SIMD và phép ánh xạ highlight đều đã chép cùng matrix đó, tất cả được sửa cùng lúc, và code làm phẳng giờ dùng đúng quy ước chiều kim đồng hồ với renderer
FlattenLoadedPageRotation viết lại một trang ra sao?
FlattenLoadedPageRotation(PageRange, Info) xử lý mọi trang trong PageRange mà góc xoay hiệu dụng là 90, 180 hay 270, và trả về số trang nó đã làm phẳng. PageRange rỗng nghĩa là mọi trang; nếu không chuỗi dùng cú pháp '1-3,7' một-based quen thuộc, và một số trang ngoài phạm vi sẽ văng exception thay vì bị bỏ qua. Các content stream gốc không bao giờ bị encode lại. Phương thức đặt một stream mới chứa q 0 -1 1 0 -Bottom Width+Left cm (cho 90 độ) lên đầu /Contents của trang, nối một stream chứa Q vào đuôi, và cuối cùng ghi một /Rotate 0 tường minh vào dictionary trang để một giá trị thừa kế trên node /Pages không xoay trang lần nữa
var
Pdf: THotPDF;
Info: THPDFRotationFlattenInfo;
Flattened: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('scanned-batch.pdf') > 0 then
begin
// '' = mọi trang; các trang 0 độ được quét nhưng để yên
Flattened := Pdf.FlattenLoadedPageRotation('', Info);
Writeln(Format('Scanned %d, flattened %d pages', [Info.ScannedPageCount, Info.FlattenedPageCount]));
Writeln(Format('Turned %d annotations, %d destinations, %d tagged geometry entries',
[Info.TransformedAnnotationCount, Info.TransformedDestinationCount,
Info.TransformedStructureGeometryCount]));
if Flattened > 0 then
Pdf.SaveLoadedDocument('scanned-batch-upright.pdf');
end;
finally
Pdf.Free;
end;
end;
Record THPDFRotationFlattenInfo đáng để log lại chứ đừng vứt đi. ScannedPageCount là cỡ của phạm vi, FlattenedPageCount bằng giá trị trả về, còn ba bộ đếm Transformed... cho biết tài liệu có link, bookmark hay hình học cấu trúc trỏ vào các trang đã xoay hay chưa. Một lô mà mọi tệp đều báo zero destination là chuyện bình thường; một tệp PDF/UA có cấu trúc mà báo zero hình học cấu trúc trong khi bạn kỳ vọng các hộp bao figure là tín hiệu phải soi thủ công
Làm phẳng viết lại những page box nào, theo thứ tự nào?
Làm phẳng chỉ viết lại những box mà trang sẵn có, và nó đọc mọi box trước khi ghi box nào. Thứ tự quan trọng vì chuỗi mặc định: GetLoadedPageBox(PageIndex, pbCropBox, ...) trả về /MediaBox khi trang không có /CropBox, còn /BleedBox, /TrimBox và /ArtBox mặc định về CropBox (§14.11.2). Một phiên bản trước đây đã đọc, biến đổi rồi ghi từng box một. Nó viết lại MediaBox trước, rồi đọc "CropBox", nhận lại MediaBox đã xoay, xoay nó lần thứ hai và ghi ra một CropBox mà trang chưa bao giờ có, thứ cắt một trang ngang thành hình vuông. Luật thừa kế cũng chia theo đúng kiểu đó: MediaBox và CropBox được tra theo chuỗi /Parent, trong khi Bleed, Trim và ArtBox chỉ được tính nếu nằm ngay trên dictionary trang, nên một /TrimBox lạc trên node /Pages bị coi là không tồn tại và không bao giờ được chép xuống trang
procedure DumpPageGeometry(Pdf: THotPDF; PageIndex: Integer);
var
L, B, R, T: Single;
begin
Writeln('Effective /Rotate: ', Pdf.GetLoadedPageRotation(PageIndex));
if Pdf.GetLoadedPageBox(PageIndex, pbMediaBox, L, B, R, T) then
Writeln(Format('MediaBox [%g %g %g %g]', [L, B, R, T]));
// True kể cả khi không có khóa /TrimBox: giá trị rơi về CropBox, rồi MediaBox
if Pdf.GetLoadedPageBox(PageIndex, pbTrimBox, L, B, R, T) then
Writeln(Format('TrimBox [%g %g %g %g]', [L, B, R, T]));
// Đặt sẵn Letter; GetLoadedPageVisibleBox để nguyên các output khi thất bại
L := 0; B := 0; R := 612; T := 792;
Pdf.GetLoadedPageVisibleBox(PageIndex, L, B, R, T);
Writeln(Format('Visible [%g %g %g %g]', [L, B, R, T]));
end;
Chạy helper ấy trước và sau khi làm phẳng thì các con số tự nói lên tất cả. Với một trang 90 độ có MediaBox [0 0 612 792], MediaBox sau khi phẳng thành [0 0 792 612]; mọi box được viết lại đều được ánh xạ qua cùng một lượt xoay chiều kim đồng hồ, lấy gốc MediaBox ban đầu làm chuẩn, nên MediaBox mới luôn bắt đầu từ gốc tọa độ và các box khác giữ vị trí của chúng bên trong nó. GetLoadedPageVisibleBox trả về thứ viewer hiển thị và máy in in ra, tức CropBox bị cắt bởi MediaBox và chuẩn hóa sao cho Left nhỏ hơn Right, và renderer, export SVG, viewer cùng đường in của HotPDF đều dùng đúng box ấy. Khi bạn cần kích thước trang mà con người nhìn thấy, hãy gọi GetLoadedPageVisibleBox thay vì đọc /MediaBox
Vì sao annotation gãy khi bạn chỉ xoay /Rect?
Annotation gãy vì appearance stream không được vẽ thẳng vào /Rect. Theo §12.5.5, viewer trước hết biến đổi /BBox của form bằng /Matrix của nó, rồi scale và dịch hộp bao của kết quả đó vào /Rect. Chỉ xoay /Rect thì một con dấu 200 × 40 bị bóp vào một ô 40 × 200, méo mó và nằm ngang. Vì thế FlattenLoadedPageRotation nhân-phải lượt xoay chiều kim đồng hồ của trang vào mỗi /Matrix appearance (cho 90 độ là [0 -1 1 0 0 0] theo quy ước vector-hàng), trải khắp các appearance /N, /R và /D cùng mọi trạng thái bên trong. Một appearance stream có thể được dùng chung bởi nhiều annotation hay trạng thái, nên mỗi stream chỉ bị xoay đúng một lần mỗi lệnh gọi. Ca duy nhất không có lời giải sạch là một stream dùng chung qua nhiều trang với các góc xoay khác nhau; nó theo trang đầu tiên chạm tới nó
Hai luật nữa giữ form field và ghi chú dính ở nguyên chỗ. Entry /MK /R của một widget (§12.5.6.19) là một góc ngược chiều kim đồng hồ, nên góc chiều kim đồng hồ của trang bị trừ đi từ nó, lấy modulo 360; bỏ qua bước này thì lần tái tạo appearance kế tiếp sẽ vẽ chữ của field theo hướng sai. Các annotation mang cờ NoRotate (bit 5, giá trị 16, §12.5.3) đứng thẳng trên một trang bị xoay và quay quanh góc trên-trái của /Rect của chúng, nên việc làm phẳng giữ nguyên bề rộng, bề cao và dáng đứng của chúng, chỉ dời góc ấy tới nơi lượt xoay đặt nó. Ngoài annotation, phương thức còn xoay /QuadPoints, /Vertices, /L và /InkList, viết lại các explicit destination gọi tên trang (điểm /XYZ, hình chữ nhật /FitR, và /FitH / /FitV hoán đổi ở 90 và 270 độ, §12.3.2.2), và biến đổi hình học cấu trúc như các entry /BBox thuộc tính cho các phần tử cấu trúc mà /Pg là trang này
Làm phẳng không phủ những gì?
Làm phẳng là một phép viết lại hình học các object riêng của một trang, và vài tình huống rơi ra ngoài nó một cách thầm lặng chứ không ồn ào
- Các trang mà góc xoay hiệu dụng đã là 0, hay MediaBox bị thiếu hay có bề rộng, bề cao bằng zero, bị bỏ qua không báo lỗi; hãy so giá trị trả về với số trang bạn kỳ vọng sẽ đổi
- Form XObject được tham chiếu từ tài nguyên trang giữ
/BBoxriêng của chúng trong không gian form, vìcmbên ngoài đã xoay chúng; phép quét cây cấu trúc chỉ bám theo/Kvà/Anên không bao giờ chui vào tài nguyên hay annotation của trang lần thứ hai - Các destination được tìm bằng cách quét mọi indirect object một lần cho mỗi trang được làm phẳng, nên một tài liệu lớn với hàng trăm trang xoay sẽ trả giá cho phép đi ấy từng trang một
- Renderer trang của HotPDF không vẽ annotation, nên một lần kiểm bằng mắt các con dấu đã xoay cần
FlattenLoadedAnnotationstrước
// Nướng appearance vào content để renderer hiển thị được chúng,
// rồi render trang 1 trước và sau khi gỡ /Rotate của nó
Pdf.FlattenLoadedAnnotations('1');
Before := Pdf.RenderLoadedPageToBitmap(0, 96);
try
Pdf.FlattenLoadedPageRotation('1', Info);
After := Pdf.RenderLoadedPageToBitmap(0, 96);
try
Assert((Before.Width = After.Width) and (Before.Height = After.Height));
// So pixel của mốc góc ở đây, đừng chỉ so kích thước
finally
After.Free;
end;
finally
Before.Free;
end;
Để tìm hiểu sâu hơn, phía annotation của câu chuyện này tiếp nối trong tự sinh appearance annotation trước khi làm phẳng, renderer đứng sau phép so sánh trước-sau được bàn trong render một trang PDF đã nạp ra bitmap, còn redaction và ghép N-up trên PDF đã nạp cho thấy cùng kỹ thuật nối content-stream mà tiền tố và hậu tố xoay dựa vào. HotPDF, kể cả FlattenLoadedPageRotation và các bộ đọc page box, có sẵn cho Delphi và C++Builder trên trang HotPDF Delphi PDF component