Compare commits

..

27 Commits

Author SHA1 Message Date
Eliot Jones
c8874c5984 #483 make skip missing fonts even more resilient to nonsense files 2022-12-11 16:18:09 -05:00
Eliot Jones
2aed996319 Merge pull request #517 from fnatzke/master
Fixes for Issue#512, 516 and 519
2022-12-09 09:39:55 -05:00
Eliot Jones
060c7bc728 Merge pull request #521 from eliotjones-roger/rotation-support-for-page-builder
add ability to rotate page by number of degrees, make builder fluent
2022-12-09 09:08:27 -05:00
Eliot Jones
6764d81958 Merge pull request #520 from mjolivet-lucca/master
Adding non regression unit test to cover PR 473
2022-12-09 08:56:22 -05:00
Eliot Jones
95df15996b add ability to rotate page by number of degrees, make builder fluent 2022-12-09 08:44:56 -05:00
Mathieu jolivet
998e768bba [ADD] adding non regression unit test to cover PR 473 2022-12-08 14:38:19 +01:00
Fred Natzke
f5fe39b285 Issue 512 revisited. Use of hashset to avoid reprocessing same token in infinite loop. 2022-12-07 17:27:34 +10:00
Fred Natzke
9ef07b0176 Fix Issue 519 Break infinite loop parsing kid token list containing parent 2022-12-06 16:29:09 +10:00
Fred Natzke
29adece983 Original #516 fix causes Tests to fail. Alternative to use pagesByNumber?.Count in Catalog for discovered pages. Some other unrelated tests failed due to source using single rather than two character newlines. Changed to test string itself for '\r' rather than environment is Unix 2022-11-30 11:10:52 +10:00
Fred Natzke
a7f64ec64b Fix Issue 516. Page Dictionary Count field has incorrect page count. Compare with PageTree children count. 2022-11-30 09:25:23 +10:00
Fred Natzke
afe473e10e Fix for Issue#512: Unable to open PDF. BruteForceSearcher::GetLastEndOfFileMarker() minimumEndOffset out by 1. 2022-11-29 17:31:23 +10:00
Eliot Jones
9c9c7c99ea ci new namespace file scope unsupported 2022-10-09 16:01:20 -04:00
Eliot Jones
e2246a88bb #482 add skip missing fonts option and pass parsing options to content stream processor
this doesn't fix the reported issue since the pdf itself is corrupted on page 8 however it will
allow recovery in some scenarios where text content isn't important.

also adds more informative error when stream unintentionally passed with non zero offset
2022-10-09 13:44:05 -04:00
Eliot Jones
c643facee0 #481 skip resource entry if null token 2022-10-09 13:06:04 -04:00
Eliot Jones
2f9a9ace9a Merge pull request #473 from grinay/master
Fix page number order.
2022-08-13 16:02:44 -04:00
grinay
19962af011 Fix page number order.
For case when root nod has reference to page the order will be incorrect.
The case if root node has reference
[2 0 R 3 0 R 10 0 R]
Where 2 0 R is intermediate node containing page 1,2,3

Where 3 0 R is intermediate node containing page 4,5,6
Where 10 0 R is page 7

without that fix 7 page will in array as page 1
2022-07-29 18:27:56 +08:00
Eliot Jones
545d1a0793 Merge branch 'master' of github.com:UglyToad/PdfPig 2022-07-02 18:09:22 -04:00
Eliot Jones
3cd81297c1 handle infinity symbol encountered in edge-case files 2022-07-02 18:09:11 -04:00
Eliot Jones
fc71a91dfa Merge pull request #463 from Jonowa/master
Added graphic functions to PageBuilder
2022-06-21 08:00:14 -04:00
Eliot Jones
f2188729a3 #453 handle messed up number format 2022-06-17 20:35:21 -04:00
Jörg Nowak
ae83861c5f Correct value of cc in DrawEllipsis 2022-06-09 07:49:50 +02:00
Jörg Nowak
21dfa3e985 Add DrawTriangle, DrawCircle and DrawEllipsis 2022-06-08 13:20:06 +02:00
Eliot Jones
559f3af5f3 Merge pull request #459 from BobLd/master
Fix #458 - GrahamScan exception
2022-05-24 08:15:04 -04:00
BobLD
5eed9fd1bc Fix #458 Check if stack has element and invert sorting order in GrahamScan(), add tests 2022-05-24 07:47:55 +01:00
Eliot Jones
ddab53e456 remove stray debugging code 2022-05-10 10:14:36 -04:00
Eliot Jones
4e490d63be #444 handle invalid tounicode map objects in tt fonts 2022-05-02 16:17:05 -04:00
Eliot Jones
03692cf42f set version to alpha of 0.1.7 for future nightly builds 2022-04-25 10:06:46 -04:00
42 changed files with 769 additions and 161 deletions

View File

@@ -10,7 +10,8 @@
public static class AdvancedTextExtraction
{
public static void Run(string filePath)
{
{
#if YET_TO_BE_DONE
var sb = new StringBuilder();
using (var document = PdfDocument.Open(filePath))
@@ -86,6 +87,7 @@
}
Console.WriteLine(sb.ToString());
#endif
}
}
}

View File

@@ -45,9 +45,14 @@
},
{7,
("Advance text extraction using layout analysis algorithms",
() => AdvancedTextExtraction.Run(Path.Combine(filesDirectory, "ICML03-081.pdf")))
}
};
() => AdvancedTextExtraction.Run(Path.Combine(filesDirectory, "ICML03-081.pdf")))
},
{
8,
("Extract Words with newline detection (example with algorithm). Issue 512",
() => OpenDocumentAndExtractWords.Run(Path.Combine(filesDirectory, "OPEN.RABBIT.ENGLISH.LOP.pdf")))
}
};
var choices = string.Join(Environment.NewLine, examples.Select(x => $"{x.Key}: {x.Value.name}"));

View File

@@ -67,7 +67,7 @@
{
return new PdfPoint(X + dx, Y);
}
/// <summary>
/// Creates a new <see cref="PdfPoint"/> which is the current point moved in the y direction relative to its current position by a value.
/// </summary>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47;net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.6</Version>
<Version>0.1.7-alpha-001</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47;net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.6</Version>
<Version>0.1.7-alpha-001</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

View File

