From 17d1d77abc620f1db9075bd6123b3049e35d8789 Mon Sep 17 00:00:00 2001 From: Eliot Jones Date: Thu, 28 Dec 2017 16:58:52 +0000 Subject: [PATCH] add more documents to test font size and add tests to check our text positions against other providers --- .../TextState/SetFontAndSizeTests.cs | 70 +++++++++++++ .../Font Size Test - from libre office.pdf | Bin 0 -> 8536 bytes ...ze Text - from google chrome print pdf.pdf | Bin 0 -> 31531 bytes .../Integration/PdfParserTests.cs | 6 +- .../Integration/SinglePageSimpleTests.cs | 89 ++++++++++++++++ .../UglyToad.Pdf.Tests.csproj | 8 ++ src/UglyToad.Pdf/Content/CropBox.cs | 20 ++++ src/UglyToad.Pdf/Content/Letter.cs | 27 ++++- src/UglyToad.Pdf/Content/Page.cs | 5 +- src/UglyToad.Pdf/Content/PageFactory.cs | 99 +++++++++++++----- src/UglyToad.Pdf/Content/PageTreeMembers.cs | 5 + src/UglyToad.Pdf/Core/TransformationMatrix.cs | 6 ++ src/UglyToad.Pdf/Cos/CosName.cs | 1 + src/UglyToad.Pdf/Geometry/UserSpaceUnit.cs | 36 +++++++ .../Graphics/ContentStreamProcessor.cs | 29 +++-- .../Operations/TextState/SetFontAndSize.cs | 12 ++- 16 files changed, 367 insertions(+), 46 deletions(-) create mode 100644 src/UglyToad.Pdf.Tests/Graphics/Operations/TextState/SetFontAndSizeTests.cs create mode 100644 src/UglyToad.Pdf.Tests/Integration/Documents/Font Size Test - from libre office.pdf create mode 100644 src/UglyToad.Pdf.Tests/Integration/Documents/Font Size Text - from google chrome print pdf.pdf create mode 100644 src/UglyToad.Pdf/Content/CropBox.cs create mode 100644 src/UglyToad.Pdf/Geometry/UserSpaceUnit.cs diff --git a/src/UglyToad.Pdf.Tests/Graphics/Operations/TextState/SetFontAndSizeTests.cs b/src/UglyToad.Pdf.Tests/Graphics/Operations/TextState/SetFontAndSizeTests.cs new file mode 100644 index 00000000..25e8f65a --- /dev/null +++ b/src/UglyToad.Pdf.Tests/Graphics/Operations/TextState/SetFontAndSizeTests.cs @@ -0,0 +1,70 @@ +namespace UglyToad.Pdf.Tests.Graphics.Operations.TextState +{ + using System; + using Pdf.Cos; + using Pdf.Graphics.Operations.TextState; + using Xunit; + + public class SetFontAndSizeTests + { + private static readonly CosName Font1Name = CosName.Create("Font1"); + + [Fact] + public void HasCorrectSymbol() + { + var symbol = SetFontAndSize.Symbol; + + Assert.Equal("Tf", symbol); + } + + [Fact] + public void SetsValues() + { + var setFontAndSize = new SetFontAndSize(Font1Name, 12.75m); + + Assert.Equal("Font1", setFontAndSize.Font.Name); + Assert.Equal(12.75m, setFontAndSize.Size); + } + + [Fact] + public void HasCorrectOperator() + { + var setFontAndSize = new SetFontAndSize(Font1Name, 12); + + Assert.Equal("Tf", setFontAndSize.Operator); + } + + [Fact] + public void NameNullThrows() + { + // ReSharper disable once ObjectCreationAsStatement + Action action = () => new SetFontAndSize(null, 6); + + Assert.Throws(action); + } + + [Fact] + public void StringRepresentationIsCorrect() + { + var setFontAndSize = new SetFontAndSize(Font1Name, 12.76m); + + Assert.Equal("/Font1 12.76 Tf", setFontAndSize.ToString()); + } + + [Fact] + public void RunSetsFontAndFontSize() + { + var setFontAndSize = new SetFontAndSize(Font1Name, 69.42m); + + var context = new TestOperationContext(); + var store = new TestResourceStore(); + + setFontAndSize.Run(context, store); + + var state = context.GetCurrentState(); + + Assert.Equal(69.42m, state.FontState.FontSize); + Assert.Equal(Font1Name, state.FontState.FontName); + } + } +} diff --git a/src/UglyToad.Pdf.Tests/Integration/Documents/Font Size Test - from libre office.pdf b/src/UglyToad.Pdf.Tests/Integration/Documents/Font Size Test - from libre office.pdf new file mode 100644 index 0000000000000000000000000000000000000000..19776c66350a737a6a74d6511a2345e6957d5bf0 GIT binary patch literal 8536 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 Fe*n89C!_!X literal 0 HcmV?d00001 diff --git a/src/UglyToad.Pdf.Tests/Integration/Documents/Font Size Text - from google chrome print pdf.pdf b/src/UglyToad.Pdf.Tests/Integration/Documents/Font Size Text - from google chrome print pdf.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0d2aaf6866736e09ccac3f27c2ae122645d1cdb0 GIT binary patch literal 31531 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 literal 0 HcmV?d00001 diff --git a/src/UglyToad.Pdf.Tests/Integration/PdfParserTests.cs b/src/UglyToad.Pdf.Tests/Integration/PdfParserTests.cs index d8d3c189..32309e5e 100644 --- a/src/UglyToad.Pdf.Tests/Integration/PdfParserTests.cs +++ b/src/UglyToad.Pdf.Tests/Integration/PdfParserTests.cs @@ -162,11 +162,7 @@ { var documentFolder = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Integration", "Documents")); - var files = Directory.GetFiles(documentFolder); - - var file = files[n]; - - return file; + return Path.Combine(documentFolder, "Single Page Simple - from google drive.pdf"); } } } diff --git a/src/UglyToad.Pdf.Tests/Integration/SinglePageSimpleTests.cs b/src/UglyToad.Pdf.Tests/Integration/SinglePageSimpleTests.cs index a2e30c16..70ea2064 100644 --- a/src/UglyToad.Pdf.Tests/Integration/SinglePageSimpleTests.cs +++ b/src/UglyToad.Pdf.Tests/Integration/SinglePageSimpleTests.cs @@ -92,6 +92,95 @@ namespace UglyToad.Pdf.Tests.Integration } } + [Fact] + public void LettersHavePdfBoxPositions() + { + var file = GetFilename(); + + var pdfBoxData = GetPdfBoxPositionData(); + var index = 0; + + using (var document = PdfDocument.Open(File.ReadAllBytes(file))) + { + var page = document.GetPage(1); + + foreach (var letter in page.Letters) + { + // Something a bit weird with how we or PdfBox handle hidden characters and spaces. + if (IgnoredHiddenCharacters.Contains(letter.Value) || string.IsNullOrWhiteSpace(letter.Value)) + { + continue; + } + + var datum = pdfBoxData[index]; + + while (IgnoredHiddenCharacters.Contains(datum.Text)) + { + index++; + datum = pdfBoxData[index]; + } + + Assert.Equal(datum.Text, letter.Value); + Assert.Equal(datum.X, letter.Location.X, 2); + + var transformed = page.Height - letter.Location.Y; + Assert.Equal(datum.Y, transformed, 2); + + Assert.Equal(datum.Width, letter.Width, 2); + + Assert.Equal(datum.FontName, letter.FontName); + + // I think we have font size wrong for now, or right, but differently correct... + + index++; + } + } + } + + [Fact] + public void LettersHaveOtherProviderPositions() + { + var file = GetFilename(); + + var pdfBoxData = GetOtherPositionData1(); + var index = 0; + + using (var document = PdfDocument.Open(File.ReadAllBytes(file))) + { + var page = document.GetPage(1); + + foreach (var letter in page.Letters) + { + // Something a bit weird with how we or this provider handle hidden characters and spaces. + if (IgnoredHiddenCharacters.Contains(letter.Value) || string.IsNullOrWhiteSpace(letter.Value)) + { + continue; + } + + var datum = pdfBoxData[index]; + + while (IgnoredHiddenCharacters.Contains(datum.Text) || datum.Text == " ") + { + index++; + datum = pdfBoxData[index]; + } + + Assert.Equal(datum.Text, letter.Value); + Assert.Equal(datum.X, letter.Location.X, 2); + + var transformed = page.Height - letter.Location.Y; + Assert.Equal(datum.Y, transformed, 2); + + // Until we get width from glyphs we're a bit out. + Assert.True(Math.Abs(datum.Width - letter.Width) < 0.03m); + + index++; + } + } + } + + + private static IReadOnlyList GetPdfBoxPositionData() { // X Y Width Letter FontSize Font diff --git a/src/UglyToad.Pdf.Tests/UglyToad.Pdf.Tests.csproj b/src/UglyToad.Pdf.Tests/UglyToad.Pdf.Tests.csproj index eaea7a91..44d0b0ce 100644 --- a/src/UglyToad.Pdf.Tests/UglyToad.Pdf.Tests.csproj +++ b/src/UglyToad.Pdf.Tests/UglyToad.Pdf.Tests.csproj @@ -10,6 +10,8 @@ + + @@ -20,6 +22,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/src/UglyToad.Pdf/Content/CropBox.cs b/src/UglyToad.Pdf/Content/CropBox.cs new file mode 100644 index 00000000..f9635fd0 --- /dev/null +++ b/src/UglyToad.Pdf/Content/CropBox.cs @@ -0,0 +1,20 @@ +namespace UglyToad.Pdf.Content +{ + using System; + using Geometry; + using Util.JetBrains.Annotations; + + /// + /// Defines the visible region, contents expanding beyond the crop box should be clipped. + /// + public class CropBox + { + [NotNull] + public PdfRectangle Bounds { get; } + + public CropBox(PdfRectangle bounds) + { + Bounds = bounds ?? throw new ArgumentNullException(nameof(bounds)); + } + } +} \ No newline at end of file diff --git a/src/UglyToad.Pdf/Content/Letter.cs b/src/UglyToad.Pdf/Content/Letter.cs index 2fa398b5..c2a2de1a 100644 --- a/src/UglyToad.Pdf/Content/Letter.cs +++ b/src/UglyToad.Pdf/Content/Letter.cs @@ -4,23 +4,46 @@ public class Letter { + /// + /// The text for this letter or unicode character. + /// public string Value { get; } public PdfPoint Location { get; } + /// + /// The width of the letter. + /// public decimal Width { get; } - public decimal FontSize { get; } + /// + /// Size defined by the Tj operator prior to our possibly incorrect transformation. + /// + internal decimal FontSize { get; } + /// + /// The name of the font. + /// public string FontName { get; } - public Letter(string value, PdfPoint location, decimal width, decimal fontSize, string fontName) + /// + /// The size of the font in points. + /// + public decimal PointSize { get; } + + internal Letter(string value, PdfPoint location, decimal width, decimal fontSize, string fontName, decimal pointSize) { Value = value; Location = location; Width = width; FontSize = fontSize; FontName = fontName; + PointSize = pointSize; + } + + public override string ToString() + { + return $"{Location} {Width} {Value} {FontName} {PointSize}"; } } } diff --git a/src/UglyToad.Pdf/Content/Page.cs b/src/UglyToad.Pdf/Content/Page.cs index bc098d89..133f0395 100644 --- a/src/UglyToad.Pdf/Content/Page.cs +++ b/src/UglyToad.Pdf/Content/Page.cs @@ -12,6 +12,8 @@ internal MediaBox MediaBox { get; } + internal CropBox CropBox { get; } + internal PageContent Content { get; } public IReadOnlyList Letters => Content?.Letters ?? new Letter[0]; @@ -26,7 +28,7 @@ /// public decimal Height { get; } - internal Page(int number, MediaBox mediaBox, PageContent content) + internal Page(int number, MediaBox mediaBox, CropBox cropBox, PageContent content) { if (number <= 0) { @@ -35,6 +37,7 @@ Number = number; MediaBox = mediaBox; + CropBox = cropBox; Content = content; Width = mediaBox.Bounds.Width; diff --git a/src/UglyToad.Pdf/Content/PageFactory.cs b/src/UglyToad.Pdf/Content/PageFactory.cs index 84a1dcf6..32e18ef4 100644 --- a/src/UglyToad.Pdf/Content/PageFactory.cs +++ b/src/UglyToad.Pdf/Content/PageFactory.cs @@ -40,6 +40,75 @@ throw new InvalidOperationException($"Page {number} had its type was specified as {type} rather than 'Page'."); } + MediaBox mediaBox = GetMediaBox(number, dictionary, pageTreeMembers, isLenientParsing); + CropBox cropBox = GetCropBox(dictionary, pageTreeMembers, mediaBox); + + if (dictionary.GetItemOrDefault(CosName.RESOURCES) is PdfDictionary resource) + { + resourceStore.LoadResourceDictionary(resource, reader, isLenientParsing); + } + + UserSpaceUnit userSpaceUnit = GetUserSpaceUnits(dictionary); + + PageContent content = default(PageContent); + + var contentObject = dictionary.GetItemOrDefault(CosName.CONTENTS) as CosObject; + if (contentObject != null) + { + var contentStream = pdfObjectParser.Parse(contentObject.ToIndirectReference(), reader, false) as PdfRawStream; + + if (contentStream == null) + { + throw new InvalidOperationException("Failed to parse the content for the page: " + number); + } + + var contents = contentStream.Decode(filterProvider); + + var operations = pageContentParser.Parse(new ByteArrayInputBytes(contents)); + + var context = new ContentStreamProcessor(mediaBox.Bounds, resourceStore, userSpaceUnit); + + content = context.Process(operations); + } + + var page = new Page(number, mediaBox, cropBox, content); + + return page; + } + + private static UserSpaceUnit GetUserSpaceUnits(PdfDictionary dictionary) + { + var spaceUnits = UserSpaceUnit.Default; + if (dictionary.TryGetValue(CosName.USER_UNIT, out var userUnitCosBase) && userUnitCosBase is ICosNumber userUnitNumber) + { + spaceUnits = new UserSpaceUnit(userUnitNumber.AsInt()); + } + + return spaceUnits; + } + + private static CropBox GetCropBox(PdfDictionary dictionary, PageTreeMembers pageTreeMembers, MediaBox mediaBox) + { + CropBox cropBox; + if (dictionary.TryGetItemOfType(CosName.CROP_BOX, out COSArray cropBoxArray)) + { + var x1 = cropBoxArray.getInt(0); + var y1 = cropBoxArray.getInt(1); + var x2 = cropBoxArray.getInt(2); + var y2 = cropBoxArray.getInt(3); + + cropBox = new CropBox(new PdfRectangle(x1, y1, x2, y2)); + } + else + { + cropBox = pageTreeMembers.GetCropBox() ?? new CropBox(mediaBox.Bounds); + } + + return cropBox; + } + + private static MediaBox GetMediaBox(int number, PdfDictionary dictionary, PageTreeMembers pageTreeMembers, bool isLenientParsing) + { MediaBox mediaBox; if (dictionary.TryGetItemOfType(CosName.MEDIA_BOX, out COSArray mediaboxArray)) { @@ -67,35 +136,7 @@ } } - if (dictionary.GetItemOrDefault(CosName.RESOURCES) is PdfDictionary resource) - { - resourceStore.LoadResourceDictionary(resource, reader, isLenientParsing); - } - - PageContent content = default(PageContent); - - var contentObject = dictionary.GetItemOrDefault(CosName.CONTENTS) as CosObject; - if (contentObject != null) - { - var contentStream = pdfObjectParser.Parse(contentObject.ToIndirectReference(), reader, false) as PdfRawStream; - - if (contentStream == null) - { - throw new InvalidOperationException("Failed to parse the content for the page: " + number); - } - - var contents = contentStream.Decode(filterProvider); - - var operations = pageContentParser.Parse(new ByteArrayInputBytes(contents)); - - var context = new ContentStreamProcessor(mediaBox.Bounds, resourceStore); - - content = context.Process(operations); - } - - var page = new Page(number, mediaBox, content); - - return page; + return mediaBox; } } } diff --git a/src/UglyToad.Pdf/Content/PageTreeMembers.cs b/src/UglyToad.Pdf/Content/PageTreeMembers.cs index 5ce5c2f6..c53ace47 100644 --- a/src/UglyToad.Pdf/Content/PageTreeMembers.cs +++ b/src/UglyToad.Pdf/Content/PageTreeMembers.cs @@ -12,5 +12,10 @@ // TODO: tree inheritance throw new NotImplementedException("Track inherited members"); } + + public CropBox GetCropBox() + { + return null; + } } } \ No newline at end of file diff --git a/src/UglyToad.Pdf/Core/TransformationMatrix.cs b/src/UglyToad.Pdf/Core/TransformationMatrix.cs index 3fedd02d..8ff4d3ef 100644 --- a/src/UglyToad.Pdf/Core/TransformationMatrix.cs +++ b/src/UglyToad.Pdf/Core/TransformationMatrix.cs @@ -18,9 +18,15 @@ private readonly decimal[] value; + /// + /// The scale for the X dimension. + /// public decimal A => value[0]; public decimal B => value[1]; public decimal C => value[3]; + /// + /// The scale for the Y dimension. + /// public decimal D => value[4]; public decimal E => value[6]; public decimal F => value[7]; diff --git a/src/UglyToad.Pdf/Cos/CosName.cs b/src/UglyToad.Pdf/Cos/CosName.cs index 7eb00c72..1da1b4bb 100644 --- a/src/UglyToad.Pdf/Cos/CosName.cs +++ b/src/UglyToad.Pdf/Cos/CosName.cs @@ -518,6 +518,7 @@ namespace UglyToad.Pdf.Cos public static readonly CosName UNIX = new CosName("Unix"); public static readonly CosName URI = new CosName("URI"); public static readonly CosName URL = new CosName("URL"); + public static readonly CosName USER_UNIT = new CosName("UserUnit"); // V public static readonly CosName V = new CosName("V"); public static readonly CosName VERISIGN_PPKVS = new CosName("VeriSign.PPKVS"); diff --git a/src/UglyToad.Pdf/Geometry/UserSpaceUnit.cs b/src/UglyToad.Pdf/Geometry/UserSpaceUnit.cs new file mode 100644 index 00000000..de93793f --- /dev/null +++ b/src/UglyToad.Pdf/Geometry/UserSpaceUnit.cs @@ -0,0 +1,36 @@ +namespace UglyToad.Pdf.Geometry +{ + using System; + + /// + /// 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). + /// + public struct UserSpaceUnit + { + public static readonly UserSpaceUnit Default = new UserSpaceUnit(1); + + /// + /// The number of points (1/72nd of an inch) corresponding to a single unit in user space. + /// + public int PointMultiples { get; } + + /// + /// Create a new unit specification for a page. + /// + public UserSpaceUnit(int pointMultiples) + { + if (pointMultiples <= 0) + { + throw new ArgumentOutOfRangeException("Cannot have a zero or negative value of point multiples: " + pointMultiples); + } + + PointMultiples = pointMultiples; + } + + public override string ToString() + { + return PointMultiples.ToString(); + } + } +} diff --git a/src/UglyToad.Pdf/Graphics/ContentStreamProcessor.cs b/src/UglyToad.Pdf/Graphics/ContentStreamProcessor.cs index 15a27cf8..c4406bf3 100644 --- a/src/UglyToad.Pdf/Graphics/ContentStreamProcessor.cs +++ b/src/UglyToad.Pdf/Graphics/ContentStreamProcessor.cs @@ -13,6 +13,7 @@ internal class ContentStreamProcessor : IOperationContext { private readonly IResourceStore resourceStore; + private readonly UserSpaceUnit userSpaceUnit; private Stack graphicsStack = new Stack(); @@ -22,9 +23,10 @@ public List Letters = new List(); - public ContentStreamProcessor(PdfRectangle cropBox, IResourceStore resourceStore) + public ContentStreamProcessor(PdfRectangle cropBox, IResourceStore resourceStore, UserSpaceUnit userSpaceUnit) { this.resourceStore = resourceStore; + this.userSpaceUnit = userSpaceUnit; graphicsStack.Push(new CurrentGraphicsState()); } @@ -77,10 +79,18 @@ { var font = resourceStore.GetFont(GetCurrentState().FontState.FontName); - var fontSize = GetCurrentState().FontState.FontSize; - var horizontalScaling = GetCurrentState().FontState.HorizontalScaling; - var characterSpacing = GetCurrentState().FontState.CharacterSpacing; + var currentState = GetCurrentState(); + var fontSize = currentState.FontState.FontSize; + var horizontalScaling = currentState.FontState.HorizontalScaling; + var characterSpacing = currentState.FontState.CharacterSpacing; + + var transformationMatrix = currentState.CurrentTransformationMatrix; + + // 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 = decimal.Round(fontSize * transformationMatrix.A, 2); + while (bytes.MoveNext()) { var code = font.ReadCharacterCode(bytes, out int codeLength); @@ -102,7 +112,9 @@ var displacement = font.GetDisplacement(code); - ShowGlyph(renderingMatrix, font, code, unicode, displacement, fontSize); + var width = (displacement.X * fontSize) * transformationMatrix.A; + + ShowGlyph(renderingMatrix, font, code, unicode, width, fontSize, pointSize); decimal tx, ty; if (font.IsVertical) @@ -122,11 +134,12 @@ } } - private void ShowGlyph(TransformationMatrix renderingMatrix, IFont font, int characterCode, string unicode, PdfVector displacement, decimal fontSize) + private void ShowGlyph(TransformationMatrix renderingMatrix, IFont font, int characterCode, string unicode, decimal width, decimal fontSize, + decimal pointSize) { var location = new PdfPoint(renderingMatrix.E, renderingMatrix.F); - - var letter = new Letter(unicode, location, displacement.X, fontSize, font.Name.Name); + + var letter = new Letter(unicode, location, width, fontSize, font.Name.Name, pointSize); Letters.Add(letter); } diff --git a/src/UglyToad.Pdf/Graphics/Operations/TextState/SetFontAndSize.cs b/src/UglyToad.Pdf/Graphics/Operations/TextState/SetFontAndSize.cs index 6f1159ba..a309dbea 100644 --- a/src/UglyToad.Pdf/Graphics/Operations/TextState/SetFontAndSize.cs +++ b/src/UglyToad.Pdf/Graphics/Operations/TextState/SetFontAndSize.cs @@ -1,7 +1,9 @@ namespace UglyToad.Pdf.Graphics.Operations.TextState { + using System; using Content; using Cos; + using Util.JetBrains.Annotations; internal class SetFontAndSize : IGraphicsStateOperation { @@ -9,13 +11,21 @@ public string Operator => Symbol; + /// + /// The name of the font as defined in the resource dictionary. + /// + [NotNull] public CosName Font { get; } + /// + /// The font program defines glyphs for a standard size. This standard size is set so that each line of text will occupy 1 unit in user space. + /// The size is the scale factor used to scale glyphs from the standard size to the display size rather than the font size in points. + /// public decimal Size { get; } public SetFontAndSize(CosName font, decimal size) { - Font = font; + Font = font ?? throw new ArgumentNullException(nameof(font)); Size = size; }