diff --git a/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs b/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs index adcc1112..c57404e9 100644 --- a/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs +++ b/src/UglyToad.PdfPig.Tests/Writer/PdfDocumentBuilderTests.cs @@ -136,7 +136,7 @@ var letters = page.AddText("Hello World!", 12, new PdfPoint(30, 50), font); - Assert.NotEmpty(page.Operations); + Assert.NotEmpty(page.CurrentStream.Operations); var b = builder.Build(); @@ -186,7 +186,7 @@ page.AddText("eé", 12, new PdfPoint(30, 520), font); - Assert.NotEmpty(page.Operations); + Assert.NotEmpty(page.CurrentStream.Operations); var b = builder.Build(); @@ -232,7 +232,7 @@ page.AddText("eé", 12, new PdfPoint(30, 520), font); - Assert.NotEmpty(page.Operations); + Assert.NotEmpty(page.CurrentStream.Operations); var b = builder.Build(); @@ -279,7 +279,7 @@ var letters = page.AddText("Hello World!", 16, new PdfPoint(30, 520), font); page.AddText("This is some further text continuing to write", 12, new PdfPoint(30, 500), font); - Assert.NotEmpty(page.Operations); + Assert.NotEmpty(page.CurrentStream.Operations); var b = builder.Build(); @@ -603,6 +603,47 @@ WriteFile(nameof(CanCreateDocumentWithFilledRectangle), file); } + [Fact] + public void CanGeneratePageWithMultipleStream() + { + var builder = new PdfDocumentBuilder(); + + var page = builder.AddPage(PageSize.A4); + + var file = TrueTypeTestHelper.GetFileBytes("Andada-Regular.ttf"); + + var font = builder.AddTrueTypeFont(file); + + var letters = page.AddText("Hello", 12, new PdfPoint(30, 50), font); + + Assert.NotEmpty(page.CurrentStream.Operations); + + page.NewContentStreamAfter(); + + page.AddText("World!", 12, new PdfPoint(50, 50), font); + + Assert.NotEmpty(page.CurrentStream.Operations); + + + var b = builder.Build(); + + WriteFile(nameof(CanGeneratePageWithMultipleStream), b); + + Assert.NotEmpty(b); + + using (var document = PdfDocument.Open(b)) + { + var page1 = document.GetPage(1); + + Assert.Equal("HelloWorld!", page1.Text); + + var h = page1.Letters[0]; + + Assert.Equal("H", h.Value); + Assert.Equal("Andada-Regular", h.FontName); + } + } + private static void WriteFile(string name, byte[] bytes, string extension = "pdf") { try diff --git a/src/UglyToad.PdfPig/Writer/PdfDocumentBuilder.cs b/src/UglyToad.PdfPig/Writer/PdfDocumentBuilder.cs index ef7c5c98..4e022bb9 100644 --- a/src/UglyToad.PdfPig/Writer/PdfDocumentBuilder.cs +++ b/src/UglyToad.PdfPig/Writer/PdfDocumentBuilder.cs @@ -308,14 +308,27 @@ namespace UglyToad.PdfPig.Writer pageDictionary[NameToken.Resources] = new DictionaryToken(individualResources); - if (page.Value.Operations.Count > 0) + if (page.Value.ContentStreams.Count == 1) { - var contentStream = WriteContentStream(page.Value.Operations); + var contentStream = WriteContentStream(page.Value.CurrentStream.Operations); var contentStreamObj = context.WriteObject(memory, contentStream); pageDictionary[NameToken.Contents] = new IndirectReferenceToken(contentStreamObj.Number); } + else if (page.Value.ContentStreams.Count > 1) + { + var streamTokens = page.Value.ContentStreams.Select(contentStream => + { + var streamToken = WriteContentStream(contentStream.Operations); + + var contentStreamObj = context.WriteObject(memory, streamToken); + + return new IndirectReferenceToken(contentStreamObj.Number); + }).ToList(); + + pageDictionary[NameToken.Contents] = new ArrayToken(streamTokens); + } var pageRef = context.WriteObject(memory, new DictionaryToken(pageDictionary)); diff --git a/src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs b/src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs index d1680c39..1eaeef1d 100644 --- a/src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs +++ b/src/UglyToad.PdfPig/Writer/PdfPageBuilder.cs @@ -27,7 +27,7 @@ public class PdfPageBuilder { private readonly PdfDocumentBuilder documentBuilder; - private readonly List operations = new List(); + private readonly List contentStreams; private readonly Dictionary resourcesDictionary = new Dictionary(); //a sequence number of ShowText operation to determine whether letters belong to same operation or not (letters that belong to different operations have less changes to belong to same word) @@ -35,8 +35,6 @@ private int imageKey = 1; - internal IReadOnlyList Operations => operations; - internal IReadOnlyDictionary Resources => resourcesDictionary; /// @@ -52,13 +50,59 @@ /// /// Access to the underlying data structures for advanced use cases. /// - public AdvancedEditing Advanced { get; } + public ContentStream CurrentStream { get; private set; } + + /// + /// Access to + /// + public IReadOnlyList ContentStreams { get; } internal PdfPageBuilder(int number, PdfDocumentBuilder documentBuilder) { this.documentBuilder = documentBuilder ?? throw new ArgumentNullException(nameof(documentBuilder)); PageNumber = number; - Advanced = new AdvancedEditing(operations); + + CurrentStream = new ContentStream(); + ContentStreams = contentStreams = new List() + { + CurrentStream + }; + } + + /// + /// Allow to append a new content stream before the current one and select it + /// + public void NewContentStreamBefore() + { + var index = Math.Max(contentStreams.IndexOf(CurrentStream) - 1, 0); + + CurrentStream = new ContentStream(); + contentStreams.Insert(index, CurrentStream); + } + + /// + /// Allow to append a new content stream after the current one and select it + /// + public void NewContentStreamAfter() + { + var index = Math.Min(contentStreams.IndexOf(CurrentStream) + 1, contentStreams.Count); + + CurrentStream = new ContentStream(); + contentStreams.Insert(index, CurrentStream); + } + + /// + /// Select a content stream from the list, by his index + /// + /// index of the content stream to be selected + public void SelectContentStream(int index) + { + if (index < 0 || index >= ContentStreams.Count) + { + throw new IndexOutOfRangeException(nameof(index)); + } + + CurrentStream = ContentStreams[index]; } /// @@ -71,16 +115,16 @@ { if (lineWidth != 1) { - operations.Add(new SetLineWidth(lineWidth)); + CurrentStream.Add(new SetLineWidth(lineWidth)); } - operations.Add(new BeginNewSubpath((decimal)from.X, (decimal)from.Y)); - operations.Add(new AppendStraightLineSegment((decimal)to.X, (decimal)to.Y)); - operations.Add(StrokePath.Value); + CurrentStream.Add(new BeginNewSubpath((decimal)from.X, (decimal)from.Y)); + CurrentStream.Add(new AppendStraightLineSegment((decimal)to.X, (decimal)to.Y)); + CurrentStream.Add(StrokePath.Value); if (lineWidth != 1) { - operations.Add(new SetLineWidth(1)); + CurrentStream.Add(new SetLineWidth(1)); } } @@ -96,23 +140,23 @@ { if (lineWidth != 1) { - operations.Add(new SetLineWidth(lineWidth)); + CurrentStream.Add(new SetLineWidth(lineWidth)); } - operations.Add(new AppendRectangle((decimal)position.X, (decimal)position.Y, width, height)); + CurrentStream.Add(new AppendRectangle((decimal)position.X, (decimal)position.Y, width, height)); if (fill) { - operations.Add(FillPathEvenOddRuleAndStroke.Value); + CurrentStream.Add(FillPathEvenOddRuleAndStroke.Value); } else { - operations.Add(StrokePath.Value); + CurrentStream.Add(StrokePath.Value); } if (lineWidth != 1) { - operations.Add(new SetLineWidth(lineWidth)); + CurrentStream.Add(new SetLineWidth(lineWidth)); } } @@ -124,8 +168,8 @@ /// Blue - 0 to 255 public void SetStrokeColor(byte r, byte g, byte b) { - operations.Add(Push.Value); - operations.Add(new SetStrokeColorDeviceRgb(RgbToDecimal(r), RgbToDecimal(g), RgbToDecimal(b))); + CurrentStream.Add(Push.Value); + CurrentStream.Add(new SetStrokeColorDeviceRgb(RgbToDecimal(r), RgbToDecimal(g), RgbToDecimal(b))); } /// @@ -136,8 +180,8 @@ /// Blue - 0 to 1 internal void SetStrokeColorExact(decimal r, decimal g, decimal b) { - operations.Add(Push.Value); - operations.Add(new SetStrokeColorDeviceRgb(CheckRgbDecimal(r, nameof(r)), + CurrentStream.Add(Push.Value); + CurrentStream.Add(new SetStrokeColorDeviceRgb(CheckRgbDecimal(r, nameof(r)), CheckRgbDecimal(g, nameof(g)), CheckRgbDecimal(b, nameof(b)))); } @@ -149,8 +193,8 @@ /// Blue - 0 to 255 public void SetTextAndFillColor(byte r, byte g, byte b) { - operations.Add(Push.Value); - operations.Add(new SetNonStrokeColorDeviceRgb(RgbToDecimal(r), RgbToDecimal(g), RgbToDecimal(b))); + CurrentStream.Add(Push.Value); + CurrentStream.Add(new SetNonStrokeColorDeviceRgb(RgbToDecimal(r), RgbToDecimal(g), RgbToDecimal(b))); } /// @@ -158,7 +202,7 @@ /// public void ResetColor() { - operations.Add(Pop.Value); + CurrentStream.Add(Pop.Value); } /// @@ -244,15 +288,15 @@ var letters = DrawLetters(text, fontProgram, fm, fontSize, textMatrix); - operations.Add(BeginText.Value); - operations.Add(new SetFontAndSize(font.Name, fontSize)); - operations.Add(new MoveToNextLineWithOffset((decimal)position.X, (decimal)position.Y)); + CurrentStream.Add(BeginText.Value); + CurrentStream.Add(new SetFontAndSize(font.Name, fontSize)); + CurrentStream.Add(new MoveToNextLineWithOffset((decimal)position.X, (decimal)position.Y)); var bytesPerShow = new List(); foreach (var letter in text) { if (char.IsWhiteSpace(letter)) { - operations.Add(new ShowText(bytesPerShow.ToArray())); + CurrentStream.Add(new ShowText(bytesPerShow.ToArray())); bytesPerShow.Clear(); } @@ -262,10 +306,10 @@ if (bytesPerShow.Count > 0) { - operations.Add(new ShowText(bytesPerShow.ToArray())); + CurrentStream.Add(new ShowText(bytesPerShow.ToArray())); } - operations.Add(EndText.Value); + CurrentStream.Add(EndText.Value); return letters; } @@ -322,16 +366,16 @@ resourcesDictionary[NameToken.Xobject] = xobjects.With(key, new IndirectReferenceToken(reference)); - operations.Add(Push.Value); + CurrentStream.Add(Push.Value); // This needs to be the placement rectangle. - operations.Add(new ModifyCurrentTransformationMatrix(new [] + CurrentStream.Add(new ModifyCurrentTransformationMatrix(new [] { (decimal)placementRectangle.Width, 0, 0, (decimal)placementRectangle.Height, (decimal)placementRectangle.BottomLeft.X, (decimal)placementRectangle.BottomLeft.Y })); - operations.Add(new InvokeNamedXObject(key)); - operations.Add(Pop.Value); + CurrentStream.Add(new InvokeNamedXObject(key)); + CurrentStream.Add(Pop.Value); return new AddedImage(reference, info.Width, info.Height); } @@ -361,16 +405,16 @@ resourcesDictionary[NameToken.Xobject] = xobjects.With(key, new IndirectReferenceToken(image.Reference)); - operations.Add(Push.Value); + CurrentStream.Add(Push.Value); // This needs to be the placement rectangle. - operations.Add(new ModifyCurrentTransformationMatrix(new[] + CurrentStream.Add(new ModifyCurrentTransformationMatrix(new[] { (decimal)placementRectangle.Width, 0, 0, (decimal)placementRectangle.Height, (decimal)placementRectangle.BottomLeft.X, (decimal)placementRectangle.BottomLeft.Y })); - operations.Add(new InvokeNamedXObject(key)); - operations.Add(Pop.Value); + CurrentStream.Add(new InvokeNamedXObject(key)); + CurrentStream.Add(Pop.Value); } /// @@ -439,16 +483,16 @@ resourcesDictionary[NameToken.Xobject] = xobjects.With(key, new IndirectReferenceToken(reference)); - operations.Add(Push.Value); + CurrentStream.Add(Push.Value); // This needs to be the placement rectangle. - operations.Add(new ModifyCurrentTransformationMatrix(new[] + CurrentStream.Add(new ModifyCurrentTransformationMatrix(new[] { (decimal)placementRectangle.Width, 0, 0, (decimal)placementRectangle.Height, (decimal)placementRectangle.BottomLeft.X, (decimal)placementRectangle.BottomLeft.Y })); - operations.Add(new InvokeNamedXObject(key)); - operations.Add(Pop.Value); + CurrentStream.Add(new InvokeNamedXObject(key)); + CurrentStream.Add(Pop.Value); return new AddedImage(reference, png.Width, png.Height); } @@ -531,7 +575,7 @@ /// /// Provides access to the raw page data structures for advanced editing use cases. /// - public class AdvancedEditing + public class ContentStream { /// /// The operations making up the page content stream. @@ -539,11 +583,16 @@ public List Operations { get; } /// - /// Create a new . + /// Create a new . /// - internal AdvancedEditing(List operations) + internal ContentStream() { - Operations = operations; + Operations = new List(); + } + + internal void Add(IGraphicsStateOperation newOperation) + { + Operations.Add(newOperation); } }