using System.IO; using System.Threading; using Docnet.Core; using Docnet.Core.Models; using PdfSharpCore.Drawing; using PdfSharpCore.Pdf; namespace MmdPdf.Services { // ============================================================ // Document rasterization cores - pure functions over a rendered // source file, no window state (MmdPdfUI refactor; split out of // FileOperations.cs' Save Flattened and Export Images flows). // The shell keeps the dialogs, the annotation burn, and the // progress overlay; progress arrives via callback, cancel via // token, exactly the BuildSearchablePdf pattern. // ============================================================ internal static class PdfRasterize { /// /// Rasterizes every page of at 150 DPI and assembles them /// into a new PDF at , each page at its original point size. /// Cancellable - nothing is saved if canceled. Runs entirely off the UI thread. /// internal static void FlattenToPdf(string sourcePath, int pageCount, (double widthPt, double heightPt)[] pageDims, string outputPath, Action progress, CancellationToken ct) { // Rasterize pages across CPU cores. Docnet/PDFium is not thread-safe, so the // pdfium render is serialized behind a lock; the PNG encode (GDI+) runs in // parallel. Pages are assembled into the PDF afterwards, in order. // // The source document is opened ONCE here. The old code re-opened it inside // the per-page loop, re-parsing the whole file on every page (O(pages) full // document parses) - the dominant cost on large files. A single scaling // factor renders each page at its own size at 150 DPI (150/72), so the doc // no longer needs reopening to apply per-page pixel dimensions. var pngPages = new byte[pageCount][]; var docGate = new object(); int done = 0; var po = new ParallelOptions { MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount) }; using var flattenReader = DocLib.Instance.GetDocReader(sourcePath, new PageDimensions(150.0 / 72.0)); Parallel.For(0, pageCount, po, i => { if (ct.IsCancellationRequested) return; // cooperative: skip remaining pages' work byte[] bgra; int rw, rh; lock (docGate) { using var pr = flattenReader.GetPageReader(i); // Composite over white (#148, Ryokoxx): PDFium leaves unpainted // background as BGRA 0,0,0,0, which used to embed a full-page // /SMask alpha channel in the flattened output. // #141: WithAnnotations, or flattening an annotated PDF silently dropped the // markup the file carried - this path builds a NEW document from the pixels. rw = pr.GetPageWidth(); rh = pr.GetPageHeight(); bgra = PdfiumInterop.RenderPageWithAnnotations(sourcePath, i, rw, rh) ?? pr.GetImage(new Docnet.Core.Converters.NaiveTransparencyRemover()); } // Encode BGRA to PNG (GDI+) outside the lock so it parallelizes. pngPages[i] = BitmapHelpers.RenderToPng(bgra, rw, rh); int n = System.Threading.Interlocked.Increment(ref done); progress(n, pageCount); }); if (ct.IsCancellationRequested) return; // canceled during render: assemble/save nothing // Assemble the output PDF in page order (PdfSharp is single-threaded). var outDoc = new PdfDocument(); try { for (int i = 0; i < pageCount; i++) { var newPage = outDoc.AddPage(); newPage.Width = XUnit.FromPoint(pageDims[i].widthPt); newPage.Height = XUnit.FromPoint(pageDims[i].heightPt); using var xi = XImage.FromStream(() => new MemoryStream(pngPages[i])); using var gfx = XGraphics.FromPdfPage(newPage); gfx.DrawImage(xi, 0, 0, newPage.Width.Point, newPage.Height.Point); } outDoc.Save(outputPath); } finally { outDoc.Dispose(); } } /// /// Renders each selected page of at /// and writes base-page-NNN.png/.jpg files into . In-app /// rotations arrive as a snapshot array (the working file has /Rotate stripped). Returns /// how many files were written; cancellation stops after the current page. /// internal static int ExportPageImages(string sourcePath, IReadOnlyList pages, int[] rotSnapshot, double dpi, bool jpeg, string outDir, string baseName, int digits, Action progress, CancellationToken ct) { int written = 0; using var dr = DocLib.Instance.GetDocReader(sourcePath, new PageDimensions(dpi / 72.0)); int done = 0; foreach (var idx in pages) { if (ct.IsCancellationRequested) return written; byte[] raw; int w, h; using (var pr = dr.GetPageReader(idx)) { // Composite over white (#148, Ryokoxx): bare GetImage leaves the // unpainted background at BGRA 0,0,0,0 - JPEG export dropped the // alpha and produced black pages, PNG came out transparent. // #141: WithAnnotations - an exported image should show the markup the file // carries, the same as the page does on screen. w = pr.GetPageWidth(); h = pr.GetPageHeight(); raw = PdfiumInterop.RenderPageWithAnnotations(sourcePath, idx, w, h) ?? pr.GetImage(new Docnet.Core.Converters.NaiveTransparencyRemover()); } int rot = idx < rotSnapshot.Length ? rotSnapshot[idx] : 0; if (rot != 0) (raw, w, h) = BitmapHelpers.RotateBitmap(raw, w, h, rot); var bytes = jpeg ? BitmapHelpers.EncodeJpeg(raw, w, h, dpi) : BitmapHelpers.RenderToPng(raw, w, h, dpi); var name = $"{baseName}-page-{(idx + 1).ToString().PadLeft(digits, '0')}.{(jpeg ? "jpg" : "png")}"; File.WriteAllBytes(Path.Combine(outDir, name), bytes); written++; int n = ++done; progress(n, pages.Count); } return written; } } }