mirror of
https://github.com/UglyToad/PdfPig.git
synced 2026-03-10 00:23:29 +08:00
Compare commits
19 Commits
edit-exist
...
debug-enco
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0370c51371 | ||
|
|
c634ec3aa2 | ||
|
|
9b7554c973 | ||
|
|
f2522f149c | ||
|
|
04f785aed4 | ||
|
|
5ffada46e3 | ||
|
|
54f227ea95 | ||
|
|
6359ba5df1 | ||
|
|
52104b6580 | ||
|
|
8860e29191 | ||
|
|
5ac7a957d0 | ||
|
|
6f26b274c4 | ||
|
|
b2b85e2f44 | ||
|
|
304c17eac9 | ||
|
|
5a82c36631 | ||
|
|
dc55fd8011 | ||
|
|
142c871c47 | ||
|
|
e42df5b5b5 | ||
|
|
f3f7533507 |
@@ -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
|
||||
|
||||
@@ -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)})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
src/UglyToad.PdfPig.Tests/Integration/Documents/pdfpig.png
Normal file
BIN
src/UglyToad.PdfPig.Tests/Integration/Documents/pdfpig.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -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()))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
114
src/UglyToad.PdfPig/Images/Png/Adam7.cs
Normal file
114
src/UglyToad.PdfPig/Images/Png/Adam7.cs
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/UglyToad.PdfPig/Images/Png/Adler32Checksum.cs
Normal file
35
src/UglyToad.PdfPig/Images/Png/Adler32Checksum.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/UglyToad.PdfPig/Images/Png/ChunkHeader.cs
Normal file
62
src/UglyToad.PdfPig/Images/Png/ChunkHeader.cs
Normal 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}).";
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/UglyToad.PdfPig/Images/Png/ColorType.cs
Normal file
28
src/UglyToad.PdfPig/Images/Png/ColorType.cs
Normal 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
|
||||
}
|
||||
}
|
||||
13
src/UglyToad.PdfPig/Images/Png/CompressionMethod.cs
Normal file
13
src/UglyToad.PdfPig/Images/Png/CompressionMethod.cs
Normal 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
|
||||
}
|
||||
}
|
||||
87
src/UglyToad.PdfPig/Images/Png/Crc32.cs
Normal file
87
src/UglyToad.PdfPig/Images/Png/Crc32.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
211
src/UglyToad.PdfPig/Images/Png/Decoder.cs
Normal file
211
src/UglyToad.PdfPig/Images/Png/Decoder.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/UglyToad.PdfPig/Images/Png/FilterMethod.cs
Normal file
13
src/UglyToad.PdfPig/Images/Png/FilterMethod.cs
Normal 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
|
||||
}
|
||||
}
|
||||
26
src/UglyToad.PdfPig/Images/Png/FilterType.cs
Normal file
26
src/UglyToad.PdfPig/Images/Png/FilterType.cs
Normal 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
|
||||
}
|
||||
}
|
||||
54
src/UglyToad.PdfPig/Images/Png/HeaderValidationResult.cs
Normal file
54
src/UglyToad.PdfPig/Images/Png/HeaderValidationResult.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/UglyToad.PdfPig/Images/Png/IChunkVisitor.cs
Normal file
15
src/UglyToad.PdfPig/Images/Png/IChunkVisitor.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
96
src/UglyToad.PdfPig/Images/Png/ImageHeader.cs
Normal file
96
src/UglyToad.PdfPig/Images/Png/ImageHeader.cs
Normal 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}.";
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/UglyToad.PdfPig/Images/Png/InterlaceMethod.cs
Normal file
17
src/UglyToad.PdfPig/Images/Png/InterlaceMethod.cs
Normal 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
|
||||
}
|
||||
}
|
||||
19
src/UglyToad.PdfPig/Images/Png/Palette.cs
Normal file
19
src/UglyToad.PdfPig/Images/Png/Palette.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
123
src/UglyToad.PdfPig/Images/Png/Pixel.cs
Normal file
123
src/UglyToad.PdfPig/Images/Png/Pixel.cs
Normal 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})";
|
||||
}
|
||||
}
|
||||
}
|
||||
89
src/UglyToad.PdfPig/Images/Png/Png.cs
Normal file
89
src/UglyToad.PdfPig/Images/Png/Png.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
159
src/UglyToad.PdfPig/Images/Png/PngBuilder.cs
Normal file
159
src/UglyToad.PdfPig/Images/Png/PngBuilder.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs
Normal file
61
src/UglyToad.PdfPig/Images/Png/PngFromPdfImageFactory.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
183
src/UglyToad.PdfPig/Images/Png/PngOpener.cs
Normal file
183
src/UglyToad.PdfPig/Images/Png/PngOpener.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/UglyToad.PdfPig/Images/Png/PngStreamWriteHelper.cs
Normal file
63
src/UglyToad.PdfPig/Images/Png/PngStreamWriteHelper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
105
src/UglyToad.PdfPig/Images/Png/RawPngData.cs
Normal file
105
src/UglyToad.PdfPig/Images/Png/RawPngData.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/UglyToad.PdfPig/Images/Png/StreamHelper.cs
Normal file
46
src/UglyToad.PdfPig/Images/Png/StreamHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}.");
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user