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); }