forked from zhongkaifu/TensorSharp
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPdfPageImageExtractor.cs
More file actions
183 lines (162 loc) · 7.58 KB
/
Copy pathPdfPageImageExtractor.cs
File metadata and controls
183 lines (162 loc) · 7.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// Copyright (c) Zhongkai Fu. All rights reserved.
// https://github.com/zhongkaifu/TensorSharp
//
// This file is part of TensorSharp.
//
// TensorSharp is licensed under the BSD-3-Clause license found in the LICENSE file in the root directory of this source tree.
//
// TensorSharp is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the BSD-3-Clause License for more details.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UglyToad.PdfPig;
using UglyToad.PdfPig.Content;
namespace TensorSharp.Models
{
/// <summary>Page images extracted from a PDF, ready to hand to a vision model.</summary>
public sealed class PdfImageResult
{
/// <summary>Absolute paths of the saved page images, in page order.</summary>
public IReadOnlyList<string> ImagePaths { get; init; }
/// <summary>Total number of pages in the source document.</summary>
public int PageCount { get; init; }
/// <summary>Number of pages that yielded at least one saved image.</summary>
public int ExtractedPageCount { get; init; }
}
/// <summary>
/// Extracts the embedded page images from a PDF and writes them out as PNG files.
///
/// This is the ingestion path for scanned / image-only PDFs — documents whose pages
/// carry no selectable text layer (see <see cref="PdfTextResult.LooksTextless"/>). For
/// those, each page is typically a single full-page raster (a scan or an exported
/// slide), so we take the largest image on each page as that page's picture and let a
/// vision model read it — mirroring how the engine already turns a video into frames.
///
/// Backed by PdfPig's <c>Page.GetImages()</c> + <c>IPdfImage.TryGetPng</c>, so it needs
/// no external rasterizer (no PDFium / Ghostscript). Note: it recovers <em>embedded</em>
/// images, not a rendered raster of vector/text content, which is exactly what an
/// image-only PDF is made of.
/// </summary>
public static class PdfPageImageExtractor
{
/// <summary>Extracts page images from a PDF on disk. See <see cref="ExtractPageImagesFromBytes"/>.</summary>
public static PdfImageResult ExtractPageImages(
string pdfPath, string outputDirectory, int maxPages = 0, string namePrefix = null, string password = null)
{
if (string.IsNullOrEmpty(pdfPath))
throw new ArgumentNullException(nameof(pdfPath));
if (!File.Exists(pdfPath))
throw new FileNotFoundException("PDF file not found.", pdfPath);
byte[] bytes = File.ReadAllBytes(pdfPath);
return ExtractPageImagesFromBytes(bytes, outputDirectory, maxPages, namePrefix, password);
}
/// <summary>
/// Extracts one image per page (the largest embedded image on each page) and writes
/// each as a PNG into <paramref name="outputDirectory"/>.
/// </summary>
/// <param name="pdfBytes">The raw PDF file contents.</param>
/// <param name="outputDirectory">Directory the PNGs are written to (created if missing).</param>
/// <param name="maxPages">Optional cap on pages processed (<c><= 0</c> = all pages).</param>
/// <param name="namePrefix">Optional base name for the emitted files (sanitized).</param>
/// <param name="password">Optional password for an encrypted PDF.</param>
/// <exception cref="InvalidDataException">The bytes are not a usable PDF.</exception>
public static PdfImageResult ExtractPageImagesFromBytes(
byte[] pdfBytes, string outputDirectory, int maxPages = 0, string namePrefix = null, string password = null)
{
if (pdfBytes == null || pdfBytes.Length == 0)
throw new ArgumentException("Empty PDF data.", nameof(pdfBytes));
if (string.IsNullOrEmpty(outputDirectory))
throw new ArgumentNullException(nameof(outputDirectory));
Directory.CreateDirectory(outputDirectory);
string prefix = SanitizeName(namePrefix);
var options = new ParsingOptions { UseLenientParsing = true, SkipMissingFonts = true };
if (!string.IsNullOrEmpty(password))
options.Password = password;
PdfDocument document;
try
{
document = PdfDocument.Open(pdfBytes, options);
}
catch (Exception ex)
{
throw new InvalidDataException(
"Could not open the PDF (it may be corrupt or password-protected): " + ex.Message, ex);
}
var paths = new List<string>();
using (document)
{
int total = document.NumberOfPages;
int limit = maxPages > 0 ? Math.Min(maxPages, total) : total;
for (int i = 1; i <= limit; i++)
{
Page page;
try { page = document.GetPage(i); }
catch { continue; }
if (TrySaveLargestImage(page, outputDirectory, prefix, i, out string savedPath))
paths.Add(savedPath);
}
return new PdfImageResult
{
ImagePaths = paths,
PageCount = total,
ExtractedPageCount = paths.Count,
};
}
}
// Picks the largest embedded image on a page (by pixel area) and writes it as PNG.
// Falls back to the next-largest if the largest can't be decoded to PNG, so a page
// isn't lost to one odd image encoding.
private static bool TrySaveLargestImage(
Page page, string outputDirectory, string prefix, int pageNumber, out string savedPath)
{
savedPath = null;
var images = new List<IPdfImage>();
try
{
foreach (IPdfImage img in page.GetImages())
images.Add(img);
}
catch
{
return false;
}
if (images.Count == 0)
return false;
// Largest first: the dominant image on a text-less page is the page itself.
images.Sort((a, b) =>
((long)b.WidthInSamples * b.HeightInSamples).CompareTo((long)a.WidthInSamples * a.HeightInSamples));
foreach (IPdfImage img in images)
{
byte[] png;
try
{
if (!img.TryGetPng(out png) || png == null || png.Length == 0)
continue;
}
catch
{
continue;
}
string fileName = $"{prefix}_p{pageNumber:D3}.png";
string path = Path.Combine(outputDirectory, fileName);
File.WriteAllBytes(path, png);
savedPath = path;
return true;
}
return false;
}
private static string SanitizeName(string name)
{
if (string.IsNullOrWhiteSpace(name))
return "pdfpage";
name = Path.GetFileNameWithoutExtension(name);
var sb = new StringBuilder(name.Length);
foreach (char c in name)
sb.Append(char.IsLetterOrDigit(c) || c == '-' || c == '_' ? c : '_');
string cleaned = sb.ToString().Trim('_');
return cleaned.Length == 0 ? "pdfpage" : cleaned;
}
}
}