This commit is contained in:
BobLd
2020-04-18 20:17:20 +01:00
28 changed files with 668 additions and 136 deletions

43
examples/ExtractImages.cs Normal file
View File

@@ -0,0 +1,43 @@
namespace UglyToad.Examples
{
using System;
using System.Linq;
using PdfPig;
using PdfPig.Content;
using PdfPig.XObjects;
internal static class ExtractImages
{
public static void Run(string filePath)
{
using (var document = PdfDocument.Open(filePath))
{
foreach (var page in document.GetPages())
{
foreach (var image in page.GetImages())
{
switch (image)
{
case XObjectImage ximg:
byte[] b;
try
{
b = ximg.Bytes.ToArray();
}
catch
{
b = ximg.RawBytes.ToArray();
}
Console.WriteLine($"Image with {b.Length} bytes and dictionary {ximg.ImageDictionary}.");
break;
case InlineImage inline:
Console.WriteLine($"Inline image: {inline.RawBytes.Count} bytes.");
break;
}
}
}
}
}
}
}

View File

@@ -0,0 +1,55 @@
namespace UglyToad.Examples
{
using System;
using System.Text;
using PdfPig;
using PdfPig.Content;
public static class OpenDocumentAndExtractWords
{
public static void Run(string filePath)
{
var sb = new StringBuilder();
using (var document = PdfDocument.Open(filePath))
{
Word previous = null;
foreach (var page in document.GetPages())
{
foreach (var word in page.GetWords())
{
if (previous != null)
{
var hasInsertedWhitespace = false;
var bothNonEmpty = previous.Letters.Count > 0 && word.Letters.Count > 0;
if (bothNonEmpty)
{
var prevLetter1 = previous.Letters[0];
var currentLetter1 = word.Letters[0];
var baselineGap = Math.Abs(prevLetter1.StartBaseLine.Y - currentLetter1.StartBaseLine.Y);
if (baselineGap > 3)
{
hasInsertedWhitespace = true;
sb.AppendLine();
}
}
if (!hasInsertedWhitespace)
{
sb.Append(" ");
}
}
sb.Append(word.Text);
previous = word;
}
}
}
Console.WriteLine(sb.ToString());
}
}
}

55
examples/Program.cs Normal file
View File

