killerpdf-portable-editor
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseKillerPDF Portable PDF Editor Skill
KillerPDF便携PDF编辑器技能
Skill by ara.so — Daily 2026 Skills collection.
KillerPDF is a single-EXE (~6 MB zipped), portable Windows PDF editor for .NET Framework 4.8. It renders PDFs via PDFium (Docnet.Core), supports inline text editing, annotations, freehand drawing, highlights, signatures, merge/split, full-text search, and printing — all without Adobe, accounts, or telemetry.
该技能由ara.so提供——属于2026每日技能合集。
KillerPDF是一款单EXE格式(压缩后约6MB)的便携Windows PDF编辑器,适用于.NET Framework 4.8。它通过PDFium(Docnet.Core)渲染PDF,支持内嵌文本编辑、批注、手绘、高亮、签名、合并/拆分、全文搜索和打印功能——全程无需Adobe软件、账户或遥测。
What KillerPDF Does
KillerPDF的功能
| Capability | Details |
|---|---|
| Rendering | High-quality PDFium rendering via Docnet.Core |
| Annotation | Text boxes, freehand draw, highlight overlays |
| Editing | Inline text editing with font matching |
| Pages | Merge multiple PDFs, split selected pages, drag-and-drop reorder |
| Signatures | Draw/save reusable signatures, click to place |
| Search | Full-text search with highlighting, drag-select to copy |
| Annotations flattened into output | |
| Distribution | Single EXE, no runtime, no admin rights |
| 功能 | 详情 |
|---|---|
| 渲染 | 通过Docnet.Core实现高质量PDFium渲染 |
| 批注 | 文本框、手绘、高亮覆盖层 |
| 编辑 | 匹配字体的内嵌文本编辑 |
| 页面管理 | 合并多个PDF、拆分指定页面、拖放重排页面 |
| 签名 | 绘制/保存可重复使用的签名,点击即可放置 |
| 搜索 | 带高亮的全文搜索,可拖拽选择复制 |
| 打印 | 批注将被扁平化到输出文件中 |
| 分发 | 单EXE格式,无需运行时环境,无需管理员权限 |
Getting the Binary
获取二进制文件
powershell
undefinedpowershell
undefinedDownload latest prebuilt release
下载最新预构建版本
Invoke-WebRequest -Uri "https://github.com/SteveTheKiller/KillerPDF/releases/latest/download/KillerPDF.zip" -OutFile "KillerPDF.zip"
Expand-Archive -Path "KillerPDF.zip" -DestinationPath ".\KillerPDF"
.\KillerPDF\KillerPDF.exe
No installer, no admin rights required. Just unzip and run.
---Invoke-WebRequest -Uri "https://github.com/SteveTheKiller/KillerPDF/releases/latest/download/KillerPDF.zip" -OutFile "KillerPDF.zip"
Expand-Archive -Path "KillerPDF.zip" -DestinationPath ".\KillerPDF"
.\KillerPDF\KillerPDF.exe
无需安装程序,无需管理员权限。只需解压即可运行。
---Building from Source
从源码构建
Requirements
要求
- Windows 10/11 x64
- .NET 8 SDK or later (to build; output targets .NET Framework 4.8)
- Git
- Windows 10/11 x64
- .NET 8 SDK或更高版本(用于构建;输出目标为.NET Framework 4.8)
- Git
Clone and Build
克隆并构建
powershell
git clone https://github.com/SteveTheKiller/KillerPDF.git
cd KillerPDF
dotnet publish -c ReleaseOutput lands in . The publish step produces:
bin/Release/net48/publish/- — single Costura-bundled executable
KillerPDF.exe - — GPL3 corresponding source archive
KillerPDF-<version>-src.zip
powershell
git clone https://github.com/SteveTheKiller/KillerPDF.git
cd KillerPDF
dotnet publish -c Release输出文件位于目录下。发布步骤会生成:
bin/Release/net48/publish/- — 由Costura打包的单可执行文件
KillerPDF.exe - — 符合GPL3协议的对应源码压缩包
KillerPDF-<version>-src.zip
Project Structure
项目结构
KillerPDF/
├── App.xaml / App.xaml.cs # Application entry, theming
├── MainWindow.xaml / .cs # Primary WPF window, toolbar, page canvas
├── PdfDocument.cs # PDFium wrapper (Docnet.Core), load/save/render
├── PageViewModel.cs # Page data binding, annotation layer
├── AnnotationCanvas.cs # Custom Canvas for drawing/annotation
├── SignatureManager.cs # Save/load/place signatures
├── MergeWindow.xaml / .cs # Merge multiple PDFs UI
├── SplitWindow.xaml / .cs # Page selection and split UI
├── SearchPanel.xaml / .cs # Full-text search UI
├── PrintHelper.cs # Flatten annotations and print
├── Models/
│ ├── Annotation.cs # Base annotation model
│ ├── TextBoxAnnotation.cs
│ ├── DrawingAnnotation.cs
│ └── HighlightAnnotation.cs
└── Resources/
└── Icons/ # SVG/PNG toolbar iconsKillerPDF/
├── App.xaml / App.xaml.cs # 应用入口、主题设置
├── MainWindow.xaml / .cs # 主WPF窗口、工具栏、页面画布
├── PdfDocument.cs # PDFium包装器(Docnet.Core),负责加载/保存/渲染
├── PageViewModel.cs # 页面数据绑定、批注层
├── AnnotationCanvas.cs # 用于绘制/批注的自定义Canvas
├── SignatureManager.cs # 保存/加载/放置签名
├── MergeWindow.xaml / .cs # 合并多个PDF的UI界面
├── SplitWindow.xaml / .cs # 页面选择与拆分UI界面
├── SearchPanel.xaml / .cs # 全文搜索UI界面
├── PrintHelper.cs # 扁平化批注并打印
├── Models/
│ ├── Annotation.cs # 基础批注模型
│ ├── TextBoxAnnotation.cs
│ ├── DrawingAnnotation.cs
│ └── HighlightAnnotation.cs
└── Resources/
└── Icons/ # SVG/PNG工具栏图标Key Dependencies
核心依赖
xml
<!-- KillerPDF.csproj (simplified) -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net48</TargetFramework>
<UseWPF>true</UseWPF>
<AssemblyName>KillerPDF</AssemblyName>
</PropertyGroup>
<ItemGroup>
<!-- PDFium rendering -->
<PackageReference Include="Docnet.Core" Version="2.6.0" />
<!-- Single-EXE bundling -->
<PackageReference Include="Costura.Fody" Version="5.7.0" />
<PackageReference Include="Fody" Version="6.8.0" />
</ItemGroup>
</Project>xml
<!-- KillerPDF.csproj (简化版) -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net48</TargetFramework>
<UseWPF>true</UseWPF>
<AssemblyName>KillerPDF</AssemblyName>
</PropertyGroup>
<ItemGroup>
<!-- PDFium渲染 -->
<PackageReference Include="Docnet.Core" Version="2.6.0" />
<!-- 单EXE打包 -->
<PackageReference Include="Costura.Fody" Version="5.7.0" />
<PackageReference Include="Fody" Version="6.8.0" />
</ItemGroup>
</Project>Core Patterns and Code Examples
核心模式与代码示例
Opening and Rendering a PDF Page
打开并渲染PDF页面
csharp
using Docnet.Core;
using Docnet.Core.Models;
using System.Windows.Media.Imaging;
using System.Runtime.InteropServices;
public class PdfDocument : IDisposable
{
private IDocLib _docLib;
private IDocReader _docReader;
private readonly string _filePath;
public int PageCount { get; private set; }
public void Open(string filePath, string password = "")
{
_filePath = filePath;
_docLib = DocLib.Instance;
_docReader = string.IsNullOrEmpty(password)
? _docLib.GetDocReader(filePath, new PageDimensions(1080, 1920))
: _docLib.GetDocReader(filePath, password, new PageDimensions(1080, 1920));
PageCount = _docReader.GetPageCount();
}
public BitmapSource RenderPage(int pageIndex, double dpi = 150)
{
using var pageReader = _docReader.GetPageReader(pageIndex);
int width = pageReader.GetPageWidth();
int height = pageReader.GetPageHeight();
var rawBytes = pageReader.GetImage(); // BGRA byte array
var bitmap = new WriteableBitmap(width, height, dpi, dpi,
System.Windows.Media.PixelFormats.Bgra32, null);
bitmap.WritePixels(
new System.Windows.Int32Rect(0, 0, width, height),
rawBytes, width * 4, 0);
bitmap.Freeze();
return bitmap;
}
public string GetPageText(int pageIndex)
{
using var pageReader = _docReader.GetPageReader(pageIndex);
return pageReader.GetText();
}
public void Dispose()
{
_docReader?.Dispose();
_docLib?.Dispose();
}
}csharp
using Docnet.Core;
using Docnet.Core.Models;
using System.Windows.Media.Imaging;
using System.Runtime.InteropServices;
public class PdfDocument : IDisposable
{
private IDocLib _docLib;
private IDocReader _docReader;
private readonly string _filePath;
public int PageCount { get; private set; }
public void Open(string filePath, string password = "")
{
_filePath = filePath;
_docLib = DocLib.Instance;
_docReader = string.IsNullOrEmpty(password)
? _docLib.GetDocReader(filePath, new PageDimensions(1080, 1920))
: _docLib.GetDocReader(filePath, password, new PageDimensions(1080, 1920));
PageCount = _docReader.GetPageCount();
}
public BitmapSource RenderPage(int pageIndex, double dpi = 150)
{
using var pageReader = _docReader.GetPageReader(pageIndex);
int width = pageReader.GetPageWidth();
int height = pageReader.GetPageHeight();
var rawBytes = pageReader.GetImage(); // BGRA字节数组
var bitmap = new WriteableBitmap(width, height, dpi, dpi,
System.Windows.Media.PixelFormats.Bgra32, null);
bitmap.WritePixels(
new System.Windows.Int32Rect(0, 0, width, height),
rawBytes, width * 4, 0);
bitmap.Freeze();
return bitmap;
}
public string GetPageText(int pageIndex)
{
using var pageReader = _docReader.GetPageReader(pageIndex);
return pageReader.GetText();
}
public void Dispose()
{
_docReader?.Dispose();
_docLib?.Dispose();
}
}Annotation Model Hierarchy
批注模型层级
csharp
// Models/Annotation.cs
public abstract class Annotation
{
public Guid Id { get; } = Guid.NewGuid();
public int PageIndex { get; set; }
public System.Windows.Rect Bounds { get; set; }
public double Opacity { get; set; } = 1.0;
public abstract UIElement ToUIElement();
public abstract void FlattenToPdfPage(PdfPage page);
}
// Models/TextBoxAnnotation.cs
public class TextBoxAnnotation : Annotation
{
public string Text { get; set; } = "";
public string FontFamily { get; set; } = "Arial";
public double FontSize { get; set; } = 12;
public System.Windows.Media.Color Color { get; set; }
= System.Windows.Media.Colors.Black;
public override UIElement ToUIElement()
{
var tb = new TextBox
{
Text = Text,
FontFamily = new System.Windows.Media.FontFamily(FontFamily),
FontSize = FontSize,
Foreground = new System.Windows.Media.SolidColorBrush(Color),
Background = System.Windows.Media.Brushes.Transparent,
BorderThickness = new Thickness(0),
AcceptsReturn = true,
Width = Bounds.Width,
Height = Bounds.Height,
Opacity = Opacity,
};
Canvas.SetLeft(tb, Bounds.X);
Canvas.SetTop(tb, Bounds.Y);
return tb;
}
public override void FlattenToPdfPage(PdfPage page)
{
// Write text at PDF coordinates using PdfSharp or iTextSharp
// KillerPDF uses PDFium for reading and a lightweight writer for output
}
}
// Models/HighlightAnnotation.cs
public class HighlightAnnotation : Annotation
{
public System.Windows.Media.Color HighlightColor { get; set; }
= System.Windows.Media.Colors.Yellow;
public override UIElement ToUIElement()
{
var rect = new System.Windows.Shapes.Rectangle
{
Fill = new System.Windows.Media.SolidColorBrush(
System.Windows.Media.Color.FromArgb(
(byte)(Opacity * 128),
HighlightColor.R, HighlightColor.G, HighlightColor.B)),
Width = Bounds.Width,
Height = Bounds.Height,
};
Canvas.SetLeft(rect, Bounds.X);
Canvas.SetTop(rect, Bounds.Y);
return rect;
}
public override void FlattenToPdfPage(PdfPage page) { /* flatten */ }
}csharp
// Models/Annotation.cs
public abstract class Annotation
{
public Guid Id { get; } = Guid.NewGuid();
public int PageIndex { get; set; }
public System.Windows.Rect Bounds { get; set; }
public double Opacity { get; set; } = 1.0;
public abstract UIElement ToUIElement();
public abstract void FlattenToPdfPage(PdfPage page);
}
// Models/TextBoxAnnotation.cs
public class TextBoxAnnotation : Annotation
{
public string Text { get; set; } = "";
public string FontFamily { get; set; } = "Arial";
public double FontSize { get; set; } = 12;
public System.Windows.Media.Color Color { get; set; }
= System.Windows.Media.Colors.Black;
public override UIElement ToUIElement()
{
var tb = new TextBox
{
Text = Text,
FontFamily = new System.Windows.Media.FontFamily(FontFamily),
FontSize = FontSize,
Foreground = new System.Windows.Media.SolidColorBrush(Color),
Background = System.Windows.Media.Brushes.Transparent,
BorderThickness = new Thickness(0),
AcceptsReturn = true,
Width = Bounds.Width,
Height = Bounds.Height,
Opacity = Opacity,
};
Canvas.SetLeft(tb, Bounds.X);
Canvas.SetTop(tb, Bounds.Y);
return tb;
}
public override void FlattenToPdfPage(PdfPage page)
{
// 使用PdfSharp或iTextSharp在PDF坐标处写入文本
// KillerPDF使用PDFium读取,使用轻量级写入器输出
}
}
// Models/HighlightAnnotation.cs
public class HighlightAnnotation : Annotation
{
public System.Windows.Media.Color HighlightColor { get; set; }
= System.Windows.Media.Colors.Yellow;
public override UIElement ToUIElement()
{
var rect = new System.Windows.Shapes.Rectangle
{
Fill = new System.Windows.Media.SolidColorBrush(
System.Windows.Media.Color.FromArgb(
(byte)(Opacity * 128),
HighlightColor.R, HighlightColor.G, HighlightColor.B)),
Width = Bounds.Width,
Height = Bounds.Height,
};
Canvas.SetLeft(rect, Bounds.X);
Canvas.SetTop(rect, Bounds.Y);
return rect;
}
public override void FlattenToPdfPage(PdfPage page) { /* 扁平化处理 */ }
}Freehand Drawing on the Annotation Canvas
在批注画布上手绘
csharp
// AnnotationCanvas.cs — custom WPF Canvas
public class AnnotationCanvas : Canvas
{
private Polyline _currentStroke;
private List<Point> _points = new();
public System.Windows.Media.Color PenColor { get; set; }
= System.Windows.Media.Colors.Red;
public double PenThickness { get; set; } = 2.0;
public bool IsDrawingMode { get; set; }
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
if (!IsDrawingMode) return;
_points.Clear();
_currentStroke = new Polyline
{
Stroke = new SolidColorBrush(PenColor),
StrokeThickness = PenThickness,
StrokeLineJoin = PenLineJoin.Round,
StrokeStartLineCap = PenLineCap.Round,
StrokeEndLineCap = PenLineCap.Round,
};
Children.Add(_currentStroke);
CaptureMouse();
AddPoint(e.GetPosition(this));
}
protected override void OnMouseMove(MouseEventArgs e)
{
if (!IsDrawingMode || _currentStroke == null
|| e.LeftButton != MouseButtonState.Pressed) return;
AddPoint(e.GetPosition(this));
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
if (!IsDrawingMode || _currentStroke == null) return;
ReleaseMouseCapture();
// Commit stroke as DrawingAnnotation
var ann = new DrawingAnnotation { Points = _points.ToList(),
Color = PenColor, Thickness = PenThickness };
AnnotationCommitted?.Invoke(this, ann);
_currentStroke = null;
}
private void AddPoint(Point p)
{
_points.Add(p);
_currentStroke.Points.Add(p);
}
public event EventHandler<DrawingAnnotation> AnnotationCommitted;
}csharp
// AnnotationCanvas.cs — 自定义WPF Canvas
public class AnnotationCanvas : Canvas
{
private Polyline _currentStroke;
private List<Point> _points = new();
public System.Windows.Media.Color PenColor { get; set; }
= System.Windows.Media.Colors.Red;
public double PenThickness { get; set; } = 2.0;
public bool IsDrawingMode { get; set; }
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
if (!IsDrawingMode) return;
_points.Clear();
_currentStroke = new Polyline
{
Stroke = new SolidColorBrush(PenColor),
StrokeThickness = PenThickness,
StrokeLineJoin = PenLineJoin.Round,
StrokeStartLineCap = PenLineCap.Round,
StrokeEndLineCap = PenLineCap.Round,
};
Children.Add(_currentStroke);
CaptureMouse();
AddPoint(e.GetPosition(this));
}
protected override void OnMouseMove(MouseEventArgs e)
{
if (!IsDrawingMode || _currentStroke == null
|| e.LeftButton != MouseButtonState.Pressed) return;
AddPoint(e.GetPosition(this));
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
if (!IsDrawingMode || _currentStroke == null) return;
ReleaseMouseCapture();
// 将笔画提交为DrawingAnnotation
var ann = new DrawingAnnotation { Points = _points.ToList(),
Color = PenColor, Thickness = PenThickness };
AnnotationCommitted?.Invoke(this, ann);
_currentStroke = null;
}
private void AddPoint(Point p)
{
_points.Add(p);
_currentStroke.Points.Add(p);
}
public event EventHandler<DrawingAnnotation> AnnotationCommitted;
}Signature Manager
签名管理器
csharp
// SignatureManager.cs
public class SignatureManager
{
private readonly string _sigDir;
public SignatureManager()
{
// Signatures stored next to EXE — portable, no AppData
_sigDir = Path.Combine(
Path.GetDirectoryName(
System.Reflection.Assembly.GetExecutingAssembly().Location)!,
"Signatures");
Directory.CreateDirectory(_sigDir);
}
// Save ink strokes as PNG
public void Save(string name, RenderTargetBitmap bitmap)
{
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
using var fs = File.OpenWrite(Path.Combine(_sigDir, name + ".png"));
encoder.Save(fs);
}
// Load all saved signatures
public IEnumerable<(string Name, BitmapImage Image)> LoadAll()
{
foreach (var file in Directory.EnumerateFiles(_sigDir, "*.png"))
{
var img = new BitmapImage(new Uri(file));
img.Freeze();
yield return (Path.GetFileNameWithoutExtension(file), img);
}
}
// Place signature as Image UIElement on canvas
public Image PlaceSignature(BitmapImage sigImage, Point position,
double scale = 1.0)
{
var img = new Image
{
Source = sigImage,
Width = sigImage.PixelWidth * scale,
Height = sigImage.PixelHeight * scale,
Opacity = 0.9,
};
Canvas.SetLeft(img, position.X);
Canvas.SetTop(img, position.Y);
return img;
}
}csharp
// SignatureManager.cs
public class SignatureManager
{
private readonly string _sigDir;
public SignatureManager()
{
// 签名存储在EXE旁边——便携性,不使用AppData
_sigDir = Path.Combine(
Path.GetDirectoryName(
System.Reflection.Assembly.GetExecutingAssembly().Location)!,
"Signatures");
Directory.CreateDirectory(_sigDir);
}
// 将墨迹笔画保存为PNG
public void Save(string name, RenderTargetBitmap bitmap)
{
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
using var fs = File.OpenWrite(Path.Combine(_sigDir, name + ".png"));
encoder.Save(fs);
}
// 加载所有已保存的签名
public IEnumerable<(string Name, BitmapImage Image)> LoadAll()
{
foreach (var file in Directory.EnumerateFiles(_sigDir, "*.png"))
{
var img = new BitmapImage(new Uri(file));
img.Freeze();
yield return (Path.GetFileNameWithoutExtension(file), img);
}
}
// 在画布上放置签名作为Image UIElement
public Image PlaceSignature(BitmapImage sigImage, Point position,
double scale = 1.0)
{
var img = new Image
{
Source = sigImage,
Width = sigImage.PixelWidth * scale,
Height = sigImage.PixelHeight * scale,
Opacity = 0.9,
};
Canvas.SetLeft(img, position.X);
Canvas.SetTop(img, position.Y);
return img;
}
}Merging PDFs
合并PDF
csharp
// MergeWindow.xaml.cs (core merge logic)
public static void MergePdfs(IEnumerable<string> inputPaths, string outputPath)
{
// KillerPDF uses PDFium C++ API via P/Invoke or a lightweight wrapper
// Conceptual pattern using PdfSharp (or equivalent internal helper):
using var output = new PdfDocument(); // PdfSharp or internal writer
foreach (var path in inputPaths)
{
using var input = PdfReader.Open(path, PdfDocumentOpenMode.Import);
for (int i = 0; i < input.PageCount; i++)
output.AddPage(input.Pages[i]);
}
output.Save(outputPath);
}csharp
// MergeWindow.xaml.cs (核心合并逻辑)
public static void MergePdfs(IEnumerable<string> inputPaths, string outputPath)
{
// KillerPDF通过P/Invoke或轻量级包装器使用PDFium C++ API
// 使用PdfSharp的概念模式(或等效的内部助手):
using var output = new PdfDocument(); // PdfSharp或内部写入器
foreach (var path in inputPaths)
{
using var input = PdfReader.Open(path, PdfDocumentOpenMode.Import);
for (int i = 0; i < input.PageCount; i++)
output.AddPage(input.Pages[i]);
}
output.Save(outputPath);
}Splitting Pages
拆分页面
csharp
public static void SplitPages(string inputPath, IEnumerable<int> pageIndices,
string outputPath)
{
using var input = PdfReader.Open(inputPath, PdfDocumentOpenMode.Import);
using var output = new PdfDocument();
foreach (var idx in pageIndices)
output.AddPage(input.Pages[idx]);
output.Save(outputPath);
}csharp
public static void SplitPages(string inputPath, IEnumerable<int> pageIndices,
string outputPath)
{
using var input = PdfReader.Open(inputPath, PdfDocumentOpenMode.Import);
using var output = new PdfDocument();
foreach (var idx in pageIndices)
output.AddPage(input.Pages[idx]);
output.Save(outputPath);
}Full-Text Search
全文搜索
csharp
// SearchPanel.xaml.cs
public List<(int PageIndex, System.Windows.Rect Rect)> Search(
string query, PdfDocument doc)
{
var results = new List<(int, System.Windows.Rect)>();
var docLib = DocLib.Instance;
using var reader = docLib.GetDocReader(doc.FilePath,
new PageDimensions(1080, 1920));
for (int i = 0; i < reader.GetPageCount(); i++)
{
using var page = reader.GetPageReader(i);
var text = page.GetText();
if (!text.Contains(query, StringComparison.OrdinalIgnoreCase))
continue;
// Get character bounding boxes for highlighting
var chars = page.GetCharacters();
// Match positions and build highlight Rects
// (KillerPDF uses character-level positions from PDFium)
int idx = text.IndexOf(query, StringComparison.OrdinalIgnoreCase);
while (idx >= 0 && idx + query.Length <= chars.Count)
{
var first = chars[idx];
var last = chars[idx + query.Length - 1];
var rect = new System.Windows.Rect(
first.Box.Left, first.Box.Top,
last.Box.Right - first.Box.Left,
last.Box.Bottom - first.Box.Top);
results.Add((i, rect));
idx = text.IndexOf(query, idx + 1,
StringComparison.OrdinalIgnoreCase);
}
}
return results;
}csharp
// SearchPanel.xaml.cs
public List<(int PageIndex, System.Windows.Rect Rect)> Search(
string query, PdfDocument doc)
{
var results = new List<(int, System.Windows.Rect)>();
var docLib = DocLib.Instance;
using var reader = docLib.GetDocReader(doc.FilePath,
new PageDimensions(1080, 1920));
for (int i = 0; i < reader.GetPageCount(); i++)
{
using var page = reader.GetPageReader(i);
var text = page.GetText();
if (!text.Contains(query, StringComparison.OrdinalIgnoreCase))
continue;
// 获取字符边界框用于高亮
var chars = page.GetCharacters();
// 匹配位置并构建高亮Rect
// (KillerPDF使用PDFium提供的字符级位置)
int idx = text.IndexOf(query, StringComparison.OrdinalIgnoreCase);
while (idx >= 0 && idx + query.Length <= chars.Count)
{
var first = chars[idx];
var last = chars[idx + query.Length - 1];
var rect = new System.Windows.Rect(
first.Box.Left, first.Box.Top,
last.Box.Right - first.Box.Left,
last.Box.Bottom - first.Box.Top);
results.Add((i, rect));
idx = text.IndexOf(query, idx + 1,
StringComparison.OrdinalIgnoreCase);
}
}
return results;
}Printing with Flattened Annotations
带扁平化批注的打印
csharp
// PrintHelper.cs
public static void PrintDocument(BitmapSource[] renderedPages, string docTitle)
{
var pd = new PrintDialog();
if (pd.ShowDialog() != true) return;
var doc = new FixedDocument();
foreach (var page in renderedPages)
{
var pageContent = new PageContent();
var fixedPage = new FixedPage
{
Width = pd.PrintableAreaWidth,
Height = pd.PrintableAreaHeight,
};
var img = new Image
{
Source = page,
Width = pd.PrintableAreaWidth,
Height = pd.PrintableAreaHeight,
Stretch = System.Windows.Media.Stretch.Uniform,
};
fixedPage.Children.Add(img);
((IAddChild)pageContent).AddChild(fixedPage);
doc.Pages.Add(pageContent);
}
var xpsWriter = PrintQueue.CreateXpsDocumentWriter(
pd.PrintQueue);
xpsWriter.Write(doc);
}
// Flatten annotations: render page + annotation canvas to BitmapSource
public static BitmapSource FlattenPage(BitmapSource pdfPageBitmap,
AnnotationCanvas canvas, int width, int height)
{
var visual = new DrawingVisual();
using (var ctx = visual.RenderOpen())
{
ctx.DrawImage(pdfPageBitmap,
new System.Windows.Rect(0, 0, width, height));
// Render canvas children over PDF
var canvasBrush = new VisualBrush(canvas);
ctx.DrawRectangle(canvasBrush, null,
new System.Windows.Rect(0, 0, width, height));
}
var rtb = new RenderTargetBitmap(width, height, 96, 96,
System.Windows.Media.PixelFormats.Pbgra32);
rtb.Render(visual);
rtb.Freeze();
return rtb;
}csharp
// PrintHelper.cs
public static void PrintDocument(BitmapSource[] renderedPages, string docTitle)
{
var pd = new PrintDialog();
if (pd.ShowDialog() != true) return;
var doc = new FixedDocument();
foreach (var page in renderedPages)
{
var pageContent = new PageContent();
var fixedPage = new FixedPage
{
Width = pd.PrintableAreaWidth,
Height = pd.PrintableAreaHeight,
};
var img = new Image
{
Source = page,
Width = pd.PrintableAreaWidth,
Height = pd.PrintableAreaHeight,
Stretch = System.Windows.Media.Stretch.Uniform,
};
fixedPage.Children.Add(img);
((IAddChild)pageContent).AddChild(fixedPage);
doc.Pages.Add(pageContent);
}
var xpsWriter = PrintQueue.CreateXpsDocumentWriter(
pd.PrintQueue);
xpsWriter.Write(doc);
}
// 扁平化批注:将页面+批注画布渲染为BitmapSource
public static BitmapSource FlattenPage(BitmapSource pdfPageBitmap,
AnnotationCanvas canvas, int width, int height)
{
var visual = new DrawingVisual();
using (var ctx = visual.RenderOpen())
{
ctx.DrawImage(pdfPageBitmap,
new System.Windows.Rect(0, 0, width, height));
// 在PDF上方渲染画布子元素
var canvasBrush = new VisualBrush(canvas);
ctx.DrawRectangle(canvasBrush, null,
new System.Windows.Rect(0, 0, width, height));
}
var rtb = new RenderTargetBitmap(width, height, 96, 96,
System.Windows.Media.PixelFormats.Pbgra32);
rtb.Render(visual);
rtb.Freeze();
return rtb;
}MainWindow Integration Pattern
MainWindow集成模式
csharp
// MainWindow.xaml.cs — typical usage wiring
public partial class MainWindow : Window
{
private PdfDocument _doc = new();
private SignatureManager _sigManager = new();
private int _currentPage = 0;
private void OpenFile_Click(object sender, RoutedEventArgs e)
{
var dlg = new OpenFileDialog { Filter = "PDF files|*.pdf" };
if (dlg.ShowDialog() != true) return;
_doc.Open(dlg.FileName);
_currentPage = 0;
ShowPage(_currentPage);
}
private void ShowPage(int index)
{
PageImage.Source = _doc.RenderPage(index, dpi: 150);
AnnotLayer.Children.Clear(); // AnnotationCanvas reset
}
private void DrawMode_Click(object sender, RoutedEventArgs e)
{
AnnotLayer.IsDrawingMode = !AnnotLayer.IsDrawingMode;
AnnotLayer.PenColor = SelectedColor; // from color picker
AnnotLayer.PenThickness = PenSizeSlider.Value;
}
private void AddTextBox_Click(object sender, RoutedEventArgs e)
{
var ann = new TextBoxAnnotation
{
PageIndex = _currentPage,
Bounds = new Rect(50, 50, 200, 40),
Text = "Double-click to edit",
};
AnnotLayer.Children.Add(ann.ToUIElement());
}
private void PlaceSignature_Click(object sender, RoutedEventArgs e)
{
if (SignatureList.SelectedItem is not (string _, BitmapImage sig)) return;
var placed = _sigManager.PlaceSignature(sig,
new Point(100, 100), scale: 0.5);
AnnotLayer.Children.Add(placed);
}
private void Save_Click(object sender, RoutedEventArgs e)
{
// Flatten and save — render each page with its annotation canvas
// then write combined output PDF
}
private void Print_Click(object sender, RoutedEventArgs e)
{
var pages = Enumerable.Range(0, _doc.PageCount)
.Select(i => PrintHelper.FlattenPage(
_doc.RenderPage(i, 150), AnnotLayer, 1080, 1920))
.ToArray();
PrintHelper.PrintDocument(pages, "KillerPDF Document");
}
}csharp
// MainWindow.xaml.cs — 典型用法绑定
public partial class MainWindow : Window
{
private PdfDocument _doc = new();
private SignatureManager _sigManager = new();
private int _currentPage = 0;
private void OpenFile_Click(object sender, RoutedEventArgs e)
{
var dlg = new OpenFileDialog { Filter = "PDF files|*.pdf" };
if (dlg.ShowDialog() != true) return;
_doc.Open(dlg.FileName);
_currentPage = 0;
ShowPage(_currentPage);
}
private void ShowPage(int index)
{
PageImage.Source = _doc.RenderPage(index, dpi: 150);
AnnotLayer.Children.Clear(); // 重置批注画布
}
private void DrawMode_Click(object sender, RoutedEventArgs e)
{
AnnotLayer.IsDrawingMode = !AnnotLayer.IsDrawingMode;
AnnotLayer.PenColor = SelectedColor; // 来自颜色选择器
AnnotLayer.PenThickness = PenSizeSlider.Value;
}
private void AddTextBox_Click(object sender, RoutedEventArgs e)
{
var ann = new TextBoxAnnotation
{
PageIndex = _currentPage,
Bounds = new Rect(50, 50, 200, 40),
Text = "双击编辑",
};
AnnotLayer.Children.Add(ann.ToUIElement());
}
private void PlaceSignature_Click(object sender, RoutedEventArgs e)
{
if (SignatureList.SelectedItem is not (string _, BitmapImage sig)) return;
var placed = _sigManager.PlaceSignature(sig,
new Point(100, 100), scale: 0.5);
AnnotLayer.Children.Add(placed);
}
private void Save_Click(object sender, RoutedEventArgs e)
{
// 扁平化并保存——渲染每个页面及其批注画布
// 然后写入合并后的输出PDF
}
private void Print_Click(object sender, RoutedEventArgs e)
{
var pages = Enumerable.Range(0, _doc.PageCount)
.Select(i => PrintHelper.FlattenPage(
_doc.RenderPage(i, 150), AnnotLayer, 1080, 1920))
.ToArray();
PrintHelper.PrintDocument(pages, "KillerPDF文档");
}
}XAML Layout Snippet
XAML布局片段
xml
<!-- MainWindow.xaml (simplified) -->
<Window x:Class="KillerPDF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:KillerPDF"
Title="KillerPDF" Width="1200" Height="800"
Background="#1E1E1E">
<DockPanel>
<!-- Toolbar -->
<ToolBar DockPanel.Dock="Top" Background="#2D2D2D">
<Button Content="Open" Click="OpenFile_Click"/>
<Button Content="Save" Click="Save_Click"/>
<Button Content="Merge" Click="Merge_Click"/>
<Button Content="Split" Click="Split_Click"/>
<Separator/>
<Button Content="Text" Click="AddTextBox_Click"/>
<Button Content="Draw" Click="DrawMode_Click"/>
<Button Content="Highlight" Click="AddHighlight_Click"/>
<Button Content="Sign" Click="PlaceSignature_Click"/>
<Separator/>
<Button Content="Search" Click="ToggleSearch_Click"/>
<Button Content="Print" Click="Print_Click"/>
</ToolBar>
<!-- Page display + annotation overlay -->
<ScrollViewer VerticalScrollBarVisibility="Auto">
<Grid>
<Image x:Name="PageImage" Stretch="Uniform"/>
<!-- Custom canvas sits on top of PDF page image -->
<local:AnnotationCanvas x:Name="AnnotLayer"
Background="Transparent"
AnnotationCommitted="AnnotLayer_AnnotationCommitted"/>
</Grid>
</ScrollViewer>
</DockPanel>
</Window>xml
<!-- MainWindow.xaml (简化版) -->
<Window x:Class="KillerPDF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:KillerPDF"
Title="KillerPDF" Width="1200" Height="800"
Background="#1E1E1E">
<DockPanel>
<!-- 工具栏 -->
<ToolBar DockPanel.Dock="Top" Background="#2D2D2D">
<Button Content="打开" Click="OpenFile_Click"/>
<Button Content="保存" Click="Save_Click"/>
<Button Content="合并" Click="Merge_Click"/>
<Button Content="拆分" Click="Split_Click"/>
<Separator/>
<Button Content="文本" Click="AddTextBox_Click"/>
<Button Content="绘制" Click="DrawMode_Click"/>
<Button Content="高亮" Click="AddHighlight_Click"/>
<Button Content="签名" Click="PlaceSignature_Click"/>
<Separator/>
<Button Content="搜索" Click="ToggleSearch_Click"/>
<Button Content="打印" Click="Print_Click"/>
</ToolBar>
<!-- 页面显示 + 批注覆盖层 -->
<ScrollViewer VerticalScrollBarVisibility="Auto">
<Grid>
<Image x:Name="PageImage" Stretch="Uniform"/>
<!-- 自定义画布位于PDF页面图像上方 -->
<local:AnnotationCanvas x:Name="AnnotLayer"
Background="Transparent"
AnnotationCommitted="AnnotLayer_AnnotationCommitted"/>
</Grid>
</ScrollViewer>
</DockPanel>
</Window>Configuration
配置
KillerPDF is intentionally config-free for end users. For contributors:
| Setting | Where |
|---|---|
| Default DPI for rendering | |
| Pen defaults | |
| Signature storage path | |
| Single-EXE bundling | |
| Build target framework | |
No registry writes, no usage — everything lives alongside the EXE.
%APPDATA%KillerPDF针对终端用户故意设计为无需配置。对于贡献者:
| 设置 | 位置 |
|---|---|
| 渲染默认DPI | |
| 画笔默认值 | |
| 签名存储路径 | |
| 单EXE打包 | |
| 构建目标框架 | |
不写入注册表,不使用——所有内容都与EXE共存。
%APPDATA%Common Patterns
常见模式
Drag-and-Drop Page Reordering
拖放重排页面
csharp
// Bind a ListBox of page thumbnails; handle drag events
private void PageList_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed && _dragItem != null)
DragDrop.DoDragDrop(PageList, _dragItem, DragDropEffects.Move);
}
private void PageList_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetData(typeof(PageViewModel)) is not PageViewModel src) return;
var target = (PageViewModel)((ListBoxItem)e.OriginalSource
.FindAncestor<ListBoxItem>()).DataContext;
int from = Pages.IndexOf(src);
int to = Pages.IndexOf(target);
Pages.Move(from, to);
}csharp
// 绑定页面缩略图列表框;处理拖拽事件
private void PageList_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed && _dragItem != null)
DragDrop.DoDragDrop(PageList, _dragItem, DragDropEffects.Move);
}
private void PageList_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetData(typeof(PageViewModel)) is not PageViewModel src) return;
var target = (PageViewModel)((ListBoxItem)e.OriginalSource
.FindAncestor<ListBoxItem>()).DataContext;
int from = Pages.IndexOf(src);
int to = Pages.IndexOf(target);
Pages.Move(from, to);
}Color Picker for Annotations
批注颜色选择器
csharp
// Simple popup color picker; or use a third-party WPF color picker NuGet
private void ColorButton_Click(object sender, RoutedEventArgs e)
{
var picker = new ColorPickerDialog { SelectedColor = AnnotLayer.PenColor };
if (picker.ShowDialog() == true)
AnnotLayer.PenColor = picker.SelectedColor;
}csharp
// 简单弹出式颜色选择器;或使用第三方WPF颜色选择器NuGet包
private void ColorButton_Click(object sender, RoutedEventArgs e)
{
var picker = new ColorPickerDialog { SelectedColor = AnnotLayer.PenColor };
if (picker.ShowDialog() == true)
AnnotLayer.PenColor = picker.SelectedColor;
}Troubleshooting
故障排除
| Problem | Cause | Fix |
|---|---|---|
| PDFium native missing from bundle | Run |
| Blank page rendered | Wrong | Verify |
| High memory on large PDFs | Holding all pages rendered simultaneously | Render on demand; dispose |
| Annotations lost on save | Flattening step skipped | Call |
Build error | Building with old SDK | Install .NET 8 SDK; it can cross-target to net48 |
| Signatures not found | EXE moved without | |
| WPF designer crash | Costura not compatible with design-time | Ignore design-time errors; run normally to test |
| 问题 | 原因 | 解决方法 |
|---|---|---|
| PDFium原生文件未打包 | 运行 |
| 渲染空白页面 | | 验证 |
| 大PDF占用高内存 | 同时渲染所有页面 | 按需渲染;每次渲染后释放 |
| 保存后批注丢失 | 跳过了扁平化步骤 | 在写入输出PDF前为每个页面调用 |
构建错误 | 使用旧SDK构建 | 安装.NET 8 SDK;它可以跨目标编译到net48 |
| 找不到签名 | EXE移动时未携带 | |
| WPF设计器崩溃 | Costura与设计时不兼容 | 忽略设计时错误;正常运行测试 |
License Note
许可证说明
KillerPDF is GPLv3. Any fork, modification, or redistribution — including commercial rebrands — must be released under GPLv3 with source available. The build pipeline automatically produces a for GPL compliance.
KillerPDF-<version>-src.zipGPLv3 — https://github.com/SteveTheKiller/KillerPDF/blob/main/LICENSEKillerPDF采用GPLv3协议。任何分叉、修改或重新分发——包括商业重命名——都必须以GPLv3协议发布并提供源码。构建流水线会自动生成以符合GPL合规要求。
KillerPDF-<version>-src.zipGPLv3 — https://github.com/SteveTheKiller/KillerPDF/blob/main/LICENSE