killerpdf-portable-editor

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

KillerPDF 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的功能

CapabilityDetails
RenderingHigh-quality PDFium rendering via Docnet.Core
AnnotationText boxes, freehand draw, highlight overlays
EditingInline text editing with font matching
PagesMerge multiple PDFs, split selected pages, drag-and-drop reorder
SignaturesDraw/save reusable signatures, click to place
SearchFull-text search with highlighting, drag-select to copy
PrintAnnotations flattened into output
DistributionSingle EXE, no runtime, no admin rights

功能详情
渲染通过Docnet.Core实现高质量PDFium渲染
批注文本框、手绘、高亮覆盖层
编辑匹配字体的内嵌文本编辑
页面管理合并多个PDF、拆分指定页面、拖放重排页面
签名绘制/保存可重复使用的签名,点击即可放置
搜索带高亮的全文搜索,可拖拽选择复制
打印批注将被扁平化到输出文件中
分发单EXE格式,无需运行时环境,无需管理员权限

Getting the Binary

获取二进制文件

powershell
undefined
powershell
undefined

Download 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 Release
Output lands in
bin/Release/net48/publish/
. The publish step produces:
  • KillerPDF.exe
    — single Costura-bundled executable
  • KillerPDF-<version>-src.zip
    — GPL3 corresponding source archive
powershell
git clone https://github.com/SteveTheKiller/KillerPDF.git
cd KillerPDF
dotnet publish -c Release
输出文件位于
bin/Release/net48/publish/
目录下。发布步骤会生成:
  • KillerPDF.exe
    — 由Costura打包的单可执行文件
  • KillerPDF-<version>-src.zip
    — 符合GPL3协议的对应源码压缩包

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 icons

KillerPDF/
├── 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:
SettingWhere
Default DPI for rendering
PdfDocument.RenderPage(dpi:)
parameter
Pen defaults
AnnotationCanvas.PenColor
,
.PenThickness
Signature storage path
SignatureManager._sigDir
(next to EXE)
Single-EXE bundling
FodyWeavers.xml
(Costura config)
Build target framework
KillerPDF.csproj
<TargetFramework>net48</TargetFramework>
No registry writes, no
%APPDATA%
usage — everything lives alongside the EXE.

KillerPDF针对终端用户故意设计为无需配置。对于贡献者:
设置位置
渲染默认DPI
PdfDocument.RenderPage(dpi:)
参数
画笔默认值
AnnotationCanvas.PenColor
,
.PenThickness
签名存储路径
SignatureManager._sigDir
(位于EXE旁边)
单EXE打包
FodyWeavers.xml
(Costura配置)
构建目标框架
KillerPDF.csproj
<TargetFramework>net48</TargetFramework>
不写入注册表,不使用
%APPDATA%
——所有内容都与EXE共存。

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

故障排除

ProblemCauseFix
DllNotFoundException: pdfium.dll
PDFium native missing from bundleRun
dotnet publish
(not just
build
); Costura bundles natives on publish
Blank page renderedWrong
PageDimensions
or index out of range
Verify
pageIndex < PageCount
; use reasonable dimensions like
(1080, 1920)
High memory on large PDFsHolding all pages rendered simultaneouslyRender on demand; dispose
IPageReader
after each render
Annotations lost on saveFlattening step skippedCall
FlattenPage
for every page before writing output PDF
Build error
net48
SDK missing
Building with old SDKInstall .NET 8 SDK; it can cross-target to net48
Signatures not foundEXE moved without
Signatures/
folder
Signatures/
folder must stay next to EXE; it's created automatically on first run
WPF designer crashCostura not compatible with design-timeIgnore design-time errors; run normally to test

问题原因解决方法
DllNotFoundException: pdfium.dll
PDFium原生文件未打包运行
dotnet publish
(不只是
build
);Costura会在发布时打包原生文件
渲染空白页面
PageDimensions
错误或索引超出范围
验证
pageIndex < PageCount
;使用合理的尺寸如
(1080, 1920)
大PDF占用高内存同时渲染所有页面按需渲染;每次渲染后释放
IPageReader
保存后批注丢失跳过了扁平化步骤在写入输出PDF前为每个页面调用
FlattenPage
构建错误
net48
SDK缺失
使用旧SDK构建安装.NET 8 SDK;它可以跨目标编译到net48
找不到签名EXE移动时未携带
Signatures/
文件夹
Signatures/
文件夹必须与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
KillerPDF-<version>-src.zip
for GPL compliance.
GPLv3 — https://github.com/SteveTheKiller/KillerPDF/blob/main/LICENSE
KillerPDF采用GPLv3协议。任何分叉、修改或重新分发——包括商业重命名——都必须以GPLv3协议发布并提供源码。构建流水线会自动生成
KillerPDF-<version>-src.zip
以符合GPL合规要求。
GPLv3 — https://github.com/SteveTheKiller/KillerPDF/blob/main/LICENSE