- 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.
475 lines
26 KiB
C#
475 lines
26 KiB
C#
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using Docnet.Core;
|
|
|
|
namespace MmdPdf.Services
|
|
{
|
|
// ============================================================
|
|
// Direct PDFium P/Invoke - the ONE home for every direct
|
|
// pdfium.dll call in the app (MmdPdfUI refactor; formerly split
|
|
// across FileOperations.cs and Links.cs).
|
|
//
|
|
// THREADING: PDFium is single-threaded. Docnet serializes every
|
|
// native call it makes on an internal static lock
|
|
// (Docnet.Core.DocLib.Lock). Every DIRECT pdfium.dll call in
|
|
// this app must hold that SAME lock, or a background Docnet
|
|
// render and a direct call (link extraction, encryption strip)
|
|
// can be inside PDFium at the same time - native heap
|
|
// corruption, exit code 0xc0000374. Confirmed from a 1.6.3
|
|
// crash dump (2026-07-17): two threads with concurrent PDFium
|
|
// frames. The raw externs are suffixed Raw; only the
|
|
// lock-holding wrappers may be called. Keeping every extern in
|
|
// this one class is what keeps the lock discipline auditable.
|
|
// ============================================================
|
|
internal static class PdfiumInterop
|
|
{
|
|
internal static readonly object PdfiumLock =
|
|
typeof(DocLib).GetField("Lock",
|
|
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)
|
|
?.GetValue(null) ?? new object();
|
|
|
|
// ---- Document / page lifecycle -------------------------------------------------------
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDF_LoadDocument", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern IntPtr FPDF_LoadDocumentRaw(
|
|
[MarshalAs(UnmanagedType.LPStr)] string filePath,
|
|
[MarshalAs(UnmanagedType.LPStr)] string? password);
|
|
internal static IntPtr FPDF_LoadDocument(string filePath, string? password)
|
|
{ lock (PdfiumLock) return FPDF_LoadDocumentRaw(filePath, password); }
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDF_CloseDocument", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern void FPDF_CloseDocumentRaw(IntPtr document);
|
|
internal static void FPDF_CloseDocument(IntPtr document)
|
|
{ lock (PdfiumLock) FPDF_CloseDocumentRaw(document); }
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDF_GetPageCount", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern int FPDF_GetPageCountRaw(IntPtr document);
|
|
private static int FPDF_GetPageCount(IntPtr document)
|
|
{ lock (PdfiumLock) return FPDF_GetPageCountRaw(document); }
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDF_LoadPage", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern IntPtr FPDF_LoadPageRaw(IntPtr document, int page_index);
|
|
internal static IntPtr FPDF_LoadPage(IntPtr document, int page_index)
|
|
{ lock (PdfiumLock) return FPDF_LoadPageRaw(document, page_index); }
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDF_ClosePage", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern void FPDF_ClosePageRaw(IntPtr page);
|
|
internal static void FPDF_ClosePage(IntPtr page)
|
|
{ lock (PdfiumLock) FPDF_ClosePageRaw(page); }
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFPage_SetRotation", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern void FPDFPage_SetRotationRaw(IntPtr page, int rotation);
|
|
private static void FPDFPage_SetRotation(IntPtr page, int rotation)
|
|
{ lock (PdfiumLock) FPDFPage_SetRotationRaw(page, rotation); }
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFPage_GenerateContent", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern bool FPDFPage_GenerateContentRaw(IntPtr page);
|
|
private static bool FPDFPage_GenerateContent(IntPtr page)
|
|
{ lock (PdfiumLock) return FPDFPage_GenerateContentRaw(page); }
|
|
|
|
// ---- Page rendering ----------------------------------------------------------------
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFBitmap_CreateEx", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern IntPtr FPDFBitmap_CreateExRaw(
|
|
int width, int height, int format, IntPtr firstScan, int stride);
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFBitmap_Destroy", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern void FPDFBitmap_DestroyRaw(IntPtr bitmap);
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFBitmap_FillRect", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern void FPDFBitmap_FillRectRaw(
|
|
IntPtr bitmap, int left, int top, int width, int height, uint color);
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDF_RenderPageBitmap", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern void FPDF_RenderPageBitmapRaw(
|
|
IntPtr bitmap, IntPtr page, int startX, int startY, int sizeX, int sizeY,
|
|
int rotate, int flags);
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFDOC_InitFormFillEnvironment", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern IntPtr FPDFDOC_InitFormFillEnvironmentRaw(
|
|
IntPtr document, IntPtr formInfo);
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFDOC_ExitFormFillEnvironment", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern void FPDFDOC_ExitFormFillEnvironmentRaw(IntPtr formHandle);
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDF_FFLDraw", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern void FPDF_FFLDrawRaw(
|
|
IntPtr formHandle, IntPtr bitmap, IntPtr page, int startX, int startY,
|
|
int sizeX, int sizeY, int rotate, int flags);
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFPage_GetAnnotCount", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern int FPDFPage_GetAnnotCountRaw(IntPtr page);
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFPage_GetAnnot", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern IntPtr FPDFPage_GetAnnotRaw(IntPtr page, int index);
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFPage_CloseAnnot", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern void FPDFPage_CloseAnnotRaw(IntPtr annot);
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFAnnot_GetSubtype", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern int FPDFAnnot_GetSubtypeRaw(IntPtr annot);
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFAnnot_GetFlags", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern int FPDFAnnot_GetFlagsRaw(IntPtr annot);
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFAnnot_SetFlags", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern int FPDFAnnot_SetFlagsRaw(IntPtr annot, int flags);
|
|
|
|
private const int FPDFBitmapBgra = 4;
|
|
private const int FpdfAnnot = 0x01;
|
|
private const int FpdfLcdText = 0x02;
|
|
private const int FpdfAnnotSubtypeWidget = 20; // fpdf_annot.h FPDF_ANNOT_WIDGET
|
|
private const int FpdfAnnotFlagHidden = 1 << 1; // fpdf_annot.h FPDF_ANNOT_FLAG_HIDDEN
|
|
|
|
// Marks every WIDGET annotation on the loaded page hidden so the FPDF_ANNOT render pass
|
|
// does not paint form-field appearances, and returns each widget's original flags by
|
|
// annotation index so RestoreWidgetAnnotationFlags can put them back before FFLDraw.
|
|
// In-memory only: this renderer's document is a one-shot load that is closed right after,
|
|
// never saved. EntryPointNotFound (an older bundled PDFium without the annot API) returns
|
|
// null and degrades to leaving the fields baked in.
|
|
private static System.Collections.Generic.Dictionary<int, int>? HideWidgetAnnotations(IntPtr page)
|
|
{
|
|
try
|
|
{
|
|
var saved = new System.Collections.Generic.Dictionary<int, int>();
|
|
int count = FPDFPage_GetAnnotCountRaw(page);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
IntPtr annot = FPDFPage_GetAnnotRaw(page, i);
|
|
if (annot == IntPtr.Zero) continue;
|
|
try
|
|
{
|
|
if (FPDFAnnot_GetSubtypeRaw(annot) == FpdfAnnotSubtypeWidget)
|
|
{
|
|
int flags = FPDFAnnot_GetFlagsRaw(annot);
|
|
saved[i] = flags;
|
|
FPDFAnnot_SetFlagsRaw(annot, flags | FpdfAnnotFlagHidden);
|
|
}
|
|
}
|
|
finally { FPDFPage_CloseAnnotRaw(annot); }
|
|
}
|
|
return saved;
|
|
}
|
|
catch { return null; /* annot API unavailable: fields stay baked, no crash */ }
|
|
}
|
|
|
|
// Puts back the widget flags HideWidgetAnnotations saved, so FFLDraw sees the original
|
|
// visibility: a genuinely hidden field stays hidden, everything else draws once there.
|
|
private static void RestoreWidgetAnnotationFlags(
|
|
IntPtr page, System.Collections.Generic.Dictionary<int, int>? saved)
|
|
{
|
|
if (saved is null) return;
|
|
try
|
|
{
|
|
foreach (var kv in saved)
|
|
{
|
|
IntPtr annot = FPDFPage_GetAnnotRaw(page, kv.Key);
|
|
if (annot == IntPtr.Zero) continue;
|
|
try { FPDFAnnot_SetFlagsRaw(annot, kv.Value); }
|
|
finally { FPDFPage_CloseAnnotRaw(annot); }
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders one page through PDFium with annotation appearance streams enabled, but without
|
|
/// Docnet's form-fill environment. This avoids the native teardown crash caused by Docnet's
|
|
/// annotation flag while still painting text notes, highlights, stamps, ink, and widget
|
|
/// appearances into viewer, print, flatten, and image-export pixels.
|
|
/// </summary>
|
|
/// <param name="includeFormFields">False for the on-screen viewer, whose live form
|
|
/// overlays already show the field values - baking them into the page bitmap as well
|
|
/// painted the same text twice, slightly offset (the "drop shadow" ghost, thanks Thomas).
|
|
/// True everywhere the pixels ARE the output: print, flatten, export, thumbnails.</param>
|
|
internal static byte[]? RenderPageWithAnnotations(
|
|
string sourcePath, int pageIndex, int width, int height,
|
|
bool transparentBackground = false, bool includeFormFields = true)
|
|
{
|
|
if (width <= 0 || height <= 0) return null;
|
|
try
|
|
{
|
|
try { _ = DocLib.Instance; } catch { }
|
|
int stride = checked(width * 4);
|
|
var bytes = new byte[checked(stride * height)];
|
|
var pinned = GCHandle.Alloc(bytes, GCHandleType.Pinned);
|
|
try
|
|
{
|
|
lock (PdfiumLock)
|
|
{
|
|
IntPtr doc = FPDF_LoadDocumentRaw(sourcePath, null);
|
|
if (doc == IntPtr.Zero) return null;
|
|
// PDFium retains this pointer until ExitFormFillEnvironment, so it must be
|
|
// stable unmanaged memory, not a temporary buffer produced by the P/Invoke
|
|
// marshaller. The bundled ABI is one 32-bit version plus 31 pointer slots.
|
|
int formInfoSize = IntPtr.Size == 8 ? 256 : 128;
|
|
IntPtr formInfo = Marshal.AllocHGlobal(formInfoSize);
|
|
for (int offset = 0; offset < formInfoSize; offset += 4)
|
|
Marshal.WriteInt32(formInfo, offset, 0);
|
|
IntPtr form = IntPtr.Zero;
|
|
for (int version = 1; version <= 2 && form == IntPtr.Zero; version++)
|
|
{
|
|
Marshal.WriteInt32(formInfo, 0, version);
|
|
form = FPDFDOC_InitFormFillEnvironmentRaw(doc, formInfo);
|
|
}
|
|
try
|
|
{
|
|
IntPtr page = FPDF_LoadPageRaw(doc, pageIndex);
|
|
if (page == IntPtr.Zero) return null;
|
|
try
|
|
{
|
|
// Widgets are hidden for the static FPDF_ANNOT pass in BOTH modes:
|
|
// the on-screen viewer replaces them with live overlays, and the
|
|
// output path paints them once via FFLDraw below - letting the
|
|
// static pass draw the /AP as well painted every field twice
|
|
// whenever the /AP layout and FFLDraw's (NeedAppearances) layout
|
|
// disagreed. If the output path has no form environment to draw
|
|
// with, the widgets stay visible so the static pass still shows them.
|
|
var savedWidgetFlags = includeFormFields && form == IntPtr.Zero
|
|
? null
|
|
: HideWidgetAnnotations(page);
|
|
IntPtr bitmap = FPDFBitmap_CreateExRaw(
|
|
width, height, FPDFBitmapBgra, pinned.AddrOfPinnedObject(), stride);
|
|
if (bitmap == IntPtr.Zero) return null;
|
|
try
|
|
{
|
|
FPDFBitmap_FillRectRaw(bitmap, 0, 0, width, height,
|
|
transparentBackground ? 0x00000000 : 0xFFFFFFFF);
|
|
FPDF_RenderPageBitmapRaw(bitmap, page, 0, 0, width, height, 0,
|
|
FpdfAnnot | FpdfLcdText);
|
|
if (includeFormFields && form != IntPtr.Zero)
|
|
{
|
|
RestoreWidgetAnnotationFlags(page, savedWidgetFlags);
|
|
FPDF_FFLDrawRaw(form, bitmap, page, 0, 0, width, height, 0,
|
|
FpdfAnnot | FpdfLcdText);
|
|
}
|
|
}
|
|
finally { FPDFBitmap_DestroyRaw(bitmap); }
|
|
}
|
|
finally
|
|
{
|
|
// This PDFium build expects the form environment to be released while
|
|
// its page is still alive. The one-shot renderer closes the page and
|
|
// document immediately afterwards, so no damaged native state is reused.
|
|
if (form != IntPtr.Zero)
|
|
{
|
|
FPDFDOC_ExitFormFillEnvironmentRaw(form);
|
|
form = IntPtr.Zero;
|
|
}
|
|
FPDF_ClosePageRaw(page);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (form != IntPtr.Zero) FPDFDOC_ExitFormFillEnvironmentRaw(form);
|
|
Marshal.FreeHGlobal(formInfo);
|
|
FPDF_CloseDocumentRaw(doc);
|
|
}
|
|
}
|
|
return bytes;
|
|
}
|
|
finally { pinned.Free(); }
|
|
}
|
|
catch { return null; }
|
|
}
|
|
|
|
// ---- Save ---------------------------------------------------------------------------
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDF_SaveWithVersion", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern bool FPDF_SaveWithVersionRaw(
|
|
IntPtr document, ref FPDF_FILEWRITE fileWrite, uint flags, int fileVersion);
|
|
private static bool FPDF_SaveWithVersion(IntPtr document, ref FPDF_FILEWRITE fileWrite, uint flags, int fileVersion)
|
|
{ lock (PdfiumLock) return FPDF_SaveWithVersionRaw(document, ref fileWrite, flags, fileVersion); }
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct FPDF_FILEWRITE
|
|
{
|
|
public int version; // must be 1
|
|
public IntPtr WriteBlock; // cdecl: int WriteBlock(FPDF_FILEWRITE*, const void*, unsigned long)
|
|
}
|
|
|
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
private delegate int PdfWriteBlockDelegate(IntPtr pThis, IntPtr pData, uint size);
|
|
|
|
private const uint FPDF_REMOVE_SECURITY = 3;
|
|
|
|
// ---- Link extraction entry points (fallback for object-stream PDFs) ------------------
|
|
// PdfSharpCore silently drops link annotations stored in object streams (linearized /
|
|
// PDF 1.5+); PDFium resolves them natively. Consumed by Links.cs' cached-handle pass.
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
internal struct FS_RECTF { public float left, top, right, bottom; }
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDF_GetPageWidth", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern double FPDF_GetPageWidthRaw(IntPtr page);
|
|
internal static double FPDF_GetPageWidth(IntPtr page)
|
|
{ lock (PdfiumLock) return FPDF_GetPageWidthRaw(page); }
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDF_GetPageHeight", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern double FPDF_GetPageHeightRaw(IntPtr page);
|
|
internal static double FPDF_GetPageHeight(IntPtr page)
|
|
{ lock (PdfiumLock) return FPDF_GetPageHeightRaw(page); }
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFLink_Enumerate", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern bool FPDFLink_EnumerateRaw(IntPtr page, ref int startPos, out IntPtr linkAnnot);
|
|
internal static bool FPDFLink_Enumerate(IntPtr page, ref int startPos, out IntPtr linkAnnot)
|
|
{ lock (PdfiumLock) return FPDFLink_EnumerateRaw(page, ref startPos, out linkAnnot); }
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFLink_GetAnnotRect", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern bool FPDFLink_GetAnnotRectRaw(IntPtr linkAnnot, out FS_RECTF rect);
|
|
internal static bool FPDFLink_GetAnnotRect(IntPtr linkAnnot, out FS_RECTF rect)
|
|
{ lock (PdfiumLock) return FPDFLink_GetAnnotRectRaw(linkAnnot, out rect); }
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFLink_GetDest", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern IntPtr FPDFLink_GetDestRaw(IntPtr document, IntPtr link);
|
|
internal static IntPtr FPDFLink_GetDest(IntPtr document, IntPtr link)
|
|
{ lock (PdfiumLock) return FPDFLink_GetDestRaw(document, link); }
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFLink_GetAction", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern IntPtr FPDFLink_GetActionRaw(IntPtr link);
|
|
internal static IntPtr FPDFLink_GetAction(IntPtr link)
|
|
{ lock (PdfiumLock) return FPDFLink_GetActionRaw(link); }
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFAction_GetType", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern uint FPDFAction_GetTypeRaw(IntPtr action);
|
|
internal static uint FPDFAction_GetType(IntPtr action)
|
|
{ lock (PdfiumLock) return FPDFAction_GetTypeRaw(action); }
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFAction_GetDest", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern IntPtr FPDFAction_GetDestRaw(IntPtr document, IntPtr action);
|
|
internal static IntPtr FPDFAction_GetDest(IntPtr document, IntPtr action)
|
|
{ lock (PdfiumLock) return FPDFAction_GetDestRaw(document, action); }
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFAction_GetURIPath", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern uint FPDFAction_GetURIPathRaw(IntPtr document, IntPtr action, byte[]? buffer, uint buflen);
|
|
internal static uint FPDFAction_GetURIPath(IntPtr document, IntPtr action, byte[]? buffer, uint buflen)
|
|
{ lock (PdfiumLock) return FPDFAction_GetURIPathRaw(document, action, buffer, buflen); }
|
|
|
|
[DllImport("pdfium.dll", EntryPoint = "FPDFDest_GetDestPageIndex", CallingConvention = CallingConvention.Cdecl)]
|
|
private static extern int FPDFDest_GetDestPageIndexRaw(IntPtr document, IntPtr dest);
|
|
internal static int FPDFDest_GetDestPageIndex(IntPtr document, IntPtr dest)
|
|
{ lock (PdfiumLock) return FPDFDest_GetDestPageIndexRaw(document, dest); }
|
|
|
|
// ---- The two direct-PDFium file operations -------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Uses PDFium to save a copy of <paramref name="sourcePath"/> with all security/encryption
|
|
/// removed. Returns true on success. Falls back gracefully if PDFium is unavailable.
|
|
/// PDFium is already initialized by Docnet; no separate init call is needed.
|
|
/// </summary>
|
|
internal static bool TryPdfiumStripEncryption(string sourcePath, string destPath)
|
|
{
|
|
try
|
|
{
|
|
// Ensure PDFium is initialized - Docnet does this lazily on first use,
|
|
// so force it now before we call PDFium P/Invoke directly.
|
|
try { _ = DocLib.Instance; } catch { }
|
|
|
|
var doc = FPDF_LoadDocument(sourcePath, null);
|
|
if (doc == IntPtr.Zero) return false;
|
|
try
|
|
{
|
|
using var ms = new MemoryStream();
|
|
PdfWriteBlockDelegate cb = (_, pData, size) =>
|
|
{
|
|
var buf = new byte[size];
|
|
Marshal.Copy(pData, buf, 0, (int)size);
|
|
ms.Write(buf, 0, (int)size);
|
|
return 1;
|
|
};
|
|
var gch = GCHandle.Alloc(cb);
|
|
try
|
|
{
|
|
var fw = new FPDF_FILEWRITE
|
|
{
|
|
version = 1,
|
|
WriteBlock = Marshal.GetFunctionPointerForDelegate(cb)
|
|
};
|
|
if (!FPDF_SaveWithVersion(doc, ref fw, FPDF_REMOVE_SECURITY, 0))
|
|
return false;
|
|
}
|
|
finally { gch.Free(); }
|
|
File.WriteAllBytes(destPath, ms.ToArray());
|
|
return true;
|
|
}
|
|
finally { FPDF_CloseDocument(doc); }
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uses PDFium to load <paramref name="sourcePath"/>, zero-out all page /Rotate values,
|
|
/// strip encryption, and save to <paramref name="destPath"/>. Returns true on success.
|
|
/// Called from SaveTempAndReload's xref-error fallback - PDFium is guaranteed to be
|
|
/// initialized by then because the page preview has already rendered via Docnet.
|
|
/// </summary>
|
|
internal static bool TryPdfiumSaveWithZeroRotations(string sourcePath, string destPath)
|
|
{
|
|
try
|
|
{
|
|
var doc = FPDF_LoadDocument(sourcePath, null);
|
|
if (doc == IntPtr.Zero)
|
|
{
|
|
try { File.AppendAllText(System.IO.Path.Combine(System.IO.Path.GetTempPath(), "mmdpdf_pdfium_debug.txt"), $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] FPDF_LoadDocument returned null for: {sourcePath}\n\n"); } catch { }
|
|
return false;
|
|
}
|
|
try
|
|
{
|
|
int pageCount = FPDF_GetPageCount(doc);
|
|
for (int i = 0; i < pageCount; i++)
|
|
{
|
|
var page = FPDF_LoadPage(doc, i);
|
|
if (page == IntPtr.Zero) continue;
|
|
try
|
|
{
|
|
FPDFPage_SetRotation(page, 0); // strip /Rotate so Docnet renders cleanly
|
|
FPDFPage_GenerateContent(page);
|
|
}
|
|
finally { FPDF_ClosePage(page); }
|
|
}
|
|
|
|
using var ms = new MemoryStream();
|
|
PdfWriteBlockDelegate cb = (_, pData, size) =>
|
|
{
|
|
var buf = new byte[size];
|
|
Marshal.Copy(pData, buf, 0, (int)size);
|
|
ms.Write(buf, 0, (int)size);
|
|
return 1;
|
|
};
|
|
var gch = GCHandle.Alloc(cb);
|
|
try
|
|
{
|
|
var fw = new FPDF_FILEWRITE
|
|
{
|
|
version = 1,
|
|
WriteBlock = Marshal.GetFunctionPointerForDelegate(cb)
|
|
};
|
|
if (!FPDF_SaveWithVersion(doc, ref fw, FPDF_REMOVE_SECURITY, 0))
|
|
return false;
|
|
}
|
|
finally { gch.Free(); }
|
|
|
|
File.WriteAllBytes(destPath, ms.ToArray());
|
|
return true;
|
|
}
|
|
finally { FPDF_CloseDocument(doc); }
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
try
|
|
{
|
|
File.AppendAllText(
|
|
System.IO.Path.Combine(System.IO.Path.GetTempPath(), "mmdpdf_pdfium_debug.txt"),
|
|
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] TryPdfiumSaveWithZeroRotations failed\n" +
|
|
$" source: {sourcePath}\n" +
|
|
$" type: {ex.GetType().FullName}\n" +
|
|
$" msg: {ex.Message}\n" +
|
|
$" stack: {ex.StackTrace}\n\n");
|
|
}
|
|
catch { /* log failure is non-fatal */ }
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|