- Services/PdfRedactText.cs: strip text whose origin falls inside a CoverAnnotation from the page content stream at save time, hooked into PdfBurn.DrawAnnotationsIntoDoc. Fixes edited values staying recoverable by text extraction. - Themes/MMD.xaml replaces all thirteen themes; picker and accent strip removed; no dark mode. - Rename KillerPDF -> MMD PDF across code, resources, packaging and locale strings; new icon. - Remove the upstream author credit and the in-app install button.
197 lines
9.7 KiB
C#
197 lines
9.7 KiB
C#
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using System.Windows;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Imaging;
|
|
|
|
namespace MmdPdf.Services
|
|
{
|
|
// ============================================================
|
|
// Raw-bitmap helpers - pure functions over BGRA pixel buffers,
|
|
// no window state. Formerly a MainWindow partial (MmdPdfUI
|
|
// refactor); shared by the render paths, thumbnails, page
|
|
// export, OCR and the CLI.
|
|
// ============================================================
|
|
internal static class BitmapHelpers
|
|
{
|
|
/// <summary>
|
|
/// Rotates a raw BGRA (4 bytes/pixel) bitmap clockwise by degrees.
|
|
/// Used because Docnet's FPDF_RenderPageBitmapWithMatrix uses a pure-scaling
|
|
/// matrix, so PDFium renders the page in its MediaBox orientation (no rotation).
|
|
/// We strip /Rotate from the temp file so content is never clipped, then rotate
|
|
/// the pixel buffer here to match the intended visual orientation.
|
|
/// </summary>
|
|
internal static (byte[] bytes, int w, int h) RotateBitmap(byte[] src, int w, int h, int degrees)
|
|
{
|
|
degrees = ((degrees % 360) + 360) % 360;
|
|
if (degrees == 0) return (src, w, h);
|
|
int newW = (degrees == 90 || degrees == 270) ? h : w;
|
|
int newH = (degrees == 90 || degrees == 270) ? w : h;
|
|
byte[] dst = new byte[newW * newH * 4];
|
|
for (int y = 0; y < h; y++)
|
|
{
|
|
for (int x = 0; x < w; x++)
|
|
{
|
|
int srcIdx = (y * w + x) * 4;
|
|
int dstX, dstY;
|
|
switch (degrees)
|
|
{
|
|
case 90: dstX = h - 1 - y; dstY = x; break; // CW
|
|
case 180: dstX = w - 1 - x; dstY = h - 1 - y; break;
|
|
default: dstX = y; dstY = w - 1 - x; break; // 270 CW
|
|
}
|
|
int dstIdx = (dstY * newW + dstX) * 4;
|
|
dst[dstIdx] = src[srcIdx];
|
|
dst[dstIdx + 1] = src[srcIdx + 1];
|
|
dst[dstIdx + 2] = src[srcIdx + 2];
|
|
dst[dstIdx + 3] = src[srcIdx + 3];
|
|
}
|
|
}
|
|
return (dst, newW, newH);
|
|
}
|
|
|
|
// ============================================================
|
|
// Document color inversion (#135, "dark mode")
|
|
// ============================================================
|
|
|
|
// Document dark-mode invert is PER PANE now (PdfViewer.DocInvert) so a split can read
|
|
// one document inverted beside a normal one - the global static that used to live here
|
|
// flipped both panes at once. DISPLAY ONLY either way: saves, prints, exports, OCR,
|
|
// thumbnails, and tool previews all keep the document's true colors.
|
|
|
|
// True = night mode inverts pictures along with everything else (the pre-carve-out
|
|
// behavior, now opt-in from the moon button's right-click menu; default off). Loaded
|
|
// from the "DocInvertImages" setting at startup.
|
|
internal static bool DocInvertImages;
|
|
|
|
/// <summary>In-place inversion for the display dark mode, applied at the Viewport render
|
|
/// sites BEFORE the pixel-buffer rotation (via InvertBgraInPlaceExcept, which carves the
|
|
/// image regions back out). PDF pages usually paint NO background - the "paper" is
|
|
/// transparent pixels compositing over the white page slot - so a plain RGB flip left
|
|
/// the page white and merely faded the ink. Composite over white and invert in one
|
|
/// step: out = a*(255-c)/255 with alpha forced opaque. White (or unpainted) paper
|
|
/// becomes black, dark ink becomes light, and opaque images get a true negative.</summary>
|
|
internal static void InvertBgraInPlace(byte[] bgra)
|
|
{
|
|
for (int i = 0; i + 3 < bgra.Length; i += 4)
|
|
{
|
|
int a = bgra[i + 3];
|
|
bgra[i] = (byte)(a * (255 - bgra[i]) / 255);
|
|
bgra[i + 1] = (byte)(a * (255 - bgra[i + 1]) / 255);
|
|
bgra[i + 2] = (byte)(a * (255 - bgra[i + 2]) / 255);
|
|
bgra[i + 3] = 255;
|
|
}
|
|
}
|
|
|
|
/// <summary>An image's bounding box as FRACTIONS of the unrotated page (top-left origin),
|
|
/// so one cached set serves every render resolution. Produced by PdfImages.GetFracRects.</summary>
|
|
internal readonly record struct FracRect(double L, double T, double R, double B);
|
|
|
|
/// <summary>
|
|
/// #135 follow-up: dark mode that does NOT invert pictures. Inverts the whole page with
|
|
/// the operator above, then applies the SAME operator once more over the image regions.
|
|
/// That second pass is exact, not approximate: for an already-inverted opaque pixel,
|
|
/// out = 255 - (a*(255-c)/255) = (a*c + (255-a)*255)/255 - the ORIGINAL pixel composited
|
|
/// over white, which is precisely what the image looked like on the normal white page.
|
|
/// Overlapping image boxes are merged per scanline so no pixel gets the operator twice.
|
|
/// </summary>
|
|
internal static void InvertBgraInPlaceExcept(byte[] bgra, int width, int height, FracRect[] keep)
|
|
{
|
|
InvertBgraInPlace(bgra);
|
|
if (keep is null || keep.Length == 0 || width <= 0 || height <= 0) return;
|
|
|
|
// Fractions -> pixel boxes, clamped. Floor/ceiling so a box never leaves a 1px
|
|
// inverted sliver of the image at its edge.
|
|
var px = new List<(int x0, int y0, int x1, int y1)>(keep.Length);
|
|
foreach (var r in keep)
|
|
{
|
|
int x0 = Math.Max(0, (int)Math.Floor(r.L * width));
|
|
int x1 = Math.Min(width, (int)Math.Ceiling(r.R * width));
|
|
int y0 = Math.Max(0, (int)Math.Floor(r.T * height));
|
|
int y1 = Math.Min(height, (int)Math.Ceiling(r.B * height));
|
|
if (x1 > x0 && y1 > y0) px.Add((x0, y0, x1, y1));
|
|
}
|
|
if (px.Count == 0) return;
|
|
|
|
var spans = new List<(int x0, int x1)>(px.Count);
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
spans.Clear();
|
|
foreach (var b in px)
|
|
if (y >= b.y0 && y < b.y1) spans.Add((b.x0, b.x1));
|
|
if (spans.Count == 0) continue;
|
|
spans.Sort((a, b) => a.x0.CompareTo(b.x0));
|
|
|
|
int row = y * width * 4;
|
|
int curStart = spans[0].x0, curEnd = spans[0].x1;
|
|
for (int s = 1; s <= spans.Count; s++)
|
|
{
|
|
if (s < spans.Count && spans[s].x0 <= curEnd)
|
|
{
|
|
if (spans[s].x1 > curEnd) curEnd = spans[s].x1;
|
|
continue;
|
|
}
|
|
for (int x = curStart; x < curEnd; x++)
|
|
{
|
|
int i = row + x * 4;
|
|
int a = bgra[i + 3];
|
|
bgra[i] = (byte)(a * (255 - bgra[i]) / 255);
|
|
bgra[i + 1] = (byte)(a * (255 - bgra[i + 1]) / 255);
|
|
bgra[i + 2] = (byte)(a * (255 - bgra[i + 2]) / 255);
|
|
bgra[i + 3] = 255;
|
|
}
|
|
if (s < spans.Count) { curStart = spans[s].x0; curEnd = spans[s].x1; }
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encodes raw BGRA pixel data from pdfium to PNG without touching the UI thread.
|
|
/// GDI+ Format32bppArgb is BGRA in memory - matches pdfium output exactly.
|
|
/// </summary>
|
|
internal static byte[] RenderToPng(byte[] bgra, int width, int height, double dpi = 96)
|
|
{
|
|
var pin = GCHandle.Alloc(bgra, GCHandleType.Pinned);
|
|
try
|
|
{
|
|
using var bmp = new System.Drawing.Bitmap(
|
|
width, height, width * 4,
|
|
System.Drawing.Imaging.PixelFormat.Format32bppArgb,
|
|
pin.AddrOfPinnedObject());
|
|
// #188: bake the render DPI into the file's metadata; GDI+ defaults to 96.
|
|
bmp.SetResolution((float)dpi, (float)dpi);
|
|
using var ms = new MemoryStream();
|
|
bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
|
|
return ms.ToArray();
|
|
}
|
|
finally { pin.Free(); }
|
|
}
|
|
|
|
// Builds a frozen bitmap sized so its baked DPI displays it at (dipW x dipH) DIPs. Shared by the
|
|
// tile and the render cache so a cached tile bitmap reuses the exact same geometry.
|
|
internal static BitmapSource BuildScaledBitmap(int w, int h, byte[] rawBytes, int dipW, int dipH)
|
|
{
|
|
var wb = new WriteableBitmap(w, h, 96.0 * w / Math.Max(1, dipW), 96.0 * h / Math.Max(1, dipH), PixelFormats.Bgra32, null);
|
|
wb.WritePixels(new Int32Rect(0, 0, w, h), rawBytes, w * 4, 0);
|
|
wb.Freeze();
|
|
return wb;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encodes raw BGRA pixel data to JPEG (quality 90) via WPF's encoder. Born as the CLI's
|
|
/// CliEncodeJpeg (no JPEG encoder existed before --to-image); homed here beside RenderToPng
|
|
/// in the MmdPdfUI refactor, shared by the CLI and the GUI image export.
|
|
/// </summary>
|
|
internal static byte[] EncodeJpeg(byte[] bgra, int width, int height, double dpi = 96)
|
|
{
|
|
// #188: dpi lands in the JFIF density header; pixel dimensions are unaffected.
|
|
var bmp = BitmapSource.Create(width, height, dpi, dpi, PixelFormats.Bgra32, null, bgra, width * 4);
|
|
var encoder = new JpegBitmapEncoder { QualityLevel = 90 };
|
|
encoder.Frames.Add(BitmapFrame.Create(bmp));
|
|
using var ms = new MemoryStream();
|
|
encoder.Save(ms);
|
|
return ms.ToArray();
|
|
}
|
|
}
|
|
}
|