Compare commits

..

19 Commits

Author SHA1 Message Date
Eliot Jones
0370c51371 add a test for #208 2020-09-03 15:53:27 +01:00
Eliot Jones
c634ec3aa2 add more informative exceptions when reading encoding fails 2020-09-03 14:36:42 +01:00
Eliot Jones
9b7554c973 #203 enable utf16 be strings to be written to the document builder 2020-08-27 09:06:14 +01:00
Eliot Jones
f2522f149c add a convenience method for extracting the original rectangle from a subpath 2020-08-22 17:04:14 +01:00
Eliot Jones
04f785aed4 attempt to find bug in #202 due to seemingly empty codespace ranges 2020-08-22 15:38:59 +01:00
Eliot Jones
5ffada46e3 Merge pull request #201 from UglyToad/png-builder-support
Png builder support
2020-08-22 15:37:20 +01:00
Eliot Jones
54f227ea95 add support for extracting grayscale images and inline images 2020-08-22 15:08:59 +01:00
Eliot Jones
6359ba5df1 handle objects without endobj markers #198 2020-08-21 18:15:30 +01:00
Eliot Jones
52104b6580 support conversion of pdf format images to png 2020-08-21 13:12:01 +01:00
Eliot Jones
8860e29191 tidy up png support 2020-08-21 12:11:27 +01:00
Eliot Jones
5ac7a957d0 add initial png support 2020-08-21 10:50:17 +01:00
Eliot Jones
6f26b274c4 Merge pull request #197 from Poltuu/fix_undefined_references
FIX : undefined references is a valid use case.
2020-08-19 17:48:09 +01:00
Eliot Jones
b2b85e2f44 Merge pull request #200 from everharder/get-encoding-from-indirectreferencetoken
FIX Error when getting Encoding from an IndirectReferenceToken referencing a Name Token
2020-08-19 17:45:21 +01:00
Daniel Eberharter
304c17eac9 if baseEncodingObject is a IndirectReferenceToken to a NameToken the previous implementation tried to resolve it as a DictionaryToken, which resultet in an PdfFormatException 2020-08-19 17:01:30 +02:00
romain v
5a82c36631 FIX : undefined references is a valid use case.
I tried to mitigate the breaking change by keep on throwing in most uses of the change method.
2020-08-17 11:10:44 +02:00
Eliot Jones
dc55fd8011 guess at possible bug in #192
it may be that we try reading a corrupted os/2 table, hard to tell without test document.
2020-07-26 14:36:35 +01:00
Eliot Jones
142c871c47 update editorconfig to override other project defaults 2020-07-26 14:29:18 +01:00
Eliot Jones
e42df5b5b5 fix remaining missing culture dependent strings #190 2020-07-26 14:14:37 +01:00
Eliot Jones
f3f7533507 add string culture in core code #190 2020-07-26 13:53:34 +01:00
62 changed files with 2261 additions and 300 deletions

View File

@@ -1,4 +1,94 @@
root = true
[*.cs]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_style = space
indent_size = 4
indent_size = 4
tab_width = 4
# New line preferences
end_of_line = crlf
insert_final_newline = false
#### .NET Coding Conventions ####
# Organize usings
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = false
# Parameter preferences
dotnet_code_quality_unused_parameters = all:suggestion
# Modifier preferences
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
# Code-block preferences
csharp_prefer_braces = true:warning
# 'using' directive preferences
csharp_using_directive_placement = inside_namespace:silent
#### Naming styles ####
# Naming rules
dotnet_naming_rule.private_or_internal_static_field_should_be_pascal_case.severity = warning
dotnet_naming_rule.private_or_internal_static_field_should_be_pascal_case.symbols = private_or_internal_static_field
dotnet_naming_rule.private_or_internal_static_field_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.private_or_internal_field_should_be_not_underscored.severity = warning
dotnet_naming_rule.private_or_internal_field_should_be_not_underscored.symbols = private_or_internal_field
dotnet_naming_rule.private_or_internal_field_should_be_not_underscored.style = not_underscored
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected
dotnet_naming_symbols.private_or_internal_field.required_modifiers =
dotnet_naming_symbols.private_or_internal_static_field.applicable_kinds = field
dotnet_naming_symbols.private_or_internal_static_field.applicable_accessibilities = internal, private, private_protected
dotnet_naming_symbols.private_or_internal_static_field.required_modifiers = static
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.not_underscored.required_prefix =
dotnet_naming_style.not_underscored.required_suffix =
dotnet_naming_style.not_underscored.word_separator =
dotnet_naming_style.not_underscored.capitalization = camel_case

View File

@@ -1,6 +1,7 @@
namespace UglyToad.PdfPig.Core
{
using System.Diagnostics;
using System.Globalization;
/// <summary>
/// A point in a PDF file.
@@ -112,7 +113,7 @@
/// <inheritdoc />
public override string ToString()
{
return $"(x:{X}, y:{Y})";
return $"(x:{X.ToString(CultureInfo.InvariantCulture)}, y:{Y.ToString(CultureInfo.InvariantCulture)})";
}
}
}

View File

@@ -1,6 +1,7 @@
namespace UglyToad.PdfPig.Core
{
using System;
using System.Globalization;
/// <summary>
/// A rectangle in a PDF file.
@@ -208,7 +209,7 @@
/// <inheritdoc />
public override string ToString()
{
return $"[{TopLeft}, {Width}, {Height}]";
return $"[{TopLeft}, {Width.ToString(CultureInfo.InvariantCulture)}, {Height.ToString(CultureInfo.InvariantCulture)}]";
}
}
}

View File

@@ -29,28 +29,13 @@
/// <summary>
/// Return true if points are organised in a clockwise order. Works only with closed paths.
/// </summary>
/// <returns></returns>
public bool IsClockwise
{
get
{
if (!IsClosed()) return false;
return shoeLaceSum > 0;
}
}
public bool IsClockwise => IsClosed() && shoeLaceSum > 0;
/// <summary>
/// Return true if points are organised in a counterclockwise order. Works only with closed paths.
/// </summary>
/// <returns></returns>
public bool IsCounterClockwise
{
get
{
if (!IsClosed()) return false;
return shoeLaceSum < 0;
}
}
public bool IsCounterClockwise => IsClosed() && shoeLaceSum < 0;
/// <summary>
/// Get the <see cref="PdfSubpath"/>'s centroid point.
@@ -58,9 +43,14 @@
public PdfPoint GetCentroid()
{
var filtered = commands.Where(c => c is Line || c is BezierCurve).ToList();
if (filtered.Count == 0) return new PdfPoint();
if (filtered.Count == 0)
{
return new PdfPoint();
}
var points = filtered.Select(GetStartPoint).ToList();
points.AddRange(filtered.Select(GetEndPoint));
return new PdfPoint(points.Average(p => p.X), points.Average(p => p.Y));
}
@@ -230,10 +220,40 @@
/// </summary>
public bool IsClosed()
{
if (Commands.Any(c => c is Close)) return true;
var filtered = Commands.Where(c => c is Line || c is BezierCurve || c is Move).ToList();
if (filtered.Count < 2) return false;
if (!GetStartPoint(filtered.First()).Equals(GetEndPoint(filtered.Last()))) return false;
var filteredCount = 0;
IPathCommand last = null;
IPathCommand first = null;
for (int i = Commands.Count - 1; i >= 0; i--)
{
var cmd = Commands[i];
if (cmd is Close)
{
return true;
}
if (cmd is Line || cmd is BezierCurve || cmd is Move)
{
if (last == null)
{
last = cmd;
}
first = cmd;
filteredCount++;
}
}
if (filteredCount < 2 || last == null || first == null)
{
return false;
}
if (!GetStartPoint(first).Equals(GetEndPoint(last)))
{
return false;
}
return true;
}
@@ -296,6 +316,42 @@
return new PdfRectangle(minX, minY, maxX, maxY);
}
/// <summary>
/// If <see cref="IsDrawnAsRectangle"/> then returns the rectangle dimensions specified. Otherwise returns <see langword="null"/>.
/// </summary>
/// <remarks>
/// Since a rectangle is interpreted as a move command followed by 3 lines and a close command this condenses the 5 commands back into a single rectangle.
/// </remarks>
public PdfRectangle? GetDrawnRectangle()
{
if (!IsDrawnAsRectangle || Commands.Count != 5)
{
return null;
}
if (!(Commands[0] is Move mv) || !(Commands[1] is Line line1) || !(Commands[2] is Line line2) || !(Commands[3] is Line line3)
|| !(Commands[4] is Close))
{
return null;
}
if (!line1.From.Equals(mv.Location) || line1.To.Y != mv.Location.Y)
{
return null;
}
var width = line1.To.X - mv.Location.X;
if (!line2.From.Equals(line1.To) || line2.To.X != line1.To.X)
{
return null;
}
var height = line2.To.Y - line1.To.Y;
return new PdfRectangle(mv.Location, new PdfPoint(mv.Location.X + width, mv.Location.Y + height));
}
/// <summary>
/// A command in a <see cref="PdfSubpath"/>.
/// </summary>
@@ -450,6 +506,7 @@
{
return From.Equals(line.From) && To.Equals(line.To);
}
return false;
}
@@ -692,17 +749,20 @@
/// </summary>
public override bool Equals(object obj)
{
if (obj is PdfSubpath path)
if (!(obj is PdfSubpath path) || Commands.Count != path.Commands.Count)
{
if (Commands.Count != path.Commands.Count) return false;
for (int i = 0; i < Commands.Count; i++)
{
if (!Commands[i].Equals(path.Commands[i])) return false;
}
return true;
return false;
}
return false;
for (int i = 0; i < Commands.Count; i++)
{
if (!Commands[i].Equals(path.Commands[i]))
{
return false;
}
}
return true;
}
/// <summary>
@@ -715,6 +775,7 @@
{
hash = hash * (i + 1) * 17 + Commands[i].GetHashCode();
}
return hash;
}
}

View File

@@ -2,6 +2,7 @@
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using Core;
@@ -184,7 +185,7 @@
if (i >= 0)
{
var value = Values[i];
stringBuilder.AppendLine(value.ToString("N"));
stringBuilder.AppendLine(value.ToString("N", CultureInfo.InvariantCulture));
}
foreach (var identifier in GetCommandsAt(i + 1))

View File

