mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-04-05 20:55:01 +08:00
Fix related to page sizes / rotation / coordinate transformations (issue 560):
The initial transformation matrix was incorrect, as it translated by the cropbox width/height instead of by the cropbox left/bottom offsets. Also, it did not translate the results back into the 1st quadrant so that (0,0) would (again) be the lower left corner origin for the cropped area. Added unit tests in new file ContentStreamProcessorTests. EFFECTIVE CHANGES: - The coordinates used for letters etc. are different now for rotated and/or cropped pages, but as those were not very consistent anyway this is probably OK. - The Page Size (A4, A3, Custom, etc.), Width and Height are now determined by the CropBox, not by the MediaBox; the CropBox ultimately determines what you see on screen and is printable. If no cropbox is defined in the PDF, it is set to the MediaBox; so in that case it is backwards compatible with the old code. - The Page MediaBox and CropBox properties are no longer rotated according to Page.Rotation. The Page Width and Height do take rotation into account (kept it backward compatible).
This commit is contained in:
parent
3a0a6e1411
commit
0413f3f1bf
@ -0,0 +1,157 @@
|
||||
namespace UglyToad.PdfPig.Tests.Graphics
|
||||
{
|
||||
using Content;
|
||||
using PdfPig.Core;
|
||||
using PdfPig.Geometry;
|
||||
using PdfPig.Graphics;
|
||||
using Xunit;
|
||||
|
||||
public class ContentStreamProcessorTests
|
||||
{
|
||||
[Fact]
|
||||
public void InitialMatrixHandlesDefaultCase()
|
||||
{
|
||||
// Normally the cropbox = mediabox, with origin 0,0
|
||||
// Take A4 as a sample page size
|
||||
var mediaBox = new PdfRectangle(0, 0, 595, 842);
|
||||
var cropBox = new PdfRectangle(0, 0, 595, 842);
|
||||
|
||||
// Sample glyph at the top-left corner, with size 10x20
|
||||
var glyph = new PdfRectangle(cropBox.Left, cropBox.Top - 20, cropBox.Left + 10, cropBox.Top);
|
||||
|
||||
GetInitialTransformationMatrices(mediaBox, cropBox, new PageRotationDegrees(0), out var m, out var i);
|
||||
var transformedGlyph = m.Transform(glyph);
|
||||
var inverseTransformedGlyph = i.Transform(transformedGlyph);
|
||||
AssertAreEqual(glyph, transformedGlyph);
|
||||
AssertAreEqual(glyph, inverseTransformedGlyph);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitialMatrixHandlesCropBoxOutsideMediaBox()
|
||||
{
|
||||
// Normally the cropbox = mediabox, with origin 0,0
|
||||
// Take A4 as a sample page size
|
||||
var mediaBox = new PdfRectangle(0, 0, 595, 842);
|
||||
var cropBox = new PdfRectangle(400, 400, 1000, 1000);
|
||||
// The "view box" is then x=[400..595] y=[400..842], i.e. size 195x442
|
||||
|
||||
// Sample points
|
||||
var pointInsideViewBox = new PdfPoint(500, 500);
|
||||
var pointBelowViewBox = new PdfPoint(500, 100);
|
||||
var pointLeftOfViewBox = new PdfPoint(200, 500);
|
||||
var pointAboveViewBox = new PdfPoint(500, 1000);
|
||||
var pointRightOfViewBox = new PdfPoint(1000, 500);
|
||||
|
||||
GetInitialTransformationMatrices(mediaBox, cropBox, new PageRotationDegrees(0), out var m, out var i);
|
||||
var pt = m.Transform(pointInsideViewBox);
|
||||
var p0 = i.Transform(pt);
|
||||
AssertAreEqual(pointInsideViewBox, p0);
|
||||
Assert.True(pt.X > 0 && pt.X < 195 && pt.Y > 0 && pt.Y < 442);
|
||||
|
||||
pt = m.Transform(pointBelowViewBox);
|
||||
p0 = i.Transform(pt);
|
||||
AssertAreEqual(pointBelowViewBox, p0);
|
||||
Assert.True(pt.X > 0 && pt.X < 195 && pt.Y < 0);
|
||||
|
||||
pt = m.Transform(pointLeftOfViewBox);
|
||||
p0 = i.Transform(pt);
|
||||
AssertAreEqual(pointLeftOfViewBox, p0);
|
||||
Assert.True(pt.X < 0 && pt.Y > 0 && pt.Y < 442);
|
||||
|
||||
// When we rotate by 180 degrees, points above/right view box
|
||||
// should get a negative coordinate.
|
||||
GetInitialTransformationMatrices(mediaBox, cropBox, new PageRotationDegrees(180), out m, out i);
|
||||
pt = m.Transform(pointInsideViewBox);
|
||||
p0 = i.Transform(pt);
|
||||
AssertAreEqual(pointInsideViewBox, p0);
|
||||
Assert.True(pt.X > 0 && pt.X < 195 && pt.Y > 0 && pt.Y < 442);
|
||||
|
||||
pt = m.Transform(pointAboveViewBox);
|
||||
p0 = i.Transform(pt);
|
||||
AssertAreEqual(pointAboveViewBox, p0);
|
||||
Assert.True(pt.X > 0 && pt.X < 195 && pt.Y < 0);
|
||||
|
||||
pt = m.Transform(pointRightOfViewBox);
|
||||
p0 = i.Transform(pt);
|
||||
AssertAreEqual(pointRightOfViewBox, p0);
|
||||
Assert.True(pt.X < 0 && pt.Y > 0 && pt.Y < 442);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitialMatrixHandlesCropBoxAndRotation()
|
||||
{
|
||||
var mediaBox = new PdfRectangle(0, 0, 595, 842);
|
||||
|
||||
// Cropbox with bottom left at (100,200) with size 300x400
|
||||
var cropBox = new PdfRectangle(100, 200, 400, 600);
|
||||
|
||||
// Sample glyph at the top-left corner, with size 10x20
|
||||
var glyph = new PdfRectangle(cropBox.Left, cropBox.Top - 20, cropBox.Left + 10, cropBox.Top);
|
||||
|
||||
// Test with 0 degrees (no rotation)
|
||||
GetInitialTransformationMatrices(mediaBox, cropBox, new PageRotationDegrees(0), out var initialMatrix, out var inverseMatrix);
|
||||
var transformedGlyph = initialMatrix.Transform(glyph);
|
||||
var inverseTransformedGlyph = inverseMatrix.Transform(transformedGlyph);
|
||||
AssertAreEqual(glyph, inverseTransformedGlyph);
|
||||
Assert.Equal(0, transformedGlyph.BottomLeft.X, 0);
|
||||
Assert.Equal(cropBox.Height - glyph.Height, transformedGlyph.BottomLeft.Y, 0);
|
||||
Assert.Equal(glyph.Width, transformedGlyph.TopRight.X, 0);
|
||||
Assert.Equal(cropBox.Height, transformedGlyph.TopRight.Y, 0);
|
||||
|
||||
|
||||
// Test with 90 degrees
|
||||
GetInitialTransformationMatrices(mediaBox, cropBox, new PageRotationDegrees(90), out initialMatrix, out inverseMatrix);
|
||||
transformedGlyph = initialMatrix.Transform(glyph);
|
||||
inverseTransformedGlyph = inverseMatrix.Transform(transformedGlyph);
|
||||
AssertAreEqual(glyph, inverseTransformedGlyph);
|
||||
Assert.Equal(cropBox.Height - glyph.Height, transformedGlyph.BottomLeft.X, 0);
|
||||
Assert.Equal(cropBox.Width, transformedGlyph.BottomLeft.Y, 0);
|
||||
Assert.Equal(cropBox.Height, transformedGlyph.TopRight.X, 0);
|
||||
Assert.Equal(cropBox.Width - glyph.Width, transformedGlyph.TopRight.Y, 0);
|
||||
|
||||
// Test with 180 degrees
|
||||
GetInitialTransformationMatrices(mediaBox, cropBox, new PageRotationDegrees(180), out initialMatrix, out inverseMatrix);
|
||||
transformedGlyph = initialMatrix.Transform(glyph);
|
||||
inverseTransformedGlyph = inverseMatrix.Transform(transformedGlyph);
|
||||
AssertAreEqual(glyph, inverseTransformedGlyph);
|
||||
Assert.Equal(cropBox.Width, transformedGlyph.BottomLeft.X, 0);
|
||||
Assert.Equal(glyph.Height, transformedGlyph.BottomLeft.Y, 0);
|
||||
Assert.Equal(cropBox.Width - glyph.Width, transformedGlyph.TopRight.X, 0);
|
||||
Assert.Equal(0, transformedGlyph.TopRight.Y, 0);
|
||||
|
||||
// Test with 270 degrees
|
||||
GetInitialTransformationMatrices(mediaBox, cropBox, new PageRotationDegrees(270), out initialMatrix, out inverseMatrix);
|
||||
transformedGlyph = initialMatrix.Transform(glyph);
|
||||
inverseTransformedGlyph = inverseMatrix.Transform(transformedGlyph);
|
||||
AssertAreEqual(glyph, inverseTransformedGlyph);
|
||||
Assert.Equal(glyph.Height, transformedGlyph.BottomLeft.X, 0);
|
||||
Assert.Equal(0, transformedGlyph.BottomLeft.Y, 0);
|
||||
Assert.Equal(0, transformedGlyph.TopRight.X, 0);
|
||||
Assert.Equal(glyph.Width, transformedGlyph.TopRight.Y, 0);
|
||||
|
||||
}
|
||||
|
||||
private static void GetInitialTransformationMatrices(
|
||||
PdfRectangle mediaBox,
|
||||
PdfRectangle cropBox,
|
||||
PageRotationDegrees rotation,
|
||||
out TransformationMatrix initialMatrix,
|
||||
out TransformationMatrix inverseMatrix)
|
||||
{
|
||||
initialMatrix = ContentStreamProcessor.GetInitialMatrix(UserSpaceUnit.Default, mediaBox, cropBox, rotation);
|
||||
inverseMatrix = initialMatrix.Inverse();
|
||||
}
|
||||
|
||||
private static void AssertAreEqual(PdfRectangle r1, PdfRectangle r2)
|
||||
{
|
||||
AssertAreEqual(r1.BottomLeft, r2.BottomLeft);
|
||||
AssertAreEqual(r1.TopRight, r2.TopRight);
|
||||
}
|
||||
|
||||
private static void AssertAreEqual(PdfPoint p1, PdfPoint p2)
|
||||
{
|
||||
Assert.Equal(p1.X, p2.X, 0);
|
||||
Assert.Equal(p1.Y, p2.Y, 0);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Annotations;
|
||||
using Geometry;
|
||||
using Graphics.Operations;
|
||||
using Tokens;
|
||||
using Util;
|
||||
@ -107,10 +108,13 @@
|
||||
Content = content;
|
||||
textLazy = new Lazy<string>(() => GetText(Content));
|
||||
|
||||
Width = mediaBox.Bounds.Width;
|
||||
Height = mediaBox.Bounds.Height;
|
||||
// Special case where cropbox is outside mediabox: use cropbox instead of intersection
|
||||
var viewBox = mediaBox.Bounds.Intersect(cropBox.Bounds) ?? cropBox.Bounds;
|
||||
|
||||
Width = rotation.SwapsAxis ? viewBox.Height : viewBox.Width;
|
||||
Height = rotation.SwapsAxis ? viewBox.Width : viewBox.Height;
|
||||
Size = viewBox.GetPageSize();
|
||||
|
||||
Size = mediaBox.Bounds.GetPageSize();
|
||||
ExperimentalAccess = new Experimental(this, annotationProvider);
|
||||
this.annotationProvider = annotationProvider;
|
||||
this.pdfScanner = pdfScanner ?? throw new ArgumentNullException(nameof(pdfScanner));
|
||||
|
@ -19,6 +19,8 @@
|
||||
using Util;
|
||||
using XObjects;
|
||||
using static PdfPig.Core.PdfSubpath;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
|
||||
internal class ContentStreamProcessor : IOperationContext
|
||||
{
|
||||
@ -48,7 +50,6 @@
|
||||
private readonly IPdfTokenScanner pdfScanner;
|
||||
private readonly IPageContentParser pageContentParser;
|
||||
private readonly ILookupFilterProvider filterProvider;
|
||||
private readonly PdfVector pageSize;
|
||||
private readonly InternalParsingOptions parsingOptions;
|
||||
private readonly MarkedContentStack markedContentStack = new MarkedContentStack();
|
||||
|
||||
@ -84,11 +85,14 @@
|
||||
{XObjectType.PostScript, new List<XObjectContentRecord>()}
|
||||
};
|
||||
|
||||
public ContentStreamProcessor(PdfRectangle cropBox, IResourceStore resourceStore, UserSpaceUnit userSpaceUnit, PageRotationDegrees rotation,
|
||||
public ContentStreamProcessor(IResourceStore resourceStore,
|
||||
UserSpaceUnit userSpaceUnit,
|
||||
PdfRectangle mediaBox,
|
||||
PdfRectangle cropBox,
|
||||
PageRotationDegrees rotation,
|
||||
IPdfTokenScanner pdfScanner,
|
||||
IPageContentParser pageContentParser,
|
||||
ILookupFilterProvider filterProvider,
|
||||
PdfVector pageSize,
|
||||
InternalParsingOptions parsingOptions)
|
||||
{
|
||||
this.resourceStore = resourceStore;
|
||||
@ -97,7 +101,6 @@
|
||||
this.pdfScanner = pdfScanner ?? throw new ArgumentNullException(nameof(pdfScanner));
|
||||
this.pageContentParser = pageContentParser ?? throw new ArgumentNullException(nameof(pageContentParser));
|
||||
this.filterProvider = filterProvider ?? throw new ArgumentNullException(nameof(filterProvider));
|
||||
this.pageSize = pageSize;
|
||||
this.parsingOptions = parsingOptions;
|
||||
|
||||
// initiate CurrentClippingPath to cropBox
|
||||
@ -108,7 +111,7 @@
|
||||
|
||||
graphicsStack.Push(new CurrentGraphicsState()
|
||||
{
|
||||
CurrentTransformationMatrix = GetInitialMatrix(),
|
||||
CurrentTransformationMatrix = GetInitialMatrix(userSpaceUnit, mediaBox, cropBox, rotation),
|
||||
CurrentClippingPath = clippingPath
|
||||
});
|
||||
|
||||
@ -116,63 +119,71 @@
|
||||
}
|
||||
|
||||
[System.Diagnostics.Contracts.Pure]
|
||||
private TransformationMatrix GetInitialMatrix()
|
||||
internal static TransformationMatrix GetInitialMatrix(UserSpaceUnit userSpaceUnit,
|
||||
PdfRectangle mediaBox,
|
||||
PdfRectangle cropBox,
|
||||
PageRotationDegrees rotation)
|
||||
{
|
||||
// TODO: this is a bit of a hack because I don't understand matrices
|
||||
// TODO: use MediaBox (i.e. pageSize) or CropBox?
|
||||
// Cater for scenario where the cropbox is larger than the mediabox.
|
||||
// If there is no intersection (method returns null), fall back to the cropbox.
|
||||
var viewBox = mediaBox.Intersect(cropBox) ?? cropBox;
|
||||
|
||||
/*
|
||||
* 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 ]
|
||||
* Warning: rotation is counter-clockwise here
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
if (rotation.Value == 0
|
||||
&& viewBox.Left == 0
|
||||
&& viewBox.Bottom == 0
|
||||
&& userSpaceUnit.PointMultiples == 1)
|
||||
{
|
||||
return TransformationMatrix.Identity;
|
||||
}
|
||||
|
||||
double cos, sin;
|
||||
double dx = 0, dy = 0;
|
||||
// Move points so that (0,0) is equal to the viewbox bottom left corner.
|
||||
var t1 = TransformationMatrix.GetTranslationMatrix(-viewBox.Left, -viewBox.Bottom);
|
||||
|
||||
// Not implemented yet: userSpaceUnit
|
||||
// if (userSpaceUnit.PointMultiples != 1)
|
||||
// {
|
||||
// var scale = TransformationMatrix.GetScaleMatrix(userSpaceUnit.PointMultiples,
|
||||
// userSpaceUnit.PointMultiples);
|
||||
// ....
|
||||
// }
|
||||
|
||||
// After rotating around the origin, our points will have negative x/y coordinates.
|
||||
// Fix this by translating them by a certain dx/dy after rotation based on the viewbox.
|
||||
double dx, dy;
|
||||
switch (rotation.Value)
|
||||
{
|
||||
case 0:
|
||||
cos = 1;
|
||||
sin = 0;
|
||||
break;
|
||||
// No need to rotate / translate after rotation, just return the initial
|
||||
// translation matrix.
|
||||
return t1;
|
||||
case 90:
|
||||
cos = 0;
|
||||
sin = 1;
|
||||
dy = pageSize.Y;
|
||||
// Move rotated points up by our (unrotated) viewbox width
|
||||
dx = 0;
|
||||
dy = viewBox.Width;
|
||||
break;
|
||||
case 180:
|
||||
cos = -1;
|
||||
sin = 0;
|
||||
dx = pageSize.X;
|
||||
dy = pageSize.Y;
|
||||
// Move rotated points up/right using the (unrotated) viewbox width/height
|
||||
dx = viewBox.Width;
|
||||
dy = viewBox.Height;
|
||||
break;
|
||||
case 270:
|
||||
cos = 0;
|
||||
sin = -1;
|
||||
dx = pageSize.X;
|
||||
// Move rotated points right using the (unrotated) viewbox height
|
||||
dx = viewBox.Height;
|
||||
dy = 0;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Invalid value for page rotation: {rotation.Value}.");
|
||||
}
|
||||
|
||||
return new TransformationMatrix(
|
||||
cos, -sin, 0,
|
||||
sin, cos, 0,
|
||||
dx, dy, 1);
|
||||
// GetRotationMatrix uses counter clockwise angles, whereas our page rotation
|
||||
// is a clockwise angle, so flip the sign.
|
||||
var r = TransformationMatrix.GetRotationMatrix(-rotation.Value);
|
||||
|
||||
// Fix up negative coordinates after rotation
|
||||
var t2 = TransformationMatrix.GetTranslationMatrix(dx, dy);
|
||||
|
||||
// Now get the final combined matrix T1 > R > T2
|
||||
return t1.Multiply(r.Multiply(t2));
|
||||
}
|
||||
|
||||
public PageContent Process(int pageNumberCurrent, IReadOnlyList<IGraphicsStateOperation> operations)
|
||||
|
@ -73,19 +73,6 @@
|
||||
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);
|
||||
|
||||
PageContent content;
|
||||
@ -171,14 +158,14 @@
|
||||
parsingOptions.Logger);
|
||||
|
||||
var context = new ContentStreamProcessor(
|
||||
cropBox.Bounds,
|
||||
resourceStore,
|
||||
userSpaceUnit,
|
||||
mediaBox.Bounds,
|
||||
cropBox.Bounds,
|
||||
rotation,
|
||||
pdfScanner,
|
||||
pageContentParser,
|
||||
filterProvider,
|
||||
new PdfVector(mediaBox.Bounds.Width, mediaBox.Bounds.Height),
|
||||
parsingOptions);
|
||||
|
||||
return context.Process(pageNumber, operations);
|
||||
@ -214,7 +201,7 @@
|
||||
return cropBox;
|
||||
}
|
||||
|
||||
cropBox = new CropBox(cropBoxArray.ToIntRectangle(pdfScanner));
|
||||
cropBox = new CropBox(cropBoxArray.ToRectangle(pdfScanner));
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -243,7 +230,7 @@
|
||||
return mediaBox;
|
||||
}
|
||||
|
||||
mediaBox = new MediaBox(mediaboxArray.ToIntRectangle(pdfScanner));
|
||||
mediaBox = new MediaBox(mediaboxArray.ToRectangle(pdfScanner));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user