diff --git a/src/UglyToad.PdfPig.Tests/Integration/Documents/SinglePage180ClockwiseRotation - from PdfPig.jpg b/src/UglyToad.PdfPig.Tests/Integration/Documents/SinglePage180ClockwiseRotation - from PdfPig.jpg
new file mode 100644
index 00000000..364debc5
Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Integration/Documents/SinglePage180ClockwiseRotation - from PdfPig.jpg differ
diff --git a/src/UglyToad.PdfPig.Tests/Integration/Documents/SinglePage180ClockwiseRotation - from PdfPig.pdf b/src/UglyToad.PdfPig.Tests/Integration/Documents/SinglePage180ClockwiseRotation - from PdfPig.pdf
new file mode 100644
index 00000000..4ae6f76b
Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Integration/Documents/SinglePage180ClockwiseRotation - from PdfPig.pdf differ
diff --git a/src/UglyToad.PdfPig.Tests/Integration/Documents/SinglePage270ClockwiseRotation - from PdfPig.jpg b/src/UglyToad.PdfPig.Tests/Integration/Documents/SinglePage270ClockwiseRotation - from PdfPig.jpg
new file mode 100644
index 00000000..509d5a67
Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Integration/Documents/SinglePage270ClockwiseRotation - from PdfPig.jpg differ
diff --git a/src/UglyToad.PdfPig.Tests/Integration/Documents/SinglePage270ClockwiseRotation - from PdfPig.pdf b/src/UglyToad.PdfPig.Tests/Integration/Documents/SinglePage270ClockwiseRotation - from PdfPig.pdf
new file mode 100644
index 00000000..621e1730
Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Integration/Documents/SinglePage270ClockwiseRotation - from PdfPig.pdf differ
diff --git a/src/UglyToad.PdfPig.Tests/Integration/Documents/SinglePage90ClockwiseRotation - from PdfPig.jpg b/src/UglyToad.PdfPig.Tests/Integration/Documents/SinglePage90ClockwiseRotation - from PdfPig.jpg
new file mode 100644
index 00000000..ca6f1fa5
Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Integration/Documents/SinglePage90ClockwiseRotation - from PdfPig.jpg differ
diff --git a/src/UglyToad.PdfPig.Tests/Integration/Documents/SinglePage90ClockwiseRotation - from PdfPig.pdf b/src/UglyToad.PdfPig.Tests/Integration/Documents/SinglePage90ClockwiseRotation - from PdfPig.pdf
new file mode 100644
index 00000000..945cd332
Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Integration/Documents/SinglePage90ClockwiseRotation - from PdfPig.pdf differ
diff --git a/src/UglyToad.PdfPig.Tests/Integration/VisualVerification/GenerateLetterBoundingBoxImages.cs b/src/UglyToad.PdfPig.Tests/Integration/VisualVerification/GenerateLetterBoundingBoxImages.cs
index f05a4692..bf5f1e07 100644
--- a/src/UglyToad.PdfPig.Tests/Integration/VisualVerification/GenerateLetterBoundingBoxImages.cs
+++ b/src/UglyToad.PdfPig.Tests/Integration/VisualVerification/GenerateLetterBoundingBoxImages.cs
@@ -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)
diff --git a/src/UglyToad.PdfPig/Content/PageRotationDegrees.cs b/src/UglyToad.PdfPig/Content/PageRotationDegrees.cs
index 6e5b96b9..5a85e2dc 100644
--- a/src/UglyToad.PdfPig/Content/PageRotationDegrees.cs
+++ b/src/UglyToad.PdfPig/Content/PageRotationDegrees.cs
@@ -3,21 +3,23 @@
using System;
using System.Diagnostics.Contracts;
using Core;
+ using Geometry;
///
/// Represents the rotation of a page in a PDF document defined by the page dictionary in degrees clockwise.
///
public struct PageRotationDegrees : IEquatable
{
- 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);
-
///
/// The rotation of the page in degrees clockwise.
///
public int Value { get; }
+ ///
+ /// Whether the rotation flips the x and y axes.
+ ///
+ public bool SwapsAxis => (Value == 90) || (Value == 270);
+
///
/// Get the rotation expressed in radians (anti-clockwise).
///
@@ -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));
}
///
diff --git a/src/UglyToad.PdfPig/Graphics/ContentStreamProcessor.cs b/src/UglyToad.PdfPig/Graphics/ContentStreamProcessor.cs
index fd7d6015..3eaafab0 100644
--- a/src/UglyToad.PdfPig/Graphics/ContentStreamProcessor.cs
+++ b/src/UglyToad.PdfPig/Graphics/ContentStreamProcessor.cs
@@ -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 graphicsStack = new Stack();
@@ -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,14 +219,18 @@
}
var boundingBox = font.GetBoundingBox(code);
-
- var rotatedTransformationMatrix = rotation.Rotate(transformationMatrix);
-
+
var transformedGlyphBounds = PerformantRectangleTransformer
- .Transform(renderingMatrix, textMatrix, rotatedTransformationMatrix, boundingBox.GlyphBounds);
+ .Transform(renderingMatrix, textMatrix, transformationMatrix, boundingBox.GlyphBounds);
var transformedPdfBounds = PerformantRectangleTransformer
- .Transform(renderingMatrix, textMatrix, rotatedTransformationMatrix, new PdfRectangle(0, 0, boundingBox.Width, 0));
+ .Transform(renderingMatrix, textMatrix, transformationMatrix, 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.
diff --git a/src/UglyToad.PdfPig/Parser/PageFactory.cs b/src/UglyToad.PdfPig/Parser/PageFactory.cs
index 49f950aa..e6ad1b15 100644
--- a/src/UglyToad.PdfPig/Parser/PageFactory.cs
+++ b/src/UglyToad.PdfPig/Parser/PageFactory.cs
@@ -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 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);
}