mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-11-24 16:53:20 +08:00
Compare commits
6 Commits
0.1.1-alph
...
0.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ed1600cab | ||
|
|
0f91017613 | ||
|
|
5094f9d9d0 | ||
|
|
98bcc16e11 | ||
|
|
7212b9e38c | ||
|
|
19462d79f0 |
@@ -123,7 +123,7 @@
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a character is whitespace or not.
|
||||
/// Determines if a character is whitespace or not, this includes newlines.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These values are specified in table 1 (page 12) of ISO 32000-1:2008.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>0.1.1-alpha001</Version>
|
||||
<Version>0.1.1</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>0.1.1-alpha001</Version>
|
||||
<Version>0.1.1</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>0.1.1-alpha001</Version>
|
||||
<Version>0.1.1</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
@@ -36,7 +36,7 @@ startxref
|
||||
216
|
||||
%%EOF";
|
||||
|
||||
private static readonly long[] TestDataOffsets =
|
||||
private static readonly long[] TestDataOffsets =
|
||||
{
|
||||
TestData.IndexOf("2 17 obj", StringComparison.OrdinalIgnoreCase),
|
||||
TestData.IndexOf("3 0 obj", StringComparison.OrdinalIgnoreCase),
|
||||
@@ -57,7 +57,7 @@ startxref
|
||||
public void SearcherFindsCorrectObjects()
|
||||
{
|
||||
var input = new ByteArrayInputBytes(OtherEncodings.StringAsLatin1Bytes(TestData));
|
||||
|
||||
|
||||
var locations = BruteForceSearcher.GetObjectLocations(input);
|
||||
|
||||
Assert.Equal(4, locations.Count);
|
||||
@@ -69,11 +69,11 @@ startxref
|
||||
public void ReaderOnlyCallsOnce()
|
||||
{
|
||||
var reader = StringBytesTestConverter.Convert(TestData, false);
|
||||
|
||||
|
||||
var locations = BruteForceSearcher.GetObjectLocations(reader.Bytes);
|
||||
|
||||
Assert.Equal(4, locations.Count);
|
||||
|
||||
|
||||
var newLocations = BruteForceSearcher.GetObjectLocations(reader.Bytes);
|
||||
|
||||
Assert.Equal(4, locations.Count);
|
||||
@@ -132,7 +132,7 @@ endobj
|
||||
5 0 obj
|
||||
<< /IsEmpty false >>
|
||||
endobj";
|
||||
|
||||
|
||||
var bytes = new ByteArrayInputBytes(OtherEncodings.StringAsLatin1Bytes(s));
|
||||
|
||||
var locations = BruteForceSearcher.GetObjectLocations(bytes);
|
||||
@@ -168,12 +168,35 @@ endobj";
|
||||
Assert.Equal(581, locations[new IndirectReference(7, 0)]);
|
||||
Assert.Equal(5068, locations[new IndirectReference(8, 0)]);
|
||||
Assert.Equal(5091, locations[new IndirectReference(9, 0)]);
|
||||
|
||||
|
||||
var s = GetStringAt(bytes, locations[new IndirectReference(3, 0)]);
|
||||
Assert.StartsWith("3 0 obj", s);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BruteForceSearcherBytesFileOffsetsCorrect()
|
||||
{
|
||||
var bytes = new ByteArrayInputBytes(File.ReadAllBytes(IntegrationHelpers.GetDocumentPath("Single Page Simple - from inkscape.pdf")));
|
||||
|
||||
var locations = BruteForceSearcher.GetObjectLocations(bytes);
|
||||
|
||||
Assert.Equal(13, locations.Count);
|
||||
|
||||
Assert.Equal(6183, locations[new IndirectReference(1, 0)]);
|
||||
Assert.Equal(244, locations[new IndirectReference(2, 0)]);
|
||||
Assert.Equal(15, locations[new IndirectReference(3, 0)]);
|
||||
Assert.Equal(222, locations[new IndirectReference(4, 0)]);
|
||||
Assert.Equal(5766, locations[new IndirectReference(5, 0)]);
|
||||
Assert.Equal(353, locations[new IndirectReference(6, 0)]);
|
||||
Assert.Equal(581, locations[new IndirectReference(7, 0)]);
|
||||
Assert.Equal(5068, locations[new IndirectReference(8, 0)]);
|
||||
Assert.Equal(5091, locations[new IndirectReference(9, 0)]);
|
||||
|
||||
var s = GetStringAt(bytes, locations[new IndirectReference(3, 0)]);
|
||||
Assert.StartsWith("3 0 obj", s);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BruteForceSearcherFileOffsetsCorrectOpenOffice()
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Content;
|
||||
using Integration;
|
||||
using PdfPig.Core;
|
||||
using PdfPig.Fonts.Standard14Fonts;
|
||||
using PdfPig.Writer;
|
||||
@@ -397,6 +398,102 @@
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanWriteSinglePageWithJpeg()
|
||||
{
|
||||
var builder = new PdfDocumentBuilder();
|
||||
var page = builder.AddPage(PageSize.A4);
|
||||
|
||||
var font = builder.AddStandard14Font(Standard14Font.Helvetica);
|
||||
|
||||
page.AddText("Smile", 12, new PdfPoint(25, page.PageSize.Height - 52), font);
|
||||
|
||||
var img = IntegrationHelpers.GetDocumentPath("smile-250-by-160.jpg", false);
|
||||
|
||||
var expectedBounds = new PdfRectangle(25, page.PageSize.Height - 300, 200, page.PageSize.Height - 200);
|
||||
|
||||
var imageBytes = File.ReadAllBytes(img);
|
||||
|
||||
page.AddJpeg(imageBytes, expectedBounds);
|
||||
|
||||
var bytes = builder.Build();
|
||||
WriteFile(nameof(CanWriteSinglePageWithJpeg), bytes);
|
||||
|
||||
using (var document = PdfDocument.Open(bytes))
|
||||
{
|
||||
var page1 = document.GetPage(1);
|
||||
|
||||
Assert.Equal("Smile", page1.Text);
|
||||
|
||||
var image = Assert.Single(page1.GetImages());
|
||||
|
||||
Assert.NotNull(image);
|
||||
|
||||
Assert.Equal(expectedBounds.BottomLeft, image.Bounds.BottomLeft);
|
||||
Assert.Equal(expectedBounds.TopRight, image.Bounds.TopRight);
|
||||
|
||||
Assert.Equal(imageBytes, image.RawBytes);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanWrite2PagesSharingJpeg()
|
||||
{
|
||||
var builder = new PdfDocumentBuilder();
|
||||
var page = builder.AddPage(PageSize.A4);
|
||||
|
||||
var font = builder.AddStandard14Font(Standard14Font.Helvetica);
|
||||
|
||||
page.AddText("Smile", 12, new PdfPoint(25, page.PageSize.Height - 52), font);
|
||||
|
||||
var img = IntegrationHelpers.GetDocumentPath("smile-250-by-160.jpg", false);
|
||||
|
||||
var expectedBounds1 = new PdfRectangle(25, page.PageSize.Height - 300, 200, page.PageSize.Height - 200);
|
||||
|
||||
var imageBytes = File.ReadAllBytes(img);
|
||||
|
||||
var expectedBounds2 = new PdfRectangle(25, 600, 75, 650);
|
||||
|
||||
var jpeg = page.AddJpeg(imageBytes, expectedBounds1);
|
||||
page.AddJpeg(jpeg, expectedBounds2);
|
||||
|
||||
var expectedBounds3 = new PdfRectangle(30, 500, 130, 550);
|
||||
|
||||
var page2 = builder.AddPage(PageSize.A4);
|
||||
page2.AddJpeg(jpeg, expectedBounds3);
|
||||
|
||||
var bytes = builder.Build();
|
||||
WriteFile(nameof(CanWrite2PagesSharingJpeg), bytes);
|
||||
|
||||
using (var document = PdfDocument.Open(bytes))
|
||||
{
|
||||
var page1 = document.GetPage(1);
|
||||
|
||||
Assert.Equal("Smile", page1.Text);
|
||||
|
||||
var page1Images = page1.GetImages().ToList();
|
||||
Assert.Equal(2, page1Images.Count);
|
||||
|
||||
var image1 = page1Images[0];
|
||||
Assert.Equal(expectedBounds1, image1.Bounds);
|
||||
|
||||
var image2 = page1Images[1];
|
||||
Assert.Equal(expectedBounds2, image2.Bounds);
|
||||
|
||||
var page2Doc = document.GetPage(2);
|
||||
|
||||
var image3 = Assert.Single(page2Doc.GetImages());
|
||||
|
||||
Assert.NotNull(image3);
|
||||
|
||||
Assert.Equal(expectedBounds3, image3.Bounds);
|
||||
|
||||
Assert.Equal(imageBytes, image1.RawBytes);
|
||||
Assert.Equal(imageBytes, image2.RawBytes);
|
||||
Assert.Equal(imageBytes, image3.RawBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteFile(string name, byte[] bytes)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>0.1.1-alpha001</Version>
|
||||
<Version>0.1.1</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -278,6 +278,9 @@
|
||||
public static readonly NameToken If = new NameToken("IF");
|
||||
public static readonly NameToken Im = new NameToken("IM");
|
||||
public static readonly NameToken Image = new NameToken("Image");
|
||||
public static readonly NameToken ImageB = new NameToken("ImageB");
|
||||
public static readonly NameToken ImageC = new NameToken("ImageC");
|
||||
public static readonly NameToken ImageI = new NameToken("ImageI");
|
||||
public static readonly NameToken ImageMask = new NameToken("ImageMask");
|
||||
public static readonly NameToken Index = new NameToken("Index");
|
||||
public static readonly NameToken Indexed = new NameToken("Indexed");
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>0.1.1-alpha001</Version>
|
||||
<Version>0.1.1</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
/// <inheritdoc />
|
||||
public void Write(Stream stream)
|
||||
{
|
||||
stream.WriteText($"/{Name}");
|
||||
stream.WriteText($"/{Name.Data}");
|
||||
stream.WriteWhiteSpace();
|
||||
stream.WriteText(Symbol);
|
||||
stream.WriteNewLine();
|
||||
|
||||
110
src/UglyToad.PdfPig/Images/JpegHandler.cs
Normal file
110
src/UglyToad.PdfPig/Images/JpegHandler.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
namespace UglyToad.PdfPig.Images
|
||||
{
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
internal static class JpegHandler
|
||||
{
|
||||
private const byte MarkerStart = 255;
|
||||
private const byte StartOfImage = 216;
|
||||
|
||||
public static JpegInformation GetInformation(Stream stream)
|
||||
{
|
||||
if (stream == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
}
|
||||
|
||||
if (!HasRecognizedHeader(stream))
|
||||
{
|
||||
throw new InvalidOperationException("The input stream did not start with the expected JPEG header [ 255 216 ]");
|
||||
}
|
||||
|
||||
var marker = JpegMarker.StartOfImage;
|
||||
|
||||
var shortBuffer = new byte[2];
|
||||
|
||||
while (marker != JpegMarker.EndOfImage)
|
||||
{
|
||||
switch (marker)
|
||||
{
|
||||
case JpegMarker.StartOfBaselineDctFrame:
|
||||
{
|
||||
// ReSharper disable once UnusedVariable
|
||||
var length = ReadShort(stream, shortBuffer);
|
||||
var bpp = stream.ReadByte();
|
||||
var height = ReadShort(stream, shortBuffer);
|
||||
var width = ReadShort(stream, shortBuffer);
|
||||
|
||||
return new JpegInformation(width, height, bpp);
|
||||
}
|
||||
case JpegMarker.StartOfProgressiveDctFrame:
|
||||
break;
|
||||
}
|
||||
|
||||
marker = (JpegMarker)ReadSegmentMarker(stream, true);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("File was a valid JPEG but the width and height could not be determined.");
|
||||
}
|
||||
|
||||
private static bool HasRecognizedHeader(Stream stream)
|
||||
{
|
||||
var bytes = new byte[2];
|
||||
|
||||
var read = stream.Read(bytes, 0, 2);
|
||||
|
||||
if (read != 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return bytes[0] == MarkerStart
|
||||
&& bytes[1] == StartOfImage;
|
||||
}
|
||||
|
||||
private static byte ReadSegmentMarker(Stream stream, bool skipData = false)
|
||||
{
|
||||
byte? previous = null;
|
||||
int currentValue;
|
||||
while ((currentValue = stream.ReadByte()) != -1)
|
||||
{
|
||||
var b = (byte)currentValue;
|
||||
|
||||
if (!skipData)
|
||||
{
|
||||
if (!previous.HasValue && b != MarkerStart)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
if (b != MarkerStart)
|
||||
{
|
||||
return b;
|
||||
}
|
||||
}
|
||||
|
||||
if (previous.HasValue && previous.Value == MarkerStart && b != MarkerStart)
|
||||
{
|
||||
return b;
|
||||
}
|
||||
|
||||
previous = b;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
private static short ReadShort(Stream stream, byte[] buffer)
|
||||
{
|
||||
var read = stream.Read(buffer, 0, 2);
|
||||
|
||||
if (read != 2)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to read a short where expected in the JPEG stream.");
|
||||
}
|
||||
|
||||
return (short) ((buffer[0] << 8) + buffer[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/UglyToad.PdfPig/Images/JpegInformation.cs
Normal file
33
src/UglyToad.PdfPig/Images/JpegInformation.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace UglyToad.PdfPig.Images
|
||||
{
|
||||
/// <summary>
|
||||
/// Information read from a JPEG image.
|
||||
/// </summary>
|
||||
internal class JpegInformation
|
||||
{
|
||||
/// <summary>
|
||||
/// Width of the image in pixels.
|
||||
/// </summary>
|
||||
public int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Height of the image in pixels.
|
||||
/// </summary>
|
||||
public int Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Bits per component.
|
||||
/// </summary>
|
||||
public int BitsPerComponent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="JpegInformation"/>.
|
||||
/// </summary>
|
||||
public JpegInformation(int width, int height, int bitsPerComponent)
|
||||
{
|
||||
Width = width;
|
||||
Height = height;
|
||||
BitsPerComponent = bitsPerComponent;
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/UglyToad.PdfPig/Images/JpegMarker.cs
Normal file
86
src/UglyToad.PdfPig/Images/JpegMarker.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
namespace UglyToad.PdfPig.Images
|
||||
{
|
||||
internal enum JpegMarker : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that this is a baseline DCT-based JPEG, and specifies the width, height, number of components, and component subsampling.
|
||||
/// </summary>
|
||||
StartOfBaselineDctFrame = 0xC0,
|
||||
/// <summary>
|
||||
/// Indicates that this is a progressive DCT-based JPEG, and specifies the width, height, number of components, and component subsampling.
|
||||
/// </summary>
|
||||
StartOfProgressiveDctFrame = 0xC2,
|
||||
/// <summary>
|
||||
/// Specifies one or more Huffman tables.
|
||||
/// </summary>
|
||||
DefineHuffmanTable = 0xC4,
|
||||
/// <summary>
|
||||
/// Begins a top-to-bottom scan of the image. In baseline images, there is generally a single scan.
|
||||
/// Progressive images usually contain multiple scans.
|
||||
/// </summary>
|
||||
StartOfScan = 0xDA,
|
||||
/// <summary>
|
||||
/// Specifies one or more quantization tables.
|
||||
/// </summary>
|
||||
DefineQuantizationTable = 0xDB,
|
||||
/// <summary>
|
||||
/// Specifies the interval between RSTn markers, in Minimum Coded Units (MCUs).
|
||||
/// This marker is followed by two bytes indicating the fixed size so it can be treated like any other variable size segment.
|
||||
/// </summary>
|
||||
DefineRestartInterval = 0xDD,
|
||||
/// <summary>
|
||||
/// Inserted every r macroblocks.
|
||||
/// </summary>
|
||||
Restart0 = 0xD0,
|
||||
/// <summary>
|
||||
/// Inserted every r macroblocks.
|
||||
/// </summary>
|
||||
Restart1 = 0xD1,
|
||||
/// <summary>
|
||||
/// Inserted every r macroblocks.
|
||||
/// </summary>
|
||||
Restart2 = 0xD2,
|
||||
/// <summary>
|
||||
/// Inserted every r macroblocks.
|
||||
/// </summary>
|
||||
Restart3 = 0xD3,
|
||||
/// <summary>
|
||||
/// Inserted every r macroblocks.
|
||||
/// </summary>
|
||||
Restart4 = 0xD4,
|
||||
/// <summary>
|
||||
/// Inserted every r macroblocks.
|
||||
/// </summary>
|
||||
Restart5 = 0xD5,
|
||||
/// <summary>
|
||||
/// Inserted every r macroblocks.
|
||||
/// </summary>
|
||||
Restart6 = 0xD6,
|
||||
/// <summary>
|
||||
/// Inserted every r macroblocks.
|
||||
/// </summary>
|
||||
Restart7 = 0xD7,
|
||||
/// <summary>
|
||||
/// Marks the start of a JPEG image file.
|
||||
/// </summary>
|
||||
StartOfImage = 0xD8,
|
||||
/// <summary>
|
||||
/// Marks the end of a JPEG image file.
|
||||
/// </summary>
|
||||
EndOfImage = 0xD9,
|
||||
ApplicationSpecific0 = 0xE0,
|
||||
ApplicationSpecific1 = 0xE1,
|
||||
ApplicationSpecific2 = 0xE2,
|
||||
ApplicationSpecific3 = 0xE3,
|
||||
ApplicationSpecific4 = 0xE4,
|
||||
ApplicationSpecific5 = 0xE5,
|
||||
ApplicationSpecific6 = 0xE6,
|
||||
ApplicationSpecific7 = 0xE7,
|
||||
ApplicationSpecific8 = 0xE8,
|
||||
ApplicationSpecific9 = 0xE9,
|
||||
/// <summary>
|
||||
/// Marks a text comment.
|
||||
/// </summary>
|
||||
Comment = 0xFE
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,8 @@
|
||||
|
||||
var currentlyInObject = false;
|
||||
|
||||
var objBuffer = new byte[4];
|
||||
|
||||
do
|
||||
{
|
||||
if (loopProtection > 1_000_000)
|
||||
@@ -94,17 +96,24 @@
|
||||
|
||||
bytes.Seek(currentOffset);
|
||||
|
||||
if (!ReadHelper.IsString(bytes, " obj"))
|
||||
bytes.Read(objBuffer);
|
||||
|
||||
if (!IsStartObjMarker(objBuffer))
|
||||
{
|
||||
currentOffset++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Current byte is ' '[obj]
|
||||
var offset = currentOffset - 1;
|
||||
var offset = currentOffset + 1;
|
||||
|
||||
bytes.Seek(offset);
|
||||
|
||||
while (ReadHelper.IsWhitespace(bytes.CurrentByte) && offset >= MinimumSearchOffset)
|
||||
{
|
||||
bytes.Seek(--offset);
|
||||
}
|
||||
|
||||
while (ReadHelper.IsDigit(bytes.CurrentByte) && offset >= MinimumSearchOffset)
|
||||
{
|
||||
generationBytes.Insert(0, (char)bytes.CurrentByte);
|
||||
@@ -113,13 +122,16 @@
|
||||
}
|
||||
|
||||
// We should now be at the space between object and generation number.
|
||||
if (!ReadHelper.IsSpace(bytes.CurrentByte))
|
||||
if (!ReadHelper.IsWhitespace(bytes.CurrentByte))
|
||||
{
|
||||
currentOffset++;
|
||||
continue;
|
||||
}
|
||||
|
||||
bytes.Seek(--offset);
|
||||
while (ReadHelper.IsWhitespace(bytes.CurrentByte))
|
||||
{
|
||||
bytes.Seek(--offset);
|
||||
}
|
||||
|
||||
while (ReadHelper.IsDigit(bytes.CurrentByte) && offset >= MinimumSearchOffset)
|
||||
{
|
||||
@@ -185,5 +197,17 @@
|
||||
bytes.Seek(originalOffset);
|
||||
return long.MaxValue;
|
||||
}
|
||||
|
||||
private static bool IsStartObjMarker(byte[] data)
|
||||
{
|
||||
if (!ReadHelper.IsWhitespace(data[0]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (data[1] == 'o' || data[1] == 'O')
|
||||
&& (data[2] == 'b' || data[2] == 'B')
|
||||
&& (data[3] == 'j' || data[3] == 'J');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net45;net451;net452;net46;net461;net462;net47</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>0.1.1-alpha001</Version>
|
||||
<Version>0.1.1</Version>
|
||||
<IsTestProject>False</IsTestProject>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
/// </summary>
|
||||
public class PdfDocumentBuilder
|
||||
{
|
||||
private readonly BuilderContext context = new BuilderContext();
|
||||
private readonly Dictionary<int, PdfPageBuilder> pages = new Dictionary<int, PdfPageBuilder>();
|
||||
private readonly Dictionary<Guid, FontStored> fonts = new Dictionary<Guid, FontStored>();
|
||||
private readonly Dictionary<Guid, ImageStored> images = new Dictionary<Guid, ImageStored>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include the document information dictionary in the produced document.
|
||||
@@ -133,7 +135,18 @@
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
|
||||
internal IndirectReference AddImage(DictionaryToken dictionary, byte[] bytes)
|
||||
{
|
||||
var reserved = context.ReserveNumber();
|
||||
|
||||
var stored = new ImageStored(dictionary, bytes, reserved);
|
||||
|
||||
images[stored.Id] = stored;
|
||||
|
||||
return new IndirectReference(reserved, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a new page with the specified size, this page will be included in the output when <see cref="Build"/> is called.
|
||||
/// </summary>
|
||||
@@ -206,7 +219,6 @@
|
||||
/// <returns>The bytes of the resulting PDF document.</returns>
|
||||
public byte[] Build()
|
||||
{
|
||||
var context = new BuilderContext();
|
||||
var fontsWritten = new Dictionary<Guid, ObjectToken>();
|
||||
using (var memory = new MemoryStream())
|
||||
{
|
||||
@@ -228,9 +240,25 @@
|
||||
fontsWritten.Add(font.Key, fontObj);
|
||||
}
|
||||
|
||||
foreach (var image in images)
|
||||
{
|
||||
var streamToken = new StreamToken(image.Value.StreamDictionary, image.Value.StreamData);
|
||||
|
||||
context.WriteObject(memory, streamToken, image.Value.ObjectNumber);
|
||||
}
|
||||
|
||||
var procSet = new List<NameToken>
|
||||
{
|
||||
NameToken.Create("PDF"),
|
||||
NameToken.Text,
|
||||
NameToken.ImageB,
|
||||
NameToken.ImageC,
|
||||
NameToken.ImageI
|
||||
};
|
||||
|
||||
var resources = new Dictionary<NameToken, IToken>
|
||||
{
|
||||
{ NameToken.ProcSet, new ArrayToken(new []{ NameToken.Create("PDF"), NameToken.Create("Text") }) }
|
||||
{ NameToken.ProcSet, new ArrayToken(procSet) }
|
||||
};
|
||||
|
||||
if (fontsWritten.Count > 0)
|
||||
@@ -249,17 +277,25 @@
|
||||
var pageReferences = new List<IndirectReferenceToken>();
|
||||
foreach (var page in pages)
|
||||
{
|
||||
var individualResources = new Dictionary<NameToken, IToken>(resources);
|
||||
var pageDictionary = new Dictionary<NameToken, IToken>
|
||||
{
|
||||
{NameToken.Type, NameToken.Page},
|
||||
{
|
||||
NameToken.Resources,
|
||||
new DictionaryToken(resources)
|
||||
},
|
||||
{NameToken.MediaBox, RectangleToArray(page.Value.PageSize)},
|
||||
{NameToken.Parent, parentIndirect}
|
||||
};
|
||||
|
||||
if (page.Value.Resources.Count > 0)
|
||||
{
|
||||
foreach (var kvp in page.Value.Resources)
|
||||
{
|
||||
// TODO: combine resources if value is dictionary or array, otherwise overwrite.
|
||||
individualResources[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
pageDictionary[NameToken.Resources] = new DictionaryToken(individualResources);
|
||||
|
||||
if (page.Value.Operations.Count > 0)
|
||||
{
|
||||
var contentStream = WriteContentStream(page.Value.Operations);
|
||||
@@ -274,12 +310,14 @@
|
||||
pageReferences.Add(new IndirectReferenceToken(pageRef.Number));
|
||||
}
|
||||
|
||||
var pagesDictionary = new DictionaryToken(new Dictionary<NameToken, IToken>
|
||||
var pagesDictionaryData = new Dictionary<NameToken, IToken>
|
||||
{
|
||||
{ NameToken.Type, NameToken.Pages },
|
||||
{ NameToken.Kids, new ArrayToken(pageReferences) },
|
||||
{ NameToken.Count, new NumericToken(pageReferences.Count) }
|
||||
});
|
||||
{NameToken.Type, NameToken.Pages},
|
||||
{NameToken.Kids, new ArrayToken(pageReferences)},
|
||||
{NameToken.Count, new NumericToken(pageReferences.Count)}
|
||||
};
|
||||
|
||||
var pagesDictionary = new DictionaryToken(pagesDictionaryData);
|
||||
|
||||
var pagesRef = context.WriteObject(memory, pagesDictionary, reserved);
|
||||
|
||||
@@ -361,6 +399,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
internal class ImageStored
|
||||
{
|
||||
public Guid Id { get; }
|
||||
|
||||
public DictionaryToken StreamDictionary { get; }
|
||||
|
||||
public byte[] StreamData { get; }
|
||||
|
||||
public int ObjectNumber { get; }
|
||||
|
||||
public ImageStored(DictionaryToken streamDictionary, byte[] streamData, int objectNumber)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
StreamDictionary = streamDictionary;
|
||||
StreamData = streamData;
|
||||
ObjectNumber = objectNumber;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A key representing a font available to use on the current document builder. Create by adding a font to a document using either
|
||||
/// <see cref="AddStandard14Font"/> or <see cref="AddTrueTypeFont"/>.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Content;
|
||||
using Core;
|
||||
using Fonts;
|
||||
@@ -14,6 +15,8 @@
|
||||
using Graphics.Operations.TextPositioning;
|
||||
using Graphics.Operations.TextShowing;
|
||||
using Graphics.Operations.TextState;
|
||||
using Images;
|
||||
using Tokens;
|
||||
|
||||
/// <summary>
|
||||
/// A builder used to add construct a page in a PDF document.
|
||||
@@ -22,12 +25,17 @@
|
||||
{
|
||||
private readonly PdfDocumentBuilder documentBuilder;
|
||||
private readonly List<IGraphicsStateOperation> operations = new List<IGraphicsStateOperation>();
|
||||
private readonly Dictionary<NameToken, IToken> resourcesDictionary = new Dictionary<NameToken, IToken>();
|
||||
|
||||
//a sequence number of ShowText operation to determine whether letters belong to same operation or not (letters that belong to different operations have less changes to belong to same word)
|
||||
private int textSequence;
|
||||
|
||||
private int imageKey = 1;
|
||||
|
||||
internal IReadOnlyList<IGraphicsStateOperation> Operations => operations;
|
||||
|
||||
internal IReadOnlyDictionary<NameToken, IToken> Resources => resourcesDictionary;
|
||||
|
||||
/// <summary>
|
||||
/// The number of this page, 1-indexed.
|
||||
/// </summary>
|
||||
@@ -250,6 +258,103 @@
|
||||
return letters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the JPEG image represented by the input bytes at the specified location.
|
||||
/// </summary>
|
||||
public AddedImage AddJpeg(byte[] fileBytes, PdfRectangle placementRectangle)
|
||||
{
|
||||
using (var stream = new MemoryStream(fileBytes))
|
||||
{
|
||||
return AddJpeg(stream, placementRectangle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the JPEG image represented by the input stream at the specified location.
|
||||
/// </summary>
|
||||
public AddedImage AddJpeg(Stream fileStream, PdfRectangle placementRectangle)
|
||||
{
|
||||
var startFrom = fileStream.Position;
|
||||
var info = JpegHandler.GetInformation(fileStream);
|
||||
|
||||
byte[] data;
|
||||
using (var memory = new MemoryStream())
|
||||
{
|
||||
fileStream.Seek(startFrom, SeekOrigin.Begin);
|
||||
fileStream.CopyTo(memory);
|
||||
data = memory.ToArray();
|
||||
}
|
||||
|
||||
var imgDictionary = new Dictionary<NameToken, IToken>
|
||||
{
|
||||
{NameToken.Type, NameToken.Xobject },
|
||||
{NameToken.Subtype, NameToken.Image },
|
||||
{NameToken.Width, new NumericToken(info.Width) },
|
||||
{NameToken.Height, new NumericToken(info.Height) },
|
||||
{NameToken.BitsPerComponent, new NumericToken(info.BitsPerComponent)},
|
||||
{NameToken.ColorSpace, NameToken.Devicergb},
|
||||
{NameToken.Filter, NameToken.DctDecode},
|
||||
{NameToken.Length, new NumericToken(data.Length)}
|
||||
};
|
||||
|
||||
var reference = documentBuilder.AddImage(new DictionaryToken(imgDictionary), data);
|
||||
|
||||
if (!resourcesDictionary.TryGetValue(NameToken.Xobject, out var xobjectsDict)
|
||||
|| !(xobjectsDict is DictionaryToken xobjects))
|
||||
{
|
||||
xobjects = new DictionaryToken(new Dictionary<NameToken, IToken>());
|
||||
resourcesDictionary[NameToken.Xobject] = xobjects;
|
||||
}
|
||||
|
||||
var key = NameToken.Create($"I{imageKey++}");
|
||||
|
||||
resourcesDictionary[NameToken.Xobject] = xobjects.With(key, new IndirectReferenceToken(reference));
|
||||
|
||||
operations.Add(Push.Value);
|
||||
// This needs to be the placement rectangle.
|
||||
operations.Add(new ModifyCurrentTransformationMatrix(new []
|
||||
{
|
||||
(decimal)placementRectangle.Width, 0,
|
||||
0, (decimal)placementRectangle.Height,
|
||||
(decimal)placementRectangle.BottomLeft.X, (decimal)placementRectangle.BottomLeft.Y
|
||||
}));
|
||||
operations.Add(new InvokeNamedXObject(key));
|
||||
operations.Add(Pop.Value);
|
||||
|
||||
return new AddedImage(reference, info.Width, info.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the JPEG image previously added using <see cref="AddJpeg(byte[],PdfRectangle)"/>,
|
||||
/// this will share the same image data to prevent duplication.
|
||||
/// </summary>
|
||||
/// <param name="image">An image previously added to this page or another page.</param>
|
||||
/// <param name="placementRectangle">The size and location to draw the image on this page.</param>
|
||||
public void AddJpeg(AddedImage image, PdfRectangle placementRectangle)
|
||||
{
|
||||
if (!resourcesDictionary.TryGetValue(NameToken.Xobject, out var xobjectsDict)
|
||||
|| !(xobjectsDict is DictionaryToken xobjects))
|
||||
{
|
||||
xobjects = new DictionaryToken(new Dictionary<NameToken, IToken>());
|
||||
resourcesDictionary[NameToken.Xobject] = xobjects;
|
||||
}
|
||||
|
||||
var key = NameToken.Create($"I{imageKey++}");
|
||||
|
||||
resourcesDictionary[NameToken.Xobject] = xobjects.With(key, new IndirectReferenceToken(image.Reference));
|
||||
|
||||
operations.Add(Push.Value);
|
||||
// This needs to be the placement rectangle.
|
||||
operations.Add(new ModifyCurrentTransformationMatrix(new[]
|
||||
{
|
||||
(decimal)placementRectangle.Width, 0,
|
||||
0, (decimal)placementRectangle.Height,
|
||||
(decimal)placementRectangle.BottomLeft.X, (decimal)placementRectangle.BottomLeft.Y
|
||||
}));
|
||||
operations.Add(new InvokeNamedXObject(key));
|
||||
operations.Add(Pop.Value);
|
||||
}
|
||||
|
||||
private List<Letter> DrawLetters(string text, IWritingFont font, TransformationMatrix fontMatrix, decimal fontSize, TransformationMatrix textMatrix)
|
||||
{
|
||||
var horizontalScaling = 1;
|
||||
@@ -343,5 +448,43 @@
|
||||
Operations = operations;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A key representing an image available to use for the current document builder.
|
||||
/// Create it by adding an image to a page using <see cref="AddJpeg(byte[],PdfRectangle)"/>.
|
||||
/// </summary>
|
||||
public class AddedImage
|
||||
{
|
||||
/// <summary>
|
||||
/// The Id uniquely identifying this image on the builder.
|
||||
/// </summary>
|
||||
internal Guid Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The reference to the stored image XObject.
|
||||
/// </summary>
|
||||
internal IndirectReference Reference { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The width of the raw image in pixels.
|
||||
/// </summary>
|
||||
public int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The height of the raw image in pixels.
|
||||
/// </summary>
|
||||
public int Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="AddedImage"/>.
|
||||
/// </summary>
|
||||
internal AddedImage(IndirectReference reference, int width, int height)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
Reference = reference;
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,9 @@
|
||||
<PackageTags>PDF;Reader;Document;Adobe;PDFBox;PdfPig;pdf-extract</PackageTags>
|
||||
<RepositoryUrl>https://github.com/UglyToad/PdfPig</RepositoryUrl>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<Version>0.1.1-alpha001</Version>
|
||||
<AssemblyVersion>0.1.0.0</AssemblyVersion>
|
||||
<FileVersion>0.1.0.0</FileVersion>
|
||||
<Version>0.1.1</Version>
|
||||
<AssemblyVersion>0.1.1.0</AssemblyVersion>
|
||||
<FileVersion>0.1.1.0</FileVersion>
|
||||
<PackageIconUrl>https://raw.githubusercontent.com/UglyToad/PdfPig/master/documentation/pdfpig.png</PackageIconUrl>
|
||||
<PackageIcon>pdfpig.png</PackageIcon>
|
||||
<Product>PdfPig</Product>
|
||||
|
||||
Reference in New Issue
Block a user