@@ -0,0 +1,34 @@
namespace UglyToad.PdfPig.Fonts
{
using System;
using System.Runtime.Serialization;
/// <summary>
/// Thrown when a PDF contains an invalid compressed data stream.
/// </summary>
[Serializable]
public class CorruptCompressedDataException : Exception
{
/// <inheritdoc />
public CorruptCompressedDataException()
{
}
/// <inheritdoc />
public CorruptCompressedDataException(string message) : base(message)
{
}
/// <inheritdoc />
public CorruptCompressedDataException(string message, Exception inner) : base(message, inner)
{
}
/// <inheritdoc />
protected CorruptCompressedDataException(
SerializationInfo info,
StreamingContext context) : base(info, context)
{
}
}
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47;net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.6</Version>
<Version>0.1.7-alpha-001</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

View File

@@ -528,6 +528,52 @@
}
}
};
public static IEnumerable<object[]> Issue458Data => new[]
{
new object[]
{
new PdfPoint[]
{
new PdfPoint(134.74199999999985, 1611.657),
new PdfPoint(277.58043749999985, 1611.657),
new PdfPoint(314.74199999999985, 1611.657),
new PdfPoint(507.0248828124999, 1611.657),
new PdfPoint(545.1419999999998, 1611.657),
new PdfPoint(632.9658281249997, 1611.657),
new PdfPoint(668.5709999999998, 1611.657),
new PdfPoint(831.3395078125002, 1611.657),
new PdfPoint(868.1139999999998, 1611.657),
new PdfPoint(892.8907187499999, 1611.657),
new PdfPoint(1010.0569999999999, 1611.6569999999997),
new PdfPoint(1046.2174003906248, 1611.7654812011715),
new PdfPoint(1010.0569999999999, 1611.657),
new PdfPoint(1046.2174003906248, 1611.7654812011717),
new PdfPoint(1085.145, 1611.8822639999996),
new PdfPoint(1255.484941406251, 1612.3932838242179),
new PdfPoint(1301.144, 1612.5302609999994),
new PdfPoint(1359.0006406250002, 1612.7038309218747),
new PdfPoint(1301.144, 1612.5302609999997),
new PdfPoint(1359.0006406250002, 1612.703830921875),
new PdfPoint(1400.915, 1612.8295739999996),
new PdfPoint(1505.379062499999, 1613.1429661874993),
new PdfPoint(1400.915, 1612.8295739999999),
new PdfPoint(1505.379062499999, 1613.1429661874995),
new PdfPoint(1543.886, 1613.2584869999996),
new PdfPoint(1600.9401015625003, 1613.4296493046872),
new PdfPoint(1641.597, 1613.5516199999997),
new PdfPoint(1764.5577421874998, 1613.9205022265612),
new PdfPoint(1641.597, 1613.55162),
new PdfPoint(1764.5577421874998, 1613.9205022265614)
},
new PdfPoint[]
{
new PdfPoint(134.74199999999985, 1611.657),
new PdfPoint(1010.0569999999999, 1611.6569999999997),
new PdfPoint(1764.5577421874998, 1613.9205022265614),
}
}
};
#endregion
[Fact]
@@ -570,7 +616,6 @@
}
}
[Theory]
[MemberData(nameof(MinimumAreaRectangleData))]
public void MinimumAreaRectangle(PdfPoint[] points, PdfPoint[] expected)
@@ -585,5 +630,51 @@
Assert.Equal(expected[i], mar[i], PointComparer);
}
}
[Theory]
[MemberData(nameof(Issue458Data))]
public void Issue458(PdfPoint[] points, PdfPoint[] expected)
{
/*
* https://github.com/UglyToad/PdfPig/issues/458
* An unhandled exception of type 'System.ArgumentOutOfRangeException' occurred in System.Linq.dll: 'Specified argument was out of the range of valid values.'
* at System.Linq.ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument argument)
* at System.Linq.Enumerable.ElementAt[TSource](IEnumerable`1 source, Int32 index)
*/
var result = GeometryExtensions.GrahamScan(points);
// Data is noisy so we just check it does not throw an exception
// and that key points are present (other points might be present,
// e.g. 'not enough equal dupplicates' or 'not collinear enough' points)
foreach (var point in expected)
{
Assert.Contains(point, result);
}
}
[Theory]
[MemberData(nameof(Issue458Data))]
public void Issue458_Inv(PdfPoint[] points, PdfPoint[] expected)
{
/*
* https://github.com/UglyToad/PdfPig/issues/458
* An unhandled exception of type 'System.ArgumentOutOfRangeException' occurred in System.Linq.dll: 'Specified argument was out of the range of valid values.'
* at System.Linq.ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument argument)
* at System.Linq.Enumerable.ElementAt[TSource](IEnumerable`1 source, Int32 index)
*/
var pointsInv = points.Select(p => new PdfPoint(p.Y, p.X)).ToArray();
var expectedInv = expected.Select(p => new PdfPoint(p.Y, p.X)).ToArray();
var result = GeometryExtensions.GrahamScan(pointsInv);
// Data is noisy so we just check it does not throw an exception
// and that key points are present (other points might be present,
// e.g. 'not enough equal dupplicates' or 'not collinear enough' points)
foreach (var point in expectedInv)
{
Assert.Contains(point, result);
}
}
}
}

View File

@@ -0,0 +1,54 @@
namespace UglyToad.PdfPig.Tests.Integration;
using System.Linq;
using Xunit;
public class IndexedPageSummaryFileTests
{
private static string GetFilename()
{
return IntegrationHelpers.GetDocumentPath("FICTIF_TABLE_INDEX.pdf");
}
[Fact]
public void HasCorrectNumberOfPages()
{
using (var document = PdfDocument.Open(GetFilename()))
{
Assert.Equal(14, document.NumberOfPages);
}
}
[Fact]
public void GetPagesWorks()
{
using (var document = PdfDocument.Open(GetFilename()))
{
var pageCount = document.GetPages().Count();
Assert.Equal(14, pageCount);
}
}
[Theory]
[InlineData("M. HERNANDEZ DANIEL", 1)]
[InlineData("M. HERNANDEZ DANIEL", 2)]
[InlineData("Mme ALIBERT CHLOE AA", 3)]
[InlineData("Mme ALIBERT CHLOE AA", 4)]
[InlineData("M. SIMPSON BART AAA", 5)]
[InlineData("M. SIMPSON BART AAA", 6)]
[InlineData("M. BOND JAMES A", 7)]
[InlineData("M. BOND JAMES A", 8)]
[InlineData("M. DE BALZAC HONORE", 9)]
[InlineData("M. DE BALZAC HONORE", 10)]
[InlineData("M. STALLONE SILVESTER", 11)]
[InlineData("M. STALLONE SILVESTER", 12)]
[InlineData("M. SCOTT MICHAEL", 13)]
[InlineData("M. SCOTT MICHAEL", 14)]
public void CheckSpecificNamesPresence_InIndexedPageNumbersFile(string searchedName, int pageNumber)
{
using var document = PdfDocument.Open(GetFilename());
var page = document.GetPage(pageNumber);
Assert.Contains(searchedName, page.Text);
}
}

View File

@@ -6,7 +6,6 @@
using Logging;
using PdfPig.Core;
using PdfPig.Graphics;
using PdfPig.Graphics.Core;
using PdfPig.Graphics.Operations.General;
using PdfPig.Graphics.Operations.SpecialGraphicsState;
using PdfPig.Graphics.Operations.TextObjects;
@@ -183,6 +182,30 @@ cm BT 0.0001 Tc 19 0 0 19 0 0 Tm /Tc1 1 Tf ( \(sleep 1; printf ""QUIT\\r\\n""\
Assert.Equal(@" (sleep 1; printf ""QUIT\r\n"") | ", text.Text);
}
[Fact]
public void HandlesWeirdNumber()
{
// Issue 453
const string s = @"/Alpha1
gs
0
0
0
rg
0.00-90
151555.0
m
302399.97
151555.0
l";
var input = StringBytesTestConverter.Convert(s, false);
var result = parser.Parse(1, input.Bytes, log);
Assert.Equal(4, result.Count);
}
private static string LineEndingsToWhiteSpace(string str)
{
return str.Replace("\r\n", " ").Replace('\n', ' ').Replace('\r', ' ');

View File

@@ -51,7 +51,7 @@
var result = FileHeaderParser.Parse(scanner.scanner, scanner.bytes, false, log);
Assert.Equal(1.2m, result.Version);
Assert.Equal(TestEnvironment.IsUnixPlatform ? 7 : 9, result.OffsetInFile);
Assert.Equal(TestEnvironment.IsSingleByteNewLine(input) ? 7 : 9, result.OffsetInFile);
}
[Fact]
@@ -66,38 +66,42 @@
[Fact]
public void HeaderPrecededByJunkNonLenientDoesNotThrow()
{
var scanner = StringBytesTestConverter.Scanner(@"one
%PDF-1.2");
{
var input = @"one
%PDF-1.2";
var scanner = StringBytesTestConverter.Scanner(input);
var result = FileHeaderParser.Parse(scanner.scanner, scanner.bytes, false, log);
Assert.Equal(1.2m, result.Version);
Assert.Equal(TestEnvironment.IsUnixPlatform ? 12 : 13, result.OffsetInFile);
Assert.Equal(TestEnvironment.IsSingleByteNewLine(input) ? 12 : 13, result.OffsetInFile);
}
[Fact]
public void HeaderPrecededByJunkLenientReads()
{
var scanner = StringBytesTestConverter.Scanner(@"one
%PDF-1.7");
{
var input = @"one
%PDF-1.7";
var scanner = StringBytesTestConverter.Scanner(input);
var result = FileHeaderParser.Parse(scanner.scanner, scanner.bytes, true, log);
Assert.Equal(1.7m, result.Version);
Assert.Equal(TestEnvironment.IsUnixPlatform ? 12 : 13, result.OffsetInFile);
Assert.Equal(TestEnvironment.IsSingleByteNewLine(input) ? 12 : 13, result.OffsetInFile);
}
[Fact]
public void HeaderPrecededByJunkDoesNotThrow()
{
var scanner = StringBytesTestConverter.Scanner(@"one two
three %PDF-1.6");
{
var s = @"one two
three %PDF-1.6";
var scanner = StringBytesTestConverter.Scanner(s);
var result = FileHeaderParser.Parse(scanner.scanner, scanner.bytes, true, log);
Assert.Equal(1.6m, result.Version);
Assert.Equal(TestEnvironment.IsUnixPlatform ? 14 : 15, result.OffsetInFile);
Assert.Equal(TestEnvironment.IsSingleByteNewLine(s) ? 14 : 15, result.OffsetInFile);
}
[Fact]

View File

@@ -4,6 +4,7 @@
public static class TestEnvironment
{
public static readonly bool IsUnixPlatform = Environment.NewLine.Length == 1;
public static bool IsSingleByteNewLine(string s) => s.IndexOf('\r') < 0;
}
}

View File

@@ -155,6 +155,12 @@
default:
if (!decimal.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
if (TryParseInvalidNumber(str, out value))
{
token = new NumericToken(value);
return true;
}
return false;
}
@@ -171,5 +177,34 @@
return false;
}
}
private static bool TryParseInvalidNumber(string numeric, out decimal result)
{
result = 0;
if (!numeric.Contains("-") && !numeric.Contains("+"))
{
return false;
}
var parts = numeric.Split(new string[] { "+", "-" }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0)
{
return false;
}
foreach (var part in parts)
{
if (!decimal.TryParse(part, NumberStyles.Any, CultureInfo.InvariantCulture, out var partNumber))
{
return false;
}
result += partNumber;
}
return true;
}
}
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47;net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.6</Version>
<Version>0.1.7-alpha-001</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47;net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.6</Version>
<Version>0.1.7-alpha-001</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

View File

@@ -29,7 +29,12 @@
/// <summary>
/// The page tree for this document containing all pages, page numbers and their dictionaries.
/// </summary>
public PageTreeNode PageTree { get; }
public PageTreeNode PageTree { get; }
/// <summary>
/// Number of discovered pages.
/// </summary>
public int? NumberOfDiscoveredPages => pagesByNumber?.Count;
/// <summary>
/// Create a new <see cref="CatalogDictionary"/>.

View File

@@ -4,6 +4,10 @@
internal interface IPageFactory
{
Page Create(int number, DictionaryToken dictionary, PageTreeMembers pageTreeMembers, bool clipPaths);
Page Create(
int number,
DictionaryToken dictionary,
PageTreeMembers pageTreeMembers,
InternalParsingOptions parsingOptions);
}
}

View File

@@ -6,7 +6,7 @@
internal interface IResourceStore
{
void LoadResourceDictionary(DictionaryToken resourceDictionary);
void LoadResourceDictionary(DictionaryToken resourceDictionary, InternalParsingOptions parsingOptions);
/// <summary>
/// Remove any named resources and associated state for the last resource dictionary loaded.

View File

@@ -44,7 +44,7 @@
/// <summary>
/// Create a <see cref="PageRotationDegrees"/>.
/// </summary>
/// <param name="rotation">Rotation in degrees clockwise.</param>
/// <param name="rotation">Rotation in degrees clockwise, must be a multiple of 90.</param>
public PageRotationDegrees(int rotation)
{
if (rotation < 0)

View File

@@ -32,7 +32,7 @@
/// <summary>
/// The number of this page if <see cref="IsPage"/> is <see langword="true"/>.
/// </summary>
public int? PageNumber { get; }
public int? PageNumber { get; internal set; }
/// <summary>
/// The child nodes of this node if <see cref="IsPage"/> is <see langword="false" />

View File

@@ -21,12 +21,21 @@
this.pdfScanner = pdfScanner ?? throw new ArgumentNullException(nameof(pdfScanner));
Count = catalog.PagesDictionary.GetIntOrDefault(NameToken.Count);
var CountOfPagesByPagesTree = catalog.PageTree.Children.Count;
var numberOfDiscoveredPages = catalog.NumberOfDiscoveredPages;
if (numberOfDiscoveredPages is null == false && Count != numberOfDiscoveredPages)
{
//log.Warning($"Dictionary Page Count {Count} different to discovered pages {numberOfDiscoveredPages}. Using {numberOfDiscoveredPages}.");
Count = numberOfDiscoveredPages.Value;
}
}
public Page GetPage(int pageNumber, bool clipPaths)
public Page GetPage(int pageNumber, InternalParsingOptions parsingOptions)
{
if (pageNumber <= 0 || pageNumber > Count)
{
parsingOptions.Logger.Error($"Page {pageNumber} requested but is out of range.");
throw new ArgumentOutOfRangeException(nameof(pageNumber),
$"Page number {pageNumber} invalid, must be between 1 and {Count}.");
}
@@ -63,7 +72,11 @@
}
}
var page = pageFactory.Create(pageNumber, pageNode.NodeDictionary, pageTreeMembers, clipPaths);
var page = pageFactory.Create(
pageNumber,
pageNode.NodeDictionary,
pageTreeMembers,
parsingOptions);
return page;
}

View File

@@ -33,7 +33,7 @@
this.fontFactory = fontFactory;
}
public void LoadResourceDictionary(DictionaryToken resourceDictionary)
public void LoadResourceDictionary(DictionaryToken resourceDictionary, InternalParsingOptions parsingOptions)
{
lastLoadedFont = (null, null);
@@ -43,7 +43,7 @@
{
var fontDictionary = DirectObjectFinder.Get<DictionaryToken>(fontBase, scanner);
LoadFontDictionary(fontDictionary);
LoadFontDictionary(fontDictionary, parsingOptions);
}
if (resourceDictionary.TryGet(NameToken.Xobject, out var xobjectBase))
@@ -52,6 +52,11 @@
foreach (var pair in xobjectDictionary.Data)
{
if (pair.Value is NullToken)
{
continue;
}
if (!(pair.Value is IndirectReferenceToken reference))
{
throw new InvalidOperationException($"Expected the XObject dictionary value for key /{pair.Key} to be an indirect reference, instead got: {pair.Value}.");
@@ -127,7 +132,7 @@
currentResourceState.Pop();
}
private void LoadFontDictionary(DictionaryToken fontDictionary)
private void LoadFontDictionary(DictionaryToken fontDictionary, InternalParsingOptions parsingOptions)
{
lastLoadedFont = (null, null);
@@ -152,7 +157,18 @@
continue;
}
loadedFonts[reference] = fontFactory.Get(fontObject);
try
{
loadedFonts[reference] = fontFactory.Get(fontObject);
}
catch
{
if (!parsingOptions.SkipMissingFonts)
{
throw;
}
}
}
else if (pair.Value is DictionaryToken fd)
{

View File

@@ -1,7 +1,6 @@
namespace UglyToad.PdfPig.Encryption
{
using System;
using System.Diagnostics;
using System.IO;
using System.Security.Cryptography;
@@ -29,11 +28,6 @@
var buffer = new byte[256];
if (data.Length > 256)
{
Debugger.Break();
}
using (var decryptor = rijndael.CreateDecryptor(rijndael.Key, rijndael.IV))
using (var input = new MemoryStream(data))
using (var output = new MemoryStream())

View File

@@ -1,5 +1,6 @@
namespace UglyToad.PdfPig.Filters
{
using Fonts;
using System;
using System.Collections.Generic;
using System.IO;
@@ -79,10 +80,17 @@
memoryStream.ReadByte();
memoryStream.ReadByte();
using (var deflate = new DeflateStream(memoryStream, CompressionMode.Decompress))
try
{
deflate.CopyTo(output);
return output.ToArray();
using (var deflate = new DeflateStream(memoryStream, CompressionMode.Decompress))
{
deflate.CopyTo(output);
return output.ToArray();
}
}
catch (InvalidDataException ex)
{
throw new CorruptCompressedDataException("Invalid Flate compressed stream encountered", ex);
}
}
}

View File

@@ -269,11 +269,12 @@
double polarAngle(PdfPoint point1, PdfPoint point2)
{
// This is used for grouping, we could use Math.Round()
return Math.Atan2(point2.Y - point1.Y, point2.X - point1.X) % Math.PI;
}
Stack<PdfPoint> stack = new Stack<PdfPoint>();
var sortedPoints = points.OrderBy(p => p.Y).ThenBy(p => p.X).ToList();
var stack = new Stack<PdfPoint>();
var sortedPoints = points.OrderBy(p => p.X).ThenBy(p => p.Y).ToList();
var P0 = sortedPoints[0];
var groups = sortedPoints.Skip(1).GroupBy(p => polarAngle(P0, p)).OrderBy(g => g.Key);
@@ -309,7 +310,7 @@
for (int i = 2; i < sortedPoints.Count; i++)
{
var point = sortedPoints[i];
while (!ccw(stack.ElementAt(1), stack.Peek(), point))
while (stack.Count > 1 && !ccw(stack.ElementAt(1), stack.Peek(), point))
{
stack.Pop();
}

View File

@@ -5,7 +5,6 @@
using Core;
using Filters;
using Geometry;
using Logging;
using Operations;
using Parser;
using PdfFonts;
@@ -49,9 +48,8 @@
private readonly IPdfTokenScanner pdfScanner;
private readonly IPageContentParser pageContentParser;
private readonly ILookupFilterProvider filterProvider;
private readonly ILog log;
private readonly bool clipPaths;
private readonly PdfVector pageSize;
private readonly InternalParsingOptions parsingOptions;
private readonly MarkedContentStack markedContentStack = new MarkedContentStack();
private Stack<CurrentGraphicsState> graphicsStack = new Stack<CurrentGraphicsState>();
@@ -90,9 +88,8 @@
IPdfTokenScanner pdfScanner,
IPageContentParser pageContentParser,
ILookupFilterProvider filterProvider,
ILog log,
bool clipPaths,
PdfVector pageSize)
PdfVector pageSize,
InternalParsingOptions parsingOptions)
{
this.resourceStore = resourceStore;
this.userSpaceUnit = userSpaceUnit;
@@ -100,9 +97,8 @@
this.pdfScanner = pdfScanner ?? throw new ArgumentNullException(nameof(pdfScanner));
this.pageContentParser = pageContentParser ?? throw new ArgumentNullException(nameof(pageContentParser));
this.filterProvider = filterProvider ?? throw new ArgumentNullException(nameof(filterProvider));
this.log = log;
this.clipPaths = clipPaths;
this.pageSize = pageSize;
this.parsingOptions = parsingOptions;
// initiate CurrentClippingPath to cropBox
var clippingSubpath = new PdfSubpath();
@@ -230,6 +226,15 @@
if (font == null)
{
if (parsingOptions.SkipMissingFonts)
{
parsingOptions.Logger.Warn($"Skipping a missing font with name {currentState.FontState.FontName} " +
$"since it is not present in the document and {nameof(InternalParsingOptions.SkipMissingFonts)} " +
"is set to true. This may result in some text being skipped and not included in the output.");
return;
}
throw new InvalidOperationException($"Could not find the font with name {currentState.FontState.FontName} in the resource store. It has not been loaded yet.");
}
@@ -253,7 +258,8 @@
if (!foundUnicode || unicode == null)
{
log.Warn($"We could not find the corresponding character with code {code} in font {font.Name}.");
parsingOptions.Logger.Warn($"We could not find the corresponding character with code {code} in font {font.Name}.");
// Try casting directly to string as in PDFBox 1.8.
unicode = new string((char)code, 1);
}
@@ -473,7 +479,7 @@
var hasResources = formStream.StreamDictionary.TryGet<DictionaryToken>(NameToken.Resources, pdfScanner, out var formResources);
if (hasResources)
{
resourceStore.LoadResourceDictionary(formResources);
resourceStore.LoadResourceDictionary(formResources, parsingOptions);
}
// 1. Save current state.
@@ -494,7 +500,7 @@
var contentStream = formStream.Decode(filterProvider, pdfScanner);
var operations = pageContentParser.Parse(pageNumber, new ByteArrayInputBytes(contentStream), log);
var operations = pageContentParser.Parse(pageNumber, new ByteArrayInputBytes(contentStream), parsingOptions.Logger);
// 3. We don't respect clipping currently.
@@ -677,7 +683,7 @@
if (CurrentPath.IsClipping)
{
if (!clipPaths)
if (!parsingOptions.ClipPaths)
{
// if we don't clip paths, add clipping path to paths
paths.Add(CurrentPath);
@@ -717,9 +723,9 @@
CurrentPath.FillColor = currentState.CurrentNonStrokingColor;
}
if (clipPaths)
if (parsingOptions.ClipPaths)
{
var clippedPath = currentState.CurrentClippingPath.Clip(CurrentPath, log);
var clippedPath = currentState.CurrentClippingPath.Clip(CurrentPath, parsingOptions.Logger);
if (clippedPath != null)
{
paths.Add(clippedPath);
@@ -745,15 +751,15 @@
AddCurrentSubpath();
CurrentPath.SetClipping(clippingRule);
if (clipPaths)
if (parsingOptions.ClipPaths)
{
var currentClipping = GetCurrentState().CurrentClippingPath;
currentClipping.SetClipping(clippingRule);
var newClippings = CurrentPath.Clip(currentClipping, log);
var newClippings = CurrentPath.Clip(currentClipping, parsingOptions.Logger);
if (newClippings == null)
{
log.Warn("Empty clipping path found. Clipping path not updated.");
parsingOptions.Logger.Warn("Empty clipping path found. Clipping path not updated.");
}
else
{
@@ -796,7 +802,7 @@
{
if (inlineImageBuilder != null)
{
log?.Error("Begin inline image (BI) command encountered while another inline image was active.");
parsingOptions.Logger.Error("Begin inline image (BI) command encountered while another inline image was active.");
}
inlineImageBuilder = new InlineImageBuilder();
@@ -806,7 +812,7 @@
{
if (inlineImageBuilder == null)
{
log?.Error("Begin inline image data (ID) command encountered without a corresponding begin inline image (BI) command.");
parsingOptions.Logger.Error("Begin inline image data (ID) command encountered without a corresponding begin inline image (BI) command.");
return;
}
@@ -817,7 +823,7 @@
{
if (inlineImageBuilder == null)
{
log?.Error("End inline image (EI) command encountered without a corresponding begin inline image (BI) command.");
parsingOptions.Logger.Error("End inline image (EI) command encountered without a corresponding begin inline image (BI) command.");
return;
}

View File

@@ -0,0 +1,35 @@
namespace UglyToad.PdfPig
{
using Logging;
using System.Collections.Generic;
/// <summary>
/// <see cref="ParsingOptions"/> but without being a public API/
/// </summary>
internal class InternalParsingOptions
{
public IReadOnlyList<string> Passwords { get; }
public bool UseLenientParsing { get; }
public bool ClipPaths { get; }
public bool SkipMissingFonts { get; }
public ILog Logger { get; }
public InternalParsingOptions(
IReadOnlyList<string> passwords,
bool useLenientParsing,
bool clipPaths,
bool skipMissingFonts,
ILog logger)
{
Passwords = passwords;
UseLenientParsing = useLenientParsing;
ClipPaths = clipPaths;
SkipMissingFonts = skipMissingFonts;
Logger = logger;
}
}
}

View File

@@ -5,8 +5,10 @@
using Content;
using Core;
using Parts;
using System.Linq;
using Tokenization.Scanner;
using Tokens;
using Util;
internal static class CatalogFactory
{
@@ -79,9 +81,13 @@
pageNumber.Increment();
return new PageTreeNode(nodeDictionaryInput, referenceInput, true, pageNumber.PageCount).WithChildren(EmptyArray<PageTreeNode>.Instance);
}
//If we got here, we have to iterate till we manage to exit
}
//If we got here, we have to iterate till we manage to exit
HashSet<int> visitedTokens = new HashSet<int>(); // As we visit each token add to this list (the hashcode of the indirect reference)
var toProcess =
new Queue<(PageTreeNode thisPage, IndirectReference reference, DictionaryToken nodeDictionary, IndirectReference parentReference,
@@ -98,8 +104,16 @@
do
{
var current = toProcess.Dequeue();
var current = toProcess.Dequeue();
var currentReferenceHash = current.reference.GetHashCode();
if (visitedTokens.Contains(currentReferenceHash))
{
continue; // don't revisit token already processed. break infinite loop. Issue #512
}
else
{
visitedTokens.Add(currentReferenceHash);
}
if (!current.nodeDictionary.TryGet(NameToken.Kids, pdfTokenScanner, out ArrayToken kids))
{
if (!isLenientParsing)
@@ -126,8 +140,6 @@
if (isChildPage)
{
pageNumber.Increment();
var kidPageNode =
new PageTreeNode(kidDictionaryToken, kidRef.Data, true, pageNumber.PageCount).WithChildren(EmptyArray<PageTreeNode>.Instance);
current.nodeChildren.Add(kidPageNode);
@@ -152,6 +164,12 @@
action();
}
foreach (var child in firstPage.Children.ToRecursiveOrderList(x=>x.Children).Where(child => child.IsPage))
{
pageNumber.Increment();
child.PageNumber = pageNumber.PageCount;
}
return firstPage;
}

View File

@@ -94,7 +94,7 @@
throw new InvalidOperationException($"Failed to read expected buffer length {gap} on page {pageNumber} " +
$"when reading inline image at offset in content: {lastEndImageOffset.Value}.");
}
// Replace the last end image operator with one containing the full set of data.
graphicsStateOperations.Remove(lastEndImage);
graphicsStateOperations.Add(new EndInlineImage(lastEndImage.ImageData.Concat(missingData).ToArray()));
@@ -142,6 +142,13 @@
graphicsStateOperations.Add(newEndInlineImage);
lastEndImageOffset = scanner.CurrentPosition - 3;
}
else if (op.Data == "inf")
{
// Value representing infinity in broken file from #467.
// Treat as zero.
precedingTokens.Add(NumericToken.Zero);
continue;
}
else
{
log.Warn($"Operator which was not understood encountered. Values was {op.Data}. Ignoring.");

View File

@@ -21,20 +21,20 @@
private readonly IResourceStore resourceStore;
private readonly ILookupFilterProvider filterProvider;
private readonly IPageContentParser pageContentParser;
private readonly ILog log;
public PageFactory(IPdfTokenScanner pdfScanner, IResourceStore resourceStore, ILookupFilterProvider filterProvider,
IPageContentParser pageContentParser,
ILog log)
public PageFactory(
IPdfTokenScanner pdfScanner,
IResourceStore resourceStore,
ILookupFilterProvider filterProvider,
IPageContentParser pageContentParser)
{
this.resourceStore = resourceStore;
this.filterProvider = filterProvider;
this.pageContentParser = pageContentParser;
this.log = log;
this.pdfScanner = pdfScanner;
}
public Page Create(int number, DictionaryToken dictionary, PageTreeMembers pageTreeMembers, bool clipPaths)
public Page Create(int number, DictionaryToken dictionary, PageTreeMembers pageTreeMembers, InternalParsingOptions parsingOptions)
{
if (dictionary == null)
{
@@ -45,11 +45,11 @@
if (type != null && !type.Equals(NameToken.Page))
{
log?.Error($"Page {number} had its type specified as {type} rather than 'Page'.");
parsingOptions.Logger.Error($"Page {number} had its type specified as {type} rather than 'Page'.");
}
MediaBox mediaBox = GetMediaBox(number, dictionary, pageTreeMembers);
CropBox cropBox = GetCropBox(dictionary, pageTreeMembers, mediaBox);
MediaBox mediaBox = GetMediaBox(number, dictionary, pageTreeMembers, parsingOptions.Logger);
CropBox cropBox = GetCropBox(dictionary, pageTreeMembers, mediaBox, parsingOptions.Logger);
var rotation = new PageRotationDegrees(pageTreeMembers.Rotation);
if (dictionary.TryGet(NameToken.Rotate, pdfScanner, out NumericToken rotateToken))
@@ -63,13 +63,13 @@
{
var resource = pageTreeMembers.ParentResources.Dequeue();
resourceStore.LoadResourceDictionary(resource);
resourceStore.LoadResourceDictionary(resource, parsingOptions);
stackDepth++;
}
if (dictionary.TryGet(NameToken.Resources, pdfScanner, out DictionaryToken resources))
{
resourceStore.LoadResourceDictionary(resources);
resourceStore.LoadResourceDictionary(resources, parsingOptions);
stackDepth++;
}
@@ -130,7 +130,7 @@
}
}
content = GetContent(number, bytes, cropBox, userSpaceUnit, rotation, clipPaths, mediaBox);
content = GetContent(number, bytes, cropBox, userSpaceUnit, rotation, mediaBox, parsingOptions);
}
else
{
@@ -143,7 +143,7 @@
var bytes = contentStream.Decode(filterProvider, pdfScanner);
content = GetContent(number, bytes, cropBox, userSpaceUnit, rotation, clipPaths, mediaBox);
content = GetContent(number, bytes, cropBox, userSpaceUnit, rotation, mediaBox, parsingOptions);
}
var page = new Page(number, dictionary, mediaBox, cropBox, rotation, content,
@@ -158,18 +158,28 @@
return page;
}
private PageContent GetContent(int pageNumber, IReadOnlyList<byte> contentBytes, CropBox cropBox, UserSpaceUnit userSpaceUnit,
PageRotationDegrees rotation, bool clipPaths, MediaBox mediaBox)
private PageContent GetContent(
int pageNumber,
IReadOnlyList<byte> contentBytes,
CropBox cropBox,
UserSpaceUnit userSpaceUnit,
PageRotationDegrees rotation,
MediaBox mediaBox,
InternalParsingOptions parsingOptions)
{
var operations = pageContentParser.Parse(pageNumber, new ByteArrayInputBytes(contentBytes),
log);
parsingOptions.Logger);
var context = new ContentStreamProcessor(cropBox.Bounds, resourceStore, userSpaceUnit, rotation, pdfScanner,
var context = new ContentStreamProcessor(
cropBox.Bounds,
resourceStore,
userSpaceUnit,
rotation,
pdfScanner,
pageContentParser,
filterProvider,
log,
clipPaths,
new PdfVector(mediaBox.Bounds.Width, mediaBox.Bounds.Height));
new PdfVector(mediaBox.Bounds.Width, mediaBox.Bounds.Height),
parsingOptions);
return context.Process(pageNumber, operations);
}
@@ -185,7 +195,11 @@
return spaceUnits;
}
private CropBox GetCropBox(DictionaryToken dictionary, PageTreeMembers pageTreeMembers, MediaBox mediaBox)
private CropBox GetCropBox(
DictionaryToken dictionary,
PageTreeMembers pageTreeMembers,
MediaBox mediaBox,
ILog log)
{
CropBox cropBox;
if (dictionary.TryGet(NameToken.CropBox, out var cropBoxObject) &&
@@ -210,7 +224,11 @@
return cropBox;
}
private MediaBox GetMediaBox(int number, DictionaryToken dictionary, PageTreeMembers pageTreeMembers)
private MediaBox GetMediaBox(
int number,
DictionaryToken dictionary,
PageTreeMembers pageTreeMembers,
ILog log)
{
MediaBox mediaBox;
if (dictionary.TryGet(NameToken.MediaBox, out var mediaboxObject)

View File

@@ -176,7 +176,7 @@
const string searchTerm = "%%EOF";
var minimumEndOffset = bytes.Length - searchTerm.Length;
var minimumEndOffset = bytes.Length - searchTerm.Length + 1; // Issue #512 - Unable to open PDF - BruteForceScan starts from earlier of two EOF marker due to min end offset off by 1
bytes.Seek(minimumEndOffset);

View File

@@ -45,9 +45,25 @@
internal static PdfDocument Open(Stream stream, ParsingOptions options)
{
var initialPosition = stream.Position;
var streamInput = new StreamInputBytes(stream, false);
return Open(streamInput, options);
try
{
return Open(streamInput, options);
}
catch (Exception ex)
{
if (initialPosition != 0)
{
throw new InvalidOperationException(
"Could not parse document due to an error, the input stream was not at position zero when provided to the Open method.",
ex);
}
throw;
}
}
private static PdfDocument Open(IInputBytes inputBytes, ParsingOptions options = null)
@@ -75,19 +91,28 @@
passwords.Add(string.Empty);
}
var document = OpenDocument(inputBytes, tokenScanner, options?.Logger ?? new NoOpLog(), isLenientParsing, passwords, clipPaths);
var finalOptions = new InternalParsingOptions(
passwords,
isLenientParsing,
clipPaths,
options?.SkipMissingFonts ?? false,
options?.Logger ?? new NoOpLog());
var document = OpenDocument(inputBytes, tokenScanner, finalOptions);
return document;
}
private static PdfDocument OpenDocument(IInputBytes inputBytes, ISeekableTokenScanner scanner, ILog log, bool isLenientParsing,
IReadOnlyList<string> passwords, bool clipPaths)
private static PdfDocument OpenDocument(
IInputBytes inputBytes,
ISeekableTokenScanner scanner,
InternalParsingOptions parsingOptions)
{
var filterProvider = new FilterProviderWithLookup(DefaultFilterProvider.Instance);
CrossReferenceTable crossReferenceTable = null;
var xrefValidator = new XrefOffsetValidator(log);
var xrefValidator = new XrefOffsetValidator(parsingOptions.Logger);
// We're ok with this since our intent is to lazily load the cross reference table.
// ReSharper disable once AccessToModifiedClosure
@@ -95,30 +120,39 @@
var pdfScanner = new PdfTokenScanner(inputBytes, locationProvider, filterProvider, NoOpEncryptionHandler.Instance);
var crossReferenceStreamParser = new CrossReferenceStreamParser(filterProvider);
var crossReferenceParser = new CrossReferenceParser(log, xrefValidator, crossReferenceStreamParser);
var crossReferenceParser = new CrossReferenceParser(parsingOptions.Logger, xrefValidator, crossReferenceStreamParser);
var version = FileHeaderParser.Parse(scanner, inputBytes, isLenientParsing, log);
var version = FileHeaderParser.Parse(scanner, inputBytes, parsingOptions.UseLenientParsing, parsingOptions.Logger);
var crossReferenceOffset = FileTrailerParser.GetFirstCrossReferenceOffset(inputBytes, scanner,
isLenientParsing) + version.OffsetInFile;
var crossReferenceOffset = FileTrailerParser.GetFirstCrossReferenceOffset(
inputBytes,
scanner,
parsingOptions.UseLenientParsing) + version.OffsetInFile;
// TODO: make this use the scanner.
var validator = new CrossReferenceOffsetValidator(xrefValidator);
crossReferenceOffset = validator.Validate(crossReferenceOffset, scanner, inputBytes, isLenientParsing);
crossReferenceOffset = validator.Validate(crossReferenceOffset, scanner, inputBytes, parsingOptions.UseLenientParsing);
crossReferenceTable = crossReferenceParser.Parse(inputBytes, isLenientParsing,
crossReferenceTable = crossReferenceParser.Parse(
inputBytes,
parsingOptions.UseLenientParsing,
crossReferenceOffset,
version.OffsetInFile,
pdfScanner,
scanner);
var (rootReference, rootDictionary) = ParseTrailer(crossReferenceTable, isLenientParsing,
var (rootReference, rootDictionary) = ParseTrailer(
crossReferenceTable,
parsingOptions.UseLenientParsing,
pdfScanner,
out var encryptionDictionary);
var encryptionHandler = encryptionDictionary != null ?
(IEncryptionHandler)new EncryptionHandler(encryptionDictionary, crossReferenceTable.Trailer, passwords)
(IEncryptionHandler)new EncryptionHandler(
encryptionDictionary,
crossReferenceTable.Trailer,
parsingOptions.Passwords)
: NoOpEncryptionHandler.Instance;
pdfScanner.UpdateEncryptionHandler(encryptionHandler);
@@ -128,35 +162,45 @@
var type1Handler = new Type1FontHandler(pdfScanner, filterProvider, encodingReader);
var fontFactory = new FontFactory(log, new Type0FontHandler(cidFontFactory,
var fontFactory = new FontFactory(parsingOptions.Logger, new Type0FontHandler(cidFontFactory,
filterProvider, pdfScanner),
new TrueTypeFontHandler(log, pdfScanner, filterProvider, encodingReader, SystemFontFinder.Instance,
new TrueTypeFontHandler(parsingOptions.Logger, pdfScanner, filterProvider, encodingReader, SystemFontFinder.Instance,
type1Handler),
type1Handler,
new Type3FontHandler(pdfScanner, filterProvider, encodingReader));
var resourceContainer = new ResourceStore(pdfScanner, fontFactory);
var information = DocumentInformationFactory.Create(pdfScanner, crossReferenceTable.Trailer, isLenientParsing);
var information = DocumentInformationFactory.Create(
pdfScanner,
crossReferenceTable.Trailer,
parsingOptions.UseLenientParsing);
var catalog = CatalogFactory.Create(rootReference, rootDictionary, pdfScanner, isLenientParsing);
var catalog = CatalogFactory.Create(
rootReference,
rootDictionary,
pdfScanner,
parsingOptions.UseLenientParsing);
var pageFactory = new PageFactory(pdfScanner, resourceContainer, filterProvider,
new PageContentParser(new ReflectionGraphicsStateOperationFactory()),
log);
var caching = new ParsingCachingProviders(resourceContainer);
new PageContentParser(new ReflectionGraphicsStateOperationFactory()));
var acroFormFactory = new AcroFormFactory(pdfScanner, filterProvider, crossReferenceTable);
var bookmarksProvider = new BookmarksProvider(log, pdfScanner);
var bookmarksProvider = new BookmarksProvider(parsingOptions.Logger, pdfScanner);
return new PdfDocument(log, inputBytes, version, crossReferenceTable, caching, pageFactory, catalog, information,
return new PdfDocument(
inputBytes,
version,
crossReferenceTable,
pageFactory,
catalog,
information,
encryptionDictionary,
pdfScanner,
filterProvider,
acroFormFactory,
bookmarksProvider,
clipPaths);
parsingOptions);
}
private static (IndirectReference, DictionaryToken) ParseTrailer(CrossReferenceTable crossReferenceTable, bool isLenientParsing, IPdfTokenScanner pdfTokenScanner,

View File

@@ -48,5 +48,11 @@
/// All passwords to try when opening this document, will include any values set for <see cref="Password"/>.
/// </summary>
public List<string> Passwords { get; set; } = new List<string>();
/// <summary>
/// Skip extracting content where the font could not be found, will result in some letters being skipped/missed
/// but will prevent the library throwing where the source PDF has some corrupted text.
/// </summary>
public bool SkipMissingFonts { get; set; } = false;
}
}

View File

@@ -10,7 +10,6 @@
using Encryption;
using Exceptions;
using Filters;
using Logging;
using Parser;
using Tokenization.Scanner;
using Tokens;
@@ -28,16 +27,9 @@
[NotNull]
private readonly HeaderVersion version;
private readonly ILog log;
private readonly IInputBytes inputBytes;
private readonly bool clipPaths;
[NotNull]
private readonly ParsingCachingProviders cachingProviders;
[CanBeNull]
private readonly EncryptionDictionary encryptionDictionary;
@@ -46,6 +38,7 @@
private readonly ILookupFilterProvider filterProvider;
private readonly BookmarksProvider bookmarksProvider;
private readonly InternalParsingOptions parsingOptions;
[NotNull]
private readonly Pages pages;
@@ -82,11 +75,10 @@
/// </summary>
public bool IsEncrypted => encryptionDictionary != null;
internal PdfDocument(ILog log,
internal PdfDocument(
IInputBytes inputBytes,
HeaderVersion version,
CrossReferenceTable crossReferenceTable,
ParsingCachingProviders cachingProviders,
IPageFactory pageFactory,
Catalog catalog,
DocumentInformation information,
@@ -95,17 +87,16 @@
ILookupFilterProvider filterProvider,
AcroFormFactory acroFormFactory,
BookmarksProvider bookmarksProvider,
bool clipPaths)
InternalParsingOptions parsingOptions)
{
this.log = log;
this.inputBytes = inputBytes;
this.version = version ?? throw new ArgumentNullException(nameof(version));
this.cachingProviders = cachingProviders ?? throw new ArgumentNullException(nameof(cachingProviders));
this.encryptionDictionary = encryptionDictionary;
this.pdfScanner = pdfScanner ?? throw new ArgumentNullException(nameof(pdfScanner));
this.filterProvider = filterProvider ?? throw new ArgumentNullException(nameof(filterProvider));
this.bookmarksProvider = bookmarksProvider ?? throw new ArgumentNullException(nameof(bookmarksProvider));
this.clipPaths = clipPaths;
this.parsingOptions = parsingOptions;
Information = information ?? throw new ArgumentNullException(nameof(information));
pages = new Pages(catalog, pageFactory, pdfScanner);
Structure = new Structure(catalog, crossReferenceTable, pdfScanner);
@@ -153,11 +144,11 @@
throw new ObjectDisposedException("Cannot access page after the document is disposed.");
}
log.Debug($"Accessing page {pageNumber}.");
parsingOptions.Logger.Debug($"Accessing page {pageNumber}.");
try
{
return pages.GetPage(pageNumber, clipPaths);
return pages.GetPage(pageNumber, parsingOptions);
}
catch (Exception ex)
{
@@ -258,7 +249,7 @@
}
catch (Exception ex)
{
log.Error("Failed disposing the PdfDocument due to an error.", ex);
parsingOptions.Logger.Error("Failed disposing the PdfDocument due to an error.", ex);
}
finally
{

View File

@@ -107,18 +107,25 @@
CMap toUnicodeCMap = null;
if (dictionary.TryGet(NameToken.ToUnicode, out var toUnicodeObj))
{
var toUnicode = DirectObjectFinder.Get<StreamToken>(toUnicodeObj, pdfScanner);
var decodedUnicodeCMap = toUnicode.Decode(filterProvider, pdfScanner);
if (decodedUnicodeCMap != null)
try
{
toUnicodeCMap = CMapCache.Parse(new ByteArrayInputBytes(decodedUnicodeCMap));
var toUnicode = DirectObjectFinder.Get<StreamToken>(toUnicodeObj, pdfScanner);
var decodedUnicodeCMap = toUnicode.Decode(filterProvider, pdfScanner);
if (decodedUnicodeCMap != null)
{
toUnicodeCMap = CMapCache.Parse(new ByteArrayInputBytes(decodedUnicodeCMap));
}
}
catch (Exception ex)
{
log.Error("Failed to decode ToUnicode CMap for a TrueType font in file due to an exception.", ex);
}
}
Encoding encoding = encodingReader.Read(dictionary, descriptor);
if (encoding == null && font?.TableRegister?.CMapTable != null
&& font.TableRegister.PostScriptTable?.GlyphNames != null)
{
@@ -199,7 +206,7 @@
$"Expected a TrueType font in the TrueType font descriptor, instead it was {descriptor.FontFile.FileType}.");
}
}
var font = TrueTypeFontParser.Parse(new TrueTypeDataBytes(new ByteArrayInputBytes(fontFile)));
return font;

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47;net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.6</Version>
<Version>0.1.7-alpha-001</Version>
<IsTestProject>False</IsTestProject>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>

View File

@@ -0,0 +1,44 @@
namespace UglyToad.PdfPig.Util
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
internal static class EnumerableExtensions
{
public static List<T> ToRecursiveOrderList<T>(this IEnumerable<T> collection,
Expression<Func<T, IEnumerable<T>>> childCollection)
{
var resultList = new List<T>();
var currentItems = new Queue<(int Index, T Item, int Depth)>(collection.Select(i => (0, i, 0)));
var depthItemCounter = 0;
var previousItemDepth = 0;
var childProperty = (PropertyInfo)((MemberExpression)childCollection.Body).Member;
while (currentItems.Count > 0)
{
var currentItem = currentItems.Dequeue();
// Reset counter for number of items at this depth when the depth changes.
if (currentItem.Depth != previousItemDepth)
{
depthItemCounter = 0;
}
var resultIndex = currentItem.Index + depthItemCounter++;
resultList.Insert(resultIndex, currentItem.Item);
var childItems = childProperty.GetValue(currentItem.Item) as IEnumerable<T> ?? Enumerable.Empty<T>();
foreach (var childItem in childItems)
{
currentItems.Enqueue((resultIndex + 1, childItem, currentItem.Depth + 1));
}
previousItemDepth = currentItem.Depth;
}
return resultList;
}
}
}

View File

@@ -3,7 +3,6 @@ namespace UglyToad.PdfPig.Writer
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Content;
@@ -559,6 +558,11 @@ namespace UglyToad.PdfPig.Writer
pageDictionary[NameToken.MediaBox] = RectangleToArray(page.Value.PageSize);
}
if (page.Value.rotation.HasValue)
{
pageDictionary[NameToken.Rotate] = new NumericToken(page.Value.rotation.Value);
}
// Adobe Acrobat errors if content streams ref'd by multiple pages, turn off
// dedup if on to avoid issues
var prev = context.AttemptDeduplication;

View File

@@ -46,6 +46,8 @@
private int imageKey = 1;
internal int? rotation;
internal IReadOnlyDictionary<string, IToken> Resources => pageDictionary.GetOrCreateDict(NameToken.Resources);
/// <summary>
@@ -131,7 +133,7 @@
/// <param name="from">The first point on the line.</param>
/// <param name="to">The last point on the line.</param>
/// <param name="lineWidth">The width of the line in user space units.</param>
public void DrawLine(PdfPoint from, PdfPoint to, decimal lineWidth = 1)
public PdfPageBuilder DrawLine(PdfPoint from, PdfPoint to, decimal lineWidth = 1)
{
if (lineWidth != 1)
{
@@ -146,6 +148,8 @@
{
currentStream.Add(new SetLineWidth(1));
}
return this;
}
/// <summary>
@@ -156,7 +160,7 @@
/// <param name="height">The height of the rectangle.</param>
/// <param name="lineWidth">The width of the line border of the rectangle.</param>
/// <param name="fill">Whether to fill with the color set by <see cref="SetTextAndFillColor"/>.</param>
public void DrawRectangle(PdfPoint position, decimal width, decimal height, decimal lineWidth = 1, bool fill = false)
public PdfPageBuilder DrawRectangle(PdfPoint position, decimal width, decimal height, decimal lineWidth = 1, bool fill = false)
{
if (lineWidth != 1)
{
@@ -178,6 +182,128 @@
{
currentStream.Add(new SetLineWidth(lineWidth));
}
return this;
}
/// <summary>
/// Set the number of degrees by which the page is rotated clockwise when displayed or printed.
/// </summary>
public PdfPageBuilder SetRotation(PageRotationDegrees degrees)
{
rotation = degrees.Value;
return this;
}
/// <summary>
/// Draws a triangle on the current page with the specified points and line width.
/// </summary>
/// <param name="point1">Position of the first corner of the triangle.</param>
/// <param name="point2">Position of the second corner of the triangle.</param>
/// <param name="point3">Position of the third corner of the triangle.</param>
/// <param name="lineWidth">The width of the line border of the triangle.</param>
/// <param name="fill">Whether to fill with the color set by <see cref="SetTextAndFillColor"/>.</param>
public PdfPageBuilder DrawTriangle(PdfPoint point1, PdfPoint point2, PdfPoint point3, decimal lineWidth = 1, bool fill = false)
{
if (lineWidth != 1)
{
currentStream.Add(new SetLineWidth(lineWidth));
}
currentStream.Add(new BeginNewSubpath((decimal)point1.X, (decimal)point1.Y));
currentStream.Add(new AppendStraightLineSegment((decimal)point2.X, (decimal)point2.Y));
currentStream.Add(new AppendStraightLineSegment((decimal)point3.X, (decimal)point3.Y));
currentStream.Add(new AppendStraightLineSegment((decimal)point1.X, (decimal)point1.Y));
if (fill)
{
currentStream.Add(FillPathEvenOddRuleAndStroke.Value);
}
else
{
currentStream.Add(StrokePath.Value);
}
if (lineWidth != 1)
{
currentStream.Add(new SetLineWidth(lineWidth));
}
return this;
}
/// <summary>
/// Draws a circle on the current page centering at the specified point with the given diameter and line width.
/// </summary>
/// <param name="center">The center position of the circle.</param>
/// <param name="diameter">The diameter of the circle.</param>
/// <param name="lineWidth">The width of the line border of the circle.</param>
/// <param name="fill">Whether to fill with the color set by <see cref="SetTextAndFillColor"/>.</param>
public PdfPageBuilder DrawCircle(PdfPoint center, decimal diameter, decimal lineWidth = 1, bool fill = false)
{
DrawEllipsis(center, diameter, diameter, lineWidth, fill);
return this;
}
/// <summary>
/// Draws an ellipsis on the current page centering at the specified point with the given width, height and line width.
/// </summary>
/// <param name="center">The center position of the ellipsis.</param>
/// <param name="width">The width of the ellipsis.</param>
/// <param name="height">The height of the ellipsis.</param>
/// <param name="lineWidth">The width of the line border of the ellipsis.</param>
/// <param name="fill">Whether to fill with the color set by <see cref="SetTextAndFillColor"/>.</param>
public PdfPageBuilder DrawEllipsis(PdfPoint center, decimal width, decimal height, decimal lineWidth = 1, bool fill = false)
{
width /= 2;
height /= 2;
// See here: https://spencermortensen.com/articles/bezier-circle/
decimal cc = 0.55228474983079m;
if (lineWidth != 1)
{
currentStream.Add(new SetLineWidth(lineWidth));
}
currentStream.Add(new BeginNewSubpath((decimal)center.X - width, (decimal)center.Y));
currentStream.Add(new AppendDualControlPointBezierCurve(
(decimal)center.X - width, (decimal)center.Y + height * cc,
(decimal)center.X - width * cc, (decimal)center.Y + height,
(decimal)center.X, (decimal)center.Y + height
));
currentStream.Add(new AppendDualControlPointBezierCurve(
(decimal)center.X + width * cc, (decimal)center.Y + height,
(decimal)center.X + width, (decimal)center.Y + height * cc,
(decimal)center.X + width, (decimal)center.Y
));
currentStream.Add(new AppendDualControlPointBezierCurve(
(decimal)center.X + width, (decimal)center.Y - height * cc,
(decimal)center.X + width * cc, (decimal)center.Y - height,
(decimal)center.X, (decimal)center.Y - height
));
currentStream.Add(new AppendDualControlPointBezierCurve(
(decimal)center.X - width * cc, (decimal)center.Y - height,
(decimal)center.X - width, (decimal)center.Y - height * cc,
(decimal)center.X - width, (decimal)center.Y
));
if (fill)
{
currentStream.Add(FillPathEvenOddRuleAndStroke.Value);
}
else
{
currentStream.Add(StrokePath.Value);
}
if (lineWidth != 1)
{
currentStream.Add(new SetLineWidth(lineWidth));
}
return this;
}
/// <summary>
@@ -186,10 +312,12 @@
/// <param name="r">Red - 0 to 255</param>
/// <param name="g">Green - 0 to 255</param>
/// <param name="b">Blue - 0 to 255</param>
public void SetStrokeColor(byte r, byte g, byte b)
public PdfPageBuilder SetStrokeColor(byte r, byte g, byte b)
{
currentStream.Add(Push.Value);
currentStream.Add(new SetStrokeColorDeviceRgb(RgbToDecimal(r), RgbToDecimal(g), RgbToDecimal(b)));
return this;
}
/// <summary>
@@ -198,11 +326,13 @@
/// <param name="r">Red - 0 to 1</param>
/// <param name="g">Green - 0 to 1</param>
/// <param name="b">Blue - 0 to 1</param>
internal void SetStrokeColorExact(decimal r, decimal g, decimal b)
internal PdfPageBuilder SetStrokeColorExact(decimal r, decimal g, decimal b)
{
currentStream.Add(Push.Value);
currentStream.Add(new SetStrokeColorDeviceRgb(CheckRgbDecimal(r, nameof(r)),
CheckRgbDecimal(g, nameof(g)), CheckRgbDecimal(b, nameof(b))));
return this;
}
/// <summary>
@@ -211,18 +341,22 @@
/// <param name="r">Red - 0 to 255</param>
/// <param name="g">Green - 0 to 255</param>
/// <param name="b">Blue - 0 to 255</param>
public void SetTextAndFillColor(byte r, byte g, byte b)
public PdfPageBuilder SetTextAndFillColor(byte r, byte g, byte b)
{
currentStream.Add(Push.Value);
currentStream.Add(new SetNonStrokeColorDeviceRgb(RgbToDecimal(r), RgbToDecimal(g), RgbToDecimal(b)));
return this;
}
/// <summary>
/// Restores the stroke, text and fill color to default (black).
/// </summary>
public void ResetColor()
public PdfPageBuilder ResetColor()
{
currentStream.Add(Pop.Value);
return this;
}
/// <summary>
@@ -346,9 +480,11 @@
/// To insert invisible text, for example output of OCR, use <c>TextRenderingMode.Neither</c>.
/// </summary>
/// <param name="mode">Text rendering mode to set.</param>
public void SetTextRenderingMode(TextRenderingMode mode)
public PdfPageBuilder SetTextRenderingMode(TextRenderingMode mode)
{
currentStream.Add(new SetTextRenderingMode(mode));
return this;
}
private NameToken GetAddedFont(PdfDocumentBuilder.AddedFont font)
@@ -585,7 +721,7 @@
/// Copy a page from unknown source to this page
/// </summary>
/// <param name="srcPage">Page to be copied</param>
public void CopyFrom(Page srcPage)
public PdfPageBuilder CopyFrom(Page srcPage)
{
if (currentStream.Operations.Count > 0)
{
@@ -599,7 +735,7 @@
// If the page doesn't have resources, then we copy the entire content stream, since not operation would collide
// with the ones already written
destinationStream.Operations.AddRange(srcPage.Operations);
return;
return this;
}
// TODO: How should we handle any other token in the page dictionary (Eg. LastModified, MediaBox, CropBox, BleedBox, TrimBox, ArtBox,
@@ -723,6 +859,8 @@
}
destinationStream.Operations.AddRange(operations);
return this;
}
private List<Letter> DrawLetters(NameToken name, string text, IWritingFont font, TransformationMatrix fontMatrix, decimal fontSize, TransformationMatrix textMatrix)

View File

@@ -11,7 +11,7 @@
<PackageTags>PDF;Reader;Document;Adobe;PDFBox;PdfPig;pdf-extract;pdf-to-text;pdf;file;text;C#;dotnet;.NET</PackageTags>
<RepositoryUrl>https://github.com/UglyToad/PdfPig</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Version>0.1.6</Version>
<Version>0.1.7-alpha-001</Version>
<AssemblyVersion>0.1.5.0</AssemblyVersion>
<FileVersion>0.1.5.0</FileVersion>
<PackageIconUrl>https://raw.githubusercontent.com/UglyToad/PdfPig/master/documentation/pdfpig.png</PackageIconUrl>