From 99dcd45015e5401d1fb428518b2435567fe1d5c3 Mon Sep 17 00:00:00 2001 From: Gerard Gunnewijk Date: Wed, 24 Feb 2021 18:14:57 +0100 Subject: [PATCH] Complete rewrite with better low-level support --- Directory.Build.props | 23 - .../Program.cs | 2 +- src/Directory.Build.props | 2 +- .../DocumentInformation.cs | 93 ++- .../Extensions/MatrixExtensions.cs | 35 -- .../Extensions/PdfPageExtensions.cs | 127 +++++ .../Extensions/PrimitiveExtensions.cs | 16 + .../Extensions/StreamExtensions.cs | 265 --------- .../Extensions/StreamFilterExtensions.cs | 17 - .../Helpers/ISpanWriteable.cs | 10 - .../Helpers/IStreamWriteable.cs | 17 - .../Helpers/MathHelper.cs | 12 - .../Helpers/PdfTypeHelper.cs | 54 -- src/Synercoding.FileFormats.Pdf/Image.cs | 85 +++ .../{Helpers => LowLevel}/ByteSizes.cs | 4 +- .../LowLevel/Catalog.cs | 38 ++ .../LowLevel/ContentStream.cs | 87 +++ .../Extensions/PdfStreamArrayExtensions.cs | 533 ++++++++++++++++++ .../Extensions/PdfStreamExtensions.cs | 155 +++++ .../Extensions/StreamFilterExtensions.cs | 16 + .../LowLevel/IPdfObject.cs | 13 + .../{Helpers => LowLevel}/IdGenerator.cs | 2 +- .../Internals}/SpanHelper.cs | 18 +- .../LowLevel/ObjectType.cs | 10 + .../LowLevel/PageTree.cs | 48 ++ .../LowLevel/PdfDictionary.cs | 281 +++++++++ .../LowLevel/PdfName.cs | 117 ++++ .../PdfReference.cs | 15 +- .../LowLevel/PdfStream.cs | 97 ++++ .../LowLevel/StreamFilter.cs | 7 + .../LowLevel/XObjectSubType.cs | 7 + .../{PdfInternals => LowLevel}/XRef/Entry.cs | 20 +- .../XRef/Section.cs | 10 +- .../{PdfInternals => LowLevel}/XRef/Table.cs | 36 +- .../XRef/TableBuilder.cs | 15 +- .../{Primitives => }/Matrix.cs | 16 +- .../PackageDetails.props | 2 +- .../PdfInternals/Header.cs | 45 -- .../PdfInternals/ObjectType.cs | 10 - .../PdfInternals/Objects/Catalog.cs | 43 -- .../PdfInternals/Objects/ContentStream.cs | 57 -- .../Objects/DocumentInformationDictionary.cs | 62 -- .../PdfInternals/Objects/IPdfObject.cs | 20 - .../PdfInternals/Objects/Image.cs | 79 --- .../PdfInternals/Objects/Page.cs | 102 ---- .../PdfInternals/Objects/PageTree.cs | 49 -- .../PdfInternals/PdfDictionary.cs | 132 ----- .../PdfInternals/StreamFilter.cs | 7 - .../PdfInternals/Trailer.cs | 49 -- .../PdfInternals/XObjectSubType.cs | 7 - src/Synercoding.FileFormats.Pdf/PdfPage.cs | 316 +++++++---- src/Synercoding.FileFormats.Pdf/PdfWriter.cs | 169 ++++-- .../Synercoding.FileFormats.Pdf.csproj | 2 +- 53 files changed, 2109 insertions(+), 1345 deletions(-) delete mode 100644 src/Synercoding.FileFormats.Pdf/Extensions/MatrixExtensions.cs create mode 100644 src/Synercoding.FileFormats.Pdf/Extensions/PdfPageExtensions.cs create mode 100644 src/Synercoding.FileFormats.Pdf/Extensions/PrimitiveExtensions.cs delete mode 100644 src/Synercoding.FileFormats.Pdf/Extensions/StreamExtensions.cs delete mode 100644 src/Synercoding.FileFormats.Pdf/Extensions/StreamFilterExtensions.cs delete mode 100644 src/Synercoding.FileFormats.Pdf/Helpers/ISpanWriteable.cs delete mode 100644 src/Synercoding.FileFormats.Pdf/Helpers/IStreamWriteable.cs delete mode 100644 src/Synercoding.FileFormats.Pdf/Helpers/MathHelper.cs delete mode 100644 src/Synercoding.FileFormats.Pdf/Helpers/PdfTypeHelper.cs create mode 100644 src/Synercoding.FileFormats.Pdf/Image.cs rename src/Synercoding.FileFormats.Pdf/{Helpers => LowLevel}/ByteSizes.cs (90%) create mode 100644 src/Synercoding.FileFormats.Pdf/LowLevel/Catalog.cs create mode 100644 src/Synercoding.FileFormats.Pdf/LowLevel/ContentStream.cs create mode 100644 src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/PdfStreamArrayExtensions.cs create mode 100644 src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/PdfStreamExtensions.cs create mode 100644 src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/StreamFilterExtensions.cs create mode 100644 src/Synercoding.FileFormats.Pdf/LowLevel/IPdfObject.cs rename src/Synercoding.FileFormats.Pdf/{Helpers => LowLevel}/IdGenerator.cs (87%) rename src/Synercoding.FileFormats.Pdf/{Helpers => LowLevel/Internals}/SpanHelper.cs (75%) create mode 100644 src/Synercoding.FileFormats.Pdf/LowLevel/ObjectType.cs create mode 100644 src/Synercoding.FileFormats.Pdf/LowLevel/PageTree.cs create mode 100644 src/Synercoding.FileFormats.Pdf/LowLevel/PdfDictionary.cs create mode 100644 src/Synercoding.FileFormats.Pdf/LowLevel/PdfName.cs rename src/Synercoding.FileFormats.Pdf/{PdfInternals => LowLevel}/PdfReference.cs (76%) create mode 100644 src/Synercoding.FileFormats.Pdf/LowLevel/PdfStream.cs create mode 100644 src/Synercoding.FileFormats.Pdf/LowLevel/StreamFilter.cs create mode 100644 src/Synercoding.FileFormats.Pdf/LowLevel/XObjectSubType.cs rename src/Synercoding.FileFormats.Pdf/{PdfInternals => LowLevel}/XRef/Entry.cs (71%) rename src/Synercoding.FileFormats.Pdf/{PdfInternals => LowLevel}/XRef/Section.cs (84%) rename src/Synercoding.FileFormats.Pdf/{PdfInternals => LowLevel}/XRef/Table.cs (67%) rename src/Synercoding.FileFormats.Pdf/{PdfInternals => LowLevel}/XRef/TableBuilder.cs (75%) rename src/Synercoding.FileFormats.Pdf/{Primitives => }/Matrix.cs (95%) delete mode 100644 src/Synercoding.FileFormats.Pdf/PdfInternals/Header.cs delete mode 100644 src/Synercoding.FileFormats.Pdf/PdfInternals/ObjectType.cs delete mode 100644 src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/Catalog.cs delete mode 100644 src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/ContentStream.cs delete mode 100644 src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/DocumentInformationDictionary.cs delete mode 100644 src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/IPdfObject.cs delete mode 100644 src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/Image.cs delete mode 100644 src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/Page.cs delete mode 100644 src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/PageTree.cs delete mode 100644 src/Synercoding.FileFormats.Pdf/PdfInternals/PdfDictionary.cs delete mode 100644 src/Synercoding.FileFormats.Pdf/PdfInternals/StreamFilter.cs delete mode 100644 src/Synercoding.FileFormats.Pdf/PdfInternals/Trailer.cs delete mode 100644 src/Synercoding.FileFormats.Pdf/PdfInternals/XObjectSubType.cs diff --git a/Directory.Build.props b/Directory.Build.props index 48b2b77..41f9818 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -48,16 +48,6 @@ - - - $(DefineConstants);SUPPORTS_CODECOVERAGE - - - - - $(DefineConstants);SUPPORTS_CODECOVERAGE - - $(DefineConstants);SUPPORTS_MATHF @@ -66,19 +56,6 @@ $(DefineConstants);SUPPORTS_NULLABLEREFATTRIBUTES - - - $(DefineConstants);SUPPORTS_MATHF - $(DefineConstants);SUPPORTS_CODECOVERAGE - - - - - $(DefineConstants);SUPPORTS_MATHF - $(DefineConstants);SUPPORTS_HASHCODE - $(DefineConstants);SUPPORTS_CODECOVERAGE - - $(DefineConstants);SUPPORTS_MATHF diff --git a/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs b/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs index b9b1a57..d172f11 100644 --- a/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs +++ b/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs @@ -1,4 +1,4 @@ -using Synercoding.FileFormats.Pdf.Primitives; +using Synercoding.FileFormats.Pdf.Extensions; using Synercoding.Primitives; using Synercoding.Primitives.Extensions; using System.IO; diff --git a/src/Directory.Build.props b/src/Directory.Build.props index ab42317..e365322 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ - net5.0;netcoreapp3.1;netcoreapp2.1;netstandard2.1;netstandard2.0;netstandard1.6 + net5.0;netcoreapp3.1;netstandard2.1 $(MSBuildAllProjects);$(MSBuildThisFileDirectory)..\Directory.Build.props src diff --git a/src/Synercoding.FileFormats.Pdf/DocumentInformation.cs b/src/Synercoding.FileFormats.Pdf/DocumentInformation.cs index 0122df5..0041a7d 100644 --- a/src/Synercoding.FileFormats.Pdf/DocumentInformation.cs +++ b/src/Synercoding.FileFormats.Pdf/DocumentInformation.cs @@ -1,3 +1,5 @@ +using Synercoding.FileFormats.Pdf.LowLevel; +using Synercoding.FileFormats.Pdf.LowLevel.Extensions; using System; namespace Synercoding.FileFormats.Pdf @@ -5,8 +7,15 @@ namespace Synercoding.FileFormats.Pdf /// /// This class contains information about the document /// - public class DocumentInformation + public class DocumentInformation : IPdfObject { + private bool _isWritten; + + internal DocumentInformation(PdfReference id) + { + Reference = id; + } + /// /// The document's title /// @@ -46,5 +55,87 @@ public class DocumentInformation /// The date and time the document was most recently modified, in human-readable form. /// public DateTime? ModDate { get; set; } + + /// + public PdfReference Reference { get; } + + internal uint WriteToStream(PdfStream stream) + { + if (_isWritten) + throw new InvalidOperationException("Object is already written to stream."); + + var position = (uint)stream.Position; + + stream.IndirectDictionary(this, static (did, dictionary) => + { + if (!string.IsNullOrWhiteSpace(did.Title)) + dictionary.Write(PdfName.Get("Title"), _toPdfHexadecimalString(did.Title!)); + if (!string.IsNullOrWhiteSpace(did.Author)) + dictionary.Write(PdfName.Get("Author"), _toPdfHexadecimalString(did.Author!)); + if (!string.IsNullOrWhiteSpace(did.Subject)) + dictionary.Write(PdfName.Get("Subject"), _toPdfHexadecimalString(did.Subject!)); + if (!string.IsNullOrWhiteSpace(did.Keywords)) + dictionary.Write(PdfName.Get("Keywords"), _toPdfHexadecimalString(did.Keywords!)); + if (!string.IsNullOrWhiteSpace(did.Creator)) + dictionary.Write(PdfName.Get("Creator"), _toPdfHexadecimalString(did.Creator!)); + if (!string.IsNullOrWhiteSpace(did.Producer)) + dictionary.Write(PdfName.Get("Producer"), _toPdfHexadecimalString(did.Producer!)); + if (did.CreationDate != null) + dictionary.Write(PdfName.Get("CreationDate"), _toPdfDate(did.CreationDate.Value)); + if (did.ModDate != null) + dictionary.Write(PdfName.Get("ModDate"), _toPdfDate(did.ModDate.Value)); + }); + + _isWritten = true; + + return position; + } + + private static string _toPdfHexadecimalString(string input) + { + var bytes = System.Text.Encoding.ASCII.GetBytes(input); + var builder = new System.Text.StringBuilder((bytes.Length * 2) + 2); + builder.Append('<'); + foreach (var b in bytes) + { + builder.Append(b.ToString("X2")); + } + builder.Append('>'); + return builder.ToString(); + } + + private static string _toPdfDate(DateTimeOffset input) + { + var datePart = input.ToString("yyyyMMddHHmmss"); + + var builder = new System.Text.StringBuilder(22); + builder.Append("(D:"); + builder.Append(datePart); + + var hours = input.Offset.Hours; + var minutes = input.Offset.Minutes; + + if (hours == 0 && minutes == 0) + { + builder.Append("Z00'00"); + } + else + { + if (hours > 0 || (hours == 0 && minutes > 0)) + { + builder.Append('+'); + } + else + { + builder.Append('-'); + } + builder.Append(Math.Abs(hours).ToString().PadLeft(2, '0')); + builder.Append('\''); + builder.Append(minutes.ToString().PadLeft(2, '0')); + } + builder.Append(')'); + + return builder.ToString(); + } } } diff --git a/src/Synercoding.FileFormats.Pdf/Extensions/MatrixExtensions.cs b/src/Synercoding.FileFormats.Pdf/Extensions/MatrixExtensions.cs deleted file mode 100644 index 7fe1a4a..0000000 --- a/src/Synercoding.FileFormats.Pdf/Extensions/MatrixExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Synercoding.FileFormats.Pdf.Primitives; -using Synercoding.Primitives; - -namespace Synercoding.FileFormats.Pdf.Extensions -{ - /// - /// Extensions to use Values as input. These will be converted to points directly - /// - public static class MatrixExtensions - { - /// - /// Create a matrix used for scaling - /// - /// The X scale - /// The Y scale - /// A scaled matrix - public static Matrix CreateScaleMatrix(Value x, Value y) - => new Matrix( - x.ConvertTo(Unit.Points).Raw, 0, - 0, y.ConvertTo(Unit.Points).Raw, - 0, 0); - - /// - /// Create a matrix used for translation - /// - /// The amount of X translation - /// The amount of Y translation - /// A matrix representing a translation - public static Matrix CreateTranslationMatrix(Value x, Value y) - => new Matrix( - 1, 0, - 0, 1, - x.ConvertTo(Unit.Points).Raw, y.ConvertTo(Unit.Points).Raw); - } -} diff --git a/src/Synercoding.FileFormats.Pdf/Extensions/PdfPageExtensions.cs b/src/Synercoding.FileFormats.Pdf/Extensions/PdfPageExtensions.cs new file mode 100644 index 0000000..fa4c8c2 --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/Extensions/PdfPageExtensions.cs @@ -0,0 +1,127 @@ +using Synercoding.Primitives; +using System.IO; + +namespace Synercoding.FileFormats.Pdf.Extensions +{ + /// + /// Extension class for + /// + public static class PdfPageExtensions + { + /// + /// Add an image to the pdf page + /// + /// The page to add the image to + /// The image to add + /// The placement matrix + /// The same to chain other calls. + public static PdfPage AddImage(this PdfPage page, Image image, Matrix matrix) + { + page.ContentStream + .SaveState() + .CTM(matrix) + .Paint(page.AddImageToResources(image)) + .RestoreState(); + + return page; + } + + /// + /// Add an image to the pdf page + /// + /// The page to add the image to + /// The image to add + /// The placement rectangle + /// The same to chain other calls. + public static PdfPage AddImage(this PdfPage page, Image image, Rectangle rectangle) + => page.AddImage(image, rectangle.AsPlacementMatrix()); + + /// + /// Add an image to the pdf page + /// + /// The page to add the image to + /// The image to add + /// The placement matrix + /// The same to chain other calls. + public static PdfPage AddImage(this PdfPage page, SixLabors.ImageSharp.Image image, Matrix matrix) + { + page.ContentStream + .SaveState() + .CTM(matrix) + .Paint(page.AddImageToResources(image)) + .RestoreState(); + + return page; + } + + /// + /// Add an image to the pdf page + /// + /// The page to add the image to + /// The image to add + /// The placement rectangle + /// The same to chain other calls. + public static PdfPage AddImage(this PdfPage page, SixLabors.ImageSharp.Image image, Rectangle rectangle) + => page.AddImage(image, rectangle.AsPlacementMatrix()); + + /// + /// Add an image to the pdf page + /// + /// The page to add the image to + /// The image to add + /// The placement matrix + /// The same to chain other calls. + public static PdfPage AddImage(this PdfPage page, Stream imageStream, Matrix matrix) + { + page.ContentStream + .SaveState() + .CTM(matrix) + .Paint(page.AddImageToResources(imageStream)) + .RestoreState(); + + return page; + } + + /// + /// Add an image to the pdf page + /// + /// The page to add the image to + /// The image to add + /// The placement rectangle + /// The same to chain other calls. + public static PdfPage AddImage(this PdfPage page, Stream imageStream, Rectangle rectangle) + => page.AddImage(imageStream, rectangle.AsPlacementMatrix()); + + /// + /// Add an image to the pdf page + /// + /// The page to add the image to + /// The image to add + /// The original width of the image + /// The original height of the image + /// The placement matrix + /// The same to chain other calls. + public static PdfPage AddImage(this PdfPage page, Stream jpgStream, int originalWidth, int originalHeight, Matrix matrix) + { + page.ContentStream + .SaveState() + .CTM(matrix) + .Paint(page.AddImageToResources(jpgStream, originalWidth, originalHeight)) + .RestoreState(); + + return page; + } + + /// + /// Add an image to the pdf page + /// + /// The page to add the image to + /// The image to add + /// The original width of the image + /// The original height of the image + /// The placement rectangle + /// The same to chain other calls. + public static PdfPage AddImage(this PdfPage page, Stream jpgStream, int originalWidth, int originalHeight, Rectangle rectangle) + => page.AddImage(jpgStream, originalWidth, originalHeight, rectangle.AsPlacementMatrix()); + } +} diff --git a/src/Synercoding.FileFormats.Pdf/Extensions/PrimitiveExtensions.cs b/src/Synercoding.FileFormats.Pdf/Extensions/PrimitiveExtensions.cs new file mode 100644 index 0000000..58b86ce --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/Extensions/PrimitiveExtensions.cs @@ -0,0 +1,16 @@ +using Synercoding.Primitives; + +namespace Synercoding.FileFormats.Pdf.Extensions +{ + public static class PrimitiveExtensions + { + public static Matrix AsPlacementMatrix(this Rectangle rectangle) + { + rectangle = rectangle.ConvertTo(Unit.Points); + return new Matrix( + rectangle.URX.Raw - rectangle.LLX.Raw, 0, + 0, rectangle.URY.Raw - rectangle.LLY.Raw, + rectangle.LLX.Raw, rectangle.LLY.Raw); + } + } +} diff --git a/src/Synercoding.FileFormats.Pdf/Extensions/StreamExtensions.cs b/src/Synercoding.FileFormats.Pdf/Extensions/StreamExtensions.cs deleted file mode 100644 index d3fc496..0000000 --- a/src/Synercoding.FileFormats.Pdf/Extensions/StreamExtensions.cs +++ /dev/null @@ -1,265 +0,0 @@ -using Synercoding.FileFormats.Pdf.Helpers; -using Synercoding.FileFormats.Pdf.PdfInternals; -using Synercoding.FileFormats.Pdf.Primitives; -using Synercoding.Primitives; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text; - -namespace Synercoding.FileFormats.Pdf.Extensions -{ - internal static class StreamExtensions - { - public static Stream IndirectStream(this Stream stream, PdfReference objectReference, Stream data, Action dictionaryAction, params StreamFilter[] streamFilters) - { - stream - ._startObject(objectReference) - .Dictionary(dictionary => - { - dictionary.Write("/Length", data.Length); - if(streamFilters != null && streamFilters.Length > 0) - { - if(streamFilters.Length == 1) - { - dictionary.Write("/Filter", streamFilters[0].ToPdfName()); - } - else - { - dictionary.Write("/Filter", streamPart => - { - streamPart - .WriteSingleByte(0x5B) // [ - .Space(); - - foreach(var filter in streamFilters) - { - streamPart - .Write(filter.ToPdfName()) - .Space(); - } - - streamPart - .WriteSingleByte(0x5D); // ] - }); - } - } - - dictionaryAction(dictionary); - }) - .Write("stream") - .NewLine() - ._copyFrom(data) - .NewLine() - .Write("endstream") - .NewLine() - ._endObject() - .NewLine(); - - return stream; - } - - public static Stream IndirectDictionary(this Stream stream, PdfReference objectReference, Action dictionaryAction) - { - stream - ._startObject(objectReference) - .Dictionary(dictionaryAction) - ._endObject() - .NewLine(); - - return stream; - } - - public static Stream Dictionary(this Stream stream, Action streamAction) - { - stream - ._startDictionary() - .NewLine(); - - streamAction(new PdfDictionary(stream)); - - stream - ._endDictionary() - .NewLine(); - - return stream; - } - - public static Stream WriteSingleByte(this Stream stream, byte value) - { - stream.WriteByte(value); - return stream; - } - - public static Stream Write(this Stream stream, Rectangle rectangle) - { - rectangle = rectangle.ConvertTo(Unit.Points); - return stream - .WriteSingleByte(0x5B) // [ - .Space() - .Write(rectangle.LLX.Raw) - .Space() - .Write(rectangle.LLY.Raw) - .Space() - .Write(rectangle.URX.Raw) - .Space() - .Write(rectangle.URY.Raw) - .Space() - .WriteSingleByte(0x5D); // ] - } - - public static Stream Write(this Stream stream, int value) - { - var intSize = ByteSizes.Size(value); - for (int i = intSize - 1; i >= 0; i--) - { - var result = (byte)( '0' + ( (int)( value / Math.Pow(10, i) ) % 10 ) ); - stream.WriteByte(result); - } - - return stream; - } - - public static Stream Write(this Stream stream, long value) - { - var intSize = ByteSizes.Size(value); - for (int i = intSize - 1; i >= 0; i--) - { - var result = (byte)( '0' + ( (int)( value / Math.Pow(10, i) ) % 10 ) ); - stream.WriteByte(result); - } - - return stream; - } - - public static Stream Write(this Stream stream, double value) - { - var stringValue = value.ToString("0.0########", CultureInfo.InvariantCulture); - var bytes = Encoding.ASCII.GetBytes(stringValue); - stream.Write(bytes, 0, bytes.Length); - - return stream; - } - - public static Stream Write(this Stream stream, string text) - { - var bytes = Encoding.ASCII.GetBytes(text); - stream.Write(bytes, 0, bytes.Length); - - return stream; - } - - public static Stream Write(this Stream stream, Matrix matrix) - { - stream - .Write(matrix.A) - .Space() - .Write(matrix.B) - .Space() - .Write(matrix.C) - .Space() - .Write(matrix.D) - .Space() - .Write(matrix.E) - .Space() - .Write(matrix.F) - .Space() - .Write("cm"); - - return stream; - } - - public static Stream Space(this Stream stream) - { - stream.WriteByte(0x20); // space - - return stream; - } - - public static Stream Write(this Stream stream, PdfReference objectReference) - { - return stream - .Write(objectReference.ObjectId) - .Space() - .Write(objectReference.Generation) - .Space() - .WriteSingleByte(0x52); // R - } - - public static Stream Write(this Stream stream, IEnumerable objectReferences) - { - stream.WriteByte(0x5B); // [ - - foreach(var objectReference in objectReferences) - { - stream.Write(objectReference); - stream.NewLine(); - } - - stream.WriteByte(0x5D); // ] - - return stream; - } - - public static Stream NewLine(this Stream stream) - { - stream.WriteByte(0x0D); - stream.WriteByte(0x0A); - - return stream; - } - - public static Stream EmptyDictionary(this Stream stream) - { - return stream._startDictionary().Space()._endDictionary(); - } - - private static Stream _startDictionary(this Stream stream) - { - stream.WriteByte(0x3C); - stream.WriteByte(0x3C); - - return stream; - } - - private static Stream _endDictionary(this Stream stream) - { - stream.WriteByte(0x3E); - stream.WriteByte(0x3E); - - return stream; - } - - private static Stream _startObject(this Stream stream, PdfReference objectReference) - { - return stream - .Write(objectReference.ObjectId) - .Space() - .Write(objectReference.Generation) - .Space() - .WriteSingleByte(0x6F) // o - .WriteSingleByte(0x62) // b - .WriteSingleByte(0x6A) // j - .NewLine(); - } - - private static Stream _endObject(this Stream stream) - { - stream.WriteByte(0x65); // e - stream.WriteByte(0x6E); // n - stream.WriteByte(0x64); // d - stream.WriteByte(0x6F); // o - stream.WriteByte(0x62); // b - stream.WriteByte(0x6A); // j - - return stream; - } - - private static Stream _copyFrom(this Stream stream, Stream data) - { - data.CopyTo(stream); - return stream; - } - } -} diff --git a/src/Synercoding.FileFormats.Pdf/Extensions/StreamFilterExtensions.cs b/src/Synercoding.FileFormats.Pdf/Extensions/StreamFilterExtensions.cs deleted file mode 100644 index f7a526f..0000000 --- a/src/Synercoding.FileFormats.Pdf/Extensions/StreamFilterExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Synercoding.FileFormats.Pdf.PdfInternals; -using System; - -namespace Synercoding.FileFormats.Pdf.Extensions -{ - internal static class StreamFilterExtensions - { - public static string ToPdfName(this StreamFilter streamFilter) - { - return streamFilter switch - { - StreamFilter.DCTDecode => "/DCTDecode", - _ => throw new NotImplementedException(), - }; - } - } -} diff --git a/src/Synercoding.FileFormats.Pdf/Helpers/ISpanWriteable.cs b/src/Synercoding.FileFormats.Pdf/Helpers/ISpanWriteable.cs deleted file mode 100644 index 33230e1..0000000 --- a/src/Synercoding.FileFormats.Pdf/Helpers/ISpanWriteable.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Synercoding.FileFormats.Pdf.Helpers -{ - internal interface ISpanWriteable - { - void FillSpan(Span bytes); - int ByteSize(); - } -} \ No newline at end of file diff --git a/src/Synercoding.FileFormats.Pdf/Helpers/IStreamWriteable.cs b/src/Synercoding.FileFormats.Pdf/Helpers/IStreamWriteable.cs deleted file mode 100644 index e78d0d1..0000000 --- a/src/Synercoding.FileFormats.Pdf/Helpers/IStreamWriteable.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.IO; - -namespace Synercoding.FileFormats.Pdf.Helpers -{ - /// - /// Interface that enables writting implementor to a stream - /// - public interface IStreamWriteable - { - /// - /// Write the object to the provided stream - /// - /// The stream to write to - /// The position in the stream where is object is written - uint WriteToStream(Stream stream); - } -} \ No newline at end of file diff --git a/src/Synercoding.FileFormats.Pdf/Helpers/MathHelper.cs b/src/Synercoding.FileFormats.Pdf/Helpers/MathHelper.cs deleted file mode 100644 index fe9e50c..0000000 --- a/src/Synercoding.FileFormats.Pdf/Helpers/MathHelper.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Synercoding.FileFormats.Pdf.Helpers -{ - internal static class MathHelper - { - public static double DegreeToRad(double degree) - { - return degree * Math.PI / 180; - } - } -} diff --git a/src/Synercoding.FileFormats.Pdf/Helpers/PdfTypeHelper.cs b/src/Synercoding.FileFormats.Pdf/Helpers/PdfTypeHelper.cs deleted file mode 100644 index 85fb603..0000000 --- a/src/Synercoding.FileFormats.Pdf/Helpers/PdfTypeHelper.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; - -namespace Synercoding.FileFormats.Pdf.Helpers -{ - internal static class PdfTypeHelper - { - public static string ToPdfHexadecimalString(string input) - { - var bytes = System.Text.Encoding.ASCII.GetBytes(input); - var builder = new System.Text.StringBuilder(bytes.Length + 2); - builder.Append("<"); - foreach (var b in bytes) - { - builder.Append(b.ToString("X2")); - } - builder.Append(">"); - return builder.ToString(); - } - - public static string ToPdfDate(DateTimeOffset input) - { - var datePart = input.ToString("yyyyMMddHHmmss"); - - var builder = new System.Text.StringBuilder(22); - builder.Append("(D:"); - builder.Append(datePart); - - var hours = input.Offset.Hours; - var minutes = input.Offset.Minutes; - - if (hours == 0 && minutes == 0) - { - builder.Append("Z00'00"); - } - else - { - if (hours > 0 || ( hours == 0 && minutes > 0 )) - { - builder.Append("+"); - } - else - { - builder.Append("-"); - } - builder.Append(Math.Abs(hours).ToString().PadLeft(2, '0')); - builder.Append("'"); - builder.Append(minutes.ToString().PadLeft(2, '0')); - } - builder.Append(")"); - - return builder.ToString(); - } - } -} diff --git a/src/Synercoding.FileFormats.Pdf/Image.cs b/src/Synercoding.FileFormats.Pdf/Image.cs new file mode 100644 index 0000000..1dd5541 --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/Image.cs @@ -0,0 +1,85 @@ +using SixLabors.ImageSharp; +using Synercoding.FileFormats.Pdf.LowLevel; +using Synercoding.FileFormats.Pdf.LowLevel.Extensions; +using System; +using System.IO; + +namespace Synercoding.FileFormats.Pdf +{ + public class Image : IPdfObject, IDisposable + { + private readonly Stream _imageStream; + private readonly int _width; + private readonly int _height; + + private bool _disposed; + private bool _isWritten; + + internal Image(PdfReference id, SixLabors.ImageSharp.Image image) + { + Reference = id; + + var ms = new MemoryStream(); + image.SaveAsJpeg(ms, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder() + { + Quality = 100, + Subsample = SixLabors.ImageSharp.Formats.Jpeg.JpegSubsample.Ratio420 + }); + _width = image.Width; + _height = image.Height; + ms.Position = 0; + _imageStream = ms; + } + + internal Image(PdfReference id, Stream jpgStream, int width, int height) + { + Reference = id; + + _width = width; + _height = height; + _imageStream = jpgStream; + } + + /// + public PdfReference Reference { get; private set; } + + /// + public void Dispose() + { + if (!_disposed) + { + _imageStream.Dispose(); + _disposed = true; + } + } + + internal bool TryWriteToStream(PdfStream stream, out uint position) + { + position = 0; + + if (_isWritten) + return false; + if (_disposed) + throw new ObjectDisposedException(nameof(_imageStream), "Internal image is already disposed"); + + position = (uint)stream.Position; + + stream.IndirectStream(this, _imageStream, this, static (image, dictionary) => + { + dictionary + .Type(ObjectType.XObject) + .SubType(XObjectSubType.Image) + .Write(PdfName.Get("Width"), image._width) + .Write(PdfName.Get("Height"), image._height) + .Write(PdfName.Get("ColorSpace"), PdfName.Get("DeviceRGB")) + .Write(PdfName.Get("BitsPerComponent"), 8) + .Write(PdfName.Get("Decode"), 0f, 1f, 0f, 1f, 0f, 1f); + }, + StreamFilter.DCTDecode); + + _isWritten = true; + + return true; + } + } +} diff --git a/src/Synercoding.FileFormats.Pdf/Helpers/ByteSizes.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/ByteSizes.cs similarity index 90% rename from src/Synercoding.FileFormats.Pdf/Helpers/ByteSizes.cs rename to src/Synercoding.FileFormats.Pdf/LowLevel/ByteSizes.cs index 44c2934..3e11bfc 100644 --- a/src/Synercoding.FileFormats.Pdf/Helpers/ByteSizes.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/ByteSizes.cs @@ -1,7 +1,7 @@ -using System; +using System; using System.Globalization; -namespace Synercoding.FileFormats.Pdf.Helpers +namespace Synercoding.FileFormats.Pdf.LowLevel { internal static class ByteSizes { diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/Catalog.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/Catalog.cs new file mode 100644 index 0000000..cbbf3a5 --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/Catalog.cs @@ -0,0 +1,38 @@ +using Synercoding.FileFormats.Pdf.LowLevel.Extensions; +using System; + +namespace Synercoding.FileFormats.Pdf.LowLevel +{ + internal class Catalog : IPdfObject + { + private bool _isWritten; + + public Catalog(PdfReference id, PageTree pageTree) + { + Reference = id; + PageTree = pageTree; + } + + public PdfReference Reference { get; } + + public PageTree PageTree { get; } + + internal uint WriteToStream(PdfStream stream) + { + if (_isWritten) + throw new InvalidOperationException("Object is already written to stream."); + + var position = (uint)stream.Position; + + stream.IndirectDictionary(this, static (catalog, dictionary) => + { + dictionary + .Type(ObjectType.Catalog) + .Write(PdfName.Get("Pages"), catalog.PageTree.Reference); + }); + _isWritten = true; + + return position; + } + } +} diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/ContentStream.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/ContentStream.cs new file mode 100644 index 0000000..6f9a02f --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/ContentStream.cs @@ -0,0 +1,87 @@ +using Synercoding.FileFormats.Pdf.LowLevel.Extensions; +using System; +using System.IO; + +namespace Synercoding.FileFormats.Pdf.LowLevel +{ + public class ContentStream : IPdfObject, IDisposable + { + private readonly MemoryStream _innerStream; + private readonly PdfStream _streamWrapper; + + private bool _isWritten; + + public ContentStream(PdfReference id) + { + _innerStream = new MemoryStream(); + _streamWrapper = new PdfStream(_innerStream); + Reference = id; + } + + public PdfReference Reference { get; } + + internal bool IsWritten => _isWritten; + + internal uint WriteToStream(PdfStream stream) + { + if (_isWritten) + { + throw new InvalidOperationException("Object is already written to stream."); + } + var position = (uint)stream.Position; + + _innerStream.Position = 0; + stream.IndirectStream(this, _innerStream); + _isWritten = true; + + return position; + } + + public void Dispose() + { + _streamWrapper.Dispose(); + } + + public ContentStream SaveState() + { + _streamWrapper.Write('q').NewLine(); + + return this; + } + + public ContentStream RestoreState() + { + _streamWrapper.Write('Q').NewLine(); + + return this; + } + + public ContentStream CTM(Matrix matrix) + { + _streamWrapper + .Write(matrix.A) + .Space() + .Write(matrix.B) + .Space() + .Write(matrix.C) + .Space() + .Write(matrix.D) + .Space() + .Write(matrix.E) + .Space() + .Write(matrix.F) + .Space() + .Write("cm") + .NewLine(); + + return this; + } + + public ContentStream Paint(PdfName resource) + { + _streamWrapper.Write(resource).Space().Write("Do").NewLine(); + + return this; + } + } +} diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/PdfStreamArrayExtensions.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/PdfStreamArrayExtensions.cs new file mode 100644 index 0000000..be8e829 --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/PdfStreamArrayExtensions.cs @@ -0,0 +1,533 @@ +using System.Collections.Generic; + +namespace Synercoding.FileFormats.Pdf.LowLevel.Extensions +{ + public static class PdfStreamArrayExtensions + { + private const byte BRACKET_OPEN = 0x5B; // [ + private const byte BRACKET_CLOSE = 0x5D; // ] + + public static PdfStream Write(this PdfStream stream, double[] array) + { + stream + .WriteByte(BRACKET_OPEN) + .Space(); + + foreach (var number in array) + stream.Write(number).Space(); + + stream.WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, double number1, double number2) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(number1) + .Space() + .Write(number2) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, double number1, double number2, double number3) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(number1) + .Space() + .Write(number2) + .Space() + .Write(number3) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, double number1, double number2, double number3, double number4) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(number1) + .Space() + .Write(number2) + .Space() + .Write(number3) + .Space() + .Write(number4) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, double number1, double number2, double number3, double number4, double number5) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(number1) + .Space() + .Write(number2) + .Space() + .Write(number3) + .Space() + .Write(number4) + .Space() + .Write(number5) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, double number1, double number2, double number3, double number4, double number5, double number6) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(number1) + .Space() + .Write(number2) + .Space() + .Write(number3) + .Space() + .Write(number4) + .Space() + .Write(number5) + .Space() + .Write(number6) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, + double number1, + double number2, + double number3, + double number4, + double number5, + double number6, + double number7) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(number1) + .Space() + .Write(number2) + .Space() + .Write(number3) + .Space() + .Write(number4) + .Space() + .Write(number5) + .Space() + .Write(number6) + .Space() + .Write(number7) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, + double number1, + double number2, + double number3, + double number4, + double number5, + double number6, + double number7, + double number8) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(number1) + .Space() + .Write(number2) + .Space() + .Write(number3) + .Space() + .Write(number4) + .Space() + .Write(number5) + .Space() + .Write(number6) + .Space() + .Write(number7) + .Space() + .Write(number8) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, int[] array) + { + stream + .WriteByte(BRACKET_OPEN) + .Space(); + + foreach (var number in array) + stream.Write(number).Space(); + + stream.WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, int number1, int number2) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(number1) + .Space() + .Write(number2) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, int number1, int number2, int number3) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(number1) + .Space() + .Write(number2) + .Space() + .Write(number3) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, int number1, int number2, int number3, int number4) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(number1) + .Space() + .Write(number2) + .Space() + .Write(number3) + .Space() + .Write(number4) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, int number1, int number2, int number3, int number4, int number5) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(number1) + .Space() + .Write(number2) + .Space() + .Write(number3) + .Space() + .Write(number4) + .Space() + .Write(number5) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, int number1, int number2, int number3, int number4, int number5, int number6) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(number1) + .Space() + .Write(number2) + .Space() + .Write(number3) + .Space() + .Write(number4) + .Space() + .Write(number5) + .Space() + .Write(number6) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, + int number1, + int number2, + int number3, + int number4, + int number5, + int number6, + int number7) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(number1) + .Space() + .Write(number2) + .Space() + .Write(number3) + .Space() + .Write(number4) + .Space() + .Write(number5) + .Space() + .Write(number6) + .Space() + .Write(number7) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, + int number1, + int number2, + int number3, + int number4, + int number5, + int number6, + int number7, + int number8) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(number1) + .Space() + .Write(number2) + .Space() + .Write(number3) + .Space() + .Write(number4) + .Space() + .Write(number5) + .Space() + .Write(number6) + .Space() + .Write(number7) + .Space() + .Write(number8) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, IEnumerable objectReferences) + { + stream.WriteByte(BRACKET_OPEN).Space(); + + foreach (var objectReference in objectReferences) + { + stream.Write(objectReference); + stream.Space(); + } + + stream.WriteByte(BRACKET_CLOSE).NewLine(); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, PdfReference reference1, PdfReference reference2) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(reference1) + .Space() + .Write(reference2) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, PdfReference reference1, PdfReference reference2, PdfReference reference3) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(reference1) + .Space() + .Write(reference2) + .Space() + .Write(reference3) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, + PdfReference reference1, + PdfReference reference2, + PdfReference reference3, + PdfReference reference4) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(reference1) + .Space() + .Write(reference2) + .Space() + .Write(reference3) + .Space() + .Write(reference4) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, + PdfReference reference1, + PdfReference reference2, + PdfReference reference3, + PdfReference reference4, + PdfReference reference5) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(reference1) + .Space() + .Write(reference2) + .Space() + .Write(reference3) + .Space() + .Write(reference4) + .Space() + .Write(reference5) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, + PdfReference reference1, + PdfReference reference2, + PdfReference reference3, + PdfReference reference4, + PdfReference reference5, + PdfReference reference6) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(reference1) + .Space() + .Write(reference2) + .Space() + .Write(reference3) + .Space() + .Write(reference4) + .Space() + .Write(reference5) + .Space() + .Write(reference6) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, + PdfReference reference1, + PdfReference reference2, + PdfReference reference3, + PdfReference reference4, + PdfReference reference5, + PdfReference reference6, + PdfReference reference7) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(reference1) + .Space() + .Write(reference2) + .Space() + .Write(reference3) + .Space() + .Write(reference4) + .Space() + .Write(reference5) + .Space() + .Write(reference6) + .Space() + .Write(reference7) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + + public static PdfStream Write(this PdfStream stream, + PdfReference reference1, + PdfReference reference2, + PdfReference reference3, + PdfReference reference4, + PdfReference reference5, + PdfReference reference6, + PdfReference reference7, + PdfReference reference8) + { + stream + .WriteByte(BRACKET_OPEN) + .Space() + .Write(reference1) + .Space() + .Write(reference2) + .Space() + .Write(reference3) + .Space() + .Write(reference4) + .Space() + .Write(reference5) + .Space() + .Write(reference6) + .Space() + .Write(reference7) + .Space() + .Write(reference8) + .Space() + .WriteByte(BRACKET_CLOSE); + + return stream; + } + } +} diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/PdfStreamExtensions.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/PdfStreamExtensions.cs new file mode 100644 index 0000000..852491c --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/PdfStreamExtensions.cs @@ -0,0 +1,155 @@ +using Synercoding.Primitives; +using System; +using System.IO; + +namespace Synercoding.FileFormats.Pdf.LowLevel.Extensions +{ + public static class PdfStreamExtensions + { + public static PdfStream Space(this PdfStream stream) + => stream.Write(' '); + + public static PdfStream NewLine(this PdfStream stream) + => stream.Write('\r').Write('\n'); + + public static PdfStream Write(this PdfStream stream, PdfName name) + => stream.Write(name.ToString()); + + public static PdfStream IndirectStream(this PdfStream contentStream, TPdfObject pdfObject, Stream stream, params StreamFilter[] streamFilters) + where TPdfObject : IPdfObject + => contentStream.IndirectStream(pdfObject, stream, _ => { }, streamFilters); + + public static PdfStream IndirectStream(this PdfStream contentStream, TPdfObject pdfObject, Stream stream, Action dictionaryAction, params StreamFilter[] streamFilters) + where TPdfObject : IPdfObject + => contentStream.IndirectStream(pdfObject, stream, dictionaryAction, static (action, dict) => action(dict), streamFilters); + + public static PdfStream IndirectStream(this PdfStream contentStream, TPdfObject pdfObject, Stream stream, TData data, Action dictionaryAction, params StreamFilter[] streamFilters) + where TPdfObject : IPdfObject + where TData : notnull + { + contentStream + .StartObject(pdfObject.Reference) + .Dictionary((stream, data, dictionaryAction, streamFilters), static (tuple, dictionary) => + { + dictionary.Write(PdfName.Get("Length"), tuple.stream.Length); + if (tuple.streamFilters != null && tuple.streamFilters.Length > 0) + { + if (tuple.streamFilters.Length == 1) + { + dictionary.Write(PdfName.Get("Filter"), tuple.streamFilters[0].ToPdfName()); + } + else + { + dictionary.Write(PdfName.Get("Filter"), tuple.streamFilters, static (filters, streamPart) => + { + streamPart + .WriteByte(0x5B) // [ + .Space(); + + foreach (var filter in filters) + { + streamPart + .Write(filter.ToPdfName()) + .Space(); + } + + streamPart + .WriteByte(0x5D); // ] + }); + } + } + + tuple.dictionaryAction(tuple.data, dictionary); + }) + .Write("stream") + .NewLine() + .CopyFrom(stream) + .NewLine() + .Write("endstream") + .NewLine() + .EndObject() + .NewLine(); + + return contentStream; + } + + public static PdfStream CopyFrom(this PdfStream stream, Stream data) + { + data.CopyTo(stream.InnerStream); + return stream; + } + + public static PdfStream IndirectDictionary(this PdfStream stream, TPdfObject pdfObject, Action dictionaryAction) + where TPdfObject : IPdfObject + { + return stream + .StartObject(pdfObject.Reference) + .Dictionary(pdfObject, dictionaryAction) + .EndObject() + .NewLine(); + } + + public static PdfStream Dictionary(this PdfStream stream, T data, Action streamAction) + { + stream + .Write('<') + .Write('<') + .NewLine(); + + streamAction(data, new PdfDictionary(stream)); + + stream + .Write('>') + .Write('>') + .NewLine(); + + return stream; + } + + public static PdfStream Dictionary(this PdfStream stream, Action streamAction) + => stream.Dictionary(streamAction, static (action, dict) => action(dict)); + + public static PdfStream EmptyDictionary(this PdfStream stream) + => stream.Dictionary(_ => { }); + + public static PdfStream Write(this PdfStream stream, PdfReference objectReference) + { + return stream + .Write(objectReference.ObjectId) + .Space() + .Write(objectReference.Generation) + .Space() + .Write('R'); + } + + public static PdfStream Write(this PdfStream stream, Rectangle rectangle) + { + rectangle = rectangle.ConvertTo(Unit.Points); + + return stream.Write(rectangle.LLX.Raw, rectangle.LLY.Raw, rectangle.URX.Raw, rectangle.URY.Raw); + } + public static PdfStream StartObject(this PdfStream stream, PdfReference objectReference) + { + return stream + .Write(objectReference.ObjectId) + .Space() + .Write(objectReference.Generation) + .Space() + .Write('o') + .Write('b') + .Write('j') + .NewLine(); + } + + public static PdfStream EndObject(this PdfStream stream) + { + return stream + .Write('e') + .Write('n') + .Write('d') + .Write('o') + .Write('b') + .Write('j'); + } + } +} diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/StreamFilterExtensions.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/StreamFilterExtensions.cs new file mode 100644 index 0000000..b667e88 --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/StreamFilterExtensions.cs @@ -0,0 +1,16 @@ +using System; + +namespace Synercoding.FileFormats.Pdf.LowLevel.Extensions +{ + internal static class StreamFilterExtensions + { + public static PdfName ToPdfName(this StreamFilter streamFilter) + { + return streamFilter switch + { + StreamFilter.DCTDecode => PdfName.Get("DCTDecode"), + _ => throw new NotImplementedException(), + }; + } + } +} diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/IPdfObject.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/IPdfObject.cs new file mode 100644 index 0000000..986e721 --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/IPdfObject.cs @@ -0,0 +1,13 @@ +namespace Synercoding.FileFormats.Pdf.LowLevel +{ + /// + /// Interface representing a pdf object + /// + public interface IPdfObject + { + /// + /// A pdf reference object that can be used to reference to this object + /// + PdfReference Reference { get; } + } +} diff --git a/src/Synercoding.FileFormats.Pdf/Helpers/IdGenerator.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/IdGenerator.cs similarity index 87% rename from src/Synercoding.FileFormats.Pdf/Helpers/IdGenerator.cs rename to src/Synercoding.FileFormats.Pdf/LowLevel/IdGenerator.cs index 16090e3..c8fb862 100644 --- a/src/Synercoding.FileFormats.Pdf/Helpers/IdGenerator.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/IdGenerator.cs @@ -1,4 +1,4 @@ -namespace Synercoding.FileFormats.Pdf.Helpers +namespace Synercoding.FileFormats.Pdf.LowLevel { internal sealed class IdGenerator { diff --git a/src/Synercoding.FileFormats.Pdf/Helpers/SpanHelper.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/Internals/SpanHelper.cs similarity index 75% rename from src/Synercoding.FileFormats.Pdf/Helpers/SpanHelper.cs rename to src/Synercoding.FileFormats.Pdf/LowLevel/Internals/SpanHelper.cs index a6cf7a3..e6b9f80 100644 --- a/src/Synercoding.FileFormats.Pdf/Helpers/SpanHelper.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/Internals/SpanHelper.cs @@ -1,7 +1,7 @@ -using System; +using System; -namespace Synercoding.FileFormats.Pdf.Helpers -{ +namespace Synercoding.FileFormats.Pdf.LowLevel.Internals +{ internal static class SpanHelper { public static int FillSpan(Span span, int integer) @@ -10,7 +10,7 @@ public static int FillSpan(Span span, int integer) var intSize = ByteSizes.Size(integer); for (int i = intSize - 1; i >= 0; i--) { - span[i] = (byte)( '0' + ( val % 10 ) ); + span[i] = (byte)('0' + (val % 10)); val = val / 10; } return intSize; @@ -22,7 +22,7 @@ public static int FillSpan(Span span, uint integer) var intSize = ByteSizes.Size(integer); for (int i = intSize - 1; i >= 0; i--) { - span[i] = (byte)( '0' + ( val % 10 ) ); + span[i] = (byte)('0' + (val % 10)); val = val / 10; } return intSize; @@ -42,13 +42,5 @@ public static int WriteIndirectObjectRef(Span bytes, int objectRef, int ge return position; } - - public static int WriteNewLine(Span bytes) - { - bytes[0] = 0x0D; // CR - bytes[1] = 0x0A; // LF - - return 2; - } } } diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/ObjectType.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/ObjectType.cs new file mode 100644 index 0000000..a8076bd --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/ObjectType.cs @@ -0,0 +1,10 @@ +namespace Synercoding.FileFormats.Pdf.LowLevel +{ + public enum ObjectType + { + Page, + Pages, + Catalog, + XObject + } +} \ No newline at end of file diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/PageTree.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/PageTree.cs new file mode 100644 index 0000000..83339c2 --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/PageTree.cs @@ -0,0 +1,48 @@ +using Synercoding.FileFormats.Pdf.LowLevel.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Synercoding.FileFormats.Pdf.LowLevel +{ + internal class PageTree : IPdfObject + { + private readonly IList _pages; + private bool _isWritten; + + public PageTree(PdfReference id) + : this(id, new List()) + { } + + public PageTree(PdfReference id, IList pages) + { + Reference = id; + _pages = pages; + } + + public PdfReference Reference { get; } + + public void AddPage(PdfPage pdfObject) + => _pages.Add(pdfObject); + + public uint WriteToStream(PdfStream stream) + { + if (_isWritten) + { + throw new InvalidOperationException("Object is already written to stream."); + } + var position = (uint)stream.Position; + + stream.IndirectDictionary(this, static (pageTree, dictionary) => + { + dictionary + .Type(ObjectType.Pages) + .Write(PdfName.Get("Kids"), pageTree._pages.Select(static p => p.Reference)) + .Write(PdfName.Get("Count"), pageTree._pages.Count()); + }); + _isWritten = true; + + return position; + } + } +} diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/PdfDictionary.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfDictionary.cs new file mode 100644 index 0000000..f8e122b --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfDictionary.cs @@ -0,0 +1,281 @@ +using Synercoding.FileFormats.Pdf.LowLevel.Extensions; +using Synercoding.Primitives; +using System; +using System.Collections.Generic; + +namespace Synercoding.FileFormats.Pdf.LowLevel +{ + public class PdfDictionary + { + private readonly PdfStream _stream; + + public PdfDictionary(PdfStream stream) + { + _stream = stream; + } + + public PdfDictionary SubType(XObjectSubType subType) + { + var nameValue = subType switch + { + XObjectSubType.Image => PdfName.Get("Image"), + _ => throw new NotImplementedException("Unknown XObjectSubType: " + subType) + }; + + _stream + .Write(PdfName.Get("Subtype")) + .Space() + .Write(nameValue) + .NewLine(); + + return this; + } + + public PdfDictionary Type(ObjectType objectType) + { + var nameValue = objectType switch + { + ObjectType.Catalog => PdfName.Get("Catalog"), + ObjectType.Page => PdfName.Get("Page"), + ObjectType.Pages => PdfName.Get("Pages"), + ObjectType.XObject => PdfName.Get("XObject"), + _ => throw new NotImplementedException("Unknown objectType: " + objectType) + }; + + _stream + .Write(PdfName.Get("Type")) + .Space() + .Write(nameValue) + .NewLine(); + + return this; + } + + public PdfDictionary Write(PdfName key, IEnumerable objectReferences) + { + _stream + .Write(key) + .Space() + .Write(objectReferences) + .NewLine(); + + return this; + } + + public PdfDictionary Write(PdfName key, double value) + { + _stream + .Write(key) + .Space() + .Write(value) + .NewLine(); + + return this; + } + + public PdfDictionary Write(PdfName key, double value1, double value2) + { + _stream + .Write(key) + .Space() + .Write(value1, value2) + .NewLine(); + + return this; + } + + public PdfDictionary Write(PdfName key, double value1, double value2, double value3) + { + _stream + .Write(key) + .Space() + .Write(value1, value2, value3) + .NewLine(); + + return this; + } + + public PdfDictionary Write(PdfName key, double value1, double value2, double value3, double value4) + { + _stream + .Write(key) + .Space() + .Write(value1, value2, value3, value4) + .NewLine(); + + return this; + } + + public PdfDictionary Write(PdfName key, double value1, double value2, double value3, double value4, double value5) + { + _stream + .Write(key) + .Space() + .Write(value1, value2, value3, value4, value5) + .NewLine(); + + return this; + } + + public PdfDictionary Write(PdfName key, double value1, double value2, double value3, double value4, double value5, double value6) + { + _stream + .Write(key) + .Space() + .Write(value1, value2, value3, value4, value5, value6) + .NewLine(); + + return this; + } + + public PdfDictionary Write(PdfName key, long value) + { + _stream + .Write(key) + .Space() + .Write(value) + .NewLine(); + + return this; + } + + public PdfDictionary Write(PdfName key, int value) + { + _stream + .Write(key) + .Space() + .Write(value) + .NewLine(); + + return this; + } + + public PdfDictionary Write(PdfName key, int value1, int value2) + { + _stream + .Write(key) + .Space() + .Write(value1, value2) + .NewLine(); + + return this; + } + + public PdfDictionary Write(PdfName key, int value1, int value2, int value3) + { + _stream + .Write(key) + .Space() + .Write(value1, value2, value3) + .NewLine(); + + return this; + } + + public PdfDictionary Write(PdfName key, int value1, int value2, int value3, int value4) + { + _stream + .Write(key) + .Space() + .Write(value1, value2, value3, value4) + .NewLine(); + + return this; + } + + public PdfDictionary Write(PdfName key, int value1, int value2, int value3, int value4, int value5) + { + _stream + .Write(key) + .Space() + .Write(value1, value2, value3, value4, value5) + .NewLine(); + + return this; + } + + public PdfDictionary Write(PdfName key, int value1, int value2, int value3, int value4, int value5, int value6) + { + _stream + .Write(key) + .Space() + .Write(value1, value2, value3, value4, value5, value6) + .NewLine(); + + return this; + } + + public PdfDictionary Write(PdfName key, string value) + { + _stream + .Write(key) + .Space() + .Write(value) + .NewLine(); + + return this; + } + public PdfDictionary Write(PdfName key, PdfName value) + { + _stream + .Write(key) + .Space() + .Write(value) + .NewLine(); + + return this; + } + + public PdfDictionary Write(PdfName key, PdfReference objectReference) + { + _stream + .Write(key) + .Space() + .Write(objectReference) + .NewLine(); + + return this; + } + + public PdfDictionary Write(PdfName key, T data, Action rawActions) + { + _stream + .Write(key) + .Space(); + rawActions(data, _stream); + _stream.NewLine(); + + return this; + } + + public PdfDictionary Write(PdfName key, Action rawActions) + => Write(key, rawActions, static (action, stream) => action(stream)); + + public PdfDictionary Write(PdfName key, Rectangle rectangle) + { + _stream + .Write(key) + .Space() + .Write(rectangle) + .NewLine(); + + return this; + } + + public PdfDictionary WriteIfNotNull(PdfName key, Rectangle? rectangle) + { + if (!rectangle.HasValue) + return this; + + return Write(key, rectangle.Value); + } + + public PdfDictionary WriteIfNotNull(PdfName key, int? value) + { + if (!value.HasValue) + return this; + + return Write(key, value.Value); + } + } +} \ No newline at end of file diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/PdfName.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfName.cs new file mode 100644 index 0000000..d843261 --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfName.cs @@ -0,0 +1,117 @@ +using Synercoding.FileFormats.Pdf.LowLevel.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Synercoding.FileFormats.Pdf.LowLevel +{ + public class PdfName + { + private static IDictionary _reservedNames = new Dictionary() + { + { "Author", new PdfName("Author") }, + { "BitsPerComponent", new PdfName("BitsPerComponent") }, + { "BleedBox", new PdfName("BleedBox") }, + { "Catalog", new PdfName("Catalog") }, + { "ColorSpace", new PdfName("ColorSpace") }, + { "Contents", new PdfName("Contents") }, + { "Count", new PdfName("Count") }, + { "CreationDate", new PdfName("CreationDate") }, + { "CropBox", new PdfName("CropBox") }, + { "DCTDecode", new PdfName("DCTDecode") }, + { "Decode", new PdfName("Decode") }, + { "DeviceRGB", new PdfName("DeviceRGB") }, + { "Filter", new PdfName("Filter") }, + { "Height", new PdfName("Height") }, + { "Image", new PdfName("Image") }, + { "Info", new PdfName("Info") }, + { "Kids", new PdfName("Kids") }, + { "Length", new PdfName("Length") }, + { "MediaBox", new PdfName("MediaBox") }, + { "Page", new PdfName("Page") }, + { "Pages", new PdfName("Pages") }, + { "Parent", new PdfName("Parent") }, + { "Producer", new PdfName("Producer") }, + { "Resources", new PdfName("Resources") }, + { "Root", new PdfName("Root") }, + { "Rotate", new PdfName("Rotate") }, + { "Size", new PdfName("Size") }, + { "Subtype", new PdfName("Subtype") }, + { "Title", new PdfName("Title") }, + { "TrimBox", new PdfName("TrimBox") }, + { "Type", new PdfName("Type") }, + { "Width", new PdfName("Width") }, + { "XObject", new PdfName("XObject") }, + }; + + public static PdfName Get(string name) + { + if (_reservedNames.TryGetValue(name, out var value)) + return value; + + System.Diagnostics.Debug.WriteLine($"{{ \"{name}\", new PdfName(\"{name}\") }},"); + + return new PdfName(name); + } + + private readonly string _encoded; + + private PdfName(string raw) + { + if (raw.Length > 127) + throw new ArgumentOutOfRangeException(nameof(raw), "The name is too long. Max length of 127 is allowed."); + if (raw.Any(c => c > 0xff)) + throw new ArgumentOutOfRangeException(nameof(raw), "The name contains non-ascii characters, and is thus not allowed."); + _encoded = _encode(raw); + } + + public override string ToString() + => _encoded; + + private static string _encode(string input) + { + int length = input.Length; + + var pdfName = new StringBuilder(length + 20); + pdfName.Append('/'); + var chars = input.ToCharArray(); + + foreach (var c in chars) + { + // 0xff because only ascii is supported in pdf names + var encoded = c switch + { + // Pdf reserved characters + ' ' => "#20", + '#' => "#23", + '%' => "#25", + '(' => "#28", + ')' => "#29", + '/' => "#2f", + '<' => "#3c", + '>' => "#3e", + '[' => "#5b", + ']' => "#5d", + '{' => "#7b", + '}' => "#7d", + // special characters + var cc when cc < 16 => $"#0{Convert.ToString(cc, 16)}", + var cc when cc > 126 || cc < 32 => '#' + Convert.ToString(cc, 16), + // "readable characters" + var cc => cc.ToString() + }; + pdfName.Append(encoded); + } + + return pdfName.ToString(); + } + + public uint WriteToStream(PdfStream stream) + { + var position = stream.Position; + stream.Write(this); + return position; + } + } +} diff --git a/src/Synercoding.FileFormats.Pdf/PdfInternals/PdfReference.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfReference.cs similarity index 76% rename from src/Synercoding.FileFormats.Pdf/PdfInternals/PdfReference.cs rename to src/Synercoding.FileFormats.Pdf/LowLevel/PdfReference.cs index ebc1a98..202092c 100644 --- a/src/Synercoding.FileFormats.Pdf/PdfInternals/PdfReference.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfReference.cs @@ -1,4 +1,4 @@ -namespace Synercoding.FileFormats.Pdf.PdfInternals +namespace Synercoding.FileFormats.Pdf.LowLevel { /// /// A struct representing a reference @@ -9,7 +9,7 @@ public readonly struct PdfReference /// Constructor for that uses generation 0 /// /// The id of the reference - public PdfReference(int objectId) + internal PdfReference(int objectId) : this(objectId, 0) { } @@ -18,7 +18,7 @@ public PdfReference(int objectId) /// /// The id of the reference /// The generation of the reference - public PdfReference(int objectId, int generation) + internal PdfReference(int objectId, int generation) { ObjectId = objectId; Generation = generation; @@ -28,9 +28,16 @@ public PdfReference(int objectId, int generation) /// The object id of this reference /// public int ObjectId { get; } + /// /// The generation of this reference /// public int Generation { get; } + + /// + public override string ToString() + { + return $"{ObjectId} {Generation}"; + } } -} +} \ No newline at end of file diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/PdfStream.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfStream.cs new file mode 100644 index 0000000..b05d8ae --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfStream.cs @@ -0,0 +1,97 @@ +using System; +using System.Globalization; +using System.IO; +using System.Text; + +namespace Synercoding.FileFormats.Pdf.LowLevel +{ + public class PdfStream : IDisposable + { + private const string NUMBER_STRING_FORMAT = "0.0########"; + + public PdfStream(Stream stream) + => InnerStream = stream; + + public uint Position + => (uint)InnerStream.Position; + + protected internal Stream InnerStream { get; } + + public PdfStream WriteByte(byte b) + { + InnerStream.WriteByte(b); + + return this; + } + + public PdfStream Write(char c) + { + return WriteByte((byte)(c & 0xFF)); + } + + public PdfStream Write(int value) + { + var intSize = ByteSizes.Size(value); + for (int i = intSize - 1; i >= 0; i--) + { + var result = (byte)('0' + ((int)(value / Math.Pow(10, i)) % 10)); + InnerStream.WriteByte(result); + } + + return this; + } + + public PdfStream Write(long value) + { + var intSize = ByteSizes.Size(value); + for (int i = intSize - 1; i >= 0; i--) + { + var result = (byte)('0' + ((int)(value / Math.Pow(10, i)) % 10)); + InnerStream.WriteByte(result); + } + + return this; + } + + public PdfStream Write(double value) + { + var stringValue = value.ToString(NUMBER_STRING_FORMAT, CultureInfo.InvariantCulture); + var bytes = Encoding.ASCII.GetBytes(stringValue); + InnerStream.Write(bytes, 0, bytes.Length); + + return this; + } + + public PdfStream Write(string text) + { + var bytes = Encoding.ASCII.GetBytes(text); + InnerStream.Write(bytes, 0, bytes.Length); + + return this; + } + + public PdfStream Write(byte[] buffer, int offset, int count) + { + InnerStream.Write(buffer, offset, count); + + return this; + } + + public PdfStream Write(ReadOnlySpan buffer) + { + InnerStream.Write(buffer); + + return this; + } + + public void Flush() + { + InnerStream.Flush(); + } + + public void Dispose() + { + InnerStream.Dispose(); + } + } +} diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/StreamFilter.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/StreamFilter.cs new file mode 100644 index 0000000..a6fb7ab --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/StreamFilter.cs @@ -0,0 +1,7 @@ +namespace Synercoding.FileFormats.Pdf.LowLevel +{ + public enum StreamFilter + { + DCTDecode + } +} diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/XObjectSubType.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/XObjectSubType.cs new file mode 100644 index 0000000..7d6287b --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/XObjectSubType.cs @@ -0,0 +1,7 @@ +namespace Synercoding.FileFormats.Pdf.LowLevel +{ + public enum XObjectSubType + { + Image + } +} \ No newline at end of file diff --git a/src/Synercoding.FileFormats.Pdf/PdfInternals/XRef/Entry.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/XRef/Entry.cs similarity index 71% rename from src/Synercoding.FileFormats.Pdf/PdfInternals/XRef/Entry.cs rename to src/Synercoding.FileFormats.Pdf/LowLevel/XRef/Entry.cs index f43b45a..9a139e6 100644 --- a/src/Synercoding.FileFormats.Pdf/PdfInternals/XRef/Entry.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/XRef/Entry.cs @@ -1,9 +1,8 @@ -using Synercoding.FileFormats.Pdf.Helpers; -using System; +using System; -namespace Synercoding.FileFormats.Pdf.PdfInternals.XRef +namespace Synercoding.FileFormats.Pdf.LowLevel.XRef { - internal struct Entry : ISpanWriteable + internal struct Entry { public Entry(uint data) : this(data, 0, false) @@ -29,8 +28,9 @@ public void FillSpan(Span bytes) _fillSpanLeadingZero(bytes.Slice(11, 5), GenerationNumber); bytes[16] = 0x20; - bytes[17] = IsFree ? (byte)0x66 : (byte)0x6E; - SpanHelper.WriteNewLine(bytes.Slice(18)); + bytes[17] = IsFree ? 0x66 : 0x6E; + bytes[18] = 0x0D; + bytes[19] = 0x0A; } public int ByteSize() @@ -43,8 +43,8 @@ private void _fillSpanLeadingZero(Span span, uint data) uint val = data; for (int i = span.Length - 1; i >= 0; i--) { - span[i] = (byte)( '0' + ( val % 10 ) ); - val = val / 10; + span[i] = (byte)('0' + (val % 10)); + val /= 10; } } @@ -53,8 +53,8 @@ private void _fillSpanLeadingZero(Span span, ushort data) int val = data; for (int i = span.Length - 1; i >= 0; i--) { - span[i] = (byte)( '0' + ( val % 10 ) ); - val = val / 10; + span[i] = (byte)('0' + (val % 10)); + val /= 10; } } } diff --git a/src/Synercoding.FileFormats.Pdf/PdfInternals/XRef/Section.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/XRef/Section.cs similarity index 84% rename from src/Synercoding.FileFormats.Pdf/PdfInternals/XRef/Section.cs rename to src/Synercoding.FileFormats.Pdf/LowLevel/XRef/Section.cs index b86a01a..19b55f4 100644 --- a/src/Synercoding.FileFormats.Pdf/PdfInternals/XRef/Section.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/XRef/Section.cs @@ -1,9 +1,9 @@ -using Synercoding.FileFormats.Pdf.Helpers; +using Synercoding.FileFormats.Pdf.LowLevel.Internals; using System; -namespace Synercoding.FileFormats.Pdf.PdfInternals.XRef +namespace Synercoding.FileFormats.Pdf.LowLevel.XRef { - internal class Section : ISpanWriteable + internal class Section { public Section(int firstObjectNumber, params Entry[] entries) { @@ -26,7 +26,7 @@ public void FillSpan(Span bytes) for (int i = 0; i < Entries.Length; i++) { - var position = bytesFilled + 2 + ( i * 20 ); + var position = bytesFilled + 2 + (i * 20); Entries[i].FillSpan(bytes.Slice(position, 20)); } } @@ -41,4 +41,4 @@ public int ByteSize() return size; } } -} \ No newline at end of file +} diff --git a/src/Synercoding.FileFormats.Pdf/PdfInternals/XRef/Table.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/XRef/Table.cs similarity index 67% rename from src/Synercoding.FileFormats.Pdf/PdfInternals/XRef/Table.cs rename to src/Synercoding.FileFormats.Pdf/LowLevel/XRef/Table.cs index c148a7f..ae42c82 100644 --- a/src/Synercoding.FileFormats.Pdf/PdfInternals/XRef/Table.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/XRef/Table.cs @@ -1,10 +1,8 @@ -using Synercoding.FileFormats.Pdf.Helpers; -using System; -using System.IO; +using System; -namespace Synercoding.FileFormats.Pdf.PdfInternals.XRef +namespace Synercoding.FileFormats.Pdf.LowLevel.XRef { - internal class Table : ISpanWriteable, IStreamWriteable + internal class Table { public Table(params Entry[] entries) { @@ -16,7 +14,18 @@ public Table(params Entry[] entries) public Section Section { get; } - public void FillSpan(Span bytes) + internal uint WriteToStream(PdfStream stream) + { + var position = (uint)stream.Position; + + var bytes = new byte[_byteSize()]; + _fillSpan(bytes); + stream.Write(bytes); + + return position; + } + + private void _fillSpan(Span bytes) { bytes[0] = 0x78; // x bytes[1] = 0x72; // r @@ -29,23 +38,12 @@ public void FillSpan(Span bytes) Section.FillSpan(bytes.Slice(6, freeSize)); } - public int ByteSize() + private int _byteSize() { int size = 6; // xref + CR LF size += Section.ByteSize(); return size; } - - public uint WriteToStream(Stream stream) - { - var position = (uint)stream.Position; - - var bytes = new byte[ByteSize()]; - FillSpan(bytes); - stream.Write(bytes, 0, bytes.Length); - - return position; - } } -} \ No newline at end of file +} diff --git a/src/Synercoding.FileFormats.Pdf/PdfInternals/XRef/TableBuilder.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/XRef/TableBuilder.cs similarity index 75% rename from src/Synercoding.FileFormats.Pdf/PdfInternals/XRef/TableBuilder.cs rename to src/Synercoding.FileFormats.Pdf/LowLevel/XRef/TableBuilder.cs index 9bbacf5..fb1245d 100644 --- a/src/Synercoding.FileFormats.Pdf/PdfInternals/XRef/TableBuilder.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/XRef/TableBuilder.cs @@ -1,8 +1,8 @@ -using Synercoding.FileFormats.Pdf.Helpers; +using System; using System.Collections.Generic; using System.Linq; -namespace Synercoding.FileFormats.Pdf.PdfInternals.XRef +namespace Synercoding.FileFormats.Pdf.LowLevel.XRef { internal class TableBuilder { @@ -27,12 +27,15 @@ public PdfReference GetId(uint position) public void SetPosition(PdfReference id, uint position) { + if (_positions[id] != -1) + throw new InvalidOperationException($"Reference {id} already has a position in the xref table."); + _positions[id] = position; } public bool Validate() { - foreach(var value in _positions.Values) + foreach (var value in _positions.Values) { if (value == -1) { @@ -45,10 +48,10 @@ public bool Validate() public Table GetXRefTable() { var entries = _positions - .OrderBy(x => x.Key.ObjectId) - .Select(x => new Entry((uint)x.Value)) + .OrderBy(static x => x.Key.ObjectId) + .Select(static x => new Entry((uint)x.Value)) .ToArray(); return new Table(entries); } } -} \ No newline at end of file +} diff --git a/src/Synercoding.FileFormats.Pdf/Primitives/Matrix.cs b/src/Synercoding.FileFormats.Pdf/Matrix.cs similarity index 95% rename from src/Synercoding.FileFormats.Pdf/Primitives/Matrix.cs rename to src/Synercoding.FileFormats.Pdf/Matrix.cs index 3d4919f..cc2a153 100644 --- a/src/Synercoding.FileFormats.Pdf/Primitives/Matrix.cs +++ b/src/Synercoding.FileFormats.Pdf/Matrix.cs @@ -1,8 +1,8 @@ -using Synercoding.Primitives; +using Synercoding.Primitives; using System; using System.Runtime.CompilerServices; -namespace Synercoding.FileFormats.Pdf.Primitives +namespace Synercoding.FileFormats.Pdf { /// /// Struct that represents a @@ -199,12 +199,12 @@ public Matrix Translate(Value x, Value y) [MethodImpl(MethodImplOptions.AggressiveInlining)] public Matrix Multiply(in Matrix other) => new Matrix( - a: ( A * other.A ) + ( B * other.C ), - b: ( A * other.B ) + ( B * other.D ), - c: ( C * other.A ) + ( D * other.C ), - d: ( C * other.B ) + ( D * other.D ), - e: ( E * other.A ) + ( F * other.C ) + other.E, - f: ( E * other.B ) + ( F * other.D ) + other.F); + a: (A * other.A) + (B * other.C), + b: (A * other.B) + (B * other.D), + c: (C * other.A) + (D * other.C), + d: (C * other.B) + (D * other.D), + e: (E * other.A) + (F * other.C) + other.E, + f: (E * other.B) + (F * other.D) + other.F); /// /// Create a matrix used for rotation diff --git a/src/Synercoding.FileFormats.Pdf/PackageDetails.props b/src/Synercoding.FileFormats.Pdf/PackageDetails.props index 3345bcb..19109bc 100644 --- a/src/Synercoding.FileFormats.Pdf/PackageDetails.props +++ b/src/Synercoding.FileFormats.Pdf/PackageDetails.props @@ -10,7 +10,7 @@ Synercoding.FileFormats.Pdf Synercoding.FileFormats.Pdf Contains classes which makes it easy to quickly create a pdf file. - Improved Matrix and added support for C# 9. + Complete refactor to support low-level operations easier. \ No newline at end of file diff --git a/src/Synercoding.FileFormats.Pdf/PdfInternals/Header.cs b/src/Synercoding.FileFormats.Pdf/PdfInternals/Header.cs deleted file mode 100644 index 38b75da..0000000 --- a/src/Synercoding.FileFormats.Pdf/PdfInternals/Header.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Synercoding.FileFormats.Pdf.Helpers; -using System; -using System.IO; - -namespace Synercoding.FileFormats.Pdf.PdfInternals -{ - internal struct Header : ISpanWriteable, IStreamWriteable - { - public int ByteSize() - { - return 17; - } - - public void FillSpan(Span bytes) - { - var position = 0; - bytes[position++] = 0x25; // % - bytes[position++] = 0x50; // P - bytes[position++] = 0x44; // D - bytes[position++] = 0x46; // F - bytes[position++] = 0x2D; // - - bytes[position++] = 0x31; // 1 - bytes[position++] = 0x2E; // . - bytes[position++] = 0x37; // 7 - position += SpanHelper.WriteNewLine(bytes.Slice(position)); - bytes[position++] = 0x25; // % - bytes[position++] = 0x81; // binary indicator > 128 - bytes[position++] = 0x82; // binary indicator > 128 - bytes[position++] = 0x83; // binary indicator > 128 - bytes[position++] = 0x84; // binary indicator > 128 - position += SpanHelper.WriteNewLine(bytes.Slice(position)); - } - - public uint WriteToStream(Stream stream) - { - var position = (uint)stream.Position; - - var bytes = new byte[ByteSize()]; - FillSpan(bytes); - stream.Write(bytes, 0, bytes.Length); - - return position; - } - } -} \ No newline at end of file diff --git a/src/Synercoding.FileFormats.Pdf/PdfInternals/ObjectType.cs b/src/Synercoding.FileFormats.Pdf/PdfInternals/ObjectType.cs deleted file mode 100644 index bb120c2..0000000 --- a/src/Synercoding.FileFormats.Pdf/PdfInternals/ObjectType.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Synercoding.FileFormats.Pdf.PdfInternals -{ - internal enum ObjectType - { - Page, - Pages, - Catalog, - XObject - } -} \ No newline at end of file diff --git a/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/Catalog.cs b/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/Catalog.cs deleted file mode 100644 index 54cbd77..0000000 --- a/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/Catalog.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Synercoding.FileFormats.Pdf.Extensions; -using System; -using System.IO; - -namespace Synercoding.FileFormats.Pdf.PdfInternals.Objects -{ - internal class Catalog : IPdfObject - { - public Catalog(PdfReference id, PdfReference pageTree) - { - Reference = id; - PageTree = pageTree; - } - - public PdfReference Reference { get; } - - public PdfReference PageTree { get; } - - public bool IsWritten { get; private set; } - - public uint WriteToStream(Stream stream) - { - if (IsWritten) - { - throw new InvalidOperationException("Object is already written to stream."); - } - var position = (uint)stream.Position; - - stream.IndirectDictionary(Reference, dictionary => - { - dictionary - .Type(ObjectType.Catalog) - .Write("/Pages", PageTree); - }); - IsWritten = true; - - return position; - } - - public void Dispose() - { } - } -} diff --git a/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/ContentStream.cs b/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/ContentStream.cs deleted file mode 100644 index 1b8f388..0000000 --- a/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/ContentStream.cs +++ /dev/null @@ -1,57 +0,0 @@ -using SixLabors.ImageSharp; -using Synercoding.FileFormats.Pdf.Extensions; -using Synercoding.FileFormats.Pdf.Primitives; -using System; -using System.IO; - -namespace Synercoding.FileFormats.Pdf.PdfInternals.Objects -{ - internal class ContentStream : IPdfObject - { - private readonly Stream _stream = new MemoryStream(); - - public ContentStream(PdfReference id) - { - Reference = id; - } - - public PdfReference Reference { get; } - - public bool IsWritten { get; private set; } - - public ContentStream AddImage(string resourceKey, Matrix matrix) - { - _stream - .Write("q") // Save graphics state - .NewLine() - .Write(matrix) // Apply matrix - .NewLine() - .Write("/" + resourceKey + " Do") // Paint image - .NewLine() - .Write("Q") // Restore graphics state - .NewLine(); - - return this; - } - - public uint WriteToStream(Stream stream) - { - if (IsWritten) - { - throw new InvalidOperationException("Object is already written to stream."); - } - var position = (uint)stream.Position; - - _stream.Position = 0; - stream.IndirectStream(Reference, _stream, _ => { }); - IsWritten = true; - - return position; - } - - public void Dispose() - { - _stream.Dispose(); - } - } -} \ No newline at end of file diff --git a/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/DocumentInformationDictionary.cs b/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/DocumentInformationDictionary.cs deleted file mode 100644 index e481cb0..0000000 --- a/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/DocumentInformationDictionary.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Synercoding.FileFormats.Pdf.Extensions; -using Synercoding.FileFormats.Pdf.Helpers; -using System; -using System.IO; - -namespace Synercoding.FileFormats.Pdf.PdfInternals.Objects -{ - internal class DocumentInformationDictionary : IPdfObject - { - public DocumentInformationDictionary(PdfReference id) - : this(id, new DocumentInformation()) - { } - - public DocumentInformationDictionary(PdfReference id, DocumentInformation data) - { - Reference = id; - Data = data ?? throw new ArgumentNullException(nameof(data)); - } - - public PdfReference Reference { get; } - - public bool IsWritten { get; private set; } - - public DocumentInformation Data { get; set; } - - public void Dispose() - { } - - public uint WriteToStream(Stream stream) - { - if (IsWritten) - { - throw new InvalidOperationException("Object is already written to stream."); - } - var position = (uint)stream.Position; - - stream.IndirectDictionary(Reference, dictionary => - { - if (!string.IsNullOrWhiteSpace(Data.Title)) - dictionary.Write("/Title", PdfTypeHelper.ToPdfHexadecimalString(Data.Title!)); - if (!string.IsNullOrWhiteSpace(Data.Author)) - dictionary.Write("/Author", PdfTypeHelper.ToPdfHexadecimalString(Data.Author!)); - if (!string.IsNullOrWhiteSpace(Data.Subject)) - dictionary.Write("/Subject", PdfTypeHelper.ToPdfHexadecimalString(Data.Subject!)); - if (!string.IsNullOrWhiteSpace(Data.Keywords)) - dictionary.Write("/Keywords", PdfTypeHelper.ToPdfHexadecimalString(Data.Keywords!)); - if (!string.IsNullOrWhiteSpace(Data.Creator)) - dictionary.Write("/Creator", PdfTypeHelper.ToPdfHexadecimalString(Data.Creator!)); - if (!string.IsNullOrWhiteSpace(Data.Producer)) - dictionary.Write("/Producer", PdfTypeHelper.ToPdfHexadecimalString(Data.Producer!)); - if (Data.CreationDate != null) - dictionary.Write("/CreationDate", PdfTypeHelper.ToPdfDate(Data.CreationDate.Value)); - if (Data.ModDate != null) - dictionary.Write("/ModDate", PdfTypeHelper.ToPdfDate(Data.ModDate.Value)); - }); - - IsWritten = true; - - return position; - } - } -} diff --git a/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/IPdfObject.cs b/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/IPdfObject.cs deleted file mode 100644 index 340f75f..0000000 --- a/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/IPdfObject.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Synercoding.FileFormats.Pdf.Helpers; -using System; - -namespace Synercoding.FileFormats.Pdf.PdfInternals.Objects -{ - /// - /// Interface representing a pdf object - /// - public interface IPdfObject : IStreamWriteable, IDisposable - { - /// - /// A pdf reference object that can be used to reference to this object - /// - PdfReference Reference { get; } - /// - /// Indicator to check whether this object has been written to the PDF stream - /// - bool IsWritten { get; } - } -} diff --git a/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/Image.cs b/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/Image.cs deleted file mode 100644 index 300aca3..0000000 --- a/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/Image.cs +++ /dev/null @@ -1,79 +0,0 @@ -using SixLabors.ImageSharp; -using Synercoding.FileFormats.Pdf.Extensions; -using System; -using System.IO; - -namespace Synercoding.FileFormats.Pdf.PdfInternals.Objects -{ - /// - /// A class representing an image - /// - public class Image : IPdfObject, IDisposable - { - private readonly SixLabors.ImageSharp.Image _image; - private bool _disposed; - - internal Image(PdfReference id, SixLabors.ImageSharp.Image image) - { - Reference = id; - _image = image; - } - - /// - public PdfReference Reference { get; private set; } - - /// - public bool IsWritten { get; private set; } - - /// - public void Dispose() - { - if(!_disposed) - { - _image.Dispose(); - _disposed = true; - } - } - - /// - public uint WriteToStream(Stream stream) - { - if(_disposed) - { - throw new ObjectDisposedException(nameof(_image), "Internal image is already disposed"); - } - if (IsWritten) - { - throw new InvalidOperationException("Object is already written to stream."); - } - IsWritten = true; - - var position = (uint)stream.Position; - - using (var ms = new MemoryStream()) - { - _image.SaveAsJpeg(ms, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder() - { - Quality = 100, - Subsample = SixLabors.ImageSharp.Formats.Jpeg.JpegSubsample.Ratio420 - }); - ms.Position = 0; - - stream.IndirectStream(Reference, ms, dictionary => - { - dictionary - .Type(ObjectType.XObject) - .SubType(XObjectSubType.Image) - .Write("/Width", _image.Width) - .Write("/Height", _image.Height) - .Write("/ColorSpace", "/DeviceRGB") - .Write("/BitsPerComponent", 8) - .Write("/Decode", "[0.0 1.0 0.0 1.0 0.0 1.0]"); - }, - StreamFilter.DCTDecode); - } - - return position; - } - } -} diff --git a/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/Page.cs b/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/Page.cs deleted file mode 100644 index 1df3759..0000000 --- a/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/Page.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Synercoding.FileFormats.Pdf.Extensions; -using Synercoding.FileFormats.Pdf.PdfInternals.XRef; -using Synercoding.Primitives; -using System; -using System.Collections.Generic; -using System.IO; - -namespace Synercoding.FileFormats.Pdf.PdfInternals.Objects -{ - internal class Page : IPdfObject - { - private readonly PdfReference _pageTreeNode; - private readonly PdfPage _page; - private readonly TableBuilder _tableBuilder; - - public Page(TableBuilder tableBuilder, PdfPage page, PdfReference pageTreeNode) - { - _tableBuilder = tableBuilder; - _page = page; - _pageTreeNode = pageTreeNode; - - Reference = tableBuilder.ReserveId(); - } - - public PdfReference Reference { get; private set; } - - public bool IsWritten { get; private set; } - - public void Dispose() - { - _page.ContentStream.Dispose(); - foreach (var image in _page.Images) - { - image.Value.Dispose(); - } - } - - public uint WriteToStream(Stream stream) - { - if (IsWritten) - { - throw new InvalidOperationException("Object is already written to stream."); - } - - var position = (uint)stream.Position; - - var dependencies = new List(); - - stream.IndirectDictionary(Reference, dictionary => - { - dictionary - .Type(ObjectType.Page) - .Write("/Parent", _pageTreeNode); - - // Boxes - dictionary - .Write("/MediaBox", _page.MediaBox) - .Write("/CropBox", _page.CropBox) - .Write("/BleedBox", _page.BleedBox) - .Write("/TrimBox", _page.TrimBox); - - // Resources - if (_page.Images.Count == 0) - { - dictionary.Write("/Resources", x => x.EmptyDictionary()); - } - else - { - dictionary.Write("/Resources", x => x.Dictionary(resources => - { - resources.Write("/XObject", y => y.Dictionary(xobject => - { - foreach (var image in _page.Images) - { - xobject.Write("/" + image.Key, image.Value.Reference); - dependencies.Add(image.Value); - } - })); - })); - } - - // Content stream - dictionary.Write("/Contents", _page.ContentStream.Reference); - dependencies.Add(_page.ContentStream); - }); - IsWritten = true; - - _tableBuilder.SetPosition(Reference, position); - - foreach (var dependency in dependencies) - { - if (!dependency.IsWritten) - { - var dependentPosition = dependency.WriteToStream(stream); - _tableBuilder.SetPosition(dependency.Reference, dependentPosition); - } - } - - return position; - } - } -} diff --git a/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/PageTree.cs b/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/PageTree.cs deleted file mode 100644 index 938a099..0000000 --- a/src/Synercoding.FileFormats.Pdf/PdfInternals/Objects/PageTree.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Synercoding.FileFormats.Pdf.Extensions; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Synercoding.FileFormats.Pdf.PdfInternals.Objects -{ - internal class PageTree : IPdfObject - { - private readonly IList _pages; - - public PageTree(PdfReference id, IList pages) - { - Reference = id; - _pages = pages; - } - - public PdfReference Reference { get; private set; } - - public bool IsWritten { get; private set; } - - public uint WriteToStream(Stream stream) - { - if (IsWritten) - { - throw new InvalidOperationException("Object is already written to stream."); - } - var position = (uint)stream.Position; - - stream.IndirectDictionary(Reference, dictionary => - { - dictionary - .Type(ObjectType.Pages) - .Write("/Kids", _pages.Select(p => p.Reference)) - .Write("/Count", _pages.Count()); - }); - IsWritten = true; - - return position; - } - - public void Dispose() - { - - } - } - -} diff --git a/src/Synercoding.FileFormats.Pdf/PdfInternals/PdfDictionary.cs b/src/Synercoding.FileFormats.Pdf/PdfInternals/PdfDictionary.cs deleted file mode 100644 index d1b76c2..0000000 --- a/src/Synercoding.FileFormats.Pdf/PdfInternals/PdfDictionary.cs +++ /dev/null @@ -1,132 +0,0 @@ -using Synercoding.FileFormats.Pdf.Extensions; -using Synercoding.Primitives; -using System; -using System.Collections.Generic; -using System.IO; - -namespace Synercoding.FileFormats.Pdf.PdfInternals -{ - internal class PdfDictionary - { - private readonly Stream _stream; - - public PdfDictionary(Stream stream) - { - _stream = stream; - } - - public PdfDictionary SubType(XObjectSubType subType) - { - var nameValue = subType switch - { - XObjectSubType.Image => "/Image", - _ => throw new NotImplementedException("Unknown XObjectSubType: " + subType) - }; - - _stream - .Write("/Subtype") - .Space() - .Write(nameValue) - .NewLine(); - - return this; - } - - public PdfDictionary Type(ObjectType objectType) - { - var nameValue = objectType switch - { - ObjectType.Catalog => "/Catalog", - ObjectType.Page => "/Page", - ObjectType.Pages => "/Pages", - ObjectType.XObject => "/XObject", - _ => throw new NotImplementedException("Unknown objectType: " + objectType) - }; - - _stream - .Write("/Type") - .Space() - .Write(nameValue) - .NewLine(); - - return this; - } - - public PdfDictionary Write(string key, IEnumerable objectReferences) - { - _stream - .Write(key) - .Space() - .Write(objectReferences) - .NewLine(); - - return this; - } - - public PdfDictionary Write(string key, int value) - { - _stream - .Write(key) - .Space() - .Write(value) - .NewLine(); - - return this; - } - - public PdfDictionary Write(string key, long value) - { - _stream - .Write(key) - .Space() - .Write(value) - .NewLine(); - - return this; - } - - public PdfDictionary Write(string key, string value) - { - _stream - .Write(key) - .Space() - .Write(value) - .NewLine(); - - return this; - } - - public PdfDictionary Write(string key, PdfReference objectReference) - { - _stream - .Write(key) - .Space() - .Write(objectReference) - .NewLine(); - - return this; - } - - public PdfDictionary Write(string key, Action rawActions) - { - _stream - .Write(key) - .Space(); - rawActions(_stream); - _stream.NewLine(); - - return this; - } - - public PdfDictionary Write(string key, Rectangle rectangle) - { - _stream - .Write(key) - .Space() - .Write(rectangle) - .NewLine(); - - return this; - } - } -} diff --git a/src/Synercoding.FileFormats.Pdf/PdfInternals/StreamFilter.cs b/src/Synercoding.FileFormats.Pdf/PdfInternals/StreamFilter.cs deleted file mode 100644 index 5ede2b8..0000000 --- a/src/Synercoding.FileFormats.Pdf/PdfInternals/StreamFilter.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Synercoding.FileFormats.Pdf.PdfInternals -{ - internal enum StreamFilter - { - DCTDecode - } -} diff --git a/src/Synercoding.FileFormats.Pdf/PdfInternals/Trailer.cs b/src/Synercoding.FileFormats.Pdf/PdfInternals/Trailer.cs deleted file mode 100644 index 98ea5ee..0000000 --- a/src/Synercoding.FileFormats.Pdf/PdfInternals/Trailer.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Synercoding.FileFormats.Pdf.Extensions; -using Synercoding.FileFormats.Pdf.Helpers; -using System.IO; - -namespace Synercoding.FileFormats.Pdf.PdfInternals -{ - internal struct Trailer : IStreamWriteable - { - public Trailer(uint startXRef, int size, PdfReference root, PdfReference documentInfo) - { - StartXRef = startXRef; - Size = size; - Root = root; - DocumentInfo = documentInfo; - } - - public uint StartXRef { get; } - public int Size { get; } - public PdfReference Root { get; } - public PdfReference DocumentInfo { get; } - - public uint WriteToStream(Stream stream) - { - var position = (uint)stream.Position; - - var size = Size; - var root = Root; - var info = DocumentInfo; - - stream - .Write("trailer") - .NewLine() - .Dictionary(dictionary => - { - dictionary - .Write("/Size", size) - .Write("/Root", root) - .Write("/Info", info); - }) - .Write("startxref") - .NewLine() - .Write(StartXRef) - .NewLine() - .Write("%%EOF"); - - return position; - } - } -} \ No newline at end of file diff --git a/src/Synercoding.FileFormats.Pdf/PdfInternals/XObjectSubType.cs b/src/Synercoding.FileFormats.Pdf/PdfInternals/XObjectSubType.cs deleted file mode 100644 index b5c0d16..0000000 --- a/src/Synercoding.FileFormats.Pdf/PdfInternals/XObjectSubType.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Synercoding.FileFormats.Pdf.PdfInternals -{ - internal enum XObjectSubType - { - Image - } -} \ No newline at end of file diff --git a/src/Synercoding.FileFormats.Pdf/PdfPage.cs b/src/Synercoding.FileFormats.Pdf/PdfPage.cs index f2d6031..8f7feb0 100644 --- a/src/Synercoding.FileFormats.Pdf/PdfPage.cs +++ b/src/Synercoding.FileFormats.Pdf/PdfPage.cs @@ -1,204 +1,278 @@ -using SixLabors.ImageSharp.Processing; -using Synercoding.FileFormats.Pdf.PdfInternals.Objects; -using Synercoding.FileFormats.Pdf.PdfInternals.XRef; -using Synercoding.FileFormats.Pdf.Primitives; +using Synercoding.FileFormats.Pdf.LowLevel; +using Synercoding.FileFormats.Pdf.LowLevel.Extensions; +using Synercoding.FileFormats.Pdf.LowLevel.XRef; using Synercoding.Primitives; using Synercoding.Primitives.Extensions; +using System; +using System.Collections; using System.Collections.Generic; -using System.IO; -using ImageSharp = SixLabors.ImageSharp; namespace Synercoding.FileFormats.Pdf { /// - /// A class that represents a pdf page + /// This class represents a page in a pdf /// - public class PdfPage + public sealed class PdfPage : IPdfObject, IDisposable { private int _imageCounter = 0; - private readonly Dictionary _images = new Dictionary(); + private readonly Map _images = new Map(); private readonly TableBuilder _tableBuilder; + private readonly PageTree _parent; - private Rectangle _trimBox = Rectangle.Zero; - private Rectangle _bleedBox = Rectangle.Zero; - private Rectangle _cropBox = Rectangle.Zero; + private int? _rotation; + private bool _isWritten; - internal PdfPage(TableBuilder tableBuilder) + internal PdfPage(TableBuilder tableBuilder, PageTree parent) { _tableBuilder = tableBuilder; + _parent = parent; + _parent.AddPage(this); + + Reference = tableBuilder.ReserveId(); ContentStream = new ContentStream(tableBuilder.ReserveId()); } - internal IReadOnlyDictionary Images => _images; - - internal ContentStream ContentStream { get; } - /// - /// The media box of the + /// The content stream of this page /// - public Rectangle MediaBox { get; set; } = Sizes.A4.AsRectangle(); + public ContentStream ContentStream { get; } + + /// + public PdfReference Reference { get; } /// - /// The cropbox of the , defaults to + /// The rotation of how the page is displayed, must be in increments of 90 /// - public Rectangle CropBox + public int? Rotation { - get => _cropBox.Equals(Rectangle.Zero) ? MediaBox : _cropBox; - set => _cropBox = value; + get => _rotation; + set + { + if (value is not null && value % 90 != 0) + throw new ArgumentOutOfRangeException(nameof(Rotation), value, "The provided value can only be increments of 90."); + + _rotation = value; + } } /// - /// The bleedbox of the , defaults to + /// The media box of the /// - public Rectangle BleedBox - { - get => _bleedBox.Equals(Rectangle.Zero) ? CropBox : _bleedBox; - set => _bleedBox = value; - } + public Rectangle MediaBox { get; set; } = Sizes.A4.AsRectangle(); /// - /// The trimbox of the , defaults to + /// The cropbox of the , defaults to /// - public Rectangle TrimBox - { - get => _trimBox.Equals(Rectangle.Zero) ? CropBox : _trimBox; - set => _trimBox = value; - } + public Rectangle? CropBox { get; set; } /// - /// Add image to the + /// The bleedbox of the /// - /// The image to be added - /// The that represents the placement on the page - /// This so calls can be chained. - public PdfPage AddImage(ImageSharp.Image image, Rectangle rectangle) - { - return _addImage(image, rectangle, true); - } + public Rectangle? BleedBox { get; set; } /// - /// Add image to the + /// The trimbox of the /// - /// The image to be added - /// The that represents the placement on the page - /// This so calls can be chained. - public PdfPage AddImage(byte[] image, Rectangle rectangle) - { - return _addImage(ImageSharp.Image.Load(image), rectangle, false); - } + public Rectangle? TrimBox { get; set; } /// - /// Add image to the + /// The artbox of the /// - /// The image to be added - /// The that represents the placement on the page - /// This so calls can be chained. - public PdfPage AddImage(Stream image, Rectangle rectangle) - { - return _addImage(ImageSharp.Image.Load(image), rectangle, false); - } + public Rectangle? Art { get; set; } /// - /// Add image to the + /// Add an image to the resources of this page /// - /// The image to be added - /// The that represents the placement on the page - /// This so calls can be chained. - public PdfPage AddImage(ImageSharp.Image image, Matrix matrix) + /// The image to add + /// The that can be used to reference this image in the + public PdfName AddImageToResources(SixLabors.ImageSharp.Image image) { - return _addImage(image, matrix, true); + var id = _tableBuilder.ReserveId(); + + var pdfImage = new Image(id, image); + + return _addImageToResources(pdfImage); } /// - /// Add image to the + /// Add an image to the resources of this page /// - /// The image to be added - /// The that represents the placement on the page - /// This so calls can be chained. - public PdfPage AddImage(byte[] image, Matrix matrix) + /// The image to add + /// The width of the image in the + /// The height of the image in the + /// The that can be used to reference this image in the + public PdfName AddImageToResources(System.IO.Stream jpgStream, int width, int height) { - return _addImage(ImageSharp.Image.Load(image), matrix, false); + var id = _tableBuilder.ReserveId(); + + var pdfImage = new Image(id, jpgStream, width, height); + + return _addImageToResources(pdfImage); } /// - /// Add image to the + /// Add an image to the resources of this page /// - /// The image to be added - /// The that represents the placement on the page - /// This so calls can be chained. - public PdfPage AddImage(Stream image, Matrix matrix) + /// The image to add + /// The that can be used to reference this image in the + public PdfName AddImageToResources(System.IO.Stream imageStream) { - return _addImage(ImageSharp.Image.Load(image), matrix, false); + using var image = SixLabors.ImageSharp.Image.Load(imageStream); + return AddImageToResources(image); } /// - /// Add image to the + /// Add an image to the resources of this page /// - /// The image to be added - /// The that represents the placement on the page - /// This so calls can be chained. - public PdfPage AddImage(Image image, Rectangle rectangle) + /// The image to add + /// The that can be used to reference this image in the + public PdfName AddImageToResources(Image image) { - rectangle = rectangle.ConvertTo(Unit.Points); - var matrix = new Matrix( - rectangle.URX.Raw - rectangle.LLX.Raw, 0, - 0, rectangle.URY.Raw - rectangle.LLY.Raw, - rectangle.LLX.Raw, rectangle.LLY.Raw); - return AddImage(image, matrix); + return _addImageToResources(image); } - /// - /// Add image to the - /// - /// The image to be added - /// The that represents the placement on the page - /// This so calls can be chained. - public PdfPage AddImage(Image image, Matrix matrix) + /// + public void Dispose() { - var key = _addImageToResources(image); - ContentStream.AddImage(key, matrix); + foreach (var kv in _images) + kv.Value.Dispose(); - return this; + ContentStream.Dispose(); } - private PdfPage _addImage(ImageSharp.Image image, Rectangle rectangle, bool clone) + private PdfName _addImageToResources(Image image) { - rectangle = rectangle.ConvertTo(Unit.Points); - var matrix = new Matrix( - rectangle.URX.Raw - rectangle.LLX.Raw, 0, - 0, rectangle.URY.Raw - rectangle.LLY.Raw, - rectangle.LLX.Raw, rectangle.LLY.Raw); + if (_images.Reverse.Contains(image)) + return _images.Reverse[image]; - return _addImage(image, matrix, clone); - } + var key = "Im" + System.Threading.Interlocked.Increment(ref _imageCounter).ToString().PadLeft(6, '0'); - private PdfPage _addImage(ImageSharp.Image image, Matrix matrix, bool clone) - { - var key = _addImageToResources(image, clone); - ContentStream.AddImage(key, matrix); + var pdfName = PdfName.Get(key); + + _images.Add(pdfName, image); - return this; + return pdfName; } - private string _addImageToResources(ImageSharp.Image image, bool clone) + internal uint WriteToStream(PdfStream stream) { - if (clone) + if (_isWritten) { - image = image.Clone(ctx => { }); + throw new InvalidOperationException("Object is already written to stream."); } - var id = _tableBuilder.ReserveId(); + var position = (uint)stream.Position; + _tableBuilder.SetPosition(Reference, position); + + stream.IndirectDictionary(this, static (page, dictionary) => + { + dictionary + .Type(ObjectType.Page) + .Write(PdfName.Get("Parent"), page._parent.Reference); + + // Boxes + dictionary + .Write(PdfName.Get("MediaBox"), page.MediaBox) + .WriteIfNotNull(PdfName.Get("CropBox"), page.CropBox) + .WriteIfNotNull(PdfName.Get("BleedBox"), page.BleedBox) + .WriteIfNotNull(PdfName.Get("TrimBox"), page.TrimBox) + .WriteIfNotNull(PdfName.Get("Rotate"), page.Rotation); + + // Resources + if (page._images.Count == 0) + { + dictionary.Write(PdfName.Get("Resources"), static x => x.EmptyDictionary()); + } + else + { + dictionary.Write(PdfName.Get("Resources"), page._images, static (images, stream) => stream.Dictionary(images, static (images, resources) => + { + resources.Write(PdfName.Get("XObject"), images, static (images, stream) => stream.Dictionary(images, static (images, xobject) => + { + foreach (var image in images) + { + xobject.Write(image.Key, image.Value.Reference); + } + })); + })); + } + + // Content stream + dictionary.Write(PdfName.Get("Contents"), page.ContentStream.Reference); + }); + + _isWritten = true; - return _addImageToResources(new Image(id, image)); + foreach (var kv in _images) + { + if (kv.Value.TryWriteToStream(stream, out uint dependentPosition)) + { + _tableBuilder.SetPosition(kv.Value.Reference, dependentPosition); + } + } + if (!ContentStream.IsWritten) + { + var dependentPosition = ContentStream.WriteToStream(stream); + _tableBuilder.SetPosition(ContentStream.Reference, dependentPosition); + } + + return position; } - private string _addImageToResources(Image image) + private sealed class Map : IEnumerable> + where T1 : notnull + where T2 : notnull { - var key = "Im" + System.Threading.Interlocked.Increment(ref _imageCounter).ToString().PadLeft(6, '0'); + private readonly IDictionary _forward = new Dictionary(); + private readonly IDictionary _reverse = new Dictionary(); - _images.Add(key, image); + public Map() + { + Forward = new Indexer(_forward); + Reverse = new Indexer(_reverse); + } - return key; + public int Count => _forward.Count; + + public void Add(T1 t1, T2 t2) + { + _forward.Add(t1, t2); + _reverse.Add(t2, t1); + } + + public IEnumerator> GetEnumerator() + { + return _forward.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public Indexer Forward { get; } + public Indexer Reverse { get; } + + public sealed class Indexer + where T3 : notnull + where T4 : notnull + { + private readonly IDictionary _dictionary; + + public Indexer(IDictionary dictionary) + { + _dictionary = dictionary; + } + + public T4 this[T3 index] + { + get => _dictionary[index]; + set => _dictionary[index] = value; + } + + public bool Contains(T3 value) + => _dictionary.ContainsKey(value); + } } } -} \ No newline at end of file +} diff --git a/src/Synercoding.FileFormats.Pdf/PdfWriter.cs b/src/Synercoding.FileFormats.Pdf/PdfWriter.cs index 76e3f8e..04b0e06 100644 --- a/src/Synercoding.FileFormats.Pdf/PdfWriter.cs +++ b/src/Synercoding.FileFormats.Pdf/PdfWriter.cs @@ -1,27 +1,23 @@ -using Synercoding.FileFormats.Pdf.PdfInternals; -using Synercoding.FileFormats.Pdf.PdfInternals.Objects; -using Synercoding.FileFormats.Pdf.PdfInternals.XRef; +using Synercoding.FileFormats.Pdf.LowLevel; +using Synercoding.FileFormats.Pdf.LowLevel.Extensions; +using Synercoding.FileFormats.Pdf.LowLevel.XRef; using System; -using System.Collections.Generic; using System.IO; using System.Reflection; namespace Synercoding.FileFormats.Pdf { /// - /// Main class for writing PDF files to streams + /// This class is the start point for creating a pdf file /// - public class PdfWriter : IDisposable + public sealed class PdfWriter : IDisposable { private readonly bool _ownsStream; - private readonly Stream _stream; + private readonly PdfStream _stream; private readonly TableBuilder _tableBuilder = new TableBuilder(); - private readonly PdfReference _documentInfo; - private readonly PdfReference _pageTreeNode; - private readonly PdfReference _catalog; - - private readonly IList _pageReferences = new List(); + private readonly PageTree _pageTree; + private readonly Catalog _catalog; /// /// Constructor for @@ -38,12 +34,17 @@ public PdfWriter(Stream stream) /// If the stream is owned, then when this is disposed, the stream is also disposed. public PdfWriter(Stream stream, bool ownsStream) { - _stream = stream; - new Header().WriteToStream(stream); + _stream = new PdfStream(stream); + _writeHeader(_stream); + + _pageTree = new PageTree(_tableBuilder.ReserveId()); + _catalog = new Catalog(_tableBuilder.ReserveId(), _pageTree); - _pageTreeNode = _tableBuilder.ReserveId(); - _catalog = _tableBuilder.ReserveId(); - _documentInfo = _tableBuilder.ReserveId(); + DocumentInformation = new DocumentInformation(_tableBuilder.ReserveId()) + { + Producer = $"Synercoding.FileFormats.Pdf {typeof(PdfWriter).GetTypeInfo().Assembly.GetName().Version}", + CreationDate = DateTime.Now + }; _ownsStream = ownsStream; } @@ -51,11 +52,19 @@ public PdfWriter(Stream stream, bool ownsStream) /// /// Document information, such as the author and title /// - public DocumentInformation DocumentInformation { get; } = new DocumentInformation() + public DocumentInformation DocumentInformation { get; } + + /// + /// Set meta information for this document + /// + /// Action used to set meta data + /// Returns this to chain calls + public PdfWriter SetDocumentInfo(Action infoAction) { - Producer = $"Synercoding.FileFormats.Pdf {typeof(PdfWriter).GetTypeInfo().Assembly.GetName().Version}", - CreationDate = DateTime.Now - }; + infoAction(DocumentInformation); + + return this; + } /// /// Add a page to the pdf file @@ -63,15 +72,21 @@ public PdfWriter(Stream stream, bool ownsStream) /// Action used to setup the page /// Returns this to chain calls public PdfWriter AddPage(Action pageAction) - { - var page = new PdfPage(_tableBuilder); - pageAction(page); + => AddPage(pageAction, static (action, page) => action(page)); - using (var obj = new Page(_tableBuilder, page, _pageTreeNode)) + /// + /// Add a page to the pdf file + /// + /// Data passed into the action + /// Action used to setup the page + /// Returns this to chain calls + public PdfWriter AddPage(T data, Action pageAction) + { + using (var page = new PdfPage(_tableBuilder, _pageTree)) { - _pageReferences.Add(obj); + pageAction(data, page); - obj.WriteToStream(_stream); + page.WriteToStream(_stream); } return this; @@ -88,28 +103,15 @@ public Image AddImage(SixLabors.ImageSharp.Image image) var pdfImage = new Image(id, image); - var position = pdfImage.WriteToStream(_stream); + if (!pdfImage.TryWriteToStream(_stream, out uint position)) + throw new InvalidOperationException("Image was just created but could not be written to stream."); _tableBuilder.SetPosition(id, position); return pdfImage; } - /// - /// Set meta information for this document - /// - /// Action used to set meta data - /// Returns this to chain calls - public PdfWriter SetDocumentInfo(Action infoAction) - { - infoAction(DocumentInformation); - - return this; - } - - /// - /// Close the PDF document by writing the pagetree, catalog, xref table and trailer to the - /// + /// public void Dispose() { _writePageTree(); @@ -128,42 +130,97 @@ public void Dispose() } } + private static void _writeHeader(PdfStream stream) + { + stream.WriteByte(0x25); // % + stream.WriteByte(0x50); // P + stream.WriteByte(0x44); // D + stream.WriteByte(0x46); // F + stream.WriteByte(0x2D); // - + stream.WriteByte(0x31); // 1 + stream.WriteByte(0x2E); // . + stream.WriteByte(0x37); // 7 + stream.WriteByte(0x0D); // CR + stream.WriteByte(0x0A); // LF + stream.WriteByte(0x25); // % + stream.WriteByte(0x81); // binary indicator > 128 + stream.WriteByte(0x82); // binary indicator > 128 + stream.WriteByte(0x83); // binary indicator > 128 + stream.WriteByte(0x84); // binary indicator > 128 + stream.WriteByte(0x0D); // CR + stream.WriteByte(0x0A); // LF + } + private void _writeDocumentInformation() { - _tableBuilder.SetPosition(_documentInfo, (uint)_stream.Position); + _tableBuilder.SetPosition(DocumentInformation.Reference, (uint)_stream.Position); - var info = new DocumentInformationDictionary(_documentInfo, DocumentInformation); - info.WriteToStream(_stream); + DocumentInformation.WriteToStream(_stream); } private void _writePageTree() { - _tableBuilder.SetPosition(_pageTreeNode, (uint)_stream.Position); + _tableBuilder.SetPosition(_pageTree.Reference, (uint)_stream.Position); - var tree = new PageTree(_pageTreeNode, _pageReferences); - tree.WriteToStream(_stream); + _pageTree.WriteToStream(_stream); } private void _writeCatalog() { - _tableBuilder.SetPosition(_catalog, (uint)_stream.Position); + _tableBuilder.SetPosition(_catalog.Reference, (uint)_stream.Position); - var catalog = new Catalog(_catalog, _pageTreeNode); - catalog.WriteToStream(_stream); + _catalog.WriteToStream(_stream); } private void _writePdfEnding() { if (!_tableBuilder.Validate()) - { throw new InvalidOperationException("XRef table is invalid."); - } var xRefTable = _tableBuilder.GetXRefTable(); uint xRefPosition = xRefTable.WriteToStream(_stream); - var trailer = new Trailer(xRefPosition, xRefTable.Section.ObjectCount, _catalog, _documentInfo); + var trailer = new Trailer(xRefPosition, xRefTable.Section.ObjectCount, _catalog, DocumentInformation); trailer.WriteToStream(_stream); } + + private readonly struct Trailer + { + public Trailer(uint startXRef, int size, Catalog root, DocumentInformation documentInfo) + { + StartXRef = startXRef; + Size = size; + Root = root; + DocumentInfo = documentInfo; + } + + public uint StartXRef { get; } + public int Size { get; } + public Catalog Root { get; } + public DocumentInformation DocumentInfo { get; } + + internal uint WriteToStream(PdfStream stream) + { + var position = (uint)stream.Position; + + stream + .Write("trailer") + .NewLine() + .Dictionary(this, static (trailer, dictionary) => + { + dictionary + .Write(PdfName.Get("Size"), trailer.Size) + .Write(PdfName.Get("Root"), trailer.Root.Reference) + .Write(PdfName.Get("Info"), trailer.DocumentInfo.Reference); + }) + .Write("startxref") + .NewLine() + .Write(StartXRef) + .NewLine() + .Write("%%EOF"); + + return position; + } + } } -} \ No newline at end of file +} diff --git a/src/Synercoding.FileFormats.Pdf/Synercoding.FileFormats.Pdf.csproj b/src/Synercoding.FileFormats.Pdf/Synercoding.FileFormats.Pdf.csproj index df101ef..ef6dd85 100644 --- a/src/Synercoding.FileFormats.Pdf/Synercoding.FileFormats.Pdf.csproj +++ b/src/Synercoding.FileFormats.Pdf/Synercoding.FileFormats.Pdf.csproj @@ -7,7 +7,7 @@ - +