Allow to have multiple content stream in a page

You can create new content stream (NewContentStreamAfter, NewContentStreamBefore) and select (SelectContentStream) which one we are editing at the moment
This commit is contained in:
InusualZ
2020-12-19 10:41:36 +00:00
parent 237fd96f9e
commit ba5bc1f031
3 changed files with 153 additions and 50 deletions

View File

@@ -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

View File

@@ -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));

View File

@@ -27,7 +27,7 @@
public class PdfPageBuilder
{
private readonly PdfDocumentBuilder documentBuilder;
private readonly List<IGraphicsStateOperation> operations = new List<IGraphicsStateOperation>();
private readonly List<ContentStream> contentStreams;
private readonly Dictionary<NameToken, IToken> resourcesDictionary = new Dictionary<NameToken, IToken>();
//a sequence number of ShowText operation to determine whether letters belong to same operation or not (letters that belong to different operations have less changes to belong to same word)
@@ -35,8 +35,6 @@
private int imageKey = 1;
internal IReadOnlyList<IGraphicsStateOperation> Operations => operations;
internal IReadOnlyDictionary<NameToken, IToken> Resources => resourcesDictionary;
/// <summary>
@@ -52,13 +50,59 @@
/// <summary>
/// Access to the underlying data structures for advanced use cases.
/// </summary>
public AdvancedEditing Advanced { get; }
public ContentStream CurrentStream { get; private set; }
/// <summary>
/// Access to
/// </summary>
public IReadOnlyList<ContentStream> 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<ContentStream>()
{
CurrentStream
};
}
/// <summary>
/// Allow to append a new content stream before the current one and select it
/// </summary>
public void NewContentStreamBefore()
{
var index = Math.Max(contentStreams.IndexOf(CurrentStream) - 1, 0);
CurrentStream = new ContentStream();
contentStreams.Insert(index, CurrentStream);
}
/// <summary>
/// Allow to append a new content stream after the current one and select it
/// </summary>
public void NewContentStreamAfter()
{
var index = Math.Min(contentStreams.IndexOf(CurrentStream) + 1, contentStreams.Count);
CurrentStream = new ContentStream();
contentStreams.Insert(index, CurrentStream);
}
/// <summary>
/// Select a content stream from the list, by his index
/// </summary>
/// <param name="index">index of the content stream to be selected</param>
public void SelectContentStream(int index)
{
if (index < 0 || index >= ContentStreams.Count)
{
throw new IndexOutOfRangeException(nameof(index));
}
CurrentStream = ContentStreams[index];
}
/// <summary>
@@ -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 @@
/// <param name="b">Blue - 0 to 255</param>
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)));
}
/// <summary>
@@ -136,8 +180,8 @@
/// <param name="b">Blue - 0 to 1</param>
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 @@
/// <param name="b">Blue - 0 to 255</param>
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)));
}
/// <summary>
@@ -158,7 +202,7 @@
/// </summary>
public void ResetColor()
{
operations.Add(Pop.Value);
CurrentStream.Add(Pop.Value);
}
/// <summary>
@@ -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<byte>();
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);
}
/// <summary>
@@ -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 @@
/// <summary>
/// Provides access to the raw page data structures for advanced editing use cases.
/// </summary>
public class AdvancedEditing
public class ContentStream
{
/// <summary>
/// The operations making up the page content stream.
@@ -539,11 +583,16 @@
public List<IGraphicsStateOperation> Operations { get; }
/// <summary>
/// Create a new <see cref="AdvancedEditing"/>.
/// Create a new <see cref="ContentStream"/>.
/// </summary>
internal AdvancedEditing(List<IGraphicsStateOperation> operations)
internal ContentStream()
{
Operations = operations;
Operations = new List<IGraphicsStateOperation>();
}
internal void Add(IGraphicsStateOperation newOperation)
{
Operations.Add(newOperation);
}
}