@@ -0,0 +1,55 @@
namespace UglyToad.Examples
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
public static class Program
{
public static void Main()
{
Console.WriteLine("Welcome to the PdfPig examples gallery!");
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
var filesDirectory = Path.Combine(baseDirectory, "..", "..", "..", "..", "src", "UglyToad.PdfPig.Tests", "Integration", "Documents");
var examples = new Dictionary<int, (string name, Action action)>
{
{1,
("Extract Words with newline detection",
() => OpenDocumentAndExtractWords.Run(Path.Combine(filesDirectory, "Two Page Text Only - from libre office.pdf")))
},
{2,
("Extract images",
() => ExtractImages.Run(Path.Combine(filesDirectory, "2006_Swedish_Touring_Car_Championship.pdf")))
}
};
var choices = string.Join(Environment.NewLine, examples.Select(x => $"{x.Key}: {x.Value.name}"));
Console.WriteLine(choices);
Console.WriteLine();
do
{
Console.Write("Enter a number to pick an example (enter 'q' to exit):");
var val = Console.ReadLine();
if (!int.TryParse(val, out var opt) || !examples.TryGetValue(opt, out var act))
{
if (string.Equals(val, "q", StringComparison.OrdinalIgnoreCase))
{
return;
}
Console.WriteLine($"No option with value: {val}.");
continue;
}
act.action.Invoke();
} while (true);
}
}
}

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\src\UglyToad.PdfPig\UglyToad.PdfPig.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,55 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.28307.1022
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UglyToad.Examples", "UglyToad.Examples.csproj", "{F72DA3EE-FBED-4271-88CC-05D883FD4DAD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UglyToad.PdfPig.Core", "..\src\UglyToad.PdfPig.Core\UglyToad.PdfPig.Core.csproj", "{B12C9CFF-879B-4C70-8C7B-7DBF953608C5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UglyToad.PdfPig.Fonts", "..\src\UglyToad.PdfPig.Fonts\UglyToad.PdfPig.Fonts.csproj", "{371FB56D-E9BE-40A2-8419-5F5F6F8FE0C0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UglyToad.PdfPig.Tokenization", "..\src\UglyToad.PdfPig.Tokenization\UglyToad.PdfPig.Tokenization.csproj", "{E5BD532A-B3D0-4975-80CD-8B0B6D70FD17}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UglyToad.PdfPig.Tokens", "..\src\UglyToad.PdfPig.Tokens\UglyToad.PdfPig.Tokens.csproj", "{50308C34-4074-4D36-AA93-57CFC812D831}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UglyToad.PdfPig", "..\src\UglyToad.PdfPig\UglyToad.PdfPig.csproj", "{75ED54D6-308F-44AD-B85E-C027F3AA80AE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F72DA3EE-FBED-4271-88CC-05D883FD4DAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F72DA3EE-FBED-4271-88CC-05D883FD4DAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F72DA3EE-FBED-4271-88CC-05D883FD4DAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F72DA3EE-FBED-4271-88CC-05D883FD4DAD}.Release|Any CPU.Build.0 = Release|Any CPU
{B12C9CFF-879B-4C70-8C7B-7DBF953608C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B12C9CFF-879B-4C70-8C7B-7DBF953608C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B12C9CFF-879B-4C70-8C7B-7DBF953608C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B12C9CFF-879B-4C70-8C7B-7DBF953608C5}.Release|Any CPU.Build.0 = Release|Any CPU
{371FB56D-E9BE-40A2-8419-5F5F6F8FE0C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{371FB56D-E9BE-40A2-8419-5F5F6F8FE0C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{371FB56D-E9BE-40A2-8419-5F5F6F8FE0C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{371FB56D-E9BE-40A2-8419-5F5F6F8FE0C0}.Release|Any CPU.Build.0 = Release|Any CPU
{E5BD532A-B3D0-4975-80CD-8B0B6D70FD17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E5BD532A-B3D0-4975-80CD-8B0B6D70FD17}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5BD532A-B3D0-4975-80CD-8B0B6D70FD17}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E5BD532A-B3D0-4975-80CD-8B0B6D70FD17}.Release|Any CPU.Build.0 = Release|Any CPU
{50308C34-4074-4D36-AA93-57CFC812D831}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{50308C34-4074-4D36-AA93-57CFC812D831}.Debug|Any CPU.Build.0 = Debug|Any CPU
{50308C34-4074-4D36-AA93-57CFC812D831}.Release|Any CPU.ActiveCfg = Release|Any CPU
{50308C34-4074-4D36-AA93-57CFC812D831}.Release|Any CPU.Build.0 = Release|Any CPU
{75ED54D6-308F-44AD-B85E-C027F3AA80AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{75ED54D6-308F-44AD-B85E-C027F3AA80AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75ED54D6-308F-44AD-B85E-C027F3AA80AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75ED54D6-308F-44AD-B85E-C027F3AA80AE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {03F738BD-74E0-4DE7-8063-0ACE16E931F1}
EndGlobalSection
EndGlobal

View File

@@ -1,4 +1,6 @@
namespace UglyToad.PdfPig.Fonts.AdobeFontMetrics
using System.Collections.Generic;
namespace UglyToad.PdfPig.Fonts.AdobeFontMetrics
{
using System;
using System.Globalization;
@@ -11,6 +13,9 @@
/// </summary>
public static class AdobeFontMetricsParser
{
private static readonly object Locker = new object();
private static readonly Dictionary<string, string> CharacterNames = new Dictionary<string, string>();
/// <summary>
/// This is a comment in a AFM file.
/// </summary>
@@ -603,7 +608,18 @@
}
case CharmetricsN:
{
metric.Name = parts[1];
lock (Locker)
{
var name = parts[1];
if (!CharacterNames.TryGetValue(name, out var cached))
{
cached = name;
CharacterNames[name] = cached;
}
metric.Name = cached;
}
break;
}
case CharmetricsB:

View File

@@ -3,7 +3,7 @@
/// <summary>
/// The meaning of the metric sets field.
/// </summary>
public enum AdobeFontMetricsWritingDirections
public enum AdobeFontMetricsWritingDirections : byte
{
/// <summary>
/// Writing direction 0 only.

View File

@@ -33,8 +33,8 @@
{
private static readonly HashSet<string> Standard14Names = new HashSet<string>();
private static readonly Dictionary<string, string> Standard14Mapping = new Dictionary<string, string>(34);
private static readonly Dictionary<string, AdobeFontMetrics> Standard14AfmMap = new Dictionary<string, AdobeFontMetrics>(34);
private static readonly Dictionary<Standard14Font, AdobeFontMetrics> Standard14AfmTypeMap = new Dictionary<Standard14Font, AdobeFontMetrics>(14);
private static readonly Dictionary<Standard14Font, string> BuilderTypesToNames = new Dictionary<Standard14Font, string>(14);
private static readonly Dictionary<string, AdobeFontMetrics> Standard14Cache = new Dictionary<string, AdobeFontMetrics>(34);
static Standard14()
{
@@ -88,9 +88,14 @@
Standard14Names.Add(fontName);
Standard14Mapping.Add(fontName, afmName);
if (Standard14AfmMap.TryGetValue(afmName, out var metrics))
if (type.HasValue)
{
Standard14AfmMap[fontName] = metrics;
BuilderTypesToNames[type.Value] = afmName;
}
if (Standard14Cache.TryGetValue(afmName, out var metrics))
{
Standard14Cache[fontName] = metrics;
}
try
@@ -112,11 +117,7 @@
bytes = new ByteArrayInputBytes(memory.ToArray());
}
Standard14AfmMap[fontName] = AdobeFontMetricsParser.Parse(bytes, true);
if (type.HasValue)
{
Standard14AfmTypeMap[type.Value] = Standard14AfmMap[fontName];
}
Standard14Cache[fontName] = AdobeFontMetricsParser.Parse(bytes, true);
}
catch (Exception ex)
{
@@ -130,7 +131,7 @@
/// </summary>
public static AdobeFontMetrics GetAdobeFontMetrics(string baseName)
{
Standard14AfmMap.TryGetValue(baseName, out var metrics);
Standard14Cache.TryGetValue(baseName, out var metrics);
return metrics;
}
@@ -140,7 +141,7 @@
/// </summary>
public static AdobeFontMetrics GetAdobeFontMetrics(Standard14Font fontType)
{
return Standard14AfmTypeMap[fontType];
return Standard14Cache[BuilderTypesToNames[fontType]];
}
/// <summary>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -16,7 +16,10 @@
private const string SingleInkscapePage = "Single Page Simple - from inkscape";
private const string MotorInsuranceClaim = "Motor Insurance claim form";
private const string PigProduction = "Pig Production Handbook";
private const string SinglePage90ClockwiseRotation = "SinglePage90ClockwiseRotation - from PdfPig";
private const string SinglePage180ClockwiseRotation = "SinglePage180ClockwiseRotation - from PdfPig";
private const string SinglePage270ClockwiseRotation = "SinglePage270ClockwiseRotation - from PdfPig";
private static string GetFilename(string name)
{
var documentFolder = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Integration", "Documents"));
@@ -95,6 +98,24 @@
Run(ByzantineGenerals, 702);
}
[Fact]
public void SinglePage90ClockwiseRotationFromPdfPig()
{
Run(SinglePage90ClockwiseRotation, 595);
}
[Fact]
public void SinglePage180ClockwiseRotationFromPdfPig()
{
Run(SinglePage180ClockwiseRotation, 842);
}
[Fact]
public void SinglePage270ClockwiseRotationFromPdfPig()
{
Run(SinglePage270ClockwiseRotation, 595);
}
private static void Run(string file, int imageHeight = 792)
{
var pdfFileName = GetFilename(file);
@@ -105,14 +126,14 @@
var page = document.GetPage(1);
var violetPen = new Pen(Color.BlueViolet, 1);
var greenPen = new Pen(Color.Crimson, 1);
var redPen = new Pen(Color.Crimson, 1);
using (var bitmap = new Bitmap(image))
using (var graphics = Graphics.FromImage(bitmap))
{
foreach (var word in page.GetWords())
{
DrawRectangle(word.BoundingBox, graphics, greenPen, imageHeight);
DrawRectangle(word.BoundingBox, graphics, redPen, imageHeight);
}
foreach (var letter in page.Letters)

View File

@@ -494,40 +494,16 @@
}
}
[Fact]
public void CanGeneratePdfA1BFile()
[Theory]
[InlineData(PdfAStandard.A1B)]
[InlineData(PdfAStandard.A1A)]
[InlineData(PdfAStandard.A2B)]
[InlineData(PdfAStandard.A2A)]
public void CanGeneratePdfAFile(PdfAStandard standard)
{
var builder = new PdfDocumentBuilder
{
ArchiveStandard = PdfAStandard.A1B
};
var page = builder.AddPage(PageSize.A4);
var font = builder.AddTrueTypeFont(TrueTypeTestHelper.GetFileBytes("Roboto-Regular.ttf"));
page.AddText("Howdy!", 12, new PdfPoint(25, 670), font);
var bytes = builder.Build();
WriteFile(nameof(CanGeneratePdfA1BFile), bytes);
using (var pdf = PdfDocument.Open(bytes, ParsingOptions.LenientParsingOff))
{
Assert.Equal(1, pdf.NumberOfPages);
Assert.True(pdf.TryGetXmpMetadata(out var xmp));
Assert.NotNull(xmp.GetXDocument());
}
}
[Fact]
public void CanGeneratePdfA1AFile()
{
var builder = new PdfDocumentBuilder
{
ArchiveStandard = PdfAStandard.A1A
ArchiveStandard = standard
};
var page = builder.AddPage(PageSize.A4);
@@ -537,39 +513,11 @@
var font = builder.AddTrueTypeFont(TrueTypeTestHelper.GetFileBytes("Roboto-Regular.ttf"));
page.AddText("Howdy PDF/A-1A!", 10, new PdfPoint(25, 700), font);
page.AddText($"Howdy PDF/{standard}!", 10, new PdfPoint(25, 700), font);
var bytes = builder.Build();
WriteFile(nameof(CanGeneratePdfA1AFile), bytes);
using (var pdf = PdfDocument.Open(bytes, ParsingOptions.LenientParsingOff))
{
Assert.Equal(1, pdf.NumberOfPages);
Assert.True(pdf.TryGetXmpMetadata(out var xmp));
Assert.NotNull(xmp.GetXDocument());
}
}
[Fact]
public void CanGeneratePdfA2BFile()
{
var builder = new PdfDocumentBuilder
{
ArchiveStandard = PdfAStandard.A2B
};
var page = builder.AddPage(PageSize.A4);
var font = builder.AddTrueTypeFont(TrueTypeTestHelper.GetFileBytes("Roboto-Regular.ttf"));
page.AddText("Howdy PDF/A-2B and welcome!", 10, new PdfPoint(25, 700), font);
var bytes = builder.Build();
WriteFile(nameof(CanGeneratePdfA2BFile), bytes);
WriteFile(nameof(CanGeneratePdfAFile) + standard, bytes);
using (var pdf = PdfDocument.Open(bytes, ParsingOptions.LenientParsingOff))
{

View File

@@ -93,5 +93,45 @@
"Expected object count to be lower than 24");
}
}
[Fact]
public void CanMergeWithObjectStream()
{
var first = IntegrationHelpers.GetDocumentPath("Single Page Simple - from google drive.pdf");
var second = IntegrationHelpers.GetDocumentPath("Multiple Page - from Mortality Statistics.pdf");
var result = PdfMerger.Merge(first, second);
WriteFile(nameof(CanMergeWithObjectStream), result);
using (var document = PdfDocument.Open(result, ParsingOptions.LenientParsingOff))
{
Assert.Equal(7, document.NumberOfPages);
foreach (var page in document.GetPages())
{
Assert.NotNull(page.Text);
}
}
}
private static void WriteFile(string name, byte[] bytes)
{
try
{
if (!Directory.Exists("Merger"))
{
Directory.CreateDirectory("Merger");
}
var output = Path.Combine("Merger", $"{name}.pdf");
File.WriteAllBytes(output, bytes);
}
catch
{
// ignored.
}
}
}
}

View File

@@ -57,9 +57,13 @@
switch (str)
{
case "-1":
token = NumericToken.MinusOne;
return true;
case "-":
case ".":
case "0":
case "0000":
token = NumericToken.Zero;
return true;
case "1":
@@ -92,9 +96,42 @@
case "10":
token = NumericToken.Ten;
return true;
case "11":
token = NumericToken.Eleven;
return true;
case "12":
token = NumericToken.Twelve;
return true;
case "13":
token = NumericToken.Thirteen;
return true;
case "14":
token = NumericToken.Fourteen;
return true;
case "15":
token = NumericToken.Fifteen;
return true;
case "16":
token = NumericToken.Sixteen;
return true;
case "17":
token = NumericToken.Seventeen;
return true;
case "18":
token = NumericToken.Eighteen;
return true;
case "19":
token = NumericToken.Nineteen;
return true;
case "20":
token = NumericToken.Twenty;
return true;
case "100":
token = NumericToken.OneHundred;
return true;
case "500":
token = NumericToken.FiveHundred;
return true;
case "1000":
token = NumericToken.OneThousand;
return true;

View File

@@ -10,6 +10,11 @@
/// </summary>
public class NumericToken : IDataToken<decimal>
{
/// <summary>
/// Single instance of numeric token for -1.
/// </summary>
public static readonly NumericToken MinusOne = new NumericToken(-1);
/// <summary>
/// Single instance of numeric token for 0.
/// </summary>
@@ -64,11 +69,66 @@
/// </summary>
public static readonly NumericToken Ten = new NumericToken(10);
/// <summary>
/// Single instance of numeric token for 11.
/// </summary>
public static readonly NumericToken Eleven = new NumericToken(11);
/// <summary>
/// Single instance of numeric token for 12.
/// </summary>
public static readonly NumericToken Twelve = new NumericToken(12);
/// <summary>
/// Single instance of numeric token for 13.
/// </summary>
public static readonly NumericToken Thirteen = new NumericToken(13);
/// <summary>
/// Single instance of numeric token for 14.
/// </summary>
public static readonly NumericToken Fourteen = new NumericToken(14);
/// <summary>
/// Single instance of numeric token for 15.
/// </summary>
public static readonly NumericToken Fifteen = new NumericToken(15);
/// <summary>
/// Single instance of numeric token for 16.
/// </summary>
public static readonly NumericToken Sixteen = new NumericToken(16);
/// <summary>
/// Single instance of numeric token for 17.
/// </summary>
public static readonly NumericToken Seventeen = new NumericToken(17);
/// <summary>
/// Single instance of numeric token for 18.
/// </summary>
public static readonly NumericToken Eighteen = new NumericToken(18);
/// <summary>
/// Single instance of numeric token for 19.
/// </summary>
public static readonly NumericToken Nineteen = new NumericToken(19);
/// <summary>
/// Single instance of numeric token for 20.
/// </summary>
public static readonly NumericToken Twenty = new NumericToken(20);
/// <summary>
/// Single instance of numeric token for 100.
/// </summary>
public static readonly NumericToken OneHundred = new NumericToken(100);
/// <summary>
/// Single instance of numeric token for 500.
/// </summary>
public static readonly NumericToken FiveHundred = new NumericToken(500);
/// <summary>
/// Single instance of numeric token for 1000.
/// </summary>

View File

@@ -3,21 +3,23 @@
using System;
using System.Diagnostics.Contracts;
using Core;
using Geometry;
/// <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>
{
private static readonly TransformationMatrix Rotate90 = TransformationMatrix.FromValues(0, -1, 1, 0);
private static readonly TransformationMatrix Rotate180 = TransformationMatrix.FromValues(-1, 0, 0, -1);
private static readonly TransformationMatrix Rotate270 = TransformationMatrix.FromValues(0, 1, -1, 0);
/// <summary>
/// The rotation of the page in degrees clockwise.
/// </summary>
public int Value { get; }
/// <summary>
/// Whether the rotation flips the x and y axes.
/// </summary>
public bool SwapsAxis => (Value == 90) || (Value == 270);
/// <summary>
/// Get the rotation expressed in radians (anti-clockwise).
/// </summary>
@@ -66,21 +68,60 @@
}
[Pure]
internal TransformationMatrix Rotate(TransformationMatrix matrix)
internal PdfRectangle Rotate(PdfRectangle rectangle, PdfVector pageSize)
{
// TODO: this is a bit of a hack because I don't understand matrices
/* There should be a single Affine Transform we can apply to any point resulting
* from a content stream operation which will rotate the point and translate it back to
* a point where the origin is in the page's lower left corner.
*
* For example this matrix represents a (clockwise) rotation and translation:
* [ cos sin tx ]
* [ -sin cos ty ]
* [ 0 0 1 ]
*
* The values of tx and ty are those required to move the origin back to the expected origin (lower-left).
* The corresponding values should be:
* Rotation: 0 90 180 270
* tx: 0 0 w w
* ty: 0 h h 0
*
* Where w and h are the page width and height after rotation.
*/
double cos, sin;
double dx = 0, dy = 0;
switch (Value)
{
case 0:
return matrix;
return rectangle;
case 90:
return Rotate90.Multiply(matrix);
cos = 0;
sin = 1;
dy = pageSize.Y;
break;
case 180:
return Rotate180.Multiply(matrix);
cos = -1;
sin = 0;
dx = pageSize.X;
dy = pageSize.Y;
break;
case 270:
return Rotate270.Multiply(matrix);
cos = 0;
sin = -1;
dx = pageSize.X;
break;
default:
throw new InvalidOperationException($"Invalid value for rotation: {Value}.");
}
PdfPoint Multiply(PdfPoint pt)
{
return new PdfPoint((pt.X * cos) + (pt.Y * sin) + dx,
(pt.X * -sin) + (pt.Y * cos) + dy);
}
return new PdfRectangle(Multiply(rectangle.TopLeft), Multiply(rectangle.TopRight),
Multiply(rectangle.BottomLeft), Multiply(rectangle.BottomRight));
}
/// <inheritdoc />

View File

@@ -3,31 +3,31 @@
/// <summary>
/// Direction of the text.
/// </summary>
public enum TextDirection
public enum TextDirection : byte
{
/// <summary>
/// Other text direction.
/// </summary>
Other,
Other = 0,
/// <summary>
/// Usual text direction (Left to Right).
/// </summary>
Horizontal,
Horizontal = 1,
/// <summary>
/// Horizontal text, upside down.
/// </summary>
Rotate180,
Rotate180 = 2,
/// <summary>
/// Rotated text going down.
/// </summary>
Rotate90,
Rotate90 = 3,
/// <summary>
/// Rotated text going up.
/// </summary>
Rotate270
Rotate270 = 4
}
}

View File

@@ -81,71 +81,93 @@
return new PdfRectangle(polygon[0], polygon[1]);
}
PdfPoint[] MBR = new PdfPoint[0];
double[] MBR = new double[8];
double Amin = double.PositiveInfinity;
int j = 1;
int k = 0;
PdfPoint Q = new PdfPoint();
PdfPoint R0 = new PdfPoint();
PdfPoint R1 = new PdfPoint();
PdfPoint u = new PdfPoint();
double QX = double.NaN;
double QY = double.NaN;
double R0X = double.NaN;
double R0Y = double.NaN;
double R1X = double.NaN;
double R1Y = double.NaN;
while (true)
{
PdfPoint Pk = polygon[k];
PdfPoint v = polygon[j].Subtract(Pk);
double r = 1.0 / v.DotProduct(v);
PdfPoint Pj = polygon[j];
double vX = Pj.X - Pk.X;
double vY = Pj.Y - Pk.Y;
double r = 1.0 / (vX * vX + vY * vY);
double tmin = 1;
double tmax = 0;
double smax = 0;
int l = -1;
double uX;
double uY;
for (j = 0; j < polygon.Count; j++)
{
PdfPoint Pj = polygon[j];
u = Pj.Subtract(Pk);
double t = u.DotProduct(v) * r;
PdfPoint Pt = new PdfPoint(t * v.X + Pk.X, t * v.Y + Pk.Y);
u = Pt.Subtract(Pj);
double s = u.DotProduct(u);
Pj = polygon[j];
uX = Pj.X - Pk.X;
uY = Pj.Y - Pk.Y;
double t = (uX * vX + uY * vY) * r;
double PtX = t * vX + Pk.X;
double PtY = t * vY + Pk.Y;
uX = PtX - Pj.X;
uY = PtY - Pj.Y;
double s = uX * uX + uY * uY;
if (t < tmin)
{
tmin = t;
R0 = Pt;
R0X = PtX;
R0Y = PtY;
}
if (t > tmax)
{
tmax = t;
R1 = Pt;
R1X = PtX;
R1Y = PtY;
}
if (s > smax)
{
smax = s;
Q = Pt;
QX = PtX;
QY = PtY;
l = j;
}
}
if (l != -1)
{
PdfPoint PlMinusQ = polygon[l].Subtract(Q);
PdfPoint R2 = R1.Add(PlMinusQ);
PdfPoint R3 = R0.Add(PlMinusQ);
PdfPoint Pl = polygon[l];
double PlMinusQX = Pl.X - QX;
double PlMinusQY = Pl.Y - QY;
u = R1.Subtract(R0);
double R2X = R1X + PlMinusQX;
double R2Y = R1Y + PlMinusQY;
double A = u.DotProduct(u) * smax;
double R3X = R0X + PlMinusQX;
double R3Y = R0Y + PlMinusQY;
uX = R1X - R0X;
uY = R1Y - R0Y;
double A = (uX * uX + uY * uY) * smax;
if (A < Amin)
{
Amin = A;
MBR = new[] { R0, R1, R2, R3 };
MBR = new[] { R0X, R0Y, R1X, R1Y, R2X, R2Y, R3X, R3Y };
}
}
@@ -156,7 +178,10 @@
if (k == polygon.Count) break;
}
return new PdfRectangle(MBR[2], MBR[3], MBR[1], MBR[0]);
return new PdfRectangle(new PdfPoint(MBR[4], MBR[5]),
new PdfPoint(MBR[6], MBR[7]),
new PdfPoint(MBR[2], MBR[3]),
new PdfPoint(MBR[0], MBR[1]));
}
/// <summary>

View File

@@ -17,7 +17,7 @@
using Tokenization.Scanner;
using Tokens;
using XObjects;
using static UglyToad.PdfPig.Core.PdfSubpath;
using static PdfPig.Core.PdfSubpath;
internal class ContentStreamProcessor : IOperationContext
{
@@ -49,6 +49,7 @@
private readonly IFilterProvider filterProvider;
private readonly ILog log;
private readonly bool clipPaths;
private readonly PdfVector pageSize;
private readonly MarkedContentStack markedContentStack = new MarkedContentStack();
private Stack<CurrentGraphicsState> graphicsStack = new Stack<CurrentGraphicsState>();
@@ -88,7 +89,8 @@
IPageContentParser pageContentParser,
IFilterProvider filterProvider,
ILog log,
bool clipPaths)
bool clipPaths,
PdfVector pageSize)
{
this.resourceStore = resourceStore;
this.userSpaceUnit = userSpaceUnit;
@@ -98,6 +100,7 @@
this.filterProvider = filterProvider ?? throw new ArgumentNullException(nameof(filterProvider));
this.log = log;
this.clipPaths = clipPaths;
this.pageSize = pageSize;
// initiate CurrentClippingPath to cropBox
var clippingSubpath = new PdfSubpath();
@@ -175,7 +178,7 @@
// TODO: this does not seem correct, produces the correct result for now but we need to revisit.
// see: https://stackoverflow.com/questions/48010235/pdf-specification-get-font-size-in-points
var pointSize = Math.Round(rotation.Rotate(transformationMatrix).Multiply(TextMatrices.TextMatrix).Multiply(fontSize).A, 2);
var pointSize = Math.Round(transformationMatrix.Multiply(TextMatrices.TextMatrix).Multiply(fontSize).A, 2);
if (pointSize < 0)
{
@@ -216,16 +219,18 @@
}
var boundingBox = font.GetBoundingBox(code);
var transformedGlyphBounds = PerformantRectangleTransformer
.Transform(renderingMatrix, textMatrix, transformationMatrix, boundingBox.GlyphBounds);
var transformedGlyphBounds = rotation.Rotate(transformationMatrix)
.Transform(textMatrix
.Transform(renderingMatrix
.Transform(boundingBox.GlyphBounds)));
var transformedPdfBounds = PerformantRectangleTransformer
.Transform(renderingMatrix, textMatrix, transformationMatrix, new PdfRectangle(0, 0, boundingBox.Width, 0));
var transformedPdfBounds = rotation.Rotate(transformationMatrix)
.Transform(textMatrix
.Transform(renderingMatrix
.Transform(new PdfRectangle(0, 0, boundingBox.Width, 0))));
if (rotation.Value > 0)
{
transformedGlyphBounds = rotation.Rotate(transformedGlyphBounds, pageSize);
transformedPdfBounds = rotation.Rotate(transformedPdfBounds, pageSize);
}
// If the text rendering mode calls for filling, the current nonstroking color in the graphics state is used;
// if it calls for stroking, the current stroking color is used.
@@ -540,7 +545,7 @@
return;
}
var currentState = this.GetCurrentState();
var currentState = GetCurrentState();
if (CurrentPath.IsStroked)
{
CurrentPath.LineDashPattern = currentState.LineDashPattern;
@@ -572,6 +577,7 @@
CurrentPath = null;
}
public void ModifyClippingIntersect(FillingRule clippingRule)
{
if (CurrentPath == null)

View File

@@ -0,0 +1,93 @@
namespace UglyToad.PdfPig.Graphics
{
using PdfPig.Core;
internal static class PerformantRectangleTransformer
{
public static PdfRectangle Transform(TransformationMatrix first, TransformationMatrix second, TransformationMatrix third, PdfRectangle rectangle)
{
var tl = rectangle.TopLeft;
var tr = rectangle.TopRight;
var bl = rectangle.BottomLeft;
var br = rectangle.BottomRight;
var topLeftX = tl.X;
var topLeftY = tl.Y;
var topRightX = tr.X;
var topRightY = tr.Y;
var bottomLeftX = bl.X;
var bottomLeftY = bl.Y;
var bottomRightX = br.X;
var bottomRightY = br.Y;
// First
var x = first.A * topLeftX + first.C * topLeftY + first.E;
var y = first.B * topLeftX + first.D * topLeftY + first.F;
topLeftX = x;
topLeftY = y;
x = first.A * topRightX + first.C * topRightY + first.E;
y = first.B * topRightX + first.D * topRightY + first.F;
topRightX = x;
topRightY = y;
x = first.A * bottomLeftX + first.C * bottomLeftY + first.E;
y = first.B * bottomLeftX + first.D * bottomLeftY + first.F;
bottomLeftX = x;
bottomLeftY = y;
x = first.A * bottomRightX + first.C * bottomRightY + first.E;
y = first.B * bottomRightX + first.D * bottomRightY + first.F;
bottomRightX = x;
bottomRightY = y;
// Second
x = second.A * topLeftX + second.C * topLeftY + second.E;
y = second.B * topLeftX + second.D * topLeftY + second.F;
topLeftX = x;
topLeftY = y;
x = second.A * topRightX + second.C * topRightY + second.E;
y = second.B * topRightX + second.D * topRightY + second.F;
topRightX = x;
topRightY = y;
x = second.A * bottomLeftX + second.C * bottomLeftY + second.E;
y = second.B * bottomLeftX + second.D * bottomLeftY + second.F;
bottomLeftX = x;
bottomLeftY = y;
x = second.A * bottomRightX + second.C * bottomRightY + second.E;
y = second.B * bottomRightX + second.D * bottomRightY + second.F;
bottomRightX = x;
bottomRightY = y;
// Third
x = third.A * topLeftX + third.C * topLeftY + third.E;
y = third.B * topLeftX + third.D * topLeftY + third.F;
topLeftX = x;
topLeftY = y;
x = third.A * topRightX + third.C * topRightY + third.E;
y = third.B * topRightX + third.D * topRightY + third.F;
topRightX = x;
topRightY = y;
x = third.A * bottomLeftX + third.C * bottomLeftY + third.E;
y = third.B * bottomLeftX + third.D * bottomLeftY + third.F;
bottomLeftX = x;
bottomLeftY = y;
x = third.A * bottomRightX + third.C * bottomRightY + third.E;
y = third.B * bottomRightX + third.D * bottomRightY + third.F;
bottomRightX = x;
bottomRightY = y;
return new PdfRectangle(new PdfPoint(topLeftX, topLeftY), new PdfPoint(topRightX, topRightY),
new PdfPoint(bottomLeftX, bottomLeftY), new PdfPoint(bottomRightX, bottomRightY));
}
}
}

View File

@@ -47,15 +47,15 @@
log?.Error($"Page {number} had its type specified as {type} rather than 'Page'.");
}
MediaBox mediaBox = GetMediaBox(number, dictionary, pageTreeMembers);
CropBox cropBox = GetCropBox(dictionary, pageTreeMembers, mediaBox);
var rotation = new PageRotationDegrees(pageTreeMembers.Rotation);
if (dictionary.TryGet(NameToken.Rotate, pdfScanner, out NumericToken rotateToken))
{
rotation = new PageRotationDegrees(rotateToken.Int);
}
MediaBox mediaBox = GetMediaBox(number, dictionary, pageTreeMembers);
CropBox cropBox = GetCropBox(dictionary, pageTreeMembers, mediaBox);
var stackDepth = 0;
while (pageTreeMembers.ParentResources.Count > 0)
@@ -71,6 +71,19 @@
resourceStore.LoadResourceDictionary(resources);
stackDepth++;
}
// Apply rotation.
if (rotation.SwapsAxis)
{
mediaBox = new MediaBox(new PdfRectangle(mediaBox.Bounds.Bottom,
mediaBox.Bounds.Left,
mediaBox.Bounds.Top,
mediaBox.Bounds.Right));
cropBox = new CropBox(new PdfRectangle(cropBox.Bounds.Bottom,
cropBox.Bounds.Left,
cropBox.Bounds.Top,
cropBox.Bounds.Right));
}
UserSpaceUnit userSpaceUnit = GetUserSpaceUnits(dictionary);
@@ -108,7 +121,7 @@
}
}
content = GetContent(number, bytes, cropBox, userSpaceUnit, rotation, clipPaths);
content = GetContent(number, bytes, cropBox, userSpaceUnit, rotation, clipPaths, mediaBox);
}
else
{
@@ -121,7 +134,7 @@
var bytes = contentStream.Decode(filterProvider);
content = GetContent(number, bytes, cropBox, userSpaceUnit, rotation, clipPaths);
content = GetContent(number, bytes, cropBox, userSpaceUnit, rotation, clipPaths, mediaBox);
}
var page = new Page(number, dictionary, mediaBox, cropBox, rotation, content,
@@ -137,7 +150,7 @@
}
private PageContent GetContent(int pageNumber, IReadOnlyList<byte> contentBytes, CropBox cropBox, UserSpaceUnit userSpaceUnit,
PageRotationDegrees rotation, bool clipPaths)
PageRotationDegrees rotation, bool clipPaths, MediaBox mediaBox)
{
var operations = pageContentParser.Parse(pageNumber, new ByteArrayInputBytes(contentBytes),
log);
@@ -146,7 +159,8 @@
pageContentParser,
filterProvider,
log,
clipPaths);
clipPaths,
new PdfVector(mediaBox.Bounds.Width, mediaBox.Bounds.Height));
return context.Process(pageNumber, operations);
}

View File

@@ -20,6 +20,10 @@
/// <summary>
/// Compliance with PDF/A2-B. Level B (basic) conformance are standards necessary for the reliable reproduction of a document's visual appearance.
/// </summary>
A2B = 3
A2B = 3,
/// <summary>
/// Compliance with PDF/A2-A. Level A (accessible) conformance are PDF/A2-B standards in addition to features intended to improve a document's accessibility.
/// </summary>
A2A = 4
}
}

View File

@@ -352,6 +352,9 @@ namespace UglyToad.PdfPig.Writer
break;
case PdfAStandard.A2B:
break;
case PdfAStandard.A2A:
PdfA1ARuleBuilder.Obey(catalogDictionary);
break;
}
}

View File

@@ -143,6 +143,10 @@ namespace UglyToad.PdfPig.Writer.Xmp
part = 2;
conformance = "B";
break;
case PdfAStandard.A2A:
part = 2;
conformance = "A";
break;
default:
throw new ArgumentOutOfRangeException(nameof(standard), standard, null);
}