Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/build-and-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ jobs:
"number=$version" >> $env:GITHUB_OUTPUT
Write-Host "Building version $version"

- name: Restore dependencies
- name: Run tests
run: dotnet test tests\OneNoteExporter.Tests\OneNoteExporter.Tests.csproj --configuration Release

- name: Restore win-x64 dependencies
run: dotnet restore OneNoteExporter.csproj --runtime win-x64

- name: Publish self-contained application
Expand Down
2 changes: 1 addition & 1 deletion AboutWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
</Border>

<StackPanel Grid.Row="2">
<TextBlock Text="Supports OneNote packages, PDF, XPS and local backup copies."
<TextBlock Text="Supports OneNote packages, HTML, PDF, XPS and local backup copies."
TextWrapping="Wrap"
TextAlignment="Center"
Foreground="{StaticResource TextSecondary}"
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 1.3.0

- Added page-by-page HTML export for complete notebooks and selected sections.
- Added generated browser indexes while preserving notebook section-group structure.
- Protected previous HTML exports with staged directory swaps and automatic recovery.
- Added collision-safe notebook, section, and page folders plus bounded OneNote COM recovery.

## 1.2.2

- Simplified the large PDF/XPS export choice to one short explanation and three clear actions.
Expand Down
7 changes: 6 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code when working with code in this reposi

## Project Overview

