vendor: import KillerPDF 1.7.5 source (GPL-3.0) as the base for MMD PDF
This commit is contained in:
@@ -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();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user