From 1972355e5aeb942f3c8734c08b4014a88715cbf9 Mon Sep 17 00:00:00 2001 From: Gerard Gunnewijk Date: Fri, 18 Oct 2024 14:23:28 +0200 Subject: [PATCH 1/4] Add support for overprinting by way of extended graphics states --- Directory.Build.targets | 2 +- .../ExtendedGraphicsState.cs | 23 +++++++++++++++++++ .../{GraphicState.cs => GraphicsState.cs} | 8 +++---- .../IContentContext.cs | 11 +++++++-- .../ITextContentContext.cs | 2 +- .../Internals/PageContentContext.cs | 11 +++++++-- .../Internals/ShapesContentContext.cs | 11 +++++++-- .../Internals/TextContentContext.cs | 11 +++++++-- .../LowLevel/ContentStream.cs | 14 +++++++++++ .../LowLevel/Graphics/LineJoinStyle.cs | 2 +- .../LowLevel/Internal/PageResources.cs | 19 +++++++++++++++ .../LowLevel/ObjectStream.cs | 21 +++++++++++++++++ .../LowLevel/PdfDictionary.cs | 16 +++++++++++++ .../LowLevel/PdfStream.cs | 12 ++++++++++ src/Synercoding.FileFormats.Pdf/PdfPage.cs | 2 +- 15 files changed, 149 insertions(+), 16 deletions(-) create mode 100644 src/Synercoding.FileFormats.Pdf/ExtendedGraphicsState.cs rename src/Synercoding.FileFormats.Pdf/{GraphicState.cs => GraphicsState.cs} (97%) diff --git a/Directory.Build.targets b/Directory.Build.targets index 35d8d32..9a3792f 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -7,7 +7,7 @@ - + diff --git a/src/Synercoding.FileFormats.Pdf/ExtendedGraphicsState.cs b/src/Synercoding.FileFormats.Pdf/ExtendedGraphicsState.cs new file mode 100644 index 0000000..5bd2db0 --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/ExtendedGraphicsState.cs @@ -0,0 +1,23 @@ +namespace Synercoding.FileFormats.Pdf; + +/// +/// Class representing an ExtGState dictionary. +/// +public sealed record class ExtendedGraphicsState +{ + /// + /// A flag specifying whether to apply overprint. + /// There are two separate overprint parameters: one for stroking and one for all other painting operations. + /// Specifying an entry sets both parameters + /// unless there is also an entry in the same graphics state parameter dictionary, + /// in which case the entry sets only the overprint parameter for stroking. + /// + public bool? Overprint { get; set; } + + /// + /// A flag specifying whether to apply overprint for painting operations other than stroking. + /// If this entry is absent, the entry, if any, sets this parameter. + /// + public bool? OverprintNonStroking { get; set; } +} + diff --git a/src/Synercoding.FileFormats.Pdf/GraphicState.cs b/src/Synercoding.FileFormats.Pdf/GraphicsState.cs similarity index 97% rename from src/Synercoding.FileFormats.Pdf/GraphicState.cs rename to src/Synercoding.FileFormats.Pdf/GraphicsState.cs index f489acf..bd74b11 100644 --- a/src/Synercoding.FileFormats.Pdf/GraphicState.cs +++ b/src/Synercoding.FileFormats.Pdf/GraphicsState.cs @@ -7,9 +7,9 @@ namespace Synercoding.FileFormats.Pdf; /// /// Class representing the grahpic state of a PDF at a certain moment in time. /// -public sealed class GraphicState +public sealed class GraphicsState { - internal GraphicState() + internal GraphicsState() { CTM = Matrix.Identity; Fill = PredefinedColors.Black; @@ -111,9 +111,9 @@ internal GraphicState() /// public double TextRise { get; internal set; } - internal GraphicState Clone() + internal GraphicsState Clone() { - return new GraphicState() + return new GraphicsState() { CTM = CTM, Fill = Fill, diff --git a/src/Synercoding.FileFormats.Pdf/IContentContext.cs b/src/Synercoding.FileFormats.Pdf/IContentContext.cs index 1f4a37e..02e80b0 100644 --- a/src/Synercoding.FileFormats.Pdf/IContentContext.cs +++ b/src/Synercoding.FileFormats.Pdf/IContentContext.cs @@ -19,7 +19,7 @@ public interface IContentContext /// /// Represents the current graphic state /// - GraphicState GraphicState { get; } + GraphicsState GraphicState { get; } /// /// Wrap the in save and restore state operators @@ -40,7 +40,7 @@ public interface IContentContext Task WrapInStateAsync(T data, Func contentOperations); /// - /// Concatenate a matrix to + /// Concatenate a matrix to /// /// The matrix to concat /// This to enable chaining operations @@ -94,5 +94,12 @@ public interface IContentContext /// The dash pattern to set /// This to enable chaining operations TSelf SetDashPattern(Dash dashPattern); + + /// + /// Set an extended graphics state (ExtGState) dictionary. + /// + /// The state to apply. + /// This to enable chaining operations + TSelf SetExtendedGraphicsState(ExtendedGraphicsState extendedGraphicsState); } diff --git a/src/Synercoding.FileFormats.Pdf/ITextContentContext.cs b/src/Synercoding.FileFormats.Pdf/ITextContentContext.cs index 0fde58e..13895a0 100644 --- a/src/Synercoding.FileFormats.Pdf/ITextContentContext.cs +++ b/src/Synercoding.FileFormats.Pdf/ITextContentContext.cs @@ -101,7 +101,7 @@ public interface ITextContentContext : IContentContext ITextContentContext ShowTextOnNextLine(string text); /// - /// Operation to show text on the next line and setting the and + /// Operation to show text on the next line and setting the and /// /// The text to show /// The word spacing to set diff --git a/src/Synercoding.FileFormats.Pdf/Internals/PageContentContext.cs b/src/Synercoding.FileFormats.Pdf/Internals/PageContentContext.cs index 1174e7d..c876f5a 100644 --- a/src/Synercoding.FileFormats.Pdf/Internals/PageContentContext.cs +++ b/src/Synercoding.FileFormats.Pdf/Internals/PageContentContext.cs @@ -6,7 +6,7 @@ namespace Synercoding.FileFormats.Pdf.Internals; internal class PageContentContext : IPageContentContext { - public PageContentContext(ContentStream contentStream, GraphicState graphicState) + public PageContentContext(ContentStream contentStream, GraphicsState graphicState) { RawContentStream = contentStream; GraphicState = graphicState; @@ -14,7 +14,7 @@ public PageContentContext(ContentStream contentStream, GraphicState graphicState public ContentStream RawContentStream { get; } - public GraphicState GraphicState { get; } + public GraphicsState GraphicState { get; } public IPageContentContext AddImage(Image image) { @@ -155,4 +155,11 @@ public async Task AddShapesAsync(T data, Func + /// Set an extended graphics state (ExtGState) dictionary using a gs operator.. + /// + /// The state to apply. + /// The to support chaining operations. + public ContentStream SetExtendedGraphicsState(ExtendedGraphicsState state) + { + var name = Resources.AddExtendedGraphicsState(state); + + InnerStream.Write(name).Space().Write("gs").NewLine(); + + return this; + } + /// /// Write the operator (m) to the stream /// diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/Graphics/LineJoinStyle.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/Graphics/LineJoinStyle.cs index f2eb82b..a0c3946 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/Graphics/LineJoinStyle.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/Graphics/LineJoinStyle.cs @@ -9,7 +9,7 @@ public enum LineJoinStyle /// The outer edges of the strokes for the two segments shall be extended until they meet at an angle. /// /// - /// If the segments meet at too sharp an angle (see ), a bevel join shall be used instead. + /// If the segments meet at too sharp an angle (see ), a bevel join shall be used instead. /// MiterJoin = 0, /// diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/Internal/PageResources.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/Internal/PageResources.cs index 7fc484e..99a4271 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/Internal/PageResources.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/Internal/PageResources.cs @@ -9,12 +9,15 @@ internal sealed class PageResources : IDisposable { private const string PREFIX_IMAGE = "Im"; private const string PREFIX_SEPARATION = "Sep"; + private const string PREFIX_EXTGSTATE = "ExGs"; private readonly TableBuilder _tableBuilder; private readonly Map _images; private readonly Dictionary _separations; private readonly Dictionary _standardFonts; + private readonly Dictionary _extendedGraphicsStates; + private int _stateCounter = 0; private int _separationCounter = 0; private int _imageCounter = 0; @@ -24,11 +27,15 @@ internal PageResources(TableBuilder tableBuilder) _images = new Map(); _separations = new Dictionary(); _standardFonts = new Dictionary(); + _extendedGraphicsStates = new Dictionary(); } public IReadOnlyDictionary Images => _images.Forward; + public IReadOnlyDictionary ExtendedGraphicsStates + => _extendedGraphicsStates; + internal IReadOnlyDictionary SeparationReferences => _separations; @@ -105,4 +112,16 @@ internal PdfName AddSeparation(Separation separation) return name; } + + internal PdfName AddExtendedGraphicsState(ExtendedGraphicsState extendedGraphicsState) + { + if (_extendedGraphicsStates.TryGetValue(extendedGraphicsState, out var name)) + return name; + + var key = PREFIX_SEPARATION + Interlocked.Increment(ref _stateCounter).ToString().PadLeft(6, '0'); + name = PdfName.Get(key); + _extendedGraphicsStates[extendedGraphicsState] = name; + + return name; + } } diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/ObjectStream.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/ObjectStream.cs index cacc7af..196931b 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/ObjectStream.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/ObjectStream.cs @@ -159,6 +159,27 @@ public ObjectStream Write(PdfPage page) } })); } + + if (resources.ExtendedGraphicsStates.Count != 0) + { + stream.Write(PdfName.Get("ExtGState"), resources.ExtendedGraphicsStates, static (extGStates, stream) => stream.Dictionary(extGStates, static (extendedGStates, extgstateDictionary) => + { + foreach(var (state, name) in extendedGStates) + { + extgstateDictionary.Write(name, state, static (state, raw) => + { + raw.Dictionary(state, static (state, dict) => + { + if (state.Overprint.HasValue) + dict.Write(PdfName.Get("OP"), state.Overprint.Value); + if (state.OverprintNonStroking.HasValue) + dict.Write(PdfName.Get("op"), state.OverprintNonStroking.Value); + }); + }); + } + })); + } + })); // Content stream diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/PdfDictionary.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfDictionary.cs index 844001d..1c16185 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/PdfDictionary.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfDictionary.cs @@ -146,6 +146,22 @@ public PdfDictionary Write(PdfName key, int value) return this; } + /// + /// Write a boolean to the dictionary + /// + /// The key of the item in the dictionary + /// The boolean to write + /// The to support chaining operations. + public PdfDictionary Write(PdfName key, bool value) + { + _stream + .Write(key) + .Space() + .Write(value); + + return this; + } + /// /// Write a text to the dictionary /// diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/PdfStream.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfStream.cs index 567b152..7b2cc7d 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/PdfStream.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfStream.cs @@ -56,6 +56,18 @@ public PdfStream Write(char c) return WriteByte((byte)( c & 0xFF )); } + /// + /// Write a to the stream + /// + /// The boolean to write. + /// The calling to support chaining operations. + public PdfStream Write(bool b) + { + return b + ? Write("true") + : Write("false"); + } + /// /// Write a to the stream /// diff --git a/src/Synercoding.FileFormats.Pdf/PdfPage.cs b/src/Synercoding.FileFormats.Pdf/PdfPage.cs index 80a3096..067879c 100644 --- a/src/Synercoding.FileFormats.Pdf/PdfPage.cs +++ b/src/Synercoding.FileFormats.Pdf/PdfPage.cs @@ -26,7 +26,7 @@ internal PdfPage(TableBuilder tableBuilder, PageTree parent) Resources = new PageResources(_tableBuilder); var contentStream = new ContentStream(tableBuilder.ReserveId(), Resources); - Content = new PageContentContext(contentStream, new GraphicState()); + Content = new PageContentContext(contentStream, new GraphicsState()); } internal PdfReference Parent From d7564c91f9b025fc63e8c1de59fc446c3b4705ab Mon Sep 17 00:00:00 2001 From: Gerard Gunnewijk Date: Fri, 18 Oct 2024 14:40:55 +0200 Subject: [PATCH 2/4] Moved ExtGState dictionaries to indirect objects, fixed prefix, and added to example --- .../Program.cs | 4 +++ .../LowLevel/Internal/PageResources.cs | 16 +++++----- .../LowLevel/ObjectStream.cs | 31 ++++++++++++------- src/Synercoding.FileFormats.Pdf/PdfWriter.cs | 3 ++ 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs b/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs index b3f5be4..41d56b5 100644 --- a/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs +++ b/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs @@ -216,6 +216,10 @@ public static void Main(string[] args) page.Content.AddShapes(trimBox, static (trim, context) => { + context.SetExtendedGraphicsState(new ExtendedGraphicsState() + { + Overprint = true + }); context.SetStroke(new SpotColor(new Separation(LowLevel.PdfName.Get("CutContour"), PredefinedColors.Magenta), 1)); context.Rectangle(trim); context.Stroke(); diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/Internal/PageResources.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/Internal/PageResources.cs index 99a4271..b73d780 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/Internal/PageResources.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/Internal/PageResources.cs @@ -15,7 +15,7 @@ internal sealed class PageResources : IDisposable private readonly Map _images; private readonly Dictionary _separations; private readonly Dictionary _standardFonts; - private readonly Dictionary _extendedGraphicsStates; + private readonly Dictionary _extendedGraphicsStates; private int _stateCounter = 0; private int _separationCounter = 0; @@ -27,13 +27,13 @@ internal PageResources(TableBuilder tableBuilder) _images = new Map(); _separations = new Dictionary(); _standardFonts = new Dictionary(); - _extendedGraphicsStates = new Dictionary(); + _extendedGraphicsStates = new Dictionary(); } public IReadOnlyDictionary Images => _images.Forward; - public IReadOnlyDictionary ExtendedGraphicsStates + public IReadOnlyDictionary ExtendedGraphicsStates => _extendedGraphicsStates; internal IReadOnlyDictionary SeparationReferences @@ -115,12 +115,12 @@ internal PdfName AddSeparation(Separation separation) internal PdfName AddExtendedGraphicsState(ExtendedGraphicsState extendedGraphicsState) { - if (_extendedGraphicsStates.TryGetValue(extendedGraphicsState, out var name)) - return name; + if (_extendedGraphicsStates.TryGetValue(extendedGraphicsState, out var tuple)) + return tuple.Name; - var key = PREFIX_SEPARATION + Interlocked.Increment(ref _stateCounter).ToString().PadLeft(6, '0'); - name = PdfName.Get(key); - _extendedGraphicsStates[extendedGraphicsState] = name; + var key = PREFIX_EXTGSTATE + Interlocked.Increment(ref _stateCounter).ToString().PadLeft(6, '0'); + var name = PdfName.Get(key); + _extendedGraphicsStates[extendedGraphicsState] = (name, _tableBuilder.ReserveId()); return name; } diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/ObjectStream.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/ObjectStream.cs index 196931b..dc6727f 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/ObjectStream.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/ObjectStream.cs @@ -162,20 +162,11 @@ public ObjectStream Write(PdfPage page) if (resources.ExtendedGraphicsStates.Count != 0) { - stream.Write(PdfName.Get("ExtGState"), resources.ExtendedGraphicsStates, static (extGStates, stream) => stream.Dictionary(extGStates, static (extendedGStates, extgstateDictionary) => + stream.Write(PdfName.Get("ExtGState"), resources.ExtendedGraphicsStates.Values, static (extGStates, stream) => stream.Dictionary(extGStates, static (extendedGStates, dict) => { - foreach(var (state, name) in extendedGStates) + foreach (var (name, reference) in extendedGStates) { - extgstateDictionary.Write(name, state, static (state, raw) => - { - raw.Dictionary(state, static (state, dict) => - { - if (state.Overprint.HasValue) - dict.Write(PdfName.Get("OP"), state.Overprint.Value); - if (state.OverprintNonStroking.HasValue) - dict.Write(PdfName.Get("op"), state.OverprintNonStroking.Value); - }); - }); + dict.Write(name, reference); } })); } @@ -238,6 +229,22 @@ public ObjectStream Write(PdfReference reference, Separation separation) return this; } + public ObjectStream Write(PdfReference reference, ExtendedGraphicsState state) + { + if (!_tableBuilder.TrySetPosition(reference, InnerStream.Position)) + return this; + + _indirectDictionary(reference, state, static (state, dict) => + { + if (state.Overprint.HasValue) + dict.Write(PdfName.Get("OP"), state.Overprint.Value); + if (state.OverprintNonStroking.HasValue) + dict.Write(PdfName.Get("op"), state.OverprintNonStroking.Value); + }); + + return this; + } + private void _indirectDictionary(PdfReference reference, T data, Action dictionaryAction) { InnerStream diff --git a/src/Synercoding.FileFormats.Pdf/PdfWriter.cs b/src/Synercoding.FileFormats.Pdf/PdfWriter.cs index 0d685d5..174b1a1 100644 --- a/src/Synercoding.FileFormats.Pdf/PdfWriter.cs +++ b/src/Synercoding.FileFormats.Pdf/PdfWriter.cs @@ -233,6 +233,9 @@ private void _writePageAndResourcesToObjectStream(PdfPage page) foreach (var (separation, (_, refId)) in page.Resources.SeparationReferences) _objectStream.Write(refId, separation); + foreach (var (state, (_, refId)) in page.Resources.ExtendedGraphicsStates) + _objectStream.Write(refId, state); + _objectStream.Write(page.Content.RawContentStream); } From 163b9fcd9bb3eadf0cee1fe15c1cf36acbe5dcdc Mon Sep 17 00:00:00 2001 From: Gerard Gunnewijk Date: Fri, 18 Oct 2024 14:44:19 +0200 Subject: [PATCH 3/4] Fix deprecated error --- .github/workflows/dotnet-core.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml index cd44a0f..18fab3b 100644 --- a/.github/workflows/dotnet-core.yml +++ b/.github/workflows/dotnet-core.yml @@ -55,7 +55,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download Artifact - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v4.1.7 with: name: nupkg - name: Push to GitHub Feed @@ -90,4 +90,4 @@ jobs: done - name: Push to NuGet Feed if: ${{ env.NUGET_FEED }} != '' - run: dotnet nuget push ./nupkg/*.nupkg --source $NUGET_FEED --skip-duplicate --api-key $NUGET_KEY \ No newline at end of file + run: dotnet nuget push ./nupkg/*.nupkg --source $NUGET_FEED --skip-duplicate --api-key $NUGET_KEY From d09933f11e32aae1a2b14eabf79ba84d08f2b8bb Mon Sep 17 00:00:00 2001 From: Gerard Gunnewijk Date: Fri, 18 Oct 2024 14:46:04 +0200 Subject: [PATCH 4/4] Fix upload artefact --- .github/workflows/dotnet-core.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml index 18fab3b..7c2b432 100644 --- a/.github/workflows/dotnet-core.yml +++ b/.github/workflows/dotnet-core.yml @@ -45,7 +45,7 @@ jobs: run: dotnet pack -v normal -c Release --no-restore --include-symbols --include-source -p:SymbolPackageFormat=snupkg -p:PackageVersion=1.0.0-pre+$GITHUB_RUN_ID src/$PROJECT_NAME/$PROJECT_NAME.*proj - name: Upload Artifact if: matrix.os == 'ubuntu-latest' - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: nupkg path: ./artifacts/pkg/Release/${{ env.PROJECT_NAME }}.*.nupkg @@ -55,7 +55,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download Artifact - uses: actions/download-artifact@v4.1.7 + uses: actions/download-artifact@v4 with: name: nupkg - name: Push to GitHub Feed