OneNote Backup Exporter (C# WPF Edition) – a pure C# WPF desktop application for exporting OneNote notebooks via the COM API to multiple formats (.onepkg, .xps, .pdf). This is a migration of the original Go/Wails project; the JSON-RPC subprocess layer has been eliminated – the WPF app calls `OneNoteService` directly in-process.
OneNote Backup Exporter (C# WPF Edition) – a pure C# WPF desktop application for exporting OneNote notebooks via the COM API to multiple formats (.onepkg, HTML, .xps, .pdf). This is a migration of the original Go/Wails project; the JSON-RPC subprocess layer has been eliminated – the WPF app calls `OneNoteService` directly in-process.

## Architecture

Expand Down Expand Up @@ -94,13 +94,18 @@ in the project, installer metadata, Git tag, GitHub release title, and About dia
jobs therefore show a preflight choice: automatically split into individual
section exports (recommended), switch to `.onepkg`, or explicitly try the whole
rendered export anyway
9. HTML is exported page by page with `pfHTML`. The app builds a browsable index and
stages the complete directory tree before replacing the previous
`<Notebook>__<stable-id>_html` folder, because OneNote emits `.htm` pages plus
companion asset folders.

### Export Formats
| UI value | PublishFormat | Extension |
|---|---|---|
| `"onepkg"` | `pfOneNotePackage` | `.onepkg` |
| `"xps"` | `pfXPS` | `.xps` |
| `"pdf"` | `pfPDF` | `.pdf` |
| `"html"` | `pfHTML` page jobs | `<Notebook>__<stable-id>_html/index.html` |
| `"localbackup"` | n/a (file copy) | n/a |

### Local Backup Mode
Expand Down
3 changes: 3 additions & 0 deletions MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@
<ComboBoxItem Tag="pdf">
PDF Document (.pdf) – Universal, lower quality
</ComboBoxItem>
<ComboBoxItem Tag="html">
HTML Web Pages – Browser-readable folder with index
</ComboBoxItem>
<ComboBoxItem x:Name="LocalBackupItem" Tag="localbackup">
Local Backup Copy – Fast, direct file copy
</ComboBoxItem>
Expand Down
1 change: 1 addition & 0 deletions Models/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class SectionInfo
public string NotebookName { get; set; } = "";
public string NotebookPath { get; set; } = "";
public string GroupName { get; set; } = ""; // empty when section is directly in notebook
public string GroupExportPath { get; set; } = ""; // collision-safe HTML group path
public string ExportFileStem { get; set; } = ""; // collision-safe; UI continues to show Name
public bool IsCloud { get; set; }
}
Expand Down
6 changes: 5 additions & 1 deletion OneNoteExporter.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

<AssemblyName>OneNoteExporter</AssemblyName>
<RootNamespace>OneNoteExporter</RootNamespace>
<Version>1.2.2</Version>
<Version>1.3.0</Version>
<ApplicationIcon>icon.ico</ApplicationIcon>
</PropertyGroup>

Expand All @@ -25,4 +25,8 @@
<PackageReference Include="Interop.Microsoft.Office.Interop.OneNote" Version="1.1.0.2" />
</ItemGroup>

<ItemGroup>
<Compile Remove="tests\**\*.cs" />
</ItemGroup>

</Project>
6 changes: 6 additions & 0 deletions OneNoteExporter.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ VisualStudioVersion = 17.8.34316.72
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneNoteExporter", "OneNoteExporter.csproj", "{3F2504E0-4F89-41D3-9A0C-0305E82C3301}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneNoteExporter.Tests", "tests\OneNoteExporter.Tests\OneNoteExporter.Tests.csproj", "{C149C964-3A10-4B2C-B41F-A0A53D77D6FD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x86 = Debug|x86
Expand All @@ -15,6 +17,10 @@ Global
{3F2504E0-4F89-41D3-9A0C-0305E82C3301}.Debug|x86.Build.0 = Debug|x86
{3F2504E0-4F89-41D3-9A0C-0305E82C3301}.Release|x86.ActiveCfg = Release|x86
{3F2504E0-4F89-41D3-9A0C-0305E82C3301}.Release|x86.Build.0 = Release|x86
{C149C964-3A10-4B2C-B41F-A0A53D77D6FD}.Debug|x86.ActiveCfg = Debug|Any CPU
{C149C964-3A10-4B2C-B41F-A0A53D77D6FD}.Debug|x86.Build.0 = Debug|Any CPU
{C149C964-3A10-4B2C-B41F-A0A53D77D6FD}.Release|x86.ActiveCfg = Release|Any CPU
{C149C964-3A10-4B2C-B41F-A0A53D77D6FD}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
3 changes: 3 additions & 0 deletions Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("OneNoteExporter.Tests")]
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
A Windows desktop application to export OneNote notebooks to portable formats.
Talks directly to OneNote Desktop via the COM API.

![version](https://img.shields.io/badge/version-1.2.2-blueviolet?style=flat-square)
![version](https://img.shields.io/badge/version-1.3.0-blueviolet?style=flat-square)
![platform](https://img.shields.io/badge/platform-Windows%2010%2F11-0078D7?style=flat-square&logo=windows&logoColor=white)
![license](https://img.shields.io/badge/license-MIT-brightgreen?style=flat-square)
![built with](https://img.shields.io/badge/built%20with-.NET%2010%20%2F%20WPF-512BD4?style=flat-square&logo=dotnet&logoColor=white)
Expand All @@ -33,14 +33,14 @@ About dialog.

## Automated builds and releases

GitHub Actions builds a self-contained Windows x64 application and compiles the Inno Setup
installer for every pull request and every push to `main`. The installer and its SHA-256
checksum are available as workflow artifacts for 14 days.
GitHub Actions runs the automated tests, builds a self-contained Windows x64 application,
and compiles the Inno Setup installer for every pull request and every push to `main`. The
installer and its SHA-256 checksum are available as workflow artifacts for 14 days.

To publish a release:

1. Update `<Version>` in `OneNoteExporter.csproj` and commit the change.
2. Create and push a matching tag, for example `v1.2.2`.
2. Create and push a matching tag, for example `v1.3.0`.
3. GitHub Actions builds the installer, creates the GitHub release with generated notes, and
uploads the installer plus checksum automatically.

Expand All @@ -55,11 +55,10 @@ The Microsoft Store version of OneNote is not supported.

## Export Formats

![Available export formats](FormatDemoBild.png)

| Format | Notes |
|---|---|
| OneNote Package (.onepkg) | Recommended. Full-fidelity, re-importable into OneNote. |
| HTML Web Pages | Browser-readable page folders plus generated notebook/section indexes. OneNote writes each page as `.htm` with any companion assets beside it. |
| XPS Document (.xps) | Good layout preservation. Large whole notebooks can be split into safer per-section exports. |
| PDF Document (.pdf) | Universal, slightly lower quality on complex pages. Large whole notebooks can be split into safer per-section exports. |
| Local Backup Copy | Copies the raw backup folder from `%LOCALAPPDATA%\Microsoft\OneNote\16.0\Sicherung`. Fastest option, all notebooks at once. |
Expand All @@ -68,6 +67,14 @@ The Microsoft Store version of OneNote is not supported.

- Password-protected sections cannot be exported. Unlock them in OneNote first.
- Cloud notebooks (OneDrive, SharePoint) must be synced before export. Large notebooks can take several minutes.
- HTML exports create a `<Notebook>__<stable-id>_html` folder with an `index.html` start
page. The stable suffix prevents same-named notebooks from overwriting each other. Complete
notebook exports link directly to every page; selected-section exports create or refresh
the notebook start page with links to the available section indexes. Each OneNote page is
published separately so notebook and section selections work even though OneNote's COM
API only provides reliable HTML publishing at page level. Section groups remain folders;
OneNote subpage levels are currently listed as normal pages. HTML is intended for offline
browser access and is not a re-importable replacement for `.onepkg`.
- OneNote does not expose notebook size through its COM interface. Before a whole-notebook
PDF/XPS export, the app therefore offers to export each section separately (recommended),
create a complete `.onepkg` backup, or consciously try the whole rendered export. Split
Expand Down
152 changes: 136 additions & 16 deletions Services/OneNoteComWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,29 +70,149 @@ public Task<ExportResult> ExportNotebookAsync(
string exportFormat = "onepkg",
IProgress<string>? progress = null,
CancellationToken ct = default)
=> RunSerializedAsync(
() => ExportAsync(
service => service.BeginNotebookExport(
notebookId, destinationPath, exportFormat, progress),
exportFormat,
progress,
ct),
ct);
=> exportFormat.Equals("html", StringComparison.OrdinalIgnoreCase)
? RunSerializedAsync(
() => ExportHtmlAsync(
service => service.BeginNotebookHtmlExport(
notebookId, destinationPath, progress),
progress,
ct),
ct)
: RunSerializedAsync(
() => ExportAsync(
service => service.BeginNotebookExport(
notebookId, destinationPath, exportFormat, progress),
exportFormat,
progress,
ct),
ct);

public Task<ExportResult> ExportSectionAsync(
SectionInfo section,
string destinationPath,
string exportFormat = "onepkg",
IProgress<string>? progress = null,
CancellationToken ct = default)
=> RunSerializedAsync(
() => ExportAsync(
service => service.BeginSectionExport(
section, destinationPath, exportFormat, progress),
exportFormat,
progress,
ct),
ct);
=> exportFormat.Equals("html", StringComparison.OrdinalIgnoreCase)
? RunSerializedAsync(
() => ExportHtmlAsync(
service => service.BeginSectionHtmlExport(
section, destinationPath, progress),
progress,
ct),
ct)
: RunSerializedAsync(
() => ExportAsync(
service => service.BeginSectionExport(
section, destinationPath, exportFormat, progress),
exportFormat,
progress,
ct),
ct);

private async Task<ExportResult> ExportHtmlAsync(
Func<OneNoteService, HtmlExportPlan> beginExport,
IProgress<string>? progress,
CancellationToken ct)
{
bool connectionRecoveryAttempted = false;

for (int attempt = 1; attempt <= MaxExportAttempts; attempt++)
{
try
{
HtmlExportPlan plan = await InvokeAsync(beginExport, ct).ConfigureAwait(false);

for (int pageIndex = 0; pageIndex < plan.Pages.Count; pageIndex++)
{
ct.ThrowIfCancellationRequested();
HtmlPageExport page = plan.Pages[pageIndex];
progress?.Report(
$"HTML page {pageIndex + 1}/{plan.Pages.Count}: {page.Name}");

await InvokeAsync(
service => service.BeginHtmlPageExport(plan, page, progress),
ct)
.ConfigureAwait(false);
ExportResult pageResult = await OneNoteService.WaitForHtmlPageAsync(
page, plan.IsCloud, progress, ct)
.ConfigureAwait(false);

if (!pageResult.Success)
{
pageResult.Message +=
" The existing HTML export was not changed. A temporary " +
"staging directory may be retained until automatic cleanup.";
return pageResult;
}
}

progress?.Report("Building the HTML index and finalizing the export...");
ct.ThrowIfCancellationRequested();
var result = new ExportResult
{
Success = true,
Message = $"Exported {plan.Pages.Count} HTML page(s) successfully."
};
ExportResult finalResult = OneNoteService.FinalizeHtmlExport(plan, result);
finalResult.AttemptCount = attempt;
finalResult.RecoveredAfterRetry = attempt > 1;

if (finalResult.RecoveredAfterRetry)
{
finalResult.Message +=
$" Export succeeded after OneNote communication recovery " +
$"(attempt {attempt}/{MaxExportAttempts}).";
}

return finalResult;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex) when (
attempt < MaxExportAttempts &&
OneNoteService.IsRecoverableComFailure(ex))
{
string failure = OneNoteService.DescribeComFailure(ex);
bool resetConnection = OneNoteService.RequiresComConnectionReset(ex);

progress?.Report(
$"OneNote communication failed: {failure}. " +
$"Automatic retry {attempt + 1}/{MaxExportAttempts} in " +
$"{ExportRetryDelay.TotalSeconds:0} seconds...");
await Task.Delay(ExportRetryDelay, ct).ConfigureAwait(false);

if (resetConnection)
{
progress?.Report("Reconnecting to OneNote Desktop...");

try
{
await RecreateServiceAsync(ct).ConfigureAwait(false);
connectionRecoveryAttempted = true;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception recoveryException)
{
return OneNoteService.CreateConnectionRecoveryFailure(
ex, recoveryException);
}
}
}
catch (Exception ex)
{
return OneNoteService.CreateExportFailure(
ex, attempt, "html", connectionRecoveryAttempted);
}
}

throw new InvalidOperationException("The HTML export retry loop ended unexpectedly.");
}

private async Task<ExportResult> ExportAsync(
Func<OneNoteService, PendingExport> beginExport,
Expand Down
Loading
Loading