mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-10-14 19:05:01 +08:00
Merge pull request #3 from GowenGit/master
letter boundaries - todo: review changes for non latin characters, it seems like we need both the bounding box and the origin to be stored for each letter since the origin is on the baseline while the bounding box can extend below.
This commit is contained in:
25
src/UglyToad.PdfPig.Tests/DecimalComparer.cs
Normal file
25
src/UglyToad.PdfPig.Tests/DecimalComparer.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UglyToad.PdfPig.Tests
|
||||
{
|
||||
internal class DecimalComparer : IEqualityComparer<decimal>
|
||||
{
|
||||
private readonly decimal precision;
|
||||
|
||||
public DecimalComparer(decimal precision)
|
||||
{
|
||||
this.precision = precision;
|
||||
}
|
||||
|
||||
public bool Equals(decimal x, decimal y)
|
||||
{
|
||||
return Math.Abs(x - y) < precision;
|
||||
}
|
||||
|
||||
public int GetHashCode(decimal obj)
|
||||
{
|
||||
return obj.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
BIN
src/UglyToad.PdfPig.Tests/Integration/Documents/Type0 Font.pdf
Normal file
BIN
src/UglyToad.PdfPig.Tests/Integration/Documents/Type0 Font.pdf
Normal file
Binary file not shown.
@@ -70,7 +70,7 @@
|
||||
break;
|
||||
}
|
||||
|
||||
var myX = pageLetter.Location.X;
|
||||
var myX = pageLetter.Rectangle.BottomLeft.X;
|
||||
var theirX = pdfBoxData[index].X;
|
||||
|
||||
var myLetter = pageLetter.Value;
|
||||
@@ -81,10 +81,13 @@
|
||||
continue;
|
||||
}
|
||||
|
||||
Assert.Equal(theirLetter, myLetter);
|
||||
Assert.Equal(theirX, myX, 2);
|
||||
var comparer = new DecimalComparer(3m);
|
||||
|
||||
Assert.Equal(pdfBoxData[index].Width, pageLetter.Width, 1);
|
||||
Assert.Equal(theirLetter, myLetter);
|
||||
|
||||
Assert.Equal(theirX, myX, comparer);
|
||||
|
||||
Assert.Equal(pdfBoxData[index].Width, pageLetter.Rectangle.Width, comparer);
|
||||
|
||||
index++;
|
||||
}
|
||||
@@ -108,7 +111,7 @@
|
||||
break;
|
||||
}
|
||||
|
||||
var myX = pageLetter.Location.X;
|
||||
var myX = pageLetter.Rectangle.Left;
|
||||
var theirX = positions[index].X;
|
||||
|
||||
var myLetter = pageLetter.Value;
|
||||
@@ -122,7 +125,7 @@
|
||||
Assert.Equal(theirLetter, myLetter);
|
||||
Assert.Equal(theirX, myX, 2);
|
||||
|
||||
Assert.Equal(positions[index].Width, pageLetter.Width, 1);
|
||||
Assert.Equal(positions[index].Width, pageLetter.Rectangle.Width, 1);
|
||||
|
||||
index++;
|
||||
}
|
||||
|
@@ -134,12 +134,12 @@ namespace UglyToad.PdfPig.Tests.Integration
|
||||
}
|
||||
|
||||
Assert.Equal(datum.Text, letter.Value);
|
||||
Assert.Equal(datum.X, letter.Location.X, 2);
|
||||
Assert.Equal(datum.X, letter.Rectangle.BottomLeft.X, 2);
|
||||
|
||||
var transformed = page.Height - letter.Location.Y;
|
||||
var transformed = page.Height - letter.Rectangle.BottomLeft.Y;
|
||||
Assert.Equal(datum.Y, transformed, 2);
|
||||
|
||||
Assert.Equal(datum.Width, letter.Width, 2);
|
||||
Assert.Equal(datum.Width, letter.Rectangle.Width, 2);
|
||||
|
||||
Assert.Equal(datum.FontName, letter.FontName);
|
||||
|
||||
@@ -179,13 +179,13 @@ namespace UglyToad.PdfPig.Tests.Integration
|
||||
}
|
||||
|
||||
Assert.Equal(datum.Text, letter.Value);
|
||||
Assert.Equal(datum.X, letter.Location.X, 2);
|
||||
Assert.Equal(datum.X, letter.Rectangle.BottomLeft.X, 2);
|
||||
|
||||
var transformed = page.Height - letter.Location.Y;
|
||||
var transformed = page.Height - letter.Rectangle.BottomLeft.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);
|
||||
Assert.True(Math.Abs(datum.Width - letter.Rectangle.Width) < 0.03m);
|
||||
|
||||
index++;
|
||||
}
|
||||
|
@@ -37,6 +37,41 @@
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasCorrectLetterBoundingBoxes()
|
||||
{
|
||||
using (var document = PdfDocument.Open(GetFilename()))
|
||||
{
|
||||
var page = document.GetPage(1);
|
||||
|
||||
var comparer = new DecimalComparer(3m);
|
||||
|
||||
Assert.Equal("I", page.Letters[0].Value);
|
||||
|
||||
Assert.Equal(90.1m, page.Letters[0].Rectangle.BottomLeft.X, comparer);
|
||||
Assert.Equal(709.2m, page.Letters[0].Rectangle.BottomLeft.Y, comparer);
|
||||
|
||||
Assert.Equal(94.0m, page.Letters[0].Rectangle.TopRight.X, comparer);
|
||||
Assert.Equal(719.89m, page.Letters[0].Rectangle.TopRight.Y, comparer);
|
||||
|
||||
Assert.Equal("a", page.Letters[5].Value);
|
||||
|
||||
Assert.Equal(114.5m, page.Letters[5].Rectangle.BottomLeft.X, comparer);
|
||||
Assert.Equal(709.2m, page.Letters[5].Rectangle.BottomLeft.Y, comparer);
|
||||
|
||||
Assert.Equal(119.82m, page.Letters[5].Rectangle.TopRight.X, comparer);
|
||||
Assert.Equal(714.89m, page.Letters[5].Rectangle.TopRight.Y, comparer);
|
||||
|
||||
Assert.Equal("f", page.Letters[16].Value);
|
||||
|
||||
Assert.Equal(169.9m, page.Letters[16].Rectangle.BottomLeft.X, comparer);
|
||||
Assert.Equal(709.2m, page.Letters[16].Rectangle.BottomLeft.Y, comparer);
|
||||
|
||||
Assert.Equal(176.89m, page.Letters[16].Rectangle.TopRight.X, comparer);
|
||||
Assert.Equal(719.89m, page.Letters[16].Rectangle.TopRight.Y, comparer);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetsCorrectPageTextIgnoringHiddenCharacters()
|
||||
{
|
||||
|
65
src/UglyToad.PdfPig.Tests/Integration/Type0FontTests.cs
Normal file
65
src/UglyToad.PdfPig.Tests/Integration/Type0FontTests.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
namespace UglyToad.PdfPig.Tests.Integration
|
||||
{
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Content;
|
||||
using Xunit;
|
||||
|
||||
public class Type0FontTests
|
||||
{
|
||||
private static string GetFilename()
|
||||
{
|
||||
var documentFolder = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "Integration", "Documents"));
|
||||
|
||||
return Path.Combine(documentFolder, "Type0 Font.pdf");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasCorrectNumberOfPages()
|
||||
{
|
||||
var file = GetFilename();
|
||||
|
||||
using (var document = PdfDocument.Open(File.ReadAllBytes(file)))
|
||||
{
|
||||
Assert.Equal(1, document.NumberOfPages);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasCorrectPageSize()
|
||||
{
|
||||
using (var document = PdfDocument.Open(GetFilename()))
|
||||
{
|
||||
var page = document.GetPage(1);
|
||||
|
||||
Assert.Equal(PageSize.Letter, page.Size);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetsCorrectPageTextIgnoringHiddenCharacters()
|
||||
{
|
||||
using (var document = PdfDocument.Open(GetFilename()))
|
||||
{
|
||||
var page = document.GetPage(1);
|
||||
|
||||
var text = string.Join(string.Empty, page.Letters.Select(x => x.Value));
|
||||
|
||||
Assert.True(text?.Contains("Powder River Examiner"));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasLetterWidthsAndHeights()
|
||||
{
|
||||
using (var document = PdfDocument.Open(GetFilename()))
|
||||
{
|
||||
var page = document.GetPage(1);
|
||||
|
||||
Assert.True((bool) page.Letters.Any(x => x.Rectangle.Width != 0));
|
||||
Assert.True((bool) page.Letters.Any(x => x.Rectangle.Height != 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -29,6 +29,7 @@
|
||||
"UglyToad.PdfPig.ParsingOptions",
|
||||
"UglyToad.PdfPig.Logging.ILog",
|
||||
"UglyToad.PdfPig.Geometry.PdfPoint",
|
||||
"UglyToad.PdfPig.Geometry.PdfRectangle",
|
||||
"UglyToad.PdfPig.Fonts.Exceptions.InvalidFontFormatException",
|
||||
"UglyToad.PdfPig.Exceptions.PdfDocumentFormatException",
|
||||
"UglyToad.PdfPig.Content.Letter",
|
||||
|
@@ -8,79 +8,23 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Fonts\TrueType\google-simple-doc.ttf" />
|
||||
<None Remove="Fonts\TrueType\Roboto-Regular.ttf" />
|
||||
<None Remove="Fonts\Type1\AdobeUtopia.pfa" />
|
||||
<None Remove="Fonts\Type1\Raleway-Black.pfb" />
|
||||
<None Remove="Integration\Documents\FarmerMac.pdf" />
|
||||
<None Remove="Integration\Documents\Font Size Test - from google chrome print pdf.pdf" />
|
||||
<None Remove="Integration\Documents\Font Size Test - from libre office.pdf" />
|
||||
<None Remove="Integration\Documents\ICML03-081.pdf" />
|
||||
<None Remove="Integration\Documents\Judgement Document.pdf" />
|
||||
<None Remove="Integration\Documents\Multiple Page - from Mortality Statistics.pdf" />
|
||||
<None Remove="Integration\Documents\Pig Production Handbook.pdf" />
|
||||
<None Remove="Integration\Documents\Pig Reproduction Powerpoint.pdf" />
|
||||
<None Remove="Integration\Documents\Single Page Form Content - from itext 1_1.pdf" />
|
||||
<None Remove="Integration\Documents\Single Page Non Latin - from acrobat distiller.pdf" />
|
||||
<None Remove="Integration\Documents\Single Page Simple - from google drive.pdf" />
|
||||
<None Remove="Integration\Documents\Single Page Simple - from open office.pdf" />
|
||||
<None Remove="Integration\Documents\Single Page Type 1 Font.pdf" />
|
||||
<None Remove="Integration\Documents\Two Page Text Only - from libre office.pdf" />
|
||||
<None Remove="Fonts\TrueType\*.ttf" />
|
||||
<None Remove="Fonts\Type1\*.pfa" />
|
||||
<None Remove="Fonts\Type1\*.pfb" />
|
||||
<None Remove="Integration\Documents\*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Fonts\TrueType\google-simple-doc.ttf">
|
||||
<EmbeddedResource Include="Fonts\TrueType\*.ttf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Fonts\TrueType\Roboto-Regular.ttf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<EmbeddedResource Include="Fonts\Type1\*.pfa">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Fonts\Type1\AdobeUtopia.pfa">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<EmbeddedResource Include="Fonts\Type1\*.pfb">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Fonts\Type1\Raleway-Black.pfb">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</EmbeddedResource>
|
||||
<Content Include="Integration\Documents\FarmerMac.pdf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Integration\Documents\Font Size Test - from libre office.pdf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Integration\Documents\Font Size Test - from google chrome print pdf.pdf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Integration\Documents\ICML03-081.pdf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Integration\Documents\Judgement Document.pdf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Integration\Documents\Multiple Page - from Mortality Statistics.pdf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Integration\Documents\Pig Production Handbook.pdf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Integration\Documents\Pig Reproduction Powerpoint.pdf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Integration\Documents\Single Page Non Latin - from acrobat distiller.pdf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Integration\Documents\Single Page Simple - from google drive.pdf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Integration\Documents\Single Page Form Content - from itext 1_1.pdf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Integration\Documents\Single Page Simple - from open office.pdf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Integration\Documents\Single Page Type 1 Font.pdf">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Integration\Documents\Two Page Text Only - from libre office.pdf">
|
||||
<Content Include="Integration\Documents\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
@@ -96,10 +40,4 @@
|
||||
<ProjectReference Include="..\UglyToad.PdfPig\UglyToad.PdfPig.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Integration\Documents\2006_Swedish_Touring_Car_Championship.pdf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@@ -13,14 +13,9 @@
|
||||
public string Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The lower-left position of the letter. Letters with descenders will extend below this point.
|
||||
/// Position of the bounding box.
|
||||
/// </summary>
|
||||
public PdfPoint Location { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The width of the letter.
|
||||
/// </summary>
|
||||
public decimal Width { get; }
|
||||
public PdfRectangle Rectangle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Size as defined in the PDF file. This is not equivalent to font size in points but is relative to other font sizes on the page.
|
||||
@@ -40,11 +35,10 @@
|
||||
/// <summary>
|
||||
/// Create a new letter to represent some text drawn by the Tj operator.
|
||||
/// </summary>
|
||||
internal Letter(string value, PdfPoint location, decimal width, decimal fontSize, string fontName, decimal pointSize)
|
||||
internal Letter(string value, PdfRectangle rectangle, decimal fontSize, string fontName, decimal pointSize)
|
||||
{
|
||||
Value = value;
|
||||
Location = location;
|
||||
Width = width;
|
||||
Rectangle = rectangle;
|
||||
FontSize = fontSize;
|
||||
FontName = fontName;
|
||||
PointSize = pointSize;
|
||||
@@ -55,7 +49,7 @@
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Location} {Width} {Value} {FontName} {PointSize}";
|
||||
return $"{Rectangle} {Value} {FontName} {PointSize}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -102,6 +102,17 @@
|
||||
return new PdfVector(x, y);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public PdfRectangle Transform(PdfRectangle original)
|
||||
{
|
||||
return new PdfRectangle(
|
||||
Transform(original.TopLeft.ToVector()),
|
||||
Transform(original.TopRight.ToVector()),
|
||||
Transform(original.BottomLeft.ToVector()),
|
||||
Transform(original.BottomRight.ToVector())
|
||||
);
|
||||
}
|
||||
|
||||
public static TransformationMatrix FromValues(decimal a, decimal b, decimal c, decimal d, decimal e, decimal f)
|
||||
=> FromArray(new[] {a, b, c, d, e, f});
|
||||
public static TransformationMatrix FromArray(decimal[] values)
|
||||
@@ -144,6 +155,26 @@
|
||||
return new TransformationMatrix(result);
|
||||
}
|
||||
|
||||
public TransformationMatrix Multiply(decimal scalar)
|
||||
{
|
||||
var result = new decimal[9];
|
||||
|
||||
for (int i = 0; i < Rows; i++)
|
||||
{
|
||||
for (int j = 0; j < Columns; j++)
|
||||
{
|
||||
var index = (i * Rows) + j;
|
||||
|
||||
for (int x = 0; x < Rows; x++)
|
||||
{
|
||||
result[index] += this[i, x] * scalar;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new TransformationMatrix(result);
|
||||
}
|
||||
|
||||
public decimal GetScalingFactorX()
|
||||
{
|
||||
var xScale = A;
|
||||
|
@@ -9,6 +9,9 @@
|
||||
internal interface ICidFontProgram
|
||||
{
|
||||
bool TryGetBoundingBox(int characterCode, out PdfRectangle boundingBox);
|
||||
|
||||
bool TryGetBoundingBox(int characterCode, Func<int, int> characterIdentifierToGlyphIndex, out PdfRectangle boundingBox);
|
||||
|
||||
bool TryGetBoundingAdvancedWidth(int characterCode, out decimal width);
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,8 @@
|
||||
|
||||
public bool IsVertical => CMap.WritingMode == WritingMode.Vertical;
|
||||
|
||||
private readonly TransformationMatrix fontMatrix = TransformationMatrix.FromValues(0.001m, 0, 0, 0.001m, 0, 0);
|
||||
|
||||
public Type0Font(NameToken baseFont, ICidFont cidFont, CMap cmap, CMap toUnicodeCMap)
|
||||
{
|
||||
BaseFont = baseFont ?? throw new ArgumentNullException(nameof(baseFont));
|
||||
@@ -69,12 +71,9 @@
|
||||
return ToUnicode.TryGet(characterCode, out value);
|
||||
}
|
||||
|
||||
public PdfVector GetDisplacement(int characterCode)
|
||||
public PdfRectangle GetBoundingBox(int characterCode)
|
||||
{
|
||||
// This width is in units scaled up by 1000
|
||||
var width = GetWidth(characterCode);
|
||||
|
||||
return new PdfVector(width / 1000, 0);
|
||||
return fontMatrix.Transform(GetBoundingBoxInGlyphSpace(characterCode));
|
||||
}
|
||||
|
||||
public decimal GetWidth(int characterCode)
|
||||
@@ -88,7 +87,7 @@
|
||||
return fromFont;
|
||||
}
|
||||
|
||||
public PdfRectangle GetBoundingBox(int characterCode)
|
||||
public PdfRectangle GetBoundingBoxInGlyphSpace(int characterCode)
|
||||
{
|
||||
var cid = CMap.ConvertToCid(characterCode);
|
||||
|
||||
|
@@ -15,10 +15,6 @@
|
||||
|
||||
bool TryGetUnicode(int characterCode, out string value);
|
||||
|
||||
PdfVector GetDisplacement(int characterCode);
|
||||
|
||||
decimal GetWidth(int characterCode);
|
||||
|
||||
PdfRectangle GetBoundingBox(int characterCode);
|
||||
|
||||
TransformationMatrix GetFontMatrix();
|
||||
|
@@ -44,8 +44,6 @@
|
||||
{
|
||||
var firstCharacter = FontDictionaryAccessHelper.GetFirstCharacter(dictionary);
|
||||
|
||||
var lastCharacter = FontDictionaryAccessHelper.GetLastCharacter(dictionary);
|
||||
|
||||
var widths = FontDictionaryAccessHelper.GetWidths(pdfScanner, dictionary, isLenientParsing);
|
||||
|
||||
var descriptor = FontDictionaryAccessHelper.GetFontDescriptor(pdfScanner, fontDescriptorFactory, dictionary, isLenientParsing);
|
||||
@@ -70,7 +68,7 @@
|
||||
|
||||
Encoding encoding = encodingReader.Read(dictionary, isLenientParsing, descriptor);
|
||||
|
||||
return new TrueTypeSimpleFont(name, firstCharacter, lastCharacter, widths, descriptor, toUnicodeCMap, encoding, font);
|
||||
return new TrueTypeSimpleFont(name, descriptor, toUnicodeCMap, encoding, font, firstCharacter, widths);
|
||||
}
|
||||
|
||||
private TrueTypeFont ParseTrueTypeFont(FontDescriptor descriptor)
|
||||
|
@@ -1,4 +1,6 @@
|
||||
namespace UglyToad.PdfPig.Fonts.Simple
|
||||
using System;
|
||||
|
||||
namespace UglyToad.PdfPig.Fonts.Simple
|
||||
{
|
||||
using Cmap;
|
||||
using Composite;
|
||||
@@ -12,17 +14,18 @@
|
||||
|
||||
internal class TrueTypeSimpleFont : IFont
|
||||
{
|
||||
private static readonly TransformationMatrix FontMatrix =
|
||||
TransformationMatrix.FromValues(1 / 1000m, 0, 0, 1 / 1000m, 0, 0);
|
||||
private readonly int firstCharacterCode;
|
||||
private readonly int lastCharacterCode;
|
||||
private readonly decimal[] widths;
|
||||
private readonly FontDescriptor descriptor;
|
||||
|
||||
[CanBeNull]
|
||||
private readonly Encoding encoding;
|
||||
|
||||
[CanBeNull]
|
||||
private readonly TrueTypeFont font;
|
||||
|
||||
private readonly int firstCharacter;
|
||||
|
||||
private readonly decimal[] widths;
|
||||
|
||||
public NameToken Name { get; }
|
||||
|
||||
public bool IsVertical { get; }
|
||||
@@ -30,18 +33,19 @@
|
||||
[NotNull]
|
||||
public ToUnicodeCMap ToUnicode { get; set; }
|
||||
|
||||
public TrueTypeSimpleFont(NameToken name, int firstCharacterCode, int lastCharacterCode, decimal[] widths,
|
||||
public TrueTypeSimpleFont(NameToken name,
|
||||
FontDescriptor descriptor,
|
||||
[CanBeNull] CMap toUnicodeCMap,
|
||||
[CanBeNull] Encoding encoding,
|
||||
[CanBeNull]TrueTypeFont font)
|
||||
[CanBeNull] TrueTypeFont font,
|
||||
int firstCharacter,
|
||||
decimal[] widths)
|
||||
{
|
||||
this.firstCharacterCode = firstCharacterCode;
|
||||
this.lastCharacterCode = lastCharacterCode;
|
||||
this.widths = widths;
|
||||
this.descriptor = descriptor;
|
||||
this.encoding = encoding;
|
||||
this.font = font;
|
||||
this.firstCharacter = firstCharacter;
|
||||
this.widths = widths;
|
||||
|
||||
Name = name;
|
||||
IsVertical = false;
|
||||
@@ -84,28 +88,12 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
public PdfVector GetDisplacement(int characterCode)
|
||||
{
|
||||
var tx = GetWidth(characterCode);
|
||||
|
||||
var box = GetBoundingBox(characterCode);
|
||||
|
||||
return new PdfVector(tx / 1000m, 0);
|
||||
}
|
||||
|
||||
public decimal GetWidth(int characterCode)
|
||||
{
|
||||
var index = characterCode - firstCharacterCode;
|
||||
|
||||
if (index < 0 || index >= widths.Length)
|
||||
{
|
||||
return descriptor.MissingWidth;
|
||||
}
|
||||
|
||||
return widths[index];
|
||||
}
|
||||
|
||||
public PdfRectangle GetBoundingBox(int characterCode)
|
||||
{
|
||||
return GetFontMatrix().Transform(GetBoundingBoxInGlyphSpace(characterCode));
|
||||
}
|
||||
|
||||
private PdfRectangle GetBoundingBoxInGlyphSpace(int characterCode)
|
||||
{
|
||||
if (font == null)
|
||||
{
|
||||
@@ -117,13 +105,36 @@
|
||||
return bounds;
|
||||
}
|
||||
|
||||
return descriptor.BoundingBox;
|
||||
if (font.TryGetBoundingAdvancedWidth(characterCode, out var width))
|
||||
{
|
||||
return new PdfRectangle(0, 0, width, 0);
|
||||
}
|
||||
|
||||
return new PdfRectangle(0, 0, GetWidth(characterCode), 0);
|
||||
}
|
||||
|
||||
private decimal GetWidth(int characterCode)
|
||||
{
|
||||
var index = characterCode - firstCharacter;
|
||||
|
||||
if (index < 0 || index >= widths.Length)
|
||||
{
|
||||
return descriptor.MissingWidth;
|
||||
}
|
||||
|
||||
return widths[index];
|
||||
}
|
||||
|
||||
public TransformationMatrix GetFontMatrix()
|
||||
{
|
||||
// TODO: should this also use units per em?
|
||||
return FontMatrix;
|
||||
var scale = 1000m;
|
||||
|
||||
if (font?.HeaderTable != null)
|
||||
{
|
||||
scale = font.HeaderTable.UnitsPerEm;
|
||||
}
|
||||
|
||||
return TransformationMatrix.FromValues(1m / scale, 0, 0, 1m / scale, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -14,11 +14,17 @@
|
||||
internal class Type1FontSimple : IFont
|
||||
{
|
||||
private readonly int firstChar;
|
||||
|
||||
private readonly int lastChar;
|
||||
|
||||
private readonly decimal[] widths;
|
||||
|
||||
private readonly FontDescriptor fontDescriptor;
|
||||
|
||||
private readonly Encoding encoding;
|
||||
|
||||
private readonly ToUnicodeCMap toUnicodeCMap;
|
||||
|
||||
private readonly TransformationMatrix fontMatrix = TransformationMatrix.FromValues(0.001m, 0, 0, 0.001m, 0, 0);
|
||||
|
||||
public NameToken Name { get; }
|
||||
@@ -80,24 +86,19 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
public PdfVector GetDisplacement(int characterCode)
|
||||
public PdfRectangle GetBoundingBox(int characterCode)
|
||||
{
|
||||
return fontMatrix.Transform(new PdfVector(GetWidth(characterCode), 0));
|
||||
return fontMatrix.Transform(GetBoundingBoxInGlyphSpace(characterCode));
|
||||
}
|
||||
|
||||
public decimal GetWidth(int characterCode)
|
||||
private PdfRectangle GetBoundingBoxInGlyphSpace(int characterCode)
|
||||
{
|
||||
if (characterCode < firstChar || characterCode > lastChar)
|
||||
{
|
||||
return 250;
|
||||
return new PdfRectangle(0, 0, 250, 0);
|
||||
}
|
||||
|
||||
return widths[characterCode - firstChar];
|
||||
}
|
||||
|
||||
public PdfRectangle GetBoundingBox(int characterCode)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
return new PdfRectangle(0, 0, widths[characterCode - firstChar], 0);
|
||||
}
|
||||
|
||||
public TransformationMatrix GetFontMatrix()
|
||||
|
@@ -9,14 +9,14 @@
|
||||
|
||||
internal class Type1Standard14Font: IFont
|
||||
{
|
||||
private static readonly TransformationMatrix FontMatrix = TransformationMatrix.FromValues(0.001m, 0, 0, 0.001m, 0, 0);
|
||||
|
||||
private readonly FontMetrics standardFontMetrics;
|
||||
private readonly Encoding encoding;
|
||||
|
||||
public NameToken Name { get; }
|
||||
public bool IsVertical { get; }
|
||||
|
||||
private readonly TransformationMatrix fontMatrix = TransformationMatrix.FromValues(0.001m, 0, 0, 0.001m, 0, 0);
|
||||
|
||||
public Type1Standard14Font(FontMetrics standardFontMetrics)
|
||||
{
|
||||
this.standardFontMetrics = standardFontMetrics ?? throw new ArgumentNullException(nameof(standardFontMetrics));
|
||||
@@ -44,31 +44,26 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
public PdfVector GetDisplacement(int characterCode)
|
||||
public PdfRectangle GetBoundingBox(int characterCode)
|
||||
{
|
||||
return FontMatrix.Transform(new PdfVector(GetWidth(characterCode), 0));
|
||||
return fontMatrix.Transform(GetBoundingBoxInGlyphSpace(characterCode));
|
||||
}
|
||||
|
||||
public decimal GetWidth(int characterCode)
|
||||
private PdfRectangle GetBoundingBoxInGlyphSpace(int characterCode)
|
||||
{
|
||||
var name = encoding.GetName(characterCode);
|
||||
|
||||
if (!standardFontMetrics.CharacterMetrics.TryGetValue(name, out var metrics))
|
||||
{
|
||||
return 250;
|
||||
return new PdfRectangle(0, 0, 250, 0);
|
||||
}
|
||||
|
||||
return metrics.WidthX;
|
||||
}
|
||||
|
||||
public PdfRectangle GetBoundingBox(int characterCode)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
return new PdfRectangle(0, 0, metrics.WidthX, 0);
|
||||
}
|
||||
|
||||
public TransformationMatrix GetFontMatrix()
|
||||
{
|
||||
return FontMatrix;
|
||||
return fontMatrix;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -63,24 +63,19 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
public PdfVector GetDisplacement(int characterCode)
|
||||
public PdfRectangle GetBoundingBox(int characterCode)
|
||||
{
|
||||
return fontMatrix.Transform(new PdfVector(GetWidth(characterCode), 0));
|
||||
return fontMatrix.Transform(GetBoundingBoxInGlyphSpace(characterCode));
|
||||
}
|
||||
|
||||
public decimal GetWidth(int characterCode)
|
||||
private PdfRectangle GetBoundingBoxInGlyphSpace(int characterCode)
|
||||
{
|
||||
if (characterCode < firstChar || characterCode > lastChar)
|
||||
{
|
||||
throw new InvalidFontFormatException($"The character code was not contained in the widths array: {characterCode}.");
|
||||
}
|
||||
|
||||
return widths[characterCode - firstChar];
|
||||
}
|
||||
|
||||
public PdfRectangle GetBoundingBox(int characterCode)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
return new PdfRectangle(0, 0, widths[characterCode - firstChar], 0); ;
|
||||
}
|
||||
|
||||
public TransformationMatrix GetFontMatrix()
|
||||
|
@@ -7,21 +7,25 @@
|
||||
private readonly int[] advancedWidths;
|
||||
private readonly short[] leftSideBearings;
|
||||
|
||||
private readonly int metricCount;
|
||||
|
||||
public string Tag => TrueTypeHeaderTable.Hmtx;
|
||||
|
||||
public TrueTypeHeaderTable DirectoryTable { get; }
|
||||
|
||||
public HorizontalMetricsTable(TrueTypeHeaderTable directoryTable, int[] advancedWidths, short[] leftSideBearings)
|
||||
public HorizontalMetricsTable(TrueTypeHeaderTable directoryTable, int[] advancedWidths, short[] leftSideBearings, int metricCount)
|
||||
{
|
||||
this.advancedWidths = advancedWidths;
|
||||
this.leftSideBearings = leftSideBearings;
|
||||
this.metricCount = metricCount;
|
||||
|
||||
DirectoryTable = directoryTable;
|
||||
}
|
||||
|
||||
public static HorizontalMetricsTable Load(TrueTypeDataBytes data, TrueTypeHeaderTable table, TableRegister tableRegister)
|
||||
{
|
||||
var metricCount = tableRegister.HorizontalHeaderTable.NumberOfHeaderMetrics;
|
||||
var glyphCount = tableRegister.MaximumProfileTable.NumberOfGlyphs;
|
||||
var metricCount = tableRegister.HorizontalHeaderTable.NumberOfHeaderMetrics;
|
||||
|
||||
data.Seek(table.Offset);
|
||||
|
||||
@@ -44,7 +48,19 @@
|
||||
leftSideBearings[metricCount + i] = data.ReadSignedShort();
|
||||
}
|
||||
|
||||
return new HorizontalMetricsTable(table, advancedWidths, leftSideBearings);
|
||||
return new HorizontalMetricsTable(table, advancedWidths, leftSideBearings, metricCount);
|
||||
}
|
||||
|
||||
public int GetAdvanceWidth(int index)
|
||||
{
|
||||
if (index < metricCount)
|
||||
{
|
||||
return advancedWidths[index];
|
||||
}
|
||||
|
||||
// monospaced fonts may not have a width for every glyph
|
||||
// the last one is for subsequent glyphs
|
||||
return advancedWidths[advancedWidths.Length - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@
|
||||
public HeaderTable HeaderTable { get; }
|
||||
public CMapTable CMapTable { get; }
|
||||
public GlyphDataTable GlyphTable { get; }
|
||||
public TableRegister TableRegister { get; }
|
||||
|
||||
public TrueTypeFont(decimal version, IReadOnlyDictionary<string, TrueTypeHeaderTable> tableHeaders, TableRegister tableRegister)
|
||||
{
|
||||
@@ -26,6 +27,7 @@
|
||||
|
||||
Version = version;
|
||||
TableHeaders = tableHeaders;
|
||||
TableRegister = tableRegister;
|
||||
HeaderTable = tableRegister.HeaderTable;
|
||||
CMapTable = tableRegister.CMapTable;
|
||||
GlyphTable = tableRegister.GlyphDataTable;
|
||||
@@ -63,5 +65,24 @@
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetBoundingAdvancedWidth(int characterCode, out decimal width)
|
||||
{
|
||||
width = 0m;
|
||||
|
||||
if (CMapTable == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CMapTable.TryGetGlyphIndex(characterCode, out var index))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
width = TableRegister.HorizontalMetricsTable.GetAdvanceWidth(index);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@@ -52,6 +52,11 @@
|
||||
Y = (decimal)y;
|
||||
}
|
||||
|
||||
internal PdfVector ToVector()
|
||||
{
|
||||
return new PdfVector(X, Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a string representation of this point.
|
||||
/// </summary>
|
||||
|
@@ -2,30 +2,74 @@
|
||||
{
|
||||
using System;
|
||||
|
||||
internal struct PdfRectangle
|
||||
/// <summary>
|
||||
/// A rectangle in a PDF file.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// PDF coordinates are defined with the origin at the lower left (0, 0).
|
||||
/// The Y-axis extends vertically upwards and the X-axis horizontally to the right.
|
||||
/// Unless otherwise specified on a per-page basis, units in PDF space are equivalent to a typographic point (1/72 inch).
|
||||
/// </remarks>
|
||||
public struct PdfRectangle
|
||||
{
|
||||
/// <summary>
|
||||
/// Top left point of the rectangle.
|
||||
/// </summary>
|
||||
public PdfPoint TopLeft { get; }
|
||||
|
||||
public PdfPoint BottomRight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Top right point of the rectangle.
|
||||
/// </summary>
|
||||
public PdfPoint TopRight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Bottom right point of the rectangle.
|
||||
/// </summary>
|
||||
public PdfPoint BottomRight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Bottom left point of the rectangle.
|
||||
/// </summary>
|
||||
public PdfPoint BottomLeft { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Width of the rectangle.
|
||||
/// </summary>
|
||||
public decimal Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Height of the rectangle.
|
||||
/// </summary>
|
||||
public decimal Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Area of the rectangle.
|
||||
/// </summary>
|
||||
public decimal Area { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Left.
|
||||
/// </summary>
|
||||
public decimal Left => TopLeft.X;
|
||||
|
||||
/// <summary>
|
||||
/// Top.
|
||||
/// </summary>
|
||||
public decimal Top => TopLeft.Y;
|
||||
|
||||
/// <summary>
|
||||
/// Right.
|
||||
/// </summary>
|
||||
public decimal Right => BottomRight.X;
|
||||
|
||||
/// <summary>
|
||||
/// Bottom.
|
||||
/// </summary>
|
||||
public decimal Bottom => BottomRight.Y;
|
||||
|
||||
public PdfRectangle(PdfPoint point1, PdfPoint point2) : this(point1.X, point1.Y, point2.X, point2.Y) { }
|
||||
public PdfRectangle(short x1, short y1, short x2, short y2) : this((decimal) x1, y1, x2, y2) { }
|
||||
public PdfRectangle(decimal x1, decimal y1, decimal x2, decimal y2)
|
||||
internal PdfRectangle(PdfPoint point1, PdfPoint point2) : this(point1.X, point1.Y, point2.X, point2.Y) { }
|
||||
internal PdfRectangle(short x1, short y1, short x2, short y2) : this((decimal) x1, y1, x2, y2) { }
|
||||
internal PdfRectangle(decimal x1, decimal y1, decimal x2, decimal y2)
|
||||
{
|
||||
var bottom = Math.Min(y1, y2);
|
||||
var top = Math.Max(y1, y2);
|
||||
@@ -41,12 +85,31 @@
|
||||
|
||||
Width = right - left;
|
||||
Height = top - bottom;
|
||||
|
||||
Area = Width * Height;
|
||||
}
|
||||
|
||||
internal PdfRectangle(PdfVector topLeft, PdfVector topRight, PdfVector bottomLeft, PdfVector bottomRight)
|
||||
{
|
||||
TopLeft = topLeft.ToPoint();
|
||||
TopRight = topRight.ToPoint();
|
||||
|
||||
BottomLeft = bottomLeft.ToPoint();
|
||||
BottomRight = bottomRight.ToPoint();
|
||||
|
||||
Width = bottomRight.Subtract(bottomLeft).GetMagnitude();
|
||||
Height = topLeft.Subtract(bottomLeft).GetMagnitude();
|
||||
|
||||
Area = Width * Height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To string override.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{TopLeft}, {BottomRight}]";
|
||||
return $"[{TopLeft}, {Width}, {Height}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,6 @@
|
||||
namespace UglyToad.PdfPig.Geometry
|
||||
using System;
|
||||
|
||||
namespace UglyToad.PdfPig.Geometry
|
||||
{
|
||||
internal struct PdfVector
|
||||
{
|
||||
@@ -17,6 +19,24 @@
|
||||
return new PdfVector(X * scale, Y * scale);
|
||||
}
|
||||
|
||||
public decimal GetMagnitude()
|
||||
{
|
||||
var doubleX = (double)X;
|
||||
var doubleY = (double)Y;
|
||||
|
||||
return (decimal)Math.Sqrt(doubleX * doubleX + doubleY * doubleY);
|
||||
}
|
||||
|
||||
public PdfVector Subtract(PdfVector vector)
|
||||
{
|
||||
return new PdfVector(X - vector.X, Y - vector.Y);
|
||||
}
|
||||
|
||||
public PdfPoint ToPoint()
|
||||
{
|
||||
return new PdfPoint(X, Y);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"({X}, {Y})";
|
||||
|
@@ -93,10 +93,12 @@
|
||||
var fontSize = currentState.FontState.FontSize;
|
||||
var horizontalScaling = currentState.FontState.HorizontalScaling / 100m;
|
||||
var characterSpacing = currentState.FontState.CharacterSpacing;
|
||||
var rise = currentState.FontState.Rise;
|
||||
|
||||
var transformationMatrix = currentState.CurrentTransformationMatrix;
|
||||
|
||||
var fontMatrix = font.GetFontMatrix();
|
||||
var renderingMatrix =
|
||||
TransformationMatrix.FromValues(fontSize * horizontalScaling, 0, 0, fontSize, 0, rise);
|
||||
|
||||
// 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
|
||||
@@ -119,28 +121,29 @@
|
||||
wordSpacing += GetCurrentState().FontState.WordSpacing;
|
||||
}
|
||||
|
||||
var renderingMatrix = TextMatrices.GetRenderingMatrix(GetCurrentState());
|
||||
|
||||
if (font.IsVertical)
|
||||
{
|
||||
throw new NotImplementedException("Vertical fonts are# currently unsupported, please submit a pull request or issue with an example file.");
|
||||
throw new NotImplementedException("Vertical fonts are currently unsupported, please submit a pull request or issue with an example file.");
|
||||
}
|
||||
|
||||
var displacement = font.GetDisplacement(code);
|
||||
|
||||
var width = displacement.X * fontSize * TextMatrices.TextMatrix.GetScalingFactorX() * transformationMatrix.A;
|
||||
var boundingBox = font.GetBoundingBox(code);
|
||||
|
||||
ShowGlyph(renderingMatrix, font, unicode, width, fontSize, pointSize);
|
||||
var transformedDisplacement = transformationMatrix
|
||||
.Transform(TextMatrices.TextMatrix
|
||||
.Transform(renderingMatrix
|
||||
.Transform(boundingBox)));
|
||||
|
||||
ShowGlyph(font, transformedDisplacement, unicode, fontSize, pointSize);
|
||||
|
||||
decimal tx, ty;
|
||||
if (font.IsVertical)
|
||||
{
|
||||
tx = 0;
|
||||
ty = displacement.Y * fontSize + characterSpacing + wordSpacing;
|
||||
ty = boundingBox.Height * fontSize + characterSpacing + wordSpacing;
|
||||
}
|
||||
else
|
||||
{
|
||||
tx = (displacement.X * fontSize + characterSpacing + wordSpacing) * horizontalScaling;
|
||||
tx = (boundingBox.Width * fontSize + characterSpacing + wordSpacing) * horizontalScaling;
|
||||
ty = 0;
|
||||
}
|
||||
|
||||
@@ -208,12 +211,9 @@
|
||||
TextMatrices.TextMatrix = newMatrix;
|
||||
}
|
||||
|
||||
private void ShowGlyph(TransformationMatrix renderingMatrix, IFont font, string unicode, decimal width, decimal fontSize,
|
||||
decimal pointSize)
|
||||
private void ShowGlyph(IFont font, PdfRectangle rectangle, string unicode, decimal fontSize, decimal pointSize)
|
||||
{
|
||||
var location = new PdfPoint(renderingMatrix.E, renderingMatrix.F);
|
||||
|
||||
var letter = new Letter(unicode, location, width, fontSize, font.Name.Data, pointSize);
|
||||
var letter = new Letter(unicode, rectangle, fontSize, font.Name.Data, pointSize);
|
||||
|
||||
Letters.Add(letter);
|
||||
}
|
||||
|
@@ -10,39 +10,5 @@
|
||||
public TransformationMatrix TextMatrix { get; set; }
|
||||
|
||||
public TransformationMatrix TextLineMatrix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Text Rendering Matrix (Trm) which maps from text space to device space.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The rendering matrix is temporary and is calculated for each glyph in a text showing operation.</para>
|
||||
/// <para>
|
||||
/// The rendering matrix is calculated as follows:<br/>
|
||||
/// | (Tfs * Th) 0 0 |<br/>
|
||||
/// | 0 Tfs 0 | * Tm * CTM<br/>
|
||||
/// | 0 Trise 1 |<br/>
|
||||
/// Where Tfs is the font size, Th is the horizontal scaling, Trise is the text rise, Tm is the current <see cref="TextMatrix"/> and CTM is the
|
||||
/// <see cref="CurrentGraphicsState.CurrentTransformationMatrix"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public TransformationMatrix GetRenderingMatrix(CurrentGraphicsState currentGraphicsState)
|
||||
{
|
||||
var fontSize = currentGraphicsState.FontState.FontSize;
|
||||
var horizontalScaling = currentGraphicsState.FontState.HorizontalScaling;
|
||||
var rise = currentGraphicsState.FontState.Rise;
|
||||
|
||||
var initialMatrix = TransformationMatrix.FromArray(new[]
|
||||
{
|
||||
(fontSize * horizontalScaling), 0, 0,
|
||||
0, fontSize, 0,
|
||||
0, rise, 1
|
||||
});
|
||||
|
||||
var multipledByTextMatrix = initialMatrix.Multiply(TextMatrix);
|
||||
|
||||
var result = multipledByTextMatrix.Multiply(currentGraphicsState.CurrentTransformationMatrix);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,26 +16,30 @@
|
||||
/// </summary>
|
||||
public class PdfDocument : IDisposable
|
||||
{
|
||||
private bool isDisposed = false;
|
||||
private bool isDisposed;
|
||||
|
||||
private readonly bool isLenientParsing;
|
||||
|
||||
[NotNull]
|
||||
private readonly HeaderVersion version;
|
||||
|
||||
[NotNull]
|
||||
private readonly CrossReferenceTable crossReferenceTable;
|
||||
|
||||
private readonly ILog log;
|
||||
|
||||
private readonly IInputBytes inputBytes;
|
||||
private readonly bool isLenientParsing;
|
||||
|
||||
[NotNull]
|
||||
private readonly ParsingCachingProviders cachingProviders;
|
||||
|
||||
private readonly IPdfTokenScanner pdfScanner;
|
||||
|
||||
[NotNull]
|
||||
internal Catalog Catalog { get; }
|
||||
[NotNull]
|
||||
private readonly Catalog catalog;
|
||||
|
||||
[NotNull]
|
||||
internal Pages Pages { get; }
|
||||
private readonly Pages pages;
|
||||
|
||||
/// <summary>
|
||||
/// The metadata associated with this document.
|
||||
@@ -51,7 +55,7 @@
|
||||
/// <summary>
|
||||
/// Get the number of pages in this document.
|
||||
/// </summary>
|
||||
public int NumberOfPages => Pages.Count;
|
||||
public int NumberOfPages => pages.Count;
|
||||
|
||||
internal PdfDocument(ILog log,
|
||||
IInputBytes inputBytes,
|
||||
@@ -71,8 +75,8 @@
|
||||
this.cachingProviders = cachingProviders ?? throw new ArgumentNullException(nameof(cachingProviders));
|
||||
this.pdfScanner = pdfScanner;
|
||||
Information = information ?? throw new ArgumentNullException(nameof(information));
|
||||
Catalog = catalog ?? throw new ArgumentNullException(nameof(catalog));
|
||||
Pages = new Pages(log, Catalog, pageFactory, isLenientParsing, pdfScanner);
|
||||
catalog = catalog ?? throw new ArgumentNullException(nameof(catalog));
|
||||
pages = new Pages(log, catalog, pageFactory, isLenientParsing, pdfScanner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -82,6 +86,7 @@
|
||||
/// <param name="options">Optional parameters controlling parsing.</param>
|
||||
/// <returns>A <see cref="PdfDocument"/> providing access to the file contents.</returns>
|
||||
public static PdfDocument Open(byte[] fileBytes, ParsingOptions options = null) => PdfDocumentFactory.Open(fileBytes, options);
|
||||
|
||||
/// <summary>
|
||||
/// Opens a file and creates a <see cref="PdfDocument"/> for reading from the provided file path.
|
||||
/// </summary>
|
||||
@@ -89,6 +94,7 @@
|
||||
/// <param name="options">Optional parameters controlling parsing.</param>
|
||||
/// <returns>A <see cref="PdfDocument"/> providing access to the file contents.</returns>
|
||||
public static PdfDocument Open(string filePath, ParsingOptions options = null) => PdfDocumentFactory.Open(filePath, options);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="PdfDocument"/> for reading from the provided stream.
|
||||
/// The caller must manage disposing the stream. The created PdfDocument will not dispose the stream.
|
||||
@@ -115,7 +121,7 @@
|
||||
|
||||
log.Debug($"Accessing page {pageNumber}.");
|
||||
|
||||
return Pages.GetPage(pageNumber);
|
||||
return pages.GetPage(pageNumber);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@@ -12,7 +12,7 @@
|
||||
<PackageTags>PDF;Reader;Document;Adobe;PDFBox;PdfPig</PackageTags>
|
||||
<RepositoryUrl>https://github.com/UglyToad/PdfPig</RepositoryUrl>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<Version>0.0.1</Version>
|
||||
<Version>0.0.5</Version>
|
||||
<AssemblyVersion>0.0.1.3</AssemblyVersion>
|
||||
<FileVersion>0.0.1.3</FileVersion>
|
||||
<PackageIconUrl>https://raw.githubusercontent.com/UglyToad/PdfPig/master/documentation/pdfpig.png</PackageIconUrl>
|
||||
@@ -20,115 +20,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\AdobeFontMetrics\Courier-Bold.afm" />
|
||||
<None Remove="Resources\AdobeFontMetrics\Courier-BoldOblique.afm" />
|
||||
<None Remove="Resources\AdobeFontMetrics\Courier-Oblique.afm" />
|
||||
<None Remove="Resources\AdobeFontMetrics\Courier.afm" />
|
||||
<None Remove="Resources\AdobeFontMetrics\Helvetica-Bold.afm" />
|
||||
<None Remove="Resources\AdobeFontMetrics\Helvetica-BoldOblique.afm" />
|
||||
<None Remove="Resources\AdobeFontMetrics\Helvetica-Oblique.afm" />
|
||||
<None Remove="Resources\AdobeFontMetrics\Helvetica.afm" />
|
||||
<None Remove="Resources\AdobeFontMetrics\MustRead.html" />
|
||||
<None Remove="Resources\AdobeFontMetrics\Symbol.afm" />
|
||||
<None Remove="Resources\AdobeFontMetrics\Times-Bold.afm" />
|
||||
<None Remove="Resources\AdobeFontMetrics\Times-BoldItalic.afm" />
|
||||
<None Remove="Resources\AdobeFontMetrics\Times-Italic.afm" />
|
||||
<None Remove="Resources\AdobeFontMetrics\Times-Roman.afm" />
|
||||
<None Remove="Resources\AdobeFontMetrics\ZapfDingbats.afm" />
|
||||
<None Remove="Resources\CMap\83pv-RKSJ-H" />
|
||||
<None Remove="Resources\CMap\90ms-RKSJ-H" />
|
||||
<None Remove="Resources\CMap\90ms-RKSJ-V" />
|
||||
<None Remove="Resources\CMap\90msp-RKSJ-H" />
|
||||
<None Remove="Resources\CMap\90msp-RKSJ-V" />
|
||||
<None Remove="Resources\CMap\90pv-RKSJ-H" />
|
||||
<None Remove="Resources\CMap\90pv-RKSJ-V" />
|
||||
<None Remove="Resources\CMap\Add-RKSJ-H" />
|
||||
<None Remove="Resources\CMap\Add-RKSJ-V" />
|
||||
<None Remove="Resources\CMap\Adobe-CNS1-0" />
|
||||
<None Remove="Resources\CMap\Adobe-CNS1-1" />
|
||||
<None Remove="Resources\CMap\Adobe-CNS1-2" />
|
||||
<None Remove="Resources\CMap\Adobe-CNS1-3" />
|
||||
<None Remove="Resources\CMap\Adobe-CNS1-4" />
|
||||
<None Remove="Resources\CMap\Adobe-CNS1-5" />
|
||||
<None Remove="Resources\CMap\Adobe-CNS1-6" />
|
||||
<None Remove="Resources\CMap\Adobe-CNS1-UCS2" />
|
||||
<None Remove="Resources\CMap\Adobe-GB1-0" />
|
||||
<None Remove="Resources\CMap\Adobe-GB1-1" />
|
||||
<None Remove="Resources\CMap\Adobe-GB1-2" />
|
||||
<None Remove="Resources\CMap\Adobe-GB1-3" />
|
||||
<None Remove="Resources\CMap\Adobe-GB1-4" />
|
||||
<None Remove="Resources\CMap\Adobe-GB1-5" />
|
||||
<None Remove="Resources\CMap\Adobe-GB1-UCS2" />
|
||||
<None Remove="Resources\CMap\Adobe-Japan1-0" />
|
||||
<None Remove="Resources\CMap\Adobe-Japan1-1" />
|
||||
<None Remove="Resources\CMap\Adobe-Japan1-2" />
|
||||
<None Remove="Resources\CMap\Adobe-Japan1-3" />
|
||||
<None Remove="Resources\CMap\Adobe-Japan1-4" />
|
||||
<None Remove="Resources\CMap\Adobe-Japan1-5" />
|
||||
<None Remove="Resources\CMap\Adobe-Japan1-6" />
|
||||
<None Remove="Resources\CMap\Adobe-Japan1-UCS2" />
|
||||
<None Remove="Resources\CMap\Adobe-Japan2-0" />
|
||||
<None Remove="Resources\CMap\Adobe-Korea1-0" />
|
||||
<None Remove="Resources\CMap\Adobe-Korea1-1" />
|
||||
<None Remove="Resources\CMap\Adobe-Korea1-2" />
|
||||
<None Remove="Resources\CMap\Adobe-Korea1-UCS2" />
|
||||
<None Remove="Resources\CMap\B5pc-H" />
|
||||
<None Remove="Resources\CMap\B5pc-V" />
|
||||
<None Remove="Resources\CMap\CNS-EUC-H" />
|
||||
<None Remove="Resources\CMap\CNS-EUC-V" />
|
||||
<None Remove="Resources\CMap\ETen-B5-H" />
|
||||
<None Remove="Resources\CMap\ETen-B5-V" />
|
||||
<None Remove="Resources\CMap\ETenms-B5-H" />
|
||||
<None Remove="Resources\CMap\ETenms-B5-V" />
|
||||
<None Remove="Resources\CMap\EUC-H" />
|
||||
<None Remove="Resources\CMap\EUC-V" />
|
||||
<None Remove="Resources\CMap\Ext-RKSJ-H" />
|
||||
<None Remove="Resources\CMap\Ext-RKSJ-V" />
|
||||
<None Remove="Resources\CMap\GB-EUC-H" />
|
||||
<None Remove="Resources\CMap\GB-EUC-V" />
|
||||
<None Remove="Resources\CMap\GBK-EUC-H" />
|
||||
<None Remove="Resources\CMap\GBK-EUC-V" />
|
||||
<None Remove="Resources\CMap\GBK2K-H" />
|
||||
<None Remove="Resources\CMap\GBK2K-V" />
|
||||
<None Remove="Resources\CMap\GBKp-EUC-H" />
|
||||
<None Remove="Resources\CMap\GBKp-EUC-V" />
|
||||
<None Remove="Resources\CMap\GBpc-EUC-H" />
|
||||
<None Remove="Resources\CMap\GBpc-EUC-V" />
|
||||
<None Remove="Resources\CMap\H" />
|
||||
<None Remove="Resources\CMap\HKscs-B5-H" />
|
||||
<None Remove="Resources\CMap\HKscs-B5-V" />
|
||||
<None Remove="Resources\CMap\Identity-H" />
|
||||
<None Remove="Resources\CMap\Identity-V" />
|
||||
<None Remove="Resources\CMap\KSC-EUC-H" />
|
||||
<None Remove="Resources\CMap\KSC-EUC-V" />
|
||||
<None Remove="Resources\CMap\KSCms-UHC-H" />
|
||||
<None Remove="Resources\CMap\KSCms-UHC-HW-H" />
|
||||
<None Remove="Resources\CMap\KSCms-UHC-HW-V" />
|
||||
<None Remove="Resources\CMap\KSCms-UHC-V" />
|
||||
<None Remove="Resources\CMap\KSCpc-EUC-H" />
|
||||
<None Remove="Resources\CMap\KSCpc-EUC-V" />
|
||||
<None Remove="Resources\CMap\UniCNS-UCS2-H" />
|
||||
<None Remove="Resources\CMap\UniCNS-UCS2-V" />
|
||||
<None Remove="Resources\CMap\UniCNS-UTF16-H" />
|
||||
<None Remove="Resources\CMap\UniCNS-UTF16-V" />
|
||||
<None Remove="Resources\CMap\UniGB-UCS2-H" />
|
||||
<None Remove="Resources\CMap\UniGB-UCS2-V" />
|
||||
<None Remove="Resources\CMap\UniGB-UTF16-H" />
|
||||
<None Remove="Resources\CMap\UniGB-UTF16-V" />
|
||||
<None Remove="Resources\CMap\UniJIS-UCS2-H" />
|
||||
<None Remove="Resources\CMap\UniJIS-UCS2-HW-H" />
|
||||
<None Remove="Resources\CMap\UniJIS-UCS2-HW-V" />
|
||||
<None Remove="Resources\CMap\UniJIS-UCS2-V" />
|
||||
<None Remove="Resources\CMap\UniJIS-UTF16-H" />
|
||||
<None Remove="Resources\CMap\UniJIS-UTF16-V" />
|
||||
<None Remove="Resources\CMap\UniKS-UCS2-H" />
|
||||
<None Remove="Resources\CMap\UniKS-UCS2-V" />
|
||||
<None Remove="Resources\CMap\UniKS-UTF16-H" />
|
||||
<None Remove="Resources\CMap\UniKS-UTF16-V" />
|
||||
<None Remove="Resources\CMap\V" />
|
||||
<None Remove="Resources\GlyphList\additional" />
|
||||
<None Remove="Resources\GlyphList\glyphlist" />
|
||||
<None Remove="Resources\GlyphList\zapfdingbats" />
|
||||
<None Remove="Resources\AdobeFontMetrics\*" />
|
||||
<None Remove="Resources\CMap\*" />
|
||||
<None Remove="Resources\GlyphList\*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -138,114 +32,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\AdobeFontMetrics\Courier-Bold.afm" />
|
||||
<EmbeddedResource Include="Resources\AdobeFontMetrics\Courier-BoldOblique.afm" />
|
||||
<EmbeddedResource Include="Resources\AdobeFontMetrics\Courier-Oblique.afm" />
|
||||
<EmbeddedResource Include="Resources\AdobeFontMetrics\Courier.afm" />
|
||||
<EmbeddedResource Include="Resources\AdobeFontMetrics\Helvetica-Bold.afm" />
|
||||
<EmbeddedResource Include="Resources\AdobeFontMetrics\Helvetica-BoldOblique.afm" />
|
||||
<EmbeddedResource Include="Resources\AdobeFontMetrics\Helvetica-Oblique.afm" />
|
||||
<EmbeddedResource Include="Resources\AdobeFontMetrics\Helvetica.afm" />
|
||||
<EmbeddedResource Include="Resources\AdobeFontMetrics\Symbol.afm" />
|
||||
<EmbeddedResource Include="Resources\AdobeFontMetrics\Times-Bold.afm" />
|
||||
<EmbeddedResource Include="Resources\AdobeFontMetrics\Times-BoldItalic.afm" />
|
||||
<EmbeddedResource Include="Resources\AdobeFontMetrics\Times-Italic.afm" />
|
||||
<EmbeddedResource Include="Resources\AdobeFontMetrics\Times-Roman.afm" />
|
||||
<EmbeddedResource Include="Resources\AdobeFontMetrics\ZapfDingbats.afm" />
|
||||
<EmbeddedResource Include="Resources\CMap\83pv-RKSJ-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\90ms-RKSJ-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\90ms-RKSJ-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\90msp-RKSJ-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\90msp-RKSJ-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\90pv-RKSJ-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\90pv-RKSJ-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\Add-RKSJ-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\Add-RKSJ-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-CNS1-0" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-CNS1-1" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-CNS1-2" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-CNS1-3" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-CNS1-4" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-CNS1-5" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-CNS1-6" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-CNS1-UCS2" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-GB1-0" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-GB1-1" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-GB1-2" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-GB1-3" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-GB1-4" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-GB1-5" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-GB1-UCS2" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-Japan1-0" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-Japan1-1" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-Japan1-2" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-Japan1-3" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-Japan1-4" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-Japan1-5" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-Japan1-6" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-Japan1-UCS2" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-Japan2-0" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-Korea1-0" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-Korea1-1" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-Korea1-2" />
|
||||
<EmbeddedResource Include="Resources\CMap\Adobe-Korea1-UCS2" />
|
||||
<EmbeddedResource Include="Resources\CMap\B5pc-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\B5pc-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\CNS-EUC-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\CNS-EUC-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\ETen-B5-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\ETen-B5-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\ETenms-B5-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\ETenms-B5-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\EUC-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\EUC-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\Ext-RKSJ-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\Ext-RKSJ-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\GB-EUC-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\GB-EUC-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\GBK-EUC-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\GBK-EUC-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\GBK2K-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\GBK2K-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\GBKp-EUC-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\GBKp-EUC-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\GBpc-EUC-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\GBpc-EUC-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\H" />
|
||||
<EmbeddedResource Include="Resources\CMap\HKscs-B5-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\HKscs-B5-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\Identity-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\Identity-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\KSC-EUC-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\KSC-EUC-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\KSCms-UHC-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\KSCms-UHC-HW-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\KSCms-UHC-HW-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\KSCms-UHC-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\KSCpc-EUC-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\KSCpc-EUC-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\UniCNS-UCS2-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\UniCNS-UCS2-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\UniCNS-UTF16-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\UniCNS-UTF16-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\UniGB-UCS2-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\UniGB-UCS2-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\UniGB-UTF16-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\UniGB-UTF16-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\UniJIS-UCS2-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\UniJIS-UCS2-HW-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\UniJIS-UCS2-HW-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\UniJIS-UCS2-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\UniJIS-UTF16-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\UniJIS-UTF16-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\UniKS-UCS2-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\UniKS-UCS2-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\UniKS-UTF16-H" />
|
||||
<EmbeddedResource Include="Resources\CMap\UniKS-UTF16-V" />
|
||||
<EmbeddedResource Include="Resources\CMap\V" />
|
||||
<EmbeddedResource Include="Resources\GlyphList\additional" />
|
||||
<EmbeddedResource Include="Resources\GlyphList\glyphlist" />
|
||||
<EmbeddedResource Include="Resources\GlyphList\zapfdingbats" />
|
||||
<EmbeddedResource Include="Resources\AdobeFontMetrics\*" />
|
||||
<EmbeddedResource Include="Resources\CMap\*" />
|
||||
<EmbeddedResource Include="Resources\GlyphList\*" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
Reference in New Issue
Block a user