@@ -35,7 +35,7 @@
var selectionFlags = data.ReadUnsignedShort();
var firstCharacterIndex = data.ReadUnsignedShort();
var lastCharacterIndex = data.ReadUnsignedShort();
var unicodeCharRange = new[] {ulCharRange1, ulCharRange2, ulCharRange3, ulCharRange4};
var unicodeCharRange = new[] { ulCharRange1, ulCharRange2, ulCharRange3, ulCharRange4 };
var vendorId = Encoding.ASCII.GetString(vendorIdBytes);
@@ -67,11 +67,42 @@
lastCharacterIndex);
}
var sTypoAscender = data.ReadSignedShort();
var sTypoDescender = data.ReadSignedShort();
var sTypoLineGap = data.ReadSignedShort();
var usWinAscent = data.ReadUnsignedShort();
var usWinDescent = data.ReadUnsignedShort();
short sTypoAscender;
short sTypoDescender;
short sTypoLineGap;
ushort usWinAscent;
ushort usWinDescent;
try
{
sTypoAscender = data.ReadSignedShort();
sTypoDescender = data.ReadSignedShort();
sTypoLineGap = data.ReadSignedShort();
usWinAscent = data.ReadUnsignedShort();
usWinDescent = data.ReadUnsignedShort();
}
catch
{
// Font may be invalid. Try falling back to shorter version...
return new Os2Table(header, version, xAvgCharWidth,
weightClass, widthClass, typeFlags, ySubscriptXSize,
ySubscriptYSize,
ySubscriptXOffset,
ySubscriptYOffset,
ySuperscriptXSize,
ySuperscriptYSize,
ySuperscriptXOffset,
ySuperscriptYOffset,
yStrikeoutSize,
yStrikeoutPosition,
familyClass,
panose,
unicodeCharRange,
vendorId,
selectionFlags,
firstCharacterIndex,
lastCharacterIndex);
}
if (version == 0)
{
@@ -93,7 +124,7 @@
selectionFlags,
firstCharacterIndex,
lastCharacterIndex,
sTypoAscender,
sTypoAscender,
sTypoDescender,
sTypoLineGap,
usWinAscent,
@@ -173,7 +204,7 @@
var usLowerOpticalPointSize = data.ReadUnsignedShort();
var usUpperOpticalPointSize = data.ReadUnsignedShort();
return new Os2Version5OpenTypeTable(header, version, xAvgCharWidth,
weightClass, widthClass, typeFlags, ySubscriptXSize,
ySubscriptYSize,

View File

@@ -2,6 +2,7 @@
{
using System;
using System.Collections.Generic;
using System.Globalization;
internal class Type1CharstringDecryptedBytes
{
@@ -25,7 +26,7 @@
{
Bytes = bytes ?? throw new ArgumentNullException(nameof(bytes));
Index = index;
Name = name ?? index.ToString();
Name = name ?? index.ToString(CultureInfo.InvariantCulture);
Source = SourceType.Charstring;
}

View File

@@ -0,0 +1,56 @@
namespace UglyToad.PdfPig.Tests.Fonts.Parser
{
using System.Text.RegularExpressions;
using PdfFonts.Parser;
using PdfPig.Core;
using PdfPig.Encryption;
using PdfPig.Tokenization.Scanner;
using PdfPig.Tokens;
using Xunit;
public class EncodingReaderTests
{
[Fact]
public void GetWinAnsiFromNameObject()
{
const string input = @"
1 0 obj
<< /Type /Font /Encoding 12 0 R /Name /WindowsFont1>>
endobj
12 0 obj
/WinAnsiEncoding
endobj";
var scanner = GetScanner(input);
var reader = new EncodingReader(scanner);
var fontDictionary = scanner.Get(new IndirectReference(1, 0));
var encoding = reader.Read(fontDictionary.Data as DictionaryToken);
Assert.NotNull(encoding);
}
private static PdfTokenScanner GetScanner(string s)
{
var input = StringBytesTestConverter.Convert(s, false);
var locationProvider = new TestObjectLocationProvider();
var regex = new Regex(@"\n(\d+)\s(\d+)\sobj");
foreach (Match match in regex.Matches(s))
{
var objN = match.Groups[1].Value;
var gen = match.Groups[2].Value;
locationProvider.Offsets[new IndirectReference(int.Parse(objN), int.Parse(gen))] = match.Index + 1;
}
return new PdfTokenScanner(input.Bytes, locationProvider,
new TestFilterProvider(), NoOpEncryptionHandler.Instance);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,6 +1,7 @@
namespace UglyToad.PdfPig.Tests.Integration
{
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using Content;
@@ -63,7 +64,10 @@
{
var stripped = x.Trim();
var parts = stripped.Split('|');
return (index: int.Parse(parts[0]), letter: parts[1], x: double.Parse(parts[2]), y: double.Parse(parts[3]));
var index = int.Parse(parts[0]);
var xval = double.Parse(parts[2], CultureInfo.InvariantCulture);
var yval = double.Parse(parts[3], CultureInfo.InvariantCulture);
return (index, letter: parts[1], x: xval, y: yval);
}).ToArray();
using (var document = PdfDocument.Open(GetFilename()))

View File

@@ -1,6 +1,7 @@
namespace UglyToad.PdfPig.Tests.Integration
{
using Content;
using Images.Png;
using Xunit;
public class SwedishTouringCarChampionshipTests
@@ -89,5 +90,29 @@
Assert.Equal("https://en.wikipedia.org/wiki/Swedish_Touring_Car_Championship", fullLink.Uri);
}
}
[Fact]
public void GetsImagesAsPng()
{
using (var document = PdfDocument.Open(GetFilename()))
{
foreach (var page in document.GetPages())
{
foreach (var image in page.GetImages())
{
if (!image.TryGetBytes(out _))
{
continue;
}
Assert.True(image.TryGetPng(out var png));
var pngActual = Png.Open(png);
Assert.NotNull(pngActual);
}
}
}
}
}
}

View File

@@ -50,6 +50,36 @@ endobj";
var reference = Assert.IsType<IndirectReferenceToken>(token.Data);
Assert.Equal(new IndirectReference(12, 7), reference.Data);
}
[Fact]
public void ReadsObjectWithUndefinedIndirectReference()
{
const string s = @"
5 0 obj
<<
/XObject <<
/Pic1 7 0 R
>>
/ProcSet [/PDF /Text /ImageC ]
/Font <<
/F0 8 0 R
/F1 9 0 R
/F2 10 0 R
/F3 0 0 R
>>
>>
endobj";
var scanner = GetScanner(s);
ReadToEnd(scanner);
var token = scanner.Get(new IndirectReference(5, 0));
Assert.NotNull(token);
token = scanner.Get(new IndirectReference(0, 0));
Assert.Null(token);
}
[Fact]

View File

@@ -1,24 +0,0 @@
namespace UglyToad.PdfPig.Tests.Writer
{
using System.IO;
using Integration;
using PdfPig.Writer;
using Xunit;
public class PdfDocumentBuilderFromExistingTests
{
[Fact]
public void LoadAndSaveExistingNoModifications()
{
var path = IntegrationHelpers.GetDocumentPath("Single Page Simple - from open office.pdf");
var bytes = File.ReadAllBytes(path);
var builder = PdfDocumentBuilder.FromPdf(bytes);
var output = builder.Build();
Assert.NotNull(output);
}
}
}

View File

@@ -529,7 +529,66 @@
}
}
private static void WriteFile(string name, byte[] bytes)
[Fact]
public void CanWriteSinglePageWithPng()
{
var builder = new PdfDocumentBuilder();
var page = builder.AddPage(PageSize.A4);
var font = builder.AddStandard14Font(Standard14Font.Helvetica);
page.AddText("Piggy", 12, new PdfPoint(25, page.PageSize.Height - 52), font);
var img = IntegrationHelpers.GetDocumentPath("pdfpig.png", false);
var expectedBounds = new PdfRectangle(25, page.PageSize.Height - 300, 200, page.PageSize.Height - 200);
var imageBytes = File.ReadAllBytes(img);
page.AddPng(imageBytes, expectedBounds);
var bytes = builder.Build();
WriteFile(nameof(CanWriteSinglePageWithPng), bytes);
using (var document = PdfDocument.Open(bytes))
{
var page1 = document.GetPage(1);
Assert.Equal("Piggy", page1.Text);
var image = Assert.Single(page1.GetImages());
Assert.NotNull(image);
Assert.Equal(expectedBounds.BottomLeft, image.Bounds.BottomLeft);
Assert.Equal(expectedBounds.TopRight, image.Bounds.TopRight);
Assert.True(image.TryGetPng(out var png));
Assert.NotNull(png);
WriteFile(nameof(CanWriteSinglePageWithPng) + "out", png, "png");
}
}
[Fact]
public void CanCreateDocumentInformationDictionaryWithNonAsciiCharacters()
{
const string littlePig = "маленький поросенок";
var builder = new PdfDocumentBuilder();
builder.DocumentInformation.Title = littlePig;
var page = builder.AddPage(PageSize.A4);
var font = builder.AddTrueTypeFont(TrueTypeTestHelper.GetFileBytes("Roboto-Regular.ttf"));
page.AddText(littlePig, 12, new PdfPoint(120, 600), font);
var file = builder.Build();
WriteFile(nameof(CanCreateDocumentInformationDictionaryWithNonAsciiCharacters), file);
using (var document = PdfDocument.Open(file))
{
Assert.Equal(littlePig, document.Information.Title);
}
}
private static void WriteFile(string name, byte[] bytes, string extension = "pdf")
{
try
{
@@ -538,7 +597,7 @@
Directory.CreateDirectory("Builder");
}
var output = Path.Combine("Builder", $"{name}.pdf");
var output = Path.Combine("Builder", $"{name}.{extension}");
File.WriteAllBytes(output, bytes);
}

View File

@@ -162,6 +162,10 @@
}
var kidObject = tokenScanner.Get(kidReferenceToken.Data);
if (kidObject is null)
{
throw new InvalidOperationException($"Could not find the object with reference: {kidReferenceToken.Data}.");
}
if (kidObject.Data is DictionaryToken kidDictionaryToken)
{

View File

@@ -89,5 +89,10 @@
/// <see cref="RawBytes"/> should be used directly.
/// </summary>
bool TryGetBytes(out IReadOnlyList<byte> bytes);
/// <summary>
/// Try to convert the image to PNG. Doesn't support conversion of JPG to PNG.
/// </summary>
bool TryGetPng(out byte[] bytes);
}
}

View File

