160 lines
8.5 KiB
C#
160 lines
8.5 KiB
C#
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();
|
|
}));
|
|
}
|
|
}
|
|
}
|