vendor: import KillerPDF 1.7.5 source (GPL-3.0) as the base for MMD PDF

This commit is contained in:
2026-08-27 06:58:22 +02:00
commit 532485a830
577 changed files with 149058 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Docnet.Core;
using Docnet.Core.Models;
using Microsoft.Win32;
using PdfSharpCore.Drawing;
using PdfSharpCore.Pdf;
using PdfSharpCore.Pdf.IO;
using KillerPDF.Services;
using PdfPigDoc = UglyToad.PdfPig.PdfDocument;
namespace KillerPDF
{
public partial class MainWindow
{
// ============================================================
// Temp save/reload
// ============================================================
private void SaveTempAndReload(bool keepAnnotations = false, bool preserveZoom = false)
{
if (_doc is null || _currentFile is null) return;
// We're about to replace the working file with a fresh temp; release the cached PDFium link
// handle for the outgoing file now (it reopens for the new temp on the post-reload re-render).
CloseLinkPdfiumDoc();
// Stop render workers tied to the outgoing file before clearing the cache. Without this,
// an already-running Grid or Continuous task can finish after the clear and put an old
// page bitmap straight back into the active session.
_secondaryRenderCts?.Cancel();
_continuousRenderCts?.Cancel();
_continuousSharpenCts?.Cancel();
// Overlay annotations are unsaved, still-editable user work. Callers that don't change
// page identity (crop) pass keepAnnotations:true so annotations on other pages survive
// the reload and stay selectable/movable; they are re-rendered after the doc reopens.
if (!keepAnnotations) _annotations.Clear();
_renderDims.Clear();
ActiveViewer.InvalidateRenderCacheExt(_active); // pages changed pixels / order: drop this tab's cached bitmaps
_renderedPrimaryPage = -1; // force a re-render after reload even if the same page stays selected (e.g. rotate)
ClearSelection();
MarkDirty();
var doc = _doc;
int selectedIdx = PageList.SelectedIndex;
// Capture page rotations, then strip them from the document before saving.
// Docnet uses FPDF_GetPageWidth/Height (MediaBox, no rotation) to size the bitmap,
// then renders with PDFium's page CTM which *does* include /Rotate. For 90/270
// the rendered landscape content overflows the portrait-sized bitmap and gets clipped.
// Stripping /Rotate to 0 before saving means Docnet renders clean unrotated content
// that fits the bitmap; RotateBitmap is applied in each render path instead.
_pageRotations.Clear();
for (int i = 0; i < doc.PageCount; i++)
{
int rot = ((doc.Pages[i].Rotate % 360) + 360) % 360;
_pageRotations[i] = rot;
doc.Pages[i].Rotate = 0;
}
var tempPath = App.MakeTempFile("temp");
try
{
PdfScrub.ScrubEmptyOutlines(doc); // #103: never write a dangling /Outlines reference
PdfScrub.ScrubDegenerateCropBoxes(doc); // never write a zero-size /CropBox (Adobe out-of-range)
doc.Save(tempPath);
doc.Close();
}
catch (Exception saveEx) when (PdfImport.IsXRefException(saveEx))
{
// PdfSharpCore fails to re-save encrypted PDFs (e.g. owner-restricted RC4 files)
// because it encounters cross-reference tokens while serializing dirty objects.
// Primary fallback: use PDFium (already initialized for the page preview) to
// load the source, strip all /Rotate values, remove encryption, and save.
// Secondary fallback: PdfSharpCore Import mode (works on some non-encrypted xref
// issues but fails on encrypted files; kept as a last resort).
doc.Close();
_doc = null;
if (!PdfiumInterop.TryPdfiumSaveWithZeroRotations(_currentFile!, tempPath) &&
!PdfImport.TryImportRepairToPath(_currentFile!, tempPath, stripRotations: true))
throw; // re-throw original if both fallbacks fail
}
// PdfSharpCore sometimes saves a file where one object's xref offset points at the
// xref table itself (object N offset = xref table position). When PdfSharp then tries
// to re-open that file in Modify mode it seeks to the xref table, reads the keyword
// "xref" as a token in an object context, and throws "Unexpected token 'xref'".
// Fix: catch the reopen failure, pipe the saved file through PDFium (which has
// robust error recovery and will rewrite a correct xref), then retry the open.
try
{
_doc = PdfReader.Open(tempPath, PdfDocumentOpenMode.Modify);
}
catch (Exception openEx) when (PdfImport.IsXRefException(openEx))
{
var fixedPath = App.MakeTempFile("fixed");
if (!PdfiumInterop.TryPdfiumSaveWithZeroRotations(tempPath, fixedPath))
throw; // PDFium also failed - re-throw original reopen error
tempPath = fixedPath;
_doc = PdfReader.Open(tempPath, PdfDocumentOpenMode.Modify);
}
_currentFile = tempPath;
// Clear once more after the old workers have observed cancellation. This closes the race
// where a worker was already inside PDFium when the first clear happened and published its
// stale result while the edited document was being saved and reopened.
ActiveViewer.InvalidateRenderCacheExt(_active);
// Restore rotations in the reopened in-memory doc so saves, form fields,
// and all other operations see the correct rotation values.
foreach (var kv in _pageRotations)
_doc.Pages[kv.Key].Rotate = kv.Value;
RefreshPageList();
if (selectedIdx >= 0 && selectedIdx < PageList.Items.Count)
PageList.SelectedIndex = selectedIdx;
else if (PageList.Items.Count > 0)
PageList.SelectedIndex = 0;
// In Continuous view the strip caches one rendered slot per page. After a
// page-modifying reload (e.g. crop) it must be rebuilt so the main view reflects the
// new pages; the slot-sizing in RenderContinuousPages makes cropped pages fit cleanly.
if (_viewMode == ViewMode.Continuous)
{
int contIdx = PageList.SelectedIndex;
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded,
(Action)(() => ActiveViewer.SetupContinuousView(contIdx)));
return;
}
// Refit synchronously so the first rendered frame uses the correct zoom. For crop we instead
// keep the current zoom (preserveZoom) so the page doesn't jump to fit the smaller cropped size -
// the user just wanted the cropped-away area removed, not a zoom change.
PagePreviewPanel.ScrollToHorizontalOffset(0);
if (preserveZoom) { _fitMode = FitMode.None; ActiveViewer.ApplyZoom(); }
else ActiveViewer.ReapplyGridOrFit();
// Deferred refit after layout settles for accurate ActualWidth.
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded, (Action)(() =>
{
PagePreviewPanel.ScrollToHorizontalOffset(0);
// RefreshPageView only manages secondary tiles and links. It does not repaint the
// primary Image, which is why the thumbnail changed after an edit while the document
// stayed stale until a view-mode switch called RenderPage. Render the primary first,
// then fit the new page dimensions.
int refreshPage = _viewMode == ViewMode.Grid ? 0 : _currentPage;
if (refreshPage >= 0) ActiveViewer.RenderPage(refreshPage);
if (preserveZoom) ActiveViewer.ApplyZoom();
else ActiveViewer.ReapplyGridOrFit();
}));
}
}
}