@@ -8,6 +8,7 @@
using Graphics.Colors;
using Graphics.Core;
using Tokens;
using Images.Png;
/// <inheritdoc />
/// <summary>
@@ -111,6 +112,9 @@
return true;
}
/// <inheritdoc />
public bool TryGetPng(out byte[] bytes) => PngFromPdfImageFactory.TryGenerate(this, out bytes);
/// <inheritdoc />
public override string ToString()
{

View File

@@ -1,14 +1,12 @@
namespace UglyToad.PdfPig.Content
{
using System;
using System.Diagnostics.Contracts;
using Core;
using Geometry;
using System.Globalization;
/// <summary>
/// Represents the rotation of a page in a PDF document defined by the page dictionary in degrees clockwise.
/// </summary>
public struct PageRotationDegrees : IEquatable<PageRotationDegrees>
public readonly struct PageRotationDegrees : IEquatable<PageRotationDegrees>
{
/// <summary>
/// The rotation of the page in degrees clockwise.
@@ -76,7 +74,7 @@
/// <inheritdoc />
public override string ToString()
{
return Value.ToString();
return Value.ToString(CultureInfo.InvariantCulture);
}
/// <inheritdoc />

View File

@@ -22,23 +22,16 @@
Count = catalog.PagesDictionary.GetIntOrDefault(NameToken.Count);
}
public Page GetPage(int pageNumber, bool clipPaths)
{
if (pageNumber <= 0 || pageNumber > Count)
{
throw new ArgumentOutOfRangeException(nameof(pageNumber),
throw new ArgumentOutOfRangeException(nameof(pageNumber),
$"Page number {pageNumber} invalid, must be between 1 and {Count}.");
}
var pageNode = catalog.GetPageNode(pageNumber);
return CreateFromPageTreeNode(pageNode, pdfScanner, pageFactory, pageNumber, clipPaths);
}
public static Page CreateFromPageTreeNode(PageTreeNode pageNode, IPdfTokenScanner pdfScanner,
IPageFactory pageFactory,
int pageNumber, bool clipPaths)
{
var pageStack = new Stack<PageTreeNode>();
var currentNode = pageNode;
@@ -49,7 +42,7 @@
}
var pageTreeMembers = new PageTreeMembers();
while (pageStack.Count > 0)
{
currentNode = pageStack.Pop();
@@ -58,7 +51,7 @@
{
pageTreeMembers.ParentResources.Enqueue(resourcesDictionary);
}
if (currentNode.NodeDictionary.TryGet(NameToken.MediaBox, pdfScanner, out ArrayToken mediaBox))
{
pageTreeMembers.MediaBox = new MediaBox(mediaBox.ToRectangle(pdfScanner));
@@ -71,7 +64,7 @@
}
var page = pageFactory.Create(pageNumber, pageNode.NodeDictionary, pageTreeMembers, clipPaths);
return page;
}
}

View File

@@ -148,7 +148,8 @@
if (fontObject == null)
{
throw new InvalidOperationException($"Could not retrieve the font with name: {pair.Key} which should have been object {objectKey}");
//This is a valid use case
continue;
}
loadedFonts[reference] = fontFactory.Get(fontObject);

View File

@@ -1,12 +1,13 @@
namespace UglyToad.PdfPig.Geometry
{
using System;
using System.Globalization;
/// <summary>
/// By default user space units correspond to 1/72nd of an inch (a typographic point).
/// The UserUnit entry in a page dictionary can define the space units as a different multiple of 1/72 (1 point).
/// </summary>
internal struct UserSpaceUnit
internal readonly struct UserSpaceUnit
{
public static readonly UserSpaceUnit Default = new UserSpaceUnit(1);
@@ -30,7 +31,7 @@
public override string ToString()
{
return PointMultiples.ToString();
return PointMultiples.ToString(CultureInfo.InvariantCulture);
}
}
}

View File

@@ -1,6 +1,7 @@
namespace UglyToad.PdfPig.Graphics.Operations
{
using System;
using System.Globalization;
using System.IO;
using PdfPig.Core;
@@ -37,7 +38,7 @@
public static void WriteDecimal(this Stream stream, decimal value)
{
stream.WriteText(value.ToString("G"));
stream.WriteText(value.ToString("G", CultureInfo.InvariantCulture));
}
public static void WriteNumberText(this Stream stream, decimal number, string text)

View File

@@ -1,6 +1,7 @@
namespace UglyToad.PdfPig.Graphics.Operations.TextState
{
using System;
using System.Globalization;
using System.IO;
using Tokens;
using Util.JetBrains.Annotations;
@@ -64,7 +65,7 @@
/// <inheritdoc />
public override string ToString()
{
return $"{Font} {Size} {Symbol}";
return $"{Font} {Size.ToString("G", CultureInfo.InvariantCulture)} {Symbol}";
}
}
}

View File

@@ -0,0 +1,114 @@
namespace UglyToad.PdfPig.Images.Png
{
using System.Collections.Generic;
internal static class Adam7
{
/// <summary>
/// For a given pass number (1 indexed) the scanline indexes of the lines included in that pass in the 8x8 grid.
/// </summary>
private static readonly IReadOnlyDictionary<int, int[]> PassToScanlineGridIndex = new Dictionary<int, int[]>
{
{ 1, new []{ 0 } },
{ 2, new []{ 0 } },
{ 3, new []{ 4 } },
{ 4, new []{ 0, 4 } },
{ 5, new []{ 2, 6 } },
{ 6, new[] { 0, 2, 4, 6 } },
{ 7, new[] { 1, 3, 5, 7 } }
};
private static readonly IReadOnlyDictionary<int, int[]> PassToScanlineColumnIndex = new Dictionary<int, int[]>
{
{ 1, new []{ 0 } },
{ 2, new []{ 4 } },
{ 3, new []{ 0, 4 } },
{ 4, new []{ 2, 6 } },
{ 5, new []{ 0, 2, 4, 6 } },
{ 6, new []{ 1, 3, 5, 7 } },
{ 7, new []{ 0, 1, 2, 3, 4, 5, 6, 7 } }
};
/*
* To go from raw image data to interlaced:
*
* An 8x8 grid is repeated over the image. There are 7 passes and the indexes in this grid correspond to the
* pass number including that pixel. Each row in the grid corresponds to a scanline.
*
* 1 6 4 6 2 6 4 6 - Scanline 0: pass 1 has pixel 0, 8, 16, etc. pass 2 has pixel 4, 12, 20, etc.
* 7 7 7 7 7 7 7 7
* 5 6 5 6 5 6 5 6
* 7 7 7 7 7 7 7 7
* 3 6 4 6 3 6 4 6
* 7 7 7 7 7 7 7 7
* 5 6 5 6 5 6 5 6
* 7 7 7 7 7 7 7 7
*
*
*
*/
public static int GetNumberOfScanlinesInPass(ImageHeader header, int pass)
{
var indices = PassToScanlineGridIndex[pass + 1];
var mod = header.Height % 8;
var fitsExactly = mod == 0;
if (fitsExactly)
{
return indices.Length * (header.Height / 8);
}
var additionalLines = 0;
for (var i = 0; i < indices.Length; i++)
{
if (indices[i] < mod)
{
additionalLines++;
}
}
return (indices.Length * (header.Height / 8)) + additionalLines;
}
public static int GetPixelsPerScanlineInPass(ImageHeader header, int pass)
{
var indices = PassToScanlineColumnIndex[pass + 1];
var mod = header.Width % 8;
var fitsExactly = mod == 0;
if (fitsExactly)
{
return indices.Length * (header.Width / 8);
}
var additionalColumns = 0;
for (int i = 0; i < indices.Length; i++)
{
if (indices[i] < mod)
{
additionalColumns++;
}
}
return (indices.Length * (header.Width / 8)) + additionalColumns;
}
public static (int x, int y) GetPixelIndexForScanlineInPass(ImageHeader header, int pass, int scanlineIndex, int indexInScanline)
{
var columnIndices = PassToScanlineColumnIndex[pass + 1];
var rows = PassToScanlineGridIndex[pass + 1];
var actualRow = scanlineIndex % rows.Length;
var actualCol = indexInScanline % columnIndices.Length;
var precedingRows = 8 * (scanlineIndex / rows.Length);
var precedingCols = 8 * (indexInScanline / columnIndices.Length);
return (precedingCols + columnIndices[actualCol], precedingRows + rows[actualRow]);
}
}
}

View File

@@ -0,0 +1,35 @@
namespace UglyToad.PdfPig.Images.Png
{
using System.Collections.Generic;
/// <summary>
/// Used to calculate the Adler-32 checksum used for ZLIB data in accordance with
/// RFC 1950: ZLIB Compressed Data Format Specification.
/// </summary>
internal static class Adler32Checksum
{
// Both sums (s1 and s2) are done modulo 65521.
private const int AdlerModulus = 65521;
/// <summary>
/// Calculate the Adler-32 checksum for some data.
/// </summary>
public static int Calculate(IEnumerable<byte> data)
{
// s1 is the sum of all bytes.
var s1 = 1;
// s2 is the sum of all s1 values.
var s2 = 0;
foreach (var b in data)
{
s1 = (s1 + b) % AdlerModulus;
s2 = (s1 + s2) % AdlerModulus;
}
// The Adler-32 checksum is stored as s2*65536 + s1.
return s2 * 65536 + s1;
}
}
}

View File

@@ -0,0 +1,62 @@
namespace UglyToad.PdfPig.Images.Png
{
using System;
/// <summary>
/// The header for a data chunk in a PNG file.
/// </summary>
internal readonly struct ChunkHeader
{
/// <summary>
/// The position/start of the chunk header within the stream.
/// </summary>
public long Position { get; }
/// <summary>
/// The length of the chunk in bytes.
/// </summary>
public int Length { get; }
/// <summary>
/// The name of the chunk, uppercase first letter means the chunk is critical (vs. ancillary).
/// </summary>
public string Name { get; }
/// <summary>
/// Whether the chunk is critical (must be read by all readers) or ancillary (may be ignored).
/// </summary>
public bool IsCritical => char.IsUpper(Name[0]);
/// <summary>
/// A public chunk is one that is defined in the International Standard or is registered in the list of public chunk types maintained by the Registration Authority.
/// Applications can also define private (unregistered) chunk types for their own purposes.
/// </summary>
public bool IsPublic => char.IsUpper(Name[1]);
/// <summary>
/// Whether the (if unrecognized) chunk is safe to copy.
/// </summary>
public bool IsSafeToCopy => char.IsUpper(Name[3]);
/// <summary>
/// Create a new <see cref="ChunkHeader"/>.
/// </summary>
public ChunkHeader(long position, int length, string name)
{
if (length < 0)
{
throw new ArgumentException($"Length less than zero ({length}) encountered when reading chunk at position {position}.");
}
Position = position;
Length = length;
Name = name;
}
/// <inheritdoc />
public override string ToString()
{
return $"{Name} at {Position} (length: {Length}).";
}
}
}

View File

@@ -0,0 +1,28 @@
namespace UglyToad.PdfPig.Images.Png
{
using System;
/// <summary>
/// Describes the interpretation of the image data.
/// </summary>
[Flags]
internal enum ColorType : byte
{
/// <summary>
/// Grayscale.
/// </summary>
None = 0,
/// <summary>
/// Colors are stored in a palette rather than directly in the data.
/// </summary>
PaletteUsed = 1,
/// <summary>
/// The image uses color.
/// </summary>
ColorUsed = 2,
/// <summary>
/// The image has an alpha channel.
/// </summary>
AlphaChannelUsed = 4
}
}

View File

@@ -0,0 +1,13 @@
namespace UglyToad.PdfPig.Images.Png
{
/// <summary>
/// The method used to compress the image data.
/// </summary>
internal enum CompressionMethod : byte
{
/// <summary>
/// Deflate/inflate compression with a sliding window of at most 32768 bytes.
/// </summary>
DeflateWithSlidingWindow = 0
}
}

View File

@@ -0,0 +1,87 @@
namespace UglyToad.PdfPig.Images.Png
{
using System.Collections.Generic;
/// <summary>
/// 32-bit Cyclic Redundancy Code used by the PNG for checking the data is intact.
/// </summary>
internal static class Crc32
{
private const uint Polynomial = 0xEDB88320;
private static readonly uint[] Lookup;
static Crc32()
{
Lookup = new uint[256];
for (uint i = 0; i < 256; i++)
{
var value = i;
for (var j = 0; j < 8; ++j)
{
if ((value & 1) != 0)
{
value = (value >> 1) ^ Polynomial;
}
else
{
value >>= 1;
}
}
Lookup[i] = value;
}
}
/// <summary>
/// Calculate the CRC32 for data.
/// </summary>
public static uint Calculate(byte[] data)
{
var crc32 = uint.MaxValue;
for (var i = 0; i < data.Length; i++)
{
var index = (crc32 ^ data[i]) & 0xFF;
crc32 = (crc32 >> 8) ^ Lookup[index];
}
return crc32 ^ uint.MaxValue;
}
/// <summary>
/// Calculate the CRC32 for data.
/// </summary>
public static uint Calculate(List<byte> data)
{
var crc32 = uint.MaxValue;
for (var i = 0; i < data.Count; i++)
{
var index = (crc32 ^ data[i]) & 0xFF;
crc32 = (crc32 >> 8) ^ Lookup[index];
}
return crc32 ^ uint.MaxValue;
}
/// <summary>
/// Calculate the combined CRC32 for data.
/// </summary>
public static uint Calculate(byte[] data, byte[] data2)
{
var crc32 = uint.MaxValue;
for (var i = 0; i < data.Length; i++)
{
var index = (crc32 ^ data[i]) & 0xFF;
crc32 = (crc32 >> 8) ^ Lookup[index];
}
for (var i = 0; i < data2.Length; i++)
{
var index = (crc32 ^ data2[i]) & 0xFF;
crc32 = (crc32 >> 8) ^ Lookup[index];
}
return crc32 ^ uint.MaxValue;
}
}
}

View File

@@ -0,0 +1,211 @@
namespace UglyToad.PdfPig.Images.Png
{
using System;
internal static class Decoder
{
public static (byte bytesPerPixel, byte samplesPerPixel) GetBytesAndSamplesPerPixel(ImageHeader header)
{
var bitDepthCorrected = (header.BitDepth + 7) / 8;
var samplesPerPixel = SamplesPerPixel(header);
return ((byte)(samplesPerPixel * bitDepthCorrected), samplesPerPixel);
}
public static byte[] Decode(byte[] decompressedData, ImageHeader header, byte bytesPerPixel, byte samplesPerPixel)
{
switch (header.InterlaceMethod)
{
case InterlaceMethod.None:
{
var bytesPerScanline = BytesPerScanline(header, samplesPerPixel);
var currentRowStartByteAbsolute = 1;
for (var rowIndex = 0; rowIndex < header.Height; rowIndex++)
{
var filterType = (FilterType)decompressedData[currentRowStartByteAbsolute - 1];
var previousRowStartByteAbsolute = (rowIndex) + (bytesPerScanline * (rowIndex - 1));
var end = currentRowStartByteAbsolute + bytesPerScanline;
for (var currentByteAbsolute = currentRowStartByteAbsolute; currentByteAbsolute < end; currentByteAbsolute++)
{
ReverseFilter(decompressedData, filterType, previousRowStartByteAbsolute, currentRowStartByteAbsolute, currentByteAbsolute, currentByteAbsolute - currentRowStartByteAbsolute, bytesPerPixel);
}
currentRowStartByteAbsolute += bytesPerScanline + 1;
}
return decompressedData;
}
case InterlaceMethod.Adam7:
{
var pixelsPerRow = header.Width * bytesPerPixel;
var newBytes = new byte[header.Height * pixelsPerRow];
var i = 0;
var previousStartRowByteAbsolute = -1;
// 7 passes
for (var pass = 0; pass < 7; pass++)
{
var numberOfScanlines = Adam7.GetNumberOfScanlinesInPass(header, pass);
var numberOfPixelsPerScanline = Adam7.GetPixelsPerScanlineInPass(header, pass);
if (numberOfScanlines <= 0 || numberOfPixelsPerScanline <= 0)
{
continue;
}
for (var scanlineIndex = 0; scanlineIndex < numberOfScanlines; scanlineIndex++)
{
var filterType = (FilterType)decompressedData[i++];
var rowStartByte = i;
for (var j = 0; j < numberOfPixelsPerScanline; j++)
{
var pixelIndex = Adam7.GetPixelIndexForScanlineInPass(header, pass, scanlineIndex, j);
for (var k = 0; k < bytesPerPixel; k++)
{
var byteLineNumber = (j * bytesPerPixel) + k;
ReverseFilter(decompressedData, filterType, previousStartRowByteAbsolute, rowStartByte, i, byteLineNumber, bytesPerPixel);
i++;
}
var start = pixelsPerRow * pixelIndex.y + pixelIndex.x * bytesPerPixel;
Array.ConstrainedCopy(decompressedData, rowStartByte + j * bytesPerPixel, newBytes, start, bytesPerPixel);
}
previousStartRowByteAbsolute = rowStartByte;
}
}
return newBytes;
}
default:
throw new ArgumentOutOfRangeException($"Invalid interlace method: {header.InterlaceMethod}.");
}
}
private static byte SamplesPerPixel(ImageHeader header)
{
switch (header.ColorType)
{
case ColorType.None:
return 1;
case ColorType.PaletteUsed:
return 1;
case ColorType.ColorUsed:
return 3;
case ColorType.AlphaChannelUsed:
return 2;
case ColorType.ColorUsed | ColorType.AlphaChannelUsed:
return 4;
default:
return 0;
}
}
private static int BytesPerScanline(ImageHeader header, byte samplesPerPixel)
{
var width = header.Width;
switch (header.BitDepth)
{
case 1:
return (width + 7) / 8;
case 2:
return (width + 3) / 4;
case 4:
return (width + 1) / 2;
case 8:
case 16:
return width * samplesPerPixel * (header.BitDepth / 8);
default:
return 0;
}
}
private static void ReverseFilter(byte[] data, FilterType type, int previousRowStartByteAbsolute, int rowStartByteAbsolute, int byteAbsolute, int rowByteIndex, int bytesPerPixel)
{
byte GetLeftByteValue()
{
var leftIndex = rowByteIndex - bytesPerPixel;
var leftValue = leftIndex >= 0 ? data[rowStartByteAbsolute + leftIndex] : (byte)0;
return leftValue;
}
byte GetAboveByteValue()
{
var upIndex = previousRowStartByteAbsolute + rowByteIndex;
return upIndex >= 0 ? data[upIndex] : (byte)0;
}
byte GetAboveLeftByteValue()
{
var index = previousRowStartByteAbsolute + rowByteIndex - bytesPerPixel;
return index < previousRowStartByteAbsolute || previousRowStartByteAbsolute < 0 ? (byte)0 : data[index];
}
// Moved out of the switch for performance.
if (type == FilterType.Up)
{
var above = previousRowStartByteAbsolute + rowByteIndex;
if (above < 0)
{
return;
}
data[byteAbsolute] += data[above];
return;
}
if (type == FilterType.Sub)
{
var leftIndex = rowByteIndex - bytesPerPixel;
if (leftIndex < 0)
{
return;
}
data[byteAbsolute] += data[rowStartByteAbsolute + leftIndex];
return;
}
switch (type)
{
case FilterType.None:
return;
case FilterType.Average:
data[byteAbsolute] += (byte)((GetLeftByteValue() + GetAboveByteValue()) / 2);
break;
case FilterType.Paeth:
var a = GetLeftByteValue();
var b = GetAboveByteValue();
var c = GetAboveLeftByteValue();
data[byteAbsolute] += GetPaethValue(a, b, c);
break;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
/// <summary>
/// Computes a simple linear function of the three neighboring pixels (left, above, upper left),
/// then chooses as predictor the neighboring pixel closest to the computed value.
/// </summary>
private static byte GetPaethValue(byte a, byte b, byte c)
{
var p = a + b - c;
var pa = Math.Abs(p - a);
var pb = Math.Abs(p - b);
var pc = Math.Abs(p - c);
if (pa <= pb && pa <= pc)
{
return a;
}
return pb <= pc ? b : c;
}
}
}

View File

@@ -0,0 +1,13 @@
namespace UglyToad.PdfPig.Images.Png
{
/// <summary>
/// Indicates the pre-processing method applied to the image data before compression.
/// </summary>
internal enum FilterMethod
{
/// <summary>
/// Adaptive filtering with five basic filter types.
/// </summary>
AdaptiveFiltering = 0
}
}

View File

@@ -0,0 +1,26 @@
namespace UglyToad.PdfPig.Images.Png
{
internal enum FilterType
{
/// <summary>
/// The raw byte is unaltered.
/// </summary>
None = 0,
/// <summary>
/// The byte to the left.
/// </summary>
Sub = 1,
/// <summary>
/// The byte above.
/// </summary>
Up = 2,
/// <summary>
/// The mean of bytes left and above, rounded down.
/// </summary>
Average = 3,
/// <summary>
/// Byte to the left, above or top-left based on Paeth's algorithm.
/// </summary>
Paeth = 4
}
}

View File

@@ -0,0 +1,54 @@
namespace UglyToad.PdfPig.Images.Png
{
internal readonly struct HeaderValidationResult
{
public static readonly byte[] ExpectedHeader = {
137,
80,
78,
71,
13,
10,
26,
10
};
public int Byte1 { get; }
public int Byte2 { get; }
public int Byte3 { get; }
public int Byte4 { get; }
public int Byte5 { get; }
public int Byte6 { get; }
public int Byte7 { get; }
public int Byte8 { get; }
public bool IsValid { get; }
public HeaderValidationResult(int byte1, int byte2, int byte3, int byte4, int byte5, int byte6, int byte7, int byte8)
{
Byte1 = byte1;
Byte2 = byte2;
Byte3 = byte3;
Byte4 = byte4;
Byte5 = byte5;
Byte6 = byte6;
Byte7 = byte7;
Byte8 = byte8;
IsValid = byte1 == ExpectedHeader[0] && byte2 == ExpectedHeader[1] && byte3 == ExpectedHeader[2]
&& byte4 == ExpectedHeader[3] && byte5 == ExpectedHeader[4] && byte6 == ExpectedHeader[5]
&& byte7 == ExpectedHeader[6] && byte8 == ExpectedHeader[7];
}
public override string ToString()
{
return $"{Byte1} {Byte2} {Byte3} {Byte4} {Byte5} {Byte6} {Byte7} {Byte8}";
}
}
}

View File

@@ -0,0 +1,15 @@
namespace UglyToad.PdfPig.Images.Png
{
using System.IO;
/// <summary>
/// Enables execution of custom logic whenever a chunk is read.
/// </summary>
internal interface IChunkVisitor
{
/// <summary>
/// Called by the PNG reader after a chunk is read.
/// </summary>
void Visit(Stream stream, ImageHeader header, ChunkHeader chunkHeader, byte[] data, byte[] crc);
}
}

View File

@@ -0,0 +1,96 @@
namespace UglyToad.PdfPig.Images.Png
{
using System;
using System.Collections.Generic;
/// <summary>
/// The high level information about the image.
/// </summary>
internal readonly struct ImageHeader
{
internal static readonly byte[] HeaderBytes = {
73, 72, 68, 82
};
private static readonly IReadOnlyDictionary<ColorType, HashSet<byte>> PermittedBitDepths = new Dictionary<ColorType, HashSet<byte>>
{
{ColorType.None, new HashSet<byte> {1, 2, 4, 8, 16}},
{ColorType.ColorUsed, new HashSet<byte> {8, 16}},
{ColorType.PaletteUsed | ColorType.ColorUsed, new HashSet<byte> {1, 2, 4, 8}},
{ColorType.AlphaChannelUsed, new HashSet<byte> {8, 16}},
{ColorType.AlphaChannelUsed | ColorType.ColorUsed, new HashSet<byte> {8, 16}},
};
/// <summary>
/// The width of the image in pixels.
/// </summary>
public int Width { get; }
/// <summary>
/// The height of the image in pixels.
/// </summary>
public int Height { get; }
/// <summary>
/// The bit depth of the image.
/// </summary>
public byte BitDepth { get; }
/// <summary>
/// The color type of the image.
/// </summary>
public ColorType ColorType { get; }
/// <summary>
/// The compression method used for the image.
/// </summary>
public CompressionMethod CompressionMethod { get; }
/// <summary>
/// The filter method used for the image.
/// </summary>
public FilterMethod FilterMethod { get; }
/// <summary>
/// The interlace method used by the image..
/// </summary>
public InterlaceMethod InterlaceMethod { get; }
/// <summary>
/// Create a new <see cref="ImageHeader"/>.
/// </summary>
public ImageHeader(int width, int height, byte bitDepth, ColorType colorType, CompressionMethod compressionMethod, FilterMethod filterMethod, InterlaceMethod interlaceMethod)
{
if (width == 0)
{
throw new ArgumentOutOfRangeException(nameof(width), "Invalid width (0) for image.");
}
if (height == 0)
{
throw new ArgumentOutOfRangeException(nameof(height), "Invalid height (0) for image.");
}
if (!PermittedBitDepths.TryGetValue(colorType, out var permitted)
|| !permitted.Contains(bitDepth))
{
throw new ArgumentException($"The bit depth {bitDepth} is not permitted for color type {colorType}.");
}
Width = width;
Height = height;
BitDepth = bitDepth;
ColorType = colorType;
CompressionMethod = compressionMethod;
FilterMethod = filterMethod;
InterlaceMethod = interlaceMethod;
}
/// <inheritdoc />
public override string ToString()
{
return $"w: {Width}, h: {Height}, bitDepth: {BitDepth}, colorType: {ColorType}, " +
$"compression: {CompressionMethod}, filter: {FilterMethod}, interlace: {InterlaceMethod}.";
}
}
}

View File

@@ -0,0 +1,17 @@
namespace UglyToad.PdfPig.Images.Png
{
/// <summary>
/// Indicates the transmission order of the image data.
/// </summary>
internal enum InterlaceMethod : byte
{
/// <summary>
/// No interlace.
/// </summary>
None = 0,
/// <summary>
/// Adam7 interlace.
/// </summary>
Adam7 = 1
}
}

View File

@@ -0,0 +1,19 @@
namespace UglyToad.PdfPig.Images.Png
{
internal class Palette
{
public byte[] Data { get; }
public Palette(byte[] data)
{
Data = data;
}
public Pixel GetPixel(int index)
{
var start = index * 3;
return new Pixel(Data[start], Data[start + 1], Data[start + 2], 255, false);
}
}
}

View File

@@ -0,0 +1,123 @@
namespace UglyToad.PdfPig.Images.Png
{
/// <summary>
/// A pixel in a <see cref="Png"/> image.
/// </summary>
internal readonly struct Pixel
{
/// <summary>
/// The red value for the pixel.
/// </summary>
public byte R { get; }
/// <summary>
/// The green value for the pixel.
/// </summary>
public byte G { get; }
/// <summary>
/// The blue value for the pixel.
/// </summary>
public byte B { get; }
/// <summary>
/// The alpha transparency value for the pixel.
/// </summary>
public byte A { get; }
/// <summary>
/// Whether the pixel is grayscale (if <see langword="true"/> <see cref="R"/>, <see cref="G"/> and <see cref="B"/> will all have the same value).
/// </summary>
public bool IsGrayscale { get; }
/// <summary>
/// Create a new <see cref="Pixel"/>.
/// </summary>
/// <param name="r">The red value for the pixel.</param>
/// <param name="g">The green value for the pixel.</param>
/// <param name="b">The blue value for the pixel.</param>
/// <param name="a">The alpha transparency value for the pixel.</param>
/// <param name="isGrayscale">Whether the pixel is grayscale.</param>
public Pixel(byte r, byte g, byte b, byte a, bool isGrayscale)
{
R = r;
G = g;
B = b;
A = a;
IsGrayscale = isGrayscale;
}
/// <summary>
/// Create a new <see cref="Pixel"/> which has <see cref="IsGrayscale"/> false and is fully opaque.
/// </summary>
/// <param name="r">The red value for the pixel.</param>
/// <param name="g">The green value for the pixel.</param>
/// <param name="b">The blue value for the pixel.</param>
public Pixel(byte r, byte g, byte b)
{
R = r;
G = g;
B = b;
A = 255;
IsGrayscale = false;
}
/// <summary>
/// Create a new grayscale <see cref="Pixel"/>.
/// </summary>
/// <param name="grayscale">The grayscale value.</param>
public Pixel(byte grayscale)
{
R = grayscale;
G = grayscale;
B = grayscale;
A = 255;
IsGrayscale = true;
}
/// <inheritdoc />
public override bool Equals(object obj)
{
if (obj is Pixel pixel)
{
return IsGrayscale == pixel.IsGrayscale
&& A == pixel.A
&& R == pixel.R
&& G == pixel.G
&& B == pixel.B;
}
return false;
}
/// <summary>
/// Whether the pixel values are equal.
/// </summary>
/// <param name="other">The other pixel.</param>
/// <returns><see langword="true"/> if all pixel values are equal otherwise <see langword="false"/>.</returns>
public bool Equals(Pixel other)
{
return R == other.R && G == other.G && B == other.B && A == other.A && IsGrayscale == other.IsGrayscale;
}
/// <inheritdoc />
public override int GetHashCode()
{
unchecked
{
var hashCode = R.GetHashCode();
hashCode = (hashCode * 397) ^ G.GetHashCode();
hashCode = (hashCode * 397) ^ B.GetHashCode();
hashCode = (hashCode * 397) ^ A.GetHashCode();
hashCode = (hashCode * 397) ^ IsGrayscale.GetHashCode();
return hashCode;
}
}
/// <inheritdoc />
public override string ToString()
{
return $"({R}, {G}, {B}, {A})";
}
}
}

View File

@@ -0,0 +1,89 @@
namespace UglyToad.PdfPig.Images.Png
{
using System;
using System.IO;
/// <summary>
/// A PNG image. Call <see cref="Open(byte[],IChunkVisitor)"/> to open from file or bytes.
/// </summary>
internal class Png
{
private readonly RawPngData data;
/// <summary>
/// The header data from the PNG image.
/// </summary>
public ImageHeader Header { get; }
/// <summary>
/// The width of the image in pixels.
/// </summary>
public int Width => Header.Width;
/// <summary>
/// The height of the image in pixels.
/// </summary>
public int Height => Header.Height;
/// <summary>
/// Whether the image has an alpha (transparency) layer.
/// </summary>
public bool HasAlphaChannel => (Header.ColorType & ColorType.AlphaChannelUsed) != 0;
internal Png(ImageHeader header, RawPngData data)
{
Header = header;
this.data = data ?? throw new ArgumentNullException(nameof(data));
}
/// <summary>
/// Get the pixel at the given column and row (x, y).
/// </summary>
/// <remarks>
/// Pixel values are generated on demand from the underlying data to prevent holding many items in memory at once, so consumers
/// should cache values if they're going to be looped over many time.
/// </remarks>
/// <param name="x">The x coordinate (column).</param>
/// <param name="y">The y coordinate (row).</param>
/// <returns>The pixel at the coordinate.</returns>
public Pixel GetPixel(int x, int y) => data.GetPixel(x, y);
/// <summary>
/// Read the PNG image from the stream.
/// </summary>
/// <param name="stream">The stream containing PNG data to be read.</param>
/// <param name="chunkVisitor">Optional: A visitor which is called whenever a chunk is read by the library.</param>
/// <returns>The <see cref="Png"/> data from the stream.</returns>
public static Png Open(Stream stream, IChunkVisitor chunkVisitor = null)
=> PngOpener.Open(stream, chunkVisitor);
/// <summary>
/// Read the PNG image from the bytes.
/// </summary>
/// <param name="bytes">The bytes of the PNG data to be read.</param>
/// <param name="chunkVisitor">Optional: A visitor which is called whenever a chunk is read by the library.</param>
/// <returns>The <see cref="Png"/> data from the bytes.</returns>
public static Png Open(byte[] bytes, IChunkVisitor chunkVisitor = null)
{
using (var memoryStream = new MemoryStream(bytes))
{
return PngOpener.Open(memoryStream, chunkVisitor);
}
}
/// <summary>
/// Read the PNG from the file path.
/// </summary>
/// <param name="filePath">The path to the PNG file to open.</param>
/// <param name="chunkVisitor">Optional: A visitor which is called whenever a chunk is read by the library.</param>
/// <remarks>This will open the file to obtain a <see cref="FileStream"/> so will lock the file during reading.</remarks>
/// <returns>The <see cref="Png"/> data from the file.</returns>
public static Png Open(string filePath, IChunkVisitor chunkVisitor = null)
{
using (var fileStream = File.OpenRead(filePath))
{
return Open(fileStream, chunkVisitor);
}
}
}
}

View File

@@ -0,0 +1,159 @@
namespace UglyToad.PdfPig.Images.Png
{
using System.IO;
using System.IO.Compression;
using System.Text;
/// <summary>
/// Used to construct PNG images. Call <see cref="Create"/> to make a new builder.
/// </summary>
internal class PngBuilder
{
private const byte Deflate32KbWindow = 120;
private const byte ChecksumBits = 1;
private readonly byte[] rawData;
private readonly bool hasAlphaChannel;
private readonly int width;
private readonly int height;
private readonly int bytesPerPixel;
/// <summary>
/// Create a builder for a PNG with the given width and size.
/// </summary>
public static PngBuilder Create(int width, int height, bool hasAlphaChannel)
{
var bpp = hasAlphaChannel ? 4 : 3;
var length = (height * width * bpp) + height;
return new PngBuilder(new byte[length], hasAlphaChannel, width, height, bpp);
}
private PngBuilder(byte[] rawData, bool hasAlphaChannel, int width, int height, int bytesPerPixel)
{
this.rawData = rawData;
this.hasAlphaChannel = hasAlphaChannel;
this.width = width;
this.height = height;
this.bytesPerPixel = bytesPerPixel;
}
/// <summary>
/// Sets the RGB pixel value for the given column (x) and row (y).
/// </summary>
public PngBuilder SetPixel(byte r, byte g, byte b, int x, int y) => SetPixel(new Pixel(r, g, b), x, y);
/// <summary>
/// Set the pixel value for the given column (x) and row (y).
/// </summary>
public PngBuilder SetPixel(Pixel pixel, int x, int y)
{
var start = (y * ((width * bytesPerPixel) + 1)) + 1 + (x * bytesPerPixel);
rawData[start++] = pixel.R;
rawData[start++] = pixel.G;
rawData[start++] = pixel.B;
if (hasAlphaChannel)
{
rawData[start] = pixel.A;
}
return this;
}
/// <summary>
/// Get the bytes of the PNG file for this builder.
/// </summary>
public byte[] Save()
{
using (var memoryStream = new MemoryStream())
{
Save(memoryStream);
return memoryStream.ToArray();
}
}
/// <summary>
/// Write the PNG file bytes to the provided stream.
/// </summary>
public void Save(Stream outputStream)
{
outputStream.Write(HeaderValidationResult.ExpectedHeader, 0, HeaderValidationResult.ExpectedHeader.Length);
var stream = new PngStreamWriteHelper(outputStream);
stream.WriteChunkLength(13);
stream.WriteChunkHeader(ImageHeader.HeaderBytes);
StreamHelper.WriteBigEndianInt32(stream, width);
StreamHelper.WriteBigEndianInt32(stream, height);
stream.WriteByte(8);
var colorType = ColorType.ColorUsed;
if (hasAlphaChannel)
{
colorType |= ColorType.AlphaChannelUsed;
}
stream.WriteByte((byte)colorType);
stream.WriteByte((byte)CompressionMethod.DeflateWithSlidingWindow);
stream.WriteByte((byte)FilterMethod.AdaptiveFiltering);
stream.WriteByte((byte)InterlaceMethod.None);
stream.WriteCrc();
var imageData = Compress(rawData);
stream.WriteChunkLength(imageData.Length);
stream.WriteChunkHeader(Encoding.ASCII.GetBytes("IDAT"));
stream.Write(imageData, 0, imageData.Length);
stream.WriteCrc();
stream.WriteChunkLength(0);
stream.WriteChunkHeader(Encoding.ASCII.GetBytes("IEND"));
stream.WriteCrc();
}
private static byte[] Compress(byte[] data)
{
const int headerLength = 2;
const int checksumLength = 4;
using (var compressStream = new MemoryStream())
using (var compressor = new DeflateStream(compressStream, CompressionLevel.Fastest, true))
{
compressor.Write(data, 0, data.Length);
compressor.Close();
compressStream.Seek(0, SeekOrigin.Begin);
var result = new byte[headerLength + compressStream.Length + checksumLength];
// Write the ZLib header.
result[0] = Deflate32KbWindow;
result[1] = ChecksumBits;
// Write the compressed data.
int streamValue;
var i = 0;
while ((streamValue = compressStream.ReadByte()) != -1)
{
result[headerLength + i] = (byte) streamValue;
i++;
}
// Write Checksum of raw data.
var checksum = Adler32Checksum.Calculate(data);
var offset = headerLength + compressStream.Length;
result[offset++] = (byte)(checksum >> 24);
result[offset++] = (byte)(checksum >> 16);
result[offset++] = (byte)(checksum >> 8);
result[offset] = (byte)(checksum >> 0);
return result;
}
}
}
}

View File

@@ -0,0 +1,61 @@
namespace UglyToad.PdfPig.Images.Png
{
using Content;
using Graphics.Colors;
internal static class PngFromPdfImageFactory
{
public static bool TryGenerate(IPdfImage image, out byte[] bytes)
{
bytes = null;
var isColorSpaceSupported = image.ColorSpace == ColorSpace.DeviceGray || image.ColorSpace == ColorSpace.DeviceRGB;
if (!isColorSpaceSupported || !image.TryGetBytes(out var bytesPure))
{
return false;
}
try
{
var is3Byte = image.ColorSpace == ColorSpace.DeviceRGB;
var multiplier = is3Byte ? 3 : 1;
var builder = PngBuilder.Create(image.WidthInSamples, image.HeightInSamples, false);
var isCorrectlySized = bytesPure.Count == (image.WidthInSamples * image.HeightInSamples * (image.BitsPerComponent / 8) * multiplier);
if (!isCorrectlySized)
{
return false;
}
var i = 0;
for (var y = 0; y < image.HeightInSamples; y++)
{
for (var x = 0; x < image.WidthInSamples; x++)
{
if (is3Byte)
{
builder.SetPixel(bytesPure[i++], bytesPure[i++], bytesPure[i++], x, y);
}
else
{
var pixel = bytesPure[i++];
builder.SetPixel(pixel, pixel, pixel, x, y);
}
}
}
bytes = builder.Save();
return true;
}
catch
{
// ignored.
}
return false;
}
}
}

View File

@@ -0,0 +1,183 @@
namespace UglyToad.PdfPig.Images.Png
{
using System;
using System.IO;
using System.IO.Compression;
using System.Text;
internal static class PngOpener
{
public static Png Open(Stream stream, IChunkVisitor chunkVisitor = null)
{
if (stream == null)
{
throw new ArgumentNullException(nameof(stream));
}
if (!stream.CanRead)
{
throw new ArgumentException($"The provided stream of type {stream.GetType().FullName} was not readable.");
}
var validHeader = HasValidHeader(stream);
if (!validHeader.IsValid)
{
throw new ArgumentException($"The provided stream did not start with the PNG header. Got {validHeader}.");
}
var crc = new byte[4];
var imageHeader = ReadImageHeader(stream, crc);
var hasEncounteredImageEnd = false;
Palette palette = null;
using (var output = new MemoryStream())
{
using (var memoryStream = new MemoryStream())
{
while (TryReadChunkHeader(stream, out var header))
{
if (hasEncounteredImageEnd)
{
throw new InvalidOperationException($"Found another chunk {header} after already reading the IEND chunk.");
}
var bytes = new byte[header.Length];
var read = stream.Read(bytes, 0, bytes.Length);
if (read != bytes.Length)
{
throw new InvalidOperationException($"Did not read {header.Length} bytes for the {header} header, only found: {read}.");
}
if (header.IsCritical)
{
switch (header.Name)
{
case "PLTE":
if (header.Length % 3 != 0)
{
throw new InvalidOperationException($"Palette data must be multiple of 3, got {header.Length}.");
}
palette = new Palette(bytes);
break;
case "IDAT":
memoryStream.Write(bytes, 0, bytes.Length);
break;
case "IEND":
hasEncounteredImageEnd = true;
break;
default:
throw new NotSupportedException($"Encountered critical header {header} which was not recognised.");
}
}
read = stream.Read(crc, 0, crc.Length);
if (read != 4)
{
throw new InvalidOperationException($"Did not read 4 bytes for the CRC, only found: {read}.");
}
var result = (int)Crc32.Calculate(Encoding.ASCII.GetBytes(header.Name), bytes);
var crcActual = (crc[0] << 24) + (crc[1] << 16) + (crc[2] << 8) + crc[3];
if (result != crcActual)
{
throw new InvalidOperationException($"CRC calculated {result} did not match file {crcActual} for chunk: {header.Name}.");
}
chunkVisitor?.Visit(stream, imageHeader, header, bytes, crc);
}
memoryStream.Flush();
memoryStream.Seek(2, SeekOrigin.Begin);
using (var deflateStream = new DeflateStream(memoryStream, CompressionMode.Decompress))
{
deflateStream.CopyTo(output);
deflateStream.Close();
}
}
var bytesOut = output.ToArray();
var (bytesPerPixel, samplesPerPixel) = Decoder.GetBytesAndSamplesPerPixel(imageHeader);
bytesOut = Decoder.Decode(bytesOut, imageHeader, bytesPerPixel, samplesPerPixel);
return new Png(imageHeader, new RawPngData(bytesOut, bytesPerPixel, imageHeader.Width, imageHeader.InterlaceMethod, palette, imageHeader.ColorType));
}
}
private static HeaderValidationResult HasValidHeader(Stream stream)
{
return new HeaderValidationResult(stream.ReadByte(), stream.ReadByte(), stream.ReadByte(), stream.ReadByte(),
stream.ReadByte(), stream.ReadByte(), stream.ReadByte(), stream.ReadByte());
}
private static bool TryReadChunkHeader(Stream stream, out ChunkHeader chunkHeader)
{
chunkHeader = default;
var position = stream.Position;
if (!StreamHelper.TryReadHeaderBytes(stream, out var headerBytes))
{
return false;
}
var length = StreamHelper.ReadBigEndianInt32(headerBytes, 0);
var name = Encoding.ASCII.GetString(headerBytes, 4, 4);
chunkHeader = new ChunkHeader(position, length, name);
return true;
}
private static ImageHeader ReadImageHeader(Stream stream, byte[] crc)
{
if (!TryReadChunkHeader(stream, out var header))
{
throw new ArgumentException("The provided stream did not contain a single chunk.");
}
if (header.Name != "IHDR")
{
throw new ArgumentException($"The first chunk was not the IHDR chunk: {header}.");
}
if (header.Length != 13)
{
throw new ArgumentException($"The first chunk did not have a length of 13 bytes: {header}.");
}
var ihdrBytes = new byte[13];
var read = stream.Read(ihdrBytes, 0, ihdrBytes.Length);
if (read != 13)
{
throw new InvalidOperationException($"Did not read 13 bytes for the IHDR, only found: {read}.");
}
read = stream.Read(crc, 0, crc.Length);
if (read != 4)
{
throw new InvalidOperationException($"Did not read 4 bytes for the CRC, only found: {read}.");
}
var width = StreamHelper.ReadBigEndianInt32(ihdrBytes, 0);
var height = StreamHelper.ReadBigEndianInt32(ihdrBytes, 4);
var bitDepth = ihdrBytes[8];
var colorType = ihdrBytes[9];
var compressionMethod = ihdrBytes[10];
var filterMethod = ihdrBytes[11];
var interlaceMethod = ihdrBytes[12];
return new ImageHeader(width, height, bitDepth, (ColorType)colorType, (CompressionMethod)compressionMethod, (FilterMethod)filterMethod,
(InterlaceMethod)interlaceMethod);
}
}
}

View File

@@ -0,0 +1,63 @@
namespace UglyToad.PdfPig.Images.Png
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
internal class PngStreamWriteHelper : Stream
{
private readonly Stream inner;
private readonly List<byte> written = new List<byte>();
public override bool CanRead => inner.CanRead;
public override bool CanSeek => inner.CanSeek;
public override bool CanWrite => inner.CanWrite;
public override long Length => inner.Length;
public override long Position
{
get => inner.Position;
set => inner.Position = value;
}
public PngStreamWriteHelper(Stream inner)
{
this.inner = inner ?? throw new ArgumentNullException(nameof(inner));
}
public override void Flush() => inner.Flush();
public void WriteChunkHeader(byte[] header)
{
written.Clear();
Write(header, 0, header.Length);
}
public void WriteChunkLength(int length)
{
StreamHelper.WriteBigEndianInt32(inner, length);
}
public override int Read(byte[] buffer, int offset, int count) => inner.Read(buffer, offset, count);
public override long Seek(long offset, SeekOrigin origin) => inner.Seek(offset, origin);
public override void SetLength(long value) => inner.SetLength(value);
public override void Write(byte[] buffer, int offset, int count)
{
written.AddRange(buffer.Skip(offset).Take(count));
inner.Write(buffer, offset, count);
}
public void WriteCrc()
{
var result = (int)Crc32.Calculate(written);
StreamHelper.WriteBigEndianInt32(inner, result);
}
}
}

View File

@@ -0,0 +1,105 @@
namespace UglyToad.PdfPig.Images.Png
{
using System;
/// <summary>
/// Provides convenience methods for indexing into a raw byte array to extract pixel values.
/// </summary>
internal class RawPngData
{
private readonly byte[] data;
private readonly int bytesPerPixel;
private readonly int width;
private readonly Palette palette;
private readonly ColorType colorType;
private readonly int rowOffset;
/// <summary>
/// Create a new <see cref="RawPngData"/>.
/// </summary>
/// <param name="data">The decoded pixel data as bytes.</param>
/// <param name="bytesPerPixel">The number of bytes in each pixel.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="interlaceMethod">The interlace method used.</param>
/// <param name="palette">The palette for images using indexed colors.</param>
/// <param name="colorType">The color type.</param>
public RawPngData(byte[] data, int bytesPerPixel, int width, InterlaceMethod interlaceMethod, Palette palette, ColorType colorType)
{
if (width < 0)
{
throw new ArgumentOutOfRangeException($"Width must be greater than or equal to 0, got {width}.");
}
this.data = data ?? throw new ArgumentNullException(nameof(data));
this.bytesPerPixel = bytesPerPixel;
this.width = width;
this.palette = palette;
this.colorType = colorType;
rowOffset = interlaceMethod == InterlaceMethod.Adam7 ? 0 : 1;
}
public Pixel GetPixel(int x, int y)
{
var rowStartPixel = (rowOffset + (rowOffset * y)) + (bytesPerPixel * width * y);
var pixelStartIndex = rowStartPixel + (bytesPerPixel * x);
var first = data[pixelStartIndex];
if (palette != null)
{
return palette.GetPixel(first);
}
switch (bytesPerPixel)
{
case 1:
return new Pixel(first, first, first, 255, true);
case 2:
switch (colorType)
{
case ColorType.None:
{
byte second = data[pixelStartIndex + 1];
var value = ToSingleByte(first, second);
return new Pixel(value, value, value, 255, true);
}
default:
return new Pixel(first, first, first, data[pixelStartIndex + 1], true);
}
case 3:
return new Pixel(first, data[pixelStartIndex + 1], data[pixelStartIndex + 2], 255, false);
case 4:
switch (colorType)
{
case ColorType.None | ColorType.AlphaChannelUsed:
{
var second = data[pixelStartIndex + 1];
var firstAlpha = data[pixelStartIndex + 2];
var secondAlpha = data[pixelStartIndex + 3];
var gray = ToSingleByte(first, second);
var alpha = ToSingleByte(firstAlpha, secondAlpha);
return new Pixel(gray, gray, gray, alpha, true);
}
default:
return new Pixel(first, data[pixelStartIndex + 1], data[pixelStartIndex + 2], data[pixelStartIndex + 3], false);
}
case 6:
return new Pixel(first, data[pixelStartIndex + 2], data[pixelStartIndex + 4], 255, false);
case 8:
return new Pixel(first, data[pixelStartIndex + 2], data[pixelStartIndex + 4], data[pixelStartIndex + 6], false);
default:
throw new InvalidOperationException($"Unreconized number of bytes per pixel: {bytesPerPixel}.");
}
}
private static byte ToSingleByte(byte first, byte second)
{
var us = (first << 8) + second;
var result = (byte)Math.Round((255 * us) / (double)ushort.MaxValue);
return result;
}
}
}

View File

@@ -0,0 +1,46 @@
namespace UglyToad.PdfPig.Images.Png
{
using System;
using System.IO;
internal static class StreamHelper
{
public static int ReadBigEndianInt32(Stream stream)
{
return (ReadOrTerminate(stream) << 24) + (ReadOrTerminate(stream) << 16)
+ (ReadOrTerminate(stream) << 8) + ReadOrTerminate(stream);
}
public static int ReadBigEndianInt32(byte[] bytes, int offset)
{
return (bytes[0 + offset] << 24) + (bytes[1 + offset] << 16)
+ (bytes[2 + offset] << 8) + bytes[3 + offset];
}
public static void WriteBigEndianInt32(Stream stream, int value)
{
stream.WriteByte((byte)(value >> 24));
stream.WriteByte((byte)(value >> 16));
stream.WriteByte((byte)(value >> 8));
stream.WriteByte((byte)value);
}
private static byte ReadOrTerminate(Stream stream)
{
var b = stream.ReadByte();
if (b == -1)
{
throw new InvalidOperationException($"Unexpected end of stream at {stream.Position}.");
}
return (byte) b;
}
public static bool TryReadHeaderBytes(Stream stream, out byte[] bytes)
{
bytes = new byte[8];
return stream.Read(bytes, 0, 8) == 8;
}
}
}

View File

@@ -43,9 +43,13 @@
return false;
}
public static T Get<T>(IndirectReference reference, IPdfTokenScanner scanner) where T : IToken
public static T Get<T>(IndirectReference reference, IPdfTokenScanner scanner) where T : class, IToken
{
var temp = scanner.Get(reference);
if (temp is null)
{
return null;
}
if (temp.Data is T locatedResult)
{
@@ -75,7 +79,7 @@
throw new PdfDocumentFormatException($"Could not find the object number {reference} with type {typeof(T).Name}.");
}
public static T Get<T>(IToken token, IPdfTokenScanner scanner) where T : IToken
public static T Get<T>(IToken token, IPdfTokenScanner scanner) where T : class, IToken
{
if (token is T result)
{
@@ -84,32 +88,7 @@
if (token is IndirectReferenceToken reference)
{
var temp = scanner.Get(reference.Data);
if (temp.Data is T locatedResult)
{
return locatedResult;
}
if (temp.Data is IndirectReferenceToken nestedReference)
{
return Get<T>(nestedReference, scanner);
}
if (temp.Data is ArrayToken array && array.Data.Count == 1)
{
var arrayElement = array.Data[0];
if (arrayElement is IndirectReferenceToken arrayReference)
{
return Get<T>(arrayReference, scanner);
}
if (arrayElement is T arrayToken)
{
return arrayToken;
}
}
return Get<T>(reference.Data, scanner);
}
throw new PdfDocumentFormatException($"Could not find the object {token} with type {typeof(T).Name}.");

View File

@@ -32,7 +32,7 @@
return true;
}
internal static T Get<T>(this DictionaryToken dictionary, NameToken name, IPdfTokenScanner scanner) where T : IToken
internal static T Get<T>(this DictionaryToken dictionary, NameToken name, IPdfTokenScanner scanner) where T : class, IToken
{
if (!dictionary.TryGet(name, out var token) || !(token is T typedToken))
{

View File

@@ -1,11 +1,12 @@
namespace UglyToad.PdfPig.PdfFonts
{
using System.Globalization;
using CidFonts;
/// <summary>
/// Specifies the character collection associated with the <see cref="ICidFont"/> (CIDFont).
/// </summary>
internal struct CharacterIdentifierSystemInfo
internal readonly struct CharacterIdentifierSystemInfo
{
/// <summary>
/// Identifies the issuer of the character collection.
@@ -31,7 +32,7 @@
public override string ToString()
{
return $"{Registry}-{Ordering}-{Supplement}";
return $"{Registry}-{Ordering}-{Supplement.ToString(CultureInfo.InvariantCulture)}";
}
}
}

View File

@@ -60,10 +60,18 @@
public bool HasUnicodeMappings => BaseFontCharacterMap.Count > 0;
private readonly int minCodeLength;
/// <summary>
/// Issue #202 seems to indicate empty codespace ranges are possible.
/// </summary>
private readonly bool hasEmptyCodespace;
private readonly int minCodeLength = 4;
private readonly int maxCodeLength;
public CMap(CharacterIdentifierSystemInfo info, int type, int wMode, string name, string version, IReadOnlyDictionary<int, string> baseFontCharacterMap, IReadOnlyList<CodespaceRange> codespaceRanges, IReadOnlyList<CidRange> cidRanges, IReadOnlyList<CidCharacterMapping> cidCharacterMappings)
public CMap(CharacterIdentifierSystemInfo info, int type, int wMode, string name, string version,
IReadOnlyDictionary<int, string> baseFontCharacterMap,
IReadOnlyList<CodespaceRange> codespaceRanges,
IReadOnlyList<CidRange> cidRanges,
IReadOnlyList<CidCharacterMapping> cidCharacterMappings)
{
if (cidCharacterMappings == null)
{
@@ -78,8 +86,17 @@
BaseFontCharacterMap = baseFontCharacterMap ?? throw new ArgumentNullException(nameof(baseFontCharacterMap));
CodespaceRanges = codespaceRanges ?? throw new ArgumentNullException(nameof(codespaceRanges));
CidRanges = cidRanges ?? throw new ArgumentNullException(nameof(cidRanges));
maxCodeLength = CodespaceRanges.Max(x => x.CodeLength);
minCodeLength = CodespaceRanges.Min(x => x.CodeLength);
// Issue #202 possible empty codespace ranges?
if (CodespaceRanges.Count > 0)
{
maxCodeLength = CodespaceRanges.Max(x => x.CodeLength);
minCodeLength = CodespaceRanges.Min(x => x.CodeLength);
}
else
{
hasEmptyCodespace = true;
}
var characterMappings = new Dictionary<int, CidCharacterMapping>();
@@ -135,6 +152,13 @@
public int ReadCode(IInputBytes bytes)
{
if (hasEmptyCodespace)
{
var data = new byte[minCodeLength];
bytes.Read(data);
return data.ToInt(minCodeLength);
}
byte[] result = new byte[maxCodeLength];
result[0] = bytes.CurrentByte;
@@ -165,7 +189,7 @@
}
}
throw new InvalidOperationException("CMap is invalid");
throw new PdfDocumentFormatException($"CMap is invalid, min code length was {minCodeLength}, max was {maxCodeLength}.");
}
private static byte ReadByte(IInputBytes bytes)

View File

@@ -2,6 +2,7 @@
{
using System;
using System.Collections.Generic;
using Core;
using Fonts;
using Fonts.Encodings;
using PdfPig.Parser.Parts;
@@ -26,7 +27,7 @@
return null;
}
if (baseEncodingObject is NameToken name)
if (DirectObjectFinder.TryGet(baseEncodingObject, pdfScanner, out NameToken name))
{
if (TryGetNamedEncoding(descriptor, name, out var namedEncoding))
{
@@ -47,13 +48,22 @@
return WinAnsiEncoding.Instance;
}
throw new InvalidOperationException($"Did not recognize named font: {name} in {fontDictionary}.");
}
DictionaryToken encodingDictionary = DirectObjectFinder.Get<DictionaryToken>(baseEncodingObject, pdfScanner);
try
{
DictionaryToken encodingDictionary = DirectObjectFinder.Get<DictionaryToken>(baseEncodingObject, pdfScanner);
var encoding = ReadEncodingDictionary(encodingDictionary, fontEncoding);
var encoding = ReadEncodingDictionary(encodingDictionary, fontEncoding);
return encoding;
return encoding;
}
catch (Exception ex)
{
throw new PdfDocumentFormatException($"Failed to locate encoding in {fontDictionary}.", ex);
}
}
private Encoding ReadEncodingDictionary(DictionaryToken encodingDictionary, Encoding fontEncoding)

View File

@@ -2,6 +2,7 @@
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Cmap;
using Core;
@@ -136,7 +137,7 @@
}
else
{
glyphName = index.ToString();
glyphName = index.ToString(CultureInfo.InvariantCulture);
}
fakeEncoding[i] = glyphName;

View File

@@ -138,7 +138,7 @@
try
{
if (!(pdfScanner.Get(descriptor.FontFile.ObjectKey.Data).Data is StreamToken stream))
if (!(pdfScanner.Get(descriptor.FontFile.ObjectKey.Data)?.Data is StreamToken stream))
{
return null;
}

View File

@@ -45,7 +45,7 @@
/// <returns>The tokenized PDF object from the file.</returns>
public ObjectToken GetObject(IndirectReference reference)
{
return TokenScanner.Get(reference);
return TokenScanner.Get(reference) ?? throw new InvalidOperationException($"Could not find the object with reference: {reference}.");
}
}
}

View File

@@ -11,6 +11,7 @@
{
/// <summary>
/// Tokenize the object with a given object number.
/// May return null when the reference is undefined
/// </summary>
/// <param name="reference">The object number for the object to tokenize.</param>
/// <returns>The tokenized object.</returns>

View File

@@ -8,9 +8,7 @@
using System.Text.RegularExpressions;
using Core;
using Encryption;
using Exceptions;
using Filters;
using Parser.Parts;
using Tokens;
internal class PdfTokenScanner : IPdfTokenScanner
@@ -144,6 +142,21 @@
coreTokenScanner.Seek(previousTokenPositions[0]);
break;
}
if (readTokens.Count == 3 && readTokens[1] is NumericToken extraObjNum && readTokens[2] is NumericToken extraGenNum)
{
// An obj was encountered after reading the actual token and the object and generation number of the following token.
var actualReference = new IndirectReference(objectNumber.Int, generation.Int);
var actualToken = encryptionHandler.Decrypt(actualReference, readTokens[0]);
CurrentToken = new ObjectToken(startPosition, actualReference, actualToken);
readTokens.Clear();
coreTokenScanner.Seek(previousTokenPositions[0]);
return true;
}
// This should never happen.
Debug.Assert(false, "Encountered a start object 'obj' operator before the end of the previous object.");
return false;
@@ -664,7 +677,7 @@
if (!objectLocationProvider.TryGetOffset(reference, out var offset))
{
throw new InvalidOperationException($"Could not find the object with reference: {reference}.");
return null;
}
// Negative offsets refer to a stream with that number.

View File

@@ -52,11 +52,6 @@ namespace UglyToad.PdfPig.Writer
/// </summary>
internal IReadOnlyDictionary<Guid, IWritingFont> Fonts => fonts.ToDictionary(x => x.Key, x => x.Value.FontProgram);
/// <summary>
/// Create a builder from an existing PDF file.
/// </summary>
public static PdfDocumentBuilder FromPdf(IReadOnlyList<byte> bytes) => PdfDocumentToPdfDocumentBuilderFactory.Convert(new ByteArrayInputBytes(bytes));
/// <summary>
/// Determines whether the bytes of the TrueType font file provided can be used in a PDF document.
/// </summary>

View File

@@ -1,142 +0,0 @@
namespace UglyToad.PdfPig.Writer
{
using System;
using System.Collections.Generic;
using Content;
using Core;
using CrossReference;
using Encryption;
using Exceptions;
using Filters;
using Graphics;
using Logging;
using Parser;
using Parser.FileStructure;
using Parser.Parts;
using PdfFonts;
using PdfFonts.Parser;
using PdfFonts.Parser.Handlers;
using PdfFonts.Parser.Parts;
using PdfPig.Fonts.SystemFonts;
using Tokenization.Scanner;
using Tokens;
internal static class PdfDocumentToPdfDocumentBuilderFactory
{
private static readonly ILog Log = new NoOpLog();
private static readonly IFilterProvider FilterProvider = DefaultFilterProvider.Instance;
public static PdfDocumentBuilder Convert(IInputBytes inputBytes)
{
if (inputBytes == null)
{
throw new ArgumentNullException(nameof(inputBytes));
}
var coreScanner = new CoreTokenScanner(inputBytes);
const bool isLenientParsing = false;
var version = FileHeaderParser.Parse(coreScanner, isLenientParsing, Log);
var crossReferenceParser = new CrossReferenceParser(Log, new XrefOffsetValidator(Log),
new Parser.Parts.CrossReference.CrossReferenceStreamParser(FilterProvider));
CrossReferenceTable crossReference = null;
// ReSharper disable once AccessToModifiedClosure
var locationProvider = new ObjectLocationProvider(() => crossReference, inputBytes);
var pdfScanner = new PdfTokenScanner(inputBytes, locationProvider, FilterProvider, NoOpEncryptionHandler.Instance);
var crossReferenceOffset = FileTrailerParser.GetFirstCrossReferenceOffset(inputBytes, coreScanner, isLenientParsing);
crossReference = crossReferenceParser.Parse(inputBytes, isLenientParsing, crossReferenceOffset, version.OffsetInFile, pdfScanner, coreScanner);
var (rootReference, rootDictionary) = ParseTrailer(crossReference, isLenientParsing,
pdfScanner,
out var encryptionDictionary);
if (encryptionDictionary != null)
{
throw new PdfDocumentEncryptedException("Unable to edit document with password");
}
var cidFontFactory = new CidFontFactory(pdfScanner, FilterProvider);
var encodingReader = new EncodingReader(pdfScanner);
var type1Handler = new Type1FontHandler(pdfScanner, FilterProvider, encodingReader);
var fontFactory = new FontFactory(Log, new Type0FontHandler(cidFontFactory,
FilterProvider, pdfScanner),
new TrueTypeFontHandler(Log, pdfScanner, FilterProvider, encodingReader, SystemFontFinder.Instance,
type1Handler),
type1Handler,
new Type3FontHandler(pdfScanner, FilterProvider, encodingReader));
var resourceContainer = new ResourceStore(pdfScanner, fontFactory);
var catalog = CatalogFactory.Create(rootReference, rootDictionary, pdfScanner, isLenientParsing);
var pageFactory = new PageFactory(pdfScanner, resourceContainer, FilterProvider,
new PageContentParser(new ReflectionGraphicsStateOperationFactory()),
Log);
var builder = new PdfDocumentBuilder();
var number = 1;
foreach (var node in GetPages(catalog.PageTree))
{
// First, what resources can we define, fonts, etc.
// Second, we need to copy resource and dictionary keys we don't understand.
// Third, we need to re-use the inherited properties where possible to prevent double work.
var page = Pages.CreateFromPageTreeNode(node, pdfScanner, pageFactory, number++, false);
var pageBuilder = builder.AddPage(page.Width, page.Height);
pageBuilder.Advanced.Operations.AddRange(page.Operations);
}
return builder;
}
private static (IndirectReference, DictionaryToken) ParseTrailer(CrossReferenceTable crossReferenceTable, bool isLenientParsing, IPdfTokenScanner pdfTokenScanner,
out EncryptionDictionary encryptionDictionary)
{
encryptionDictionary = null;
if (crossReferenceTable.Trailer.EncryptionToken != null)
{
if (!DirectObjectFinder.TryGet(crossReferenceTable.Trailer.EncryptionToken, pdfTokenScanner, out DictionaryToken encryptionDictionaryToken))
{
throw new PdfDocumentFormatException($"Unrecognized encryption token in trailer: {crossReferenceTable.Trailer.EncryptionToken}.");
}
encryptionDictionary = EncryptionDictionaryFactory.Read(encryptionDictionaryToken, pdfTokenScanner);
}
var rootDictionary = DirectObjectFinder.Get<DictionaryToken>(crossReferenceTable.Trailer.Root, pdfTokenScanner);
if (!rootDictionary.ContainsKey(NameToken.Type) && isLenientParsing)
{
rootDictionary = rootDictionary.With(NameToken.Type, NameToken.Catalog);
}
return (crossReferenceTable.Trailer.Root, rootDictionary);
}
private static IEnumerable<PageTreeNode> GetPages(PageTreeNode root)
{
if (root.IsPage)
{
yield return root;
yield break;
}
foreach (var child in root.Children)
{
foreach (var node in GetPages(child))
{
yield return node;
}
}
}
}
}

View File

@@ -19,6 +19,7 @@
using PdfFonts;
using Tokens;
using Graphics.Operations.PathPainting;
using Images.Png;
/// <summary>
/// A builder used to add construct a page in a PDF document.
@@ -332,7 +333,13 @@
/// </summary>
/// <param name="image">An image previously added to this page or another page.</param>
/// <param name="placementRectangle">The size and location to draw the image on this page.</param>
public void AddJpeg(AddedImage image, PdfRectangle placementRectangle)
public void AddJpeg(AddedImage image, PdfRectangle placementRectangle) => AddImage(image, placementRectangle);
/// <summary>
/// Adds the image previously added using <see cref="AddJpeg(byte[], PdfRectangle)"/>
/// or <see cref="AddPng(byte[], PdfRectangle)"/> sharing the same image to prevent duplication.
/// </summary>
public void AddImage(AddedImage image, PdfRectangle placementRectangle)
{
if (!resourcesDictionary.TryGetValue(NameToken.Xobject, out var xobjectsDict)
|| !(xobjectsDict is DictionaryToken xobjects))
@@ -357,6 +364,86 @@
operations.Add(Pop.Value);
}
/// <summary>
/// Adds the PNG image represented by the input bytes at the specified location.
/// </summary>
public AddedImage AddPng(byte[] pngBytes, PdfRectangle placementRectangle)
{
using (var memoryStream = new MemoryStream(pngBytes))
{
return AddPng(memoryStream, placementRectangle);
}
}
/// <summary>
/// Adds the PNG image represented by the input stream at the specified location.
/// </summary>
public AddedImage AddPng(Stream pngStream, PdfRectangle placementRectangle)
{
var png = Png.Open(pngStream);
byte[] data;
var pixelBuffer = new byte[3];
using (var memoryStream = new MemoryStream())
{
for (var rowIndex = 0; rowIndex < png.Height; rowIndex++)
{
for (var colIndex = 0; colIndex < png.Width; colIndex++)
{
var pixel = png.GetPixel(colIndex, rowIndex);
pixelBuffer[0] = pixel.R;
pixelBuffer[1] = pixel.G;
pixelBuffer[2] = pixel.B;
memoryStream.Write(pixelBuffer, 0, pixelBuffer.Length);
}
}
data = memoryStream.ToArray();
}
var compressed = DataCompresser.CompressBytes(data);
var imgDictionary = new Dictionary<NameToken, IToken>
{
{NameToken.Type, NameToken.Xobject },
{NameToken.Subtype, NameToken.Image },
{NameToken.Width, new NumericToken(png.Width) },
{NameToken.Height, new NumericToken(png.Height) },
{NameToken.BitsPerComponent, new NumericToken(png.Header.BitDepth)},
{NameToken.ColorSpace, NameToken.Devicergb},
{NameToken.Filter, NameToken.FlateDecode},
{NameToken.Length, new NumericToken(compressed.Length)}
};
var reference = documentBuilder.AddImage(new DictionaryToken(imgDictionary), compressed);
if (!resourcesDictionary.TryGetValue(NameToken.Xobject, out var xobjectsDict)
|| !(xobjectsDict is DictionaryToken xobjects))
{
xobjects = new DictionaryToken(new Dictionary<NameToken, IToken>());
resourcesDictionary[NameToken.Xobject] = xobjects;
}
var key = NameToken.Create($"I{imageKey++}");
resourcesDictionary[NameToken.Xobject] = xobjects.With(key, new IndirectReferenceToken(reference));
operations.Add(Push.Value);
// This needs to be the placement rectangle.
operations.Add(new ModifyCurrentTransformationMatrix(new[]
{
(decimal)placementRectangle.Width, 0,
0, (decimal)placementRectangle.Height,
(decimal)placementRectangle.BottomLeft.X, (decimal)placementRectangle.BottomLeft.Y
}));
operations.Add(new InvokeNamedXObject(key));
operations.Add(Pop.Value);
return new AddedImage(reference, png.Width, png.Height);
}
private List<Letter> DrawLetters(string text, IWritingFont font, TransformationMatrix fontMatrix, decimal fontSize, TransformationMatrix textMatrix)
{
var horizontalScaling = 1;

View File

@@ -2,11 +2,12 @@
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Core;
using Graphics.Operations;
using Tokens;
/// <summary>
/// This class would lazily flush all token. Allowing us to make changes to references without need to rewrite the whole stream
/// </summary>
@@ -29,13 +30,15 @@
Stream = baseStream ?? throw new ArgumentNullException(nameof(baseStream));
DisposeStream = disposeStream;
}
public void Flush(decimal version, IndirectReferenceToken catalogReference)
{
if (catalogReference == null)
{
throw new ArgumentNullException(nameof(catalogReference));
WriteString($"%PDF-{version:0.0}", Stream);
}
WriteString($"%PDF-{version.ToString("0.0", CultureInfo.InvariantCulture)}", Stream);
Stream.WriteText("%");
Stream.WriteByte(169);
@@ -46,7 +49,7 @@
var offsets = new Dictionary<IndirectReference, long>();
ObjectToken catalogToken = null;
foreach(var pair in tokenReferences)
foreach (var pair in tokenReferences)
{
var referenceToken = pair.Key;
var token = pair.Value;
@@ -97,7 +100,7 @@
return reference;
}
public int ReserveNumber()
{
var reserved = CurrentNumber;
@@ -107,10 +110,10 @@
}
public IndirectReferenceToken ReserveNumberToken()
{
{
return new IndirectReferenceToken(new IndirectReference(ReserveNumber(), 0));
}
public byte[] ToArray()
{
var currentPosition = Stream.Position;
@@ -119,7 +122,9 @@
var bytes = new byte[Stream.Length];
if (Stream.Read(bytes, 0, bytes.Length) != bytes.Length)
{
throw new Exception("Unable to read all the bytes from stream");
}
Stream.Seek(currentPosition, SeekOrigin.Begin);
@@ -137,7 +142,7 @@
Stream?.Dispose();
Stream = null;
}
private IndirectReferenceToken AddToken(IToken token, int reservedNumber)
{
var reference = new IndirectReference(reservedNumber, 0);
@@ -148,7 +153,7 @@
private IndirectReferenceToken FindToken(IToken token)
{
foreach(var pair in tokenReferences)
foreach (var pair in tokenReferences)
{
var reference = pair.Key;
var storedToken = pair.Value;
@@ -160,7 +165,7 @@
return null;
}
private static void WriteString(string text, Stream stream)
{
var bytes = OtherEncodings.StringAsLatin1Bytes(text);

View File

@@ -368,8 +368,28 @@
private static void WriteString(StringToken stringToken, Stream outputStream)
{
if (stringToken.EncodedWith == StringToken.Encoding.Iso88591)
{
var isUtf16 = false;
for (var i = 0; i < stringToken.Data.Length; i++)
{
var c = stringToken.Data[i];
// Close enough.
if (c > 250)
{
isUtf16 = true;
break;
}
}
if (isUtf16)
{
stringToken = new StringToken(stringToken.Data, StringToken.Encoding.Utf16BE);
}
}
outputStream.WriteByte(StringStart);
var bytes = OtherEncodings.StringAsLatin1Bytes(stringToken.Data);
var bytes = stringToken.GetBytes();
outputStream.Write(bytes, 0, bytes.Length);
outputStream.WriteByte(StringEnd);

View File

@@ -6,6 +6,7 @@
using Core;
using Graphics.Colors;
using Graphics.Core;
using Images.Png;
using Tokens;
using Util.JetBrains.Annotations;
@@ -107,6 +108,9 @@
return true;
}
/// <inheritdoc />
public bool TryGetPng(out byte[] bytes) => PngFromPdfImageFactory.TryGenerate(this, out bytes);
/// <inheritdoc />
public override string ToString()
{

View File

@@ -9,7 +9,7 @@
<Description>Reads text content from PDF documents and supports document creation. Apache 2.0 licensed.</Description>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/UglyToad/PdfPig</PackageProjectUrl>
<PackageTags>PDF;Reader;Document;Adobe;PDFBox;PdfPig;pdf-extract</PackageTags>
<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.2</Version>