diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml index cd44a0f..fe1a006 100644 --- a/.github/workflows/dotnet-core.yml +++ b/.github/workflows/dotnet-core.yml @@ -15,7 +15,7 @@ env: # Project name to pack and publish PROJECT_NAME: Synercoding.FileFormats.Pdf # GitHub Packages Feed settings - GITHUB_FEED: https://nuget.pkg.github.com/synercoder/ + GITHUB_FEED: https://nuget.pkg.github.com/synercoder/index.json GITHUB_USER: synercoder GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Official NuGet Feed settings @@ -33,7 +33,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Restore run: dotnet restore - name: Build @@ -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,15 +55,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Download Artifact - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v4 with: name: nupkg - - name: Push to GitHub Feed - run: | - for f in ./nupkg/*.nupkg - do - curl -vX PUT -u "$GITHUB_USER:$GITHUB_TOKEN" -F package=@$f $GITHUB_FEED - done + path: ./nupkg + - name: Setup .NET Core @ Latest + uses: actions/setup-dotnet@v1 + - name: Publish Nuget to GitHub registry + run: dotnet nuget push ./nupkg/*.nupkg -k ${GITHUB_TOKEN} -s ${GITHUB_FEED} --skip-duplicate + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} deploy: needs: build if: github.event_name == 'release' @@ -73,7 +74,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Create Release NuGet package run: | arrTag=(${GITHUB_REF//\// }) @@ -82,12 +83,10 @@ jobs: VERSION="${VERSION//v}" echo Clean Version: $VERSION dotnet pack -v normal -c Release --include-symbols --include-source -p:SymbolPackageFormat=snupkg -p:PackageVersion=$VERSION -o nupkg src/$PROJECT_NAME/$PROJECT_NAME.*proj - - name: Push to GitHub Feed - run: | - for f in ./nupkg/*.nupkg - do - curl -vX PUT -u "$GITHUB_USER:$GITHUB_TOKEN" -F package=@$f $GITHUB_FEED - done + - name: Publish Nuget to GitHub registry + run: dotnet nuget push ./nupkg/*.nupkg -k ${GITHUB_TOKEN} -s ${GITHUB_FEED} --skip-duplicate + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - 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 diff --git a/Directory.Build.props b/Directory.Build.props index 4c76e77..d268479 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -41,7 +41,7 @@ - 11.0 + 12.0 enable true strict 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/README.md b/README.md index cde46aa..93bae7d 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,10 @@ The PDF files created in this library are not fully PDF 1.7 compliant, because t This shortcoming shall be remedied when broader font support is implemented. ### Sample program images -The sample project called *Synercoding.FileFormats.Pdf.ConsoleTester* uses multiple images. Those images were taken from [Pexels.com](https://www.pexels.com/royalty-free-images/) and are licensed under the [Pexels License](https://www.pexels.com/photo-license/). +The sample project called *Synercoding.FileFormats.Pdf.ConsoleTester* uses multiple images. +Those images were taken from: +- [Pexels.com](https://www.pexels.com/royalty-free-images/) and are licensed under the [Pexels License](https://www.pexels.com/photo-license/) +- [FreePngImg.com](https://freepngimg.com/png/59872-jaguar-panther-royalty-free-cougar-black-cheetah) and are licensed under [Creative Commons (CC BY-NC 4.0)](https://creativecommons.org/licenses/by-nc/4.0/) ## Sample usage diff --git a/samples/Synercoding.FileFormats.Pdf.ConsoleTester/FreePngImage_com/59872-jaguar-panther-royalty-free-cougar-black-cheetah.png b/samples/Synercoding.FileFormats.Pdf.ConsoleTester/FreePngImage_com/59872-jaguar-panther-royalty-free-cougar-black-cheetah.png new file mode 100644 index 0000000..a1ffe71 Binary files /dev/null and b/samples/Synercoding.FileFormats.Pdf.ConsoleTester/FreePngImage_com/59872-jaguar-panther-royalty-free-cougar-black-cheetah.png differ diff --git a/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs b/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs index b3f5be4..298ee5a 100644 --- a/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs +++ b/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs @@ -1,3 +1,4 @@ +using SixLabors.ImageSharp.PixelFormats; using Synercoding.FileFormats.Pdf.Extensions; using Synercoding.FileFormats.Pdf.LowLevel.Colors; using Synercoding.FileFormats.Pdf.LowLevel.Colors.ColorSpaces; @@ -63,7 +64,7 @@ public static void Main(string[] args) }); using (var forestStream = File.OpenRead("Pexels_com/android-wallpaper-art-backlit-1114897.jpg")) - using (var forestImage = SixLabors.ImageSharp.Image.Load(forestStream)) + using (var forestImage = SixLabors.ImageSharp.Image.Load(forestStream)) { var scale = (double)forestImage.Width / forestImage.Height; @@ -81,7 +82,7 @@ public static void Main(string[] args) page.TrimBox = trimBox; using (var barrenStream = File.OpenRead("Pexels_com/arid-barren-desert-1975514.jpg")) - using (var barrenImage = SixLabors.ImageSharp.Image.Load(barrenStream)) + using (var barrenImage = SixLabors.ImageSharp.Image.Load(barrenStream)) { var scale = (double)barrenImage.Width / barrenImage.Height; @@ -175,7 +176,7 @@ public static void Main(string[] args) page.TrimBox = trimBox; using (var forestStream = File.OpenRead("Pexels_com/android-wallpaper-art-backlit-1114897.jpg")) - using (var forestImage = SixLabors.ImageSharp.Image.Load(forestStream)) + using (var forestImage = SixLabors.ImageSharp.Image.Load(forestStream)) { var scale = (double)forestImage.Width / forestImage.Height; @@ -187,7 +188,7 @@ public static void Main(string[] args) }); using (var blurStream = File.OpenRead("Pexels_com/4k-wallpaper-blur-bokeh-1484253.jpg")) - using (var blurImage = SixLabors.ImageSharp.Image.Load(blurStream)) + using (var blurImage = SixLabors.ImageSharp.Image.Load(blurStream)) { var reusedImage = writer.AddImage(blurImage); @@ -216,12 +217,43 @@ 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(); }); }); } + + using (var pantherPngStream = File.OpenRead("FreePngImage_com/59872-jaguar-panther-royalty-free-cougar-black-cheetah.png")) + using (var pantherImage = SixLabors.ImageSharp.Image.Load(pantherPngStream)) + { + var pantherImg = writer.AddImage(pantherImage); + var transparentPanther = writer.AddSeparationImage(new Separation(LowLevel.PdfName.Get("White"), PredefinedColors.Yellow), pantherImage, GrayScaleMethod.AlphaChannel); + + writer.AddPage(page => + { + page.MediaBox = mediaBox; + page.TrimBox = trimBox; + + var scale = (double)transparentPanther.Width / transparentPanther.Height; + var pantherSize = new Rectangle(0, 0, 216, 216 / scale, Unit.Millimeters); + + page.Content.AddImage(pantherImage, pantherSize); + + page.Content.WrapInState(pantherImage, (image, content) => + { + content.SetExtendedGraphicsState(new ExtendedGraphicsState() + { + Overprint = true + }); + content.AddImage(transparentPanther, pantherSize); + }); + }); + } } } } diff --git a/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Synercoding.FileFormats.Pdf.ConsoleTester.csproj b/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Synercoding.FileFormats.Pdf.ConsoleTester.csproj index 2d5a218..049d071 100644 --- a/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Synercoding.FileFormats.Pdf.ConsoleTester.csproj +++ b/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Synercoding.FileFormats.Pdf.ConsoleTester.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 @@ -15,4 +15,10 @@ + + + PreserveNewest + + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 896375d..94964a7 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ - net7.0;net6.0 + net8.0 $(MSBuildAllProjects);$(MSBuildThisFileDirectory)..\Directory.Build.props src 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/Extensions/IPageContentContextExtensions.cs b/src/Synercoding.FileFormats.Pdf/Extensions/IPageContentContextExtensions.cs index 9d7e0d6..e04ad6e 100644 --- a/src/Synercoding.FileFormats.Pdf/Extensions/IPageContentContextExtensions.cs +++ b/src/Synercoding.FileFormats.Pdf/Extensions/IPageContentContextExtensions.cs @@ -1,3 +1,4 @@ +using SixLabors.ImageSharp.PixelFormats; using Synercoding.FileFormats.Pdf.Internals; using Synercoding.FileFormats.Pdf.LowLevel.Colors.ColorSpaces; using Synercoding.FileFormats.Pdf.LowLevel.Text; @@ -61,7 +62,7 @@ public static IPageContentContext AddImage(this IPageContentContext context, Sys /// The same to enable chaining operations. public static IPageContentContext AddImage(this IPageContentContext context, System.IO.Stream stream) { - using var image = SixLabors.ImageSharp.Image.Load(stream); + using var image = SixLabors.ImageSharp.Image.Load(stream); return context.AddImage(image); } @@ -73,7 +74,7 @@ public static IPageContentContext AddImage(this IPageContentContext context, Sys /// The image to place /// The placement matrix to use /// The same to enable chaining operations. - public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image image, Matrix matrix) + public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image image, Matrix matrix) { return context.WrapInState((image, matrix), static (tuple, context) => { @@ -89,7 +90,7 @@ public static IPageContentContext AddImage(this IPageContentContext context, Six /// The image to place /// The rectangle of where to place the image. /// The same to enable chaining operations. - public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image image, Rectangle rectangle) + public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image image, Rectangle rectangle) => context.AddImage(image, rectangle.AsPlacementMatrix()); /// @@ -98,7 +99,7 @@ public static IPageContentContext AddImage(this IPageContentContext context, Six /// The context to add the image to. /// The image to place /// The same to enable chaining operations. - public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image image) + public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image image) { var name = context.RawContentStream.Resources.AddImage(image); 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/GrayScaleMethod.cs b/src/Synercoding.FileFormats.Pdf/GrayScaleMethod.cs new file mode 100644 index 0000000..96cdc55 --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/GrayScaleMethod.cs @@ -0,0 +1,28 @@ +namespace Synercoding.FileFormats.Pdf; + +/// +/// What method is used to generate a 1 component grayscale pixel byte array +/// +public enum GrayScaleMethod +{ + /// + /// Use the red channel + /// + RedChannel, + /// + /// Use the green channel + /// + GreenChannel, + /// + /// Use the blue channel + /// + BlueChannel, + /// + /// Use the alpha channel + /// + AlphaChannel, + /// + /// Use the average of the Red, Green and Blue channels. + /// + AverageOfRGBChannels +} 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/Image.cs b/src/Synercoding.FileFormats.Pdf/Image.cs index 9500f2f..019ffeb 100644 --- a/src/Synercoding.FileFormats.Pdf/Image.cs +++ b/src/Synercoding.FileFormats.Pdf/Image.cs @@ -1,54 +1,20 @@ using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; using Synercoding.FileFormats.Pdf.LowLevel; using Synercoding.FileFormats.Pdf.LowLevel.Colors.ColorSpaces; +using Synercoding.FileFormats.Pdf.LowLevel.XRef; +using System.IO.Compression; namespace Synercoding.FileFormats.Pdf; /// /// Class representing an image inside a pdf /// -public sealed class Image : IDisposable +public class Image : IDisposable { - private bool _disposed; + private protected bool _disposed; - 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, - ColorType = SixLabors.ImageSharp.Formats.Jpeg.JpegEncodingColor.YCbCrRatio444 - }); - Width = image.Width; - Height = image.Height; - ColorSpace = DeviceRGB.Instance.Name; - DecodeArray = new double[] { 0, 1, 0, 1, 0, 1 }; - ms.Position = 0; - RawStream = ms; - } - - internal Image(PdfReference id, Stream jpgStream, int width, int height, ColorSpace colorSpace) - { - Reference = id; - - Width = width; - Height = height; - RawStream = jpgStream; - - var (csName, decodeArray) = colorSpace switch - { - DeviceCMYK cmyk => (cmyk.Name, new double[] { 0, 1, 0, 1, 0, 1, 0, 1 }), - DeviceRGB rgb => (rgb.Name, new double[] { 0, 1, 0, 1, 0, 1 }), - _ => throw new ArgumentOutOfRangeException(nameof(colorSpace), $"The provided color space {colorSpace} is currently not supported.") - }; - - ColorSpace = csName; - DecodeArray = decodeArray; - } - - internal Image(PdfReference id, Stream jpgStream, int width, int height, PdfName colorSpace, double[] decodeArray) + internal Image(PdfReference id, Stream jpgStream, int width, int height, ColorSpace colorSpace, Image? softMask, params StreamFilter[] filters) { Reference = id; @@ -56,9 +22,12 @@ internal Image(PdfReference id, Stream jpgStream, int width, int height, PdfName Height = height; RawStream = jpgStream; ColorSpace = colorSpace; - DecodeArray = decodeArray; + SoftMask = softMask; + Filters = filters; } + internal Image? SoftMask { get; private set; } + internal Stream RawStream { get; private set; } /// @@ -79,12 +48,9 @@ internal Image(PdfReference id, Stream jpgStream, int width, int height, PdfName /// /// The name of the colorspace used in this /// - public PdfName ColorSpace { get; } + public ColorSpace ColorSpace { get; } - /// - /// The decode array used in this - /// - public double[] DecodeArray { get; } + internal StreamFilter[] Filters { get; } = Array.Empty(); /// public void Dispose() @@ -95,4 +61,74 @@ public void Dispose() _disposed = true; } } + + private static Stream _encodeToJpg(SixLabors.ImageSharp.Image image) + { + var ms = new MemoryStream(); + image.SaveAsJpeg(ms, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder() + { + Quality = 100, + ColorType = SixLabors.ImageSharp.Formats.Jpeg.JpegEncodingColor.YCbCrRatio444 + }); + ms.Position = 0; + + return ms; + } + + internal static Image Get(TableBuilder tableBuilder, Image image) + { + return new Image(tableBuilder.ReserveId(), _encodeToJpg(image), image.Width, image.Height, DeviceRGB.Instance, GetMask(tableBuilder, image), StreamFilter.DCTDecode); + } + + internal static Image? GetMask(TableBuilder tableBuilder, Image image) + { + var hasTrans = image.Metadata.TryGetPngMetadata(out var pngMeta) + && + ( + pngMeta.ColorType == SixLabors.ImageSharp.Formats.Png.PngColorType.RgbWithAlpha + || pngMeta.ColorType == SixLabors.ImageSharp.Formats.Png.PngColorType.GrayscaleWithAlpha + ); + + return hasTrans + ? new Image(tableBuilder.ReserveId(), AsImageByteStream(image, GrayScaleMethod.AlphaChannel), image.Width, image.Height, DeviceGray.Instance, null, StreamFilter.FlateDecode) + : null; + } + + internal static Stream AsImageByteStream(Image image, GrayScaleMethod grayScaleMethod) + { + using (var byteStream = new MemoryStream()) + { + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + var pixelRow = accessor.GetRowSpan(y); + + // pixelRow.Length has the same value as accessor.Width, + // but using pixelRow.Length allows the JIT to optimize away bounds checks: + for (int x = 0; x < pixelRow.Length; x++) + { + // Get a reference to the pixel at position x + ref Rgba32 pixel = ref pixelRow[x]; + + var pixelValue = grayScaleMethod switch + { + GrayScaleMethod.AlphaChannel => pixel.A, + GrayScaleMethod.RedChannel => pixel.R, + GrayScaleMethod.GreenChannel => pixel.G, + GrayScaleMethod.BlueChannel => pixel.B, + GrayScaleMethod.AverageOfRGBChannels => (byte)( ( pixel.R + pixel.G + pixel.B ) / 3 ), + _ => throw new NotImplementedException() + }; + + byteStream.WriteByte(pixelValue); + } + } + }); + + byteStream.Position = 0; + + return PdfWriter.FlateEncode(byteStream); + } + } } 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/Extensions/StreamFilterExtensions.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/StreamFilterExtensions.cs index fd39a5f..54d44ec 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/StreamFilterExtensions.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/StreamFilterExtensions.cs @@ -7,6 +7,7 @@ public static PdfName ToPdfName(this StreamFilter streamFilter) return streamFilter switch { StreamFilter.DCTDecode => PdfName.Get("DCTDecode"), + StreamFilter.FlateDecode => PdfName.Get("FlateDecode"), _ => throw new NotImplementedException(), }; } 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..97e1028 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/Internal/PageResources.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/Internal/PageResources.cs @@ -1,3 +1,4 @@ +using SixLabors.ImageSharp.PixelFormats; using Synercoding.FileFormats.Pdf.Internals; using Synercoding.FileFormats.Pdf.LowLevel.Colors.ColorSpaces; using Synercoding.FileFormats.Pdf.LowLevel.Text; @@ -9,12 +10,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 +28,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; @@ -49,25 +57,14 @@ public PdfName AddJpgUnsafe(System.IO.Stream jpgStream, int originalWidth, int o { var id = _tableBuilder.ReserveId(); - var pdfImage = new Image(id, jpgStream, originalWidth, originalHeight, colorSpace); + var pdfImage = new Image(id, jpgStream, originalWidth, originalHeight, colorSpace, null, StreamFilter.DCTDecode); return AddImage(pdfImage); } - public PdfName AddJpgUnsafe(System.IO.Stream jpgStream, int originalWidth, int originalHeight, PdfName colorSpace, double[] decodeArray) + public PdfName AddImage(SixLabors.ImageSharp.Image image) { - var id = _tableBuilder.ReserveId(); - - var pdfImage = new Image(id, jpgStream, originalWidth, originalHeight, colorSpace, decodeArray); - - return AddImage(pdfImage); - } - - public PdfName AddImage(SixLabors.ImageSharp.Image image) - { - var id = _tableBuilder.ReserveId(); - - var pdfImage = new Image(id, image); + var pdfImage = Image.Get(_tableBuilder, image); return AddImage(pdfImage); } @@ -101,7 +98,19 @@ internal PdfName AddSeparation(Separation separation) var key = PREFIX_SEPARATION + System.Threading.Interlocked.Increment(ref _separationCounter).ToString().PadLeft(6, '0'); var name = PdfName.Get(key); - _separations[separation] = (name, _tableBuilder.ReserveId()); + _separations[separation] = (name, _tableBuilder.GetSeparationId(separation)); + + return name; + } + + internal PdfName AddExtendedGraphicsState(ExtendedGraphicsState extendedGraphicsState) + { + if (_extendedGraphicsStates.TryGetValue(extendedGraphicsState, out var tuple)) + return tuple.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 cacc7af..d6ec4d5 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/ObjectStream.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/ObjectStream.cs @@ -3,6 +3,7 @@ using Synercoding.FileFormats.Pdf.LowLevel.Internal; using Synercoding.FileFormats.Pdf.LowLevel.Text; using Synercoding.FileFormats.Pdf.LowLevel.XRef; +using System.IO.Compression; namespace Synercoding.FileFormats.Pdf.LowLevel; @@ -26,7 +27,8 @@ public ObjectStream Write(ContentStream contentStream) if (!_tableBuilder.TrySetPosition(contentStream.Reference, InnerStream.Position)) return this; - _indirectStream(contentStream.Reference, contentStream.InnerStream.InnerStream); + using (var flateStream = PdfWriter.FlateEncode(contentStream.InnerStream.InnerStream)) + _indirectStream(contentStream.Reference, flateStream, StreamFilter.FlateDecode); return this; } @@ -92,19 +94,43 @@ public ObjectStream Write(Image image) if (!_tableBuilder.TrySetPosition(image.Reference, InnerStream.Position)) return this; - _indirectStream(image.Reference, image.RawStream, image, static (image, dictionary) => + _indirectStream(image.Reference, image.RawStream, (image, _tableBuilder), static (tuple, dictionary) => { + var (image, tableBuilder) = tuple; dictionary .Write(PdfName.Get("Type"), PdfName.Get("XObject")) .Write(PdfName.Get("Subtype"), PdfName.Get("Image")) .Write(PdfName.Get("Width"), image.Width) .Write(PdfName.Get("Height"), image.Height) - .Write(PdfName.Get("ColorSpace"), image.ColorSpace) .Write(PdfName.Get("BitsPerComponent"), 8) - .Write(PdfName.Get("Decode"), image.DecodeArray); - }, StreamFilter.DCTDecode); + .Write(PdfName.Get("Decode"), _decodeArray(image.ColorSpace)) + .WriteIfNotNull(PdfName.Get("SMask"), image.SoftMask?.Reference); + + + if (image.ColorSpace is Separation separation) + { + var sepId = tableBuilder.GetSeparationId(separation); + dictionary.Write(PdfName.Get("ColorSpace"), sepId); + } + else + { + dictionary.Write(PdfName.Get("ColorSpace"), image.ColorSpace.Name); + } + }, image.Filters); + + if (image.SoftMask != null) + Write(image.SoftMask); + + if(image.ColorSpace is Separation separation) + Write(separation); return this; + + static double[] _decodeArray(ColorSpace colorSpace) + => Enumerable.Range(0, colorSpace.Components) + .Select(_ => new double[] { 0, 1 }) + .SelectMany(x => x) + .ToArray(); } public ObjectStream Write(PdfPage page) @@ -159,6 +185,18 @@ public ObjectStream Write(PdfPage page) } })); } + + if (resources.ExtendedGraphicsStates.Count != 0) + { + stream.Write(PdfName.Get("ExtGState"), resources.ExtendedGraphicsStates.Values, static (extGStates, stream) => stream.Dictionary(extGStates, static (extendedGStates, dict) => + { + foreach (var (name, reference) in extendedGStates) + { + dict.Write(name, reference); + } + })); + } + })); // Content stream @@ -184,13 +222,15 @@ public ObjectStream Write(PdfReference reference, Type1StandardFont font) return this; } - public ObjectStream Write(PdfReference reference, Separation separation) + public ObjectStream Write(Separation separation) { - if (!_tableBuilder.TrySetPosition(reference, InnerStream.Position)) + var id = _tableBuilder.GetSeparationId(separation); + + if (!_tableBuilder.TrySetPosition(id, InnerStream.Position)) return this; InnerStream - .StartObject(reference) + .StartObject(id) .WriteByte(BRACKET_OPEN) .Write(PdfName.Get("Separation")) .Write(separation.Name) @@ -217,6 +257,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/LowLevel/PdfDictionary.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfDictionary.cs index 844001d..2b50ae8 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 /// @@ -299,7 +315,18 @@ public PdfDictionary WriteIfNotNull(PdfName key, Rectangle? rectangle) : this; /// - /// Write a number to the stream if it is not null + /// Writes a pdf reference to the dictionary if it is not null + /// + /// The key of the item in the dictionary + /// The reference to add. + /// The to support chaining operations. + public PdfDictionary WriteIfNotNull(PdfName key, PdfReference? value) + => value.HasValue + ? Write(key, value.Value) + : this; + + /// + /// Write a number to the dictionary if it is not null /// /// The key of the item in the dictionary /// The number to write. @@ -310,7 +337,7 @@ public PdfDictionary WriteIfNotNull(PdfName key, int? value) : this; /// - /// Write a number to the stream if it is not null + /// Write a number to the dictionary if it is not null /// /// The key of the item in the dictionary /// The number to write. diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/PdfReference.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfReference.cs index f57ea69..9a3f10d 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/PdfReference.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfReference.cs @@ -1,9 +1,11 @@ +using System.Diagnostics.CodeAnalysis; + namespace Synercoding.FileFormats.Pdf.LowLevel; /// /// A struct representing a reference /// -public readonly struct PdfReference +public readonly struct PdfReference : IEquatable { /// /// Constructor for that uses generation 0 @@ -34,9 +36,26 @@ internal PdfReference(int objectId, int generation) /// public int Generation { get; } + /// + public bool Equals(PdfReference other) + => ObjectId == other.ObjectId && Generation == other.Generation; + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + => obj is PdfReference pdfRef && Equals(pdfRef); + + /// + public override int GetHashCode() + => HashCode.Combine(ObjectId, Generation); + /// public override string ToString() - { - return $"{ObjectId} {Generation}"; - } + => $"{ObjectId} {Generation}"; + + /// + public static bool operator ==(PdfReference left, PdfReference right) + => left.Equals(right); + + /// + public static bool operator !=(PdfReference left, PdfReference right) => !( left == right ); } 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/LowLevel/StreamFilter.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/StreamFilter.cs index f546b1b..5aede75 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/StreamFilter.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/StreamFilter.cs @@ -9,5 +9,12 @@ internal enum StreamFilter /// Decompress data encoded using a DCT (discrete cosine transform) technique based on the JPEG standard, /// reproducing image sample data that approximates the original data. /// - DCTDecode + DCTDecode, + + /// + /// The Flate method is based on the public-domain zlib/deflate compression method, + /// which is a variable-length Lempel-Ziv adaptive compression method cascaded + /// with adaptive Huffman coding. It is fully defined in Internet RFC 1950, and Internet RFC 1951. + /// + FlateDecode, } diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/XRef/TableBuilder.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/XRef/TableBuilder.cs index f6b104b..ab01ca1 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/XRef/TableBuilder.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/XRef/TableBuilder.cs @@ -1,3 +1,4 @@ +using Synercoding.FileFormats.Pdf.LowLevel.Colors.ColorSpaces; using Synercoding.FileFormats.Pdf.LowLevel.Internal; namespace Synercoding.FileFormats.Pdf.LowLevel.XRef; @@ -6,6 +7,18 @@ internal class TableBuilder { private readonly IdGenerator _idGen = new IdGenerator(); private readonly Dictionary _positions = new Dictionary(); + private readonly Dictionary _addedSeparations = new Dictionary(); + + public PdfReference GetSeparationId(Separation separation) + { + if (_addedSeparations.TryGetValue(separation, out var id)) + return id; + + id = ReserveId(); + _addedSeparations.Add(separation, id); + return id; + + } public PdfReference ReserveId() { 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 diff --git a/src/Synercoding.FileFormats.Pdf/PdfWriter.cs b/src/Synercoding.FileFormats.Pdf/PdfWriter.cs index 0d685d5..8851695 100644 --- a/src/Synercoding.FileFormats.Pdf/PdfWriter.cs +++ b/src/Synercoding.FileFormats.Pdf/PdfWriter.cs @@ -1,8 +1,11 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; using Synercoding.FileFormats.Pdf.LowLevel; using Synercoding.FileFormats.Pdf.LowLevel.Colors.ColorSpaces; using Synercoding.FileFormats.Pdf.LowLevel.Extensions; using Synercoding.FileFormats.Pdf.LowLevel.Internal; using Synercoding.FileFormats.Pdf.LowLevel.XRef; +using System.IO.Compression; using System.Reflection; namespace Synercoding.FileFormats.Pdf; @@ -147,13 +150,35 @@ public async Task AddPageAsync(T data, Func page /// /// The image that needs to be added. /// The image reference that can be used in pages - public Image AddImage(SixLabors.ImageSharp.Image image) + public Image AddImage(Image image) + { + _throwWhenEndingWritten(); + + var pdfImage = Image.Get(_tableBuilder, image); + + _objectStream.Write(pdfImage); + + return pdfImage; + } + + /// + /// Add a separation image to the . + /// + /// The to use. + /// The image to use. + /// The to use. + /// The SeparationImage reference that can be used in pages + public Image AddSeparationImage(Separation separation, Image image, GrayScaleMethod grayScaleMethod) { _throwWhenEndingWritten(); var id = _tableBuilder.ReserveId(); - var pdfImage = new Image(id, image); + var mask = Image.GetMask(_tableBuilder, image); + + var imageStream = Image.AsImageByteStream(image, grayScaleMethod); + + var pdfImage = new Image(id, imageStream, image.Width, image.Height, separation, mask, StreamFilter.FlateDecode); _objectStream.Write(pdfImage); @@ -177,7 +202,7 @@ public Image AddJpgUnsafe(Stream jpgStream, int originalWidth, int originalHeigh var id = _tableBuilder.ReserveId(); - var pdfImage = new Image(id, jpgStream, originalWidth, originalHeight, colorSpace); + var pdfImage = new Image(id, jpgStream, originalWidth, originalHeight, colorSpace, null, StreamFilter.DCTDecode); _objectStream.Write(pdfImage); @@ -230,12 +255,32 @@ private void _writePageAndResourcesToObjectStream(PdfPage page) foreach (var (font, refId) in page.Resources.FontReferences) _objectStream.Write(refId, font); - foreach (var (separation, (_, refId)) in page.Resources.SeparationReferences) - _objectStream.Write(refId, separation); + foreach (var (separation, _) in page.Resources.SeparationReferences) + _objectStream.Write(separation); + + foreach (var (state, (_, refId)) in page.Resources.ExtendedGraphicsStates) + _objectStream.Write(refId, state); _objectStream.Write(page.Content.RawContentStream); } + internal static Stream FlateEncode(Stream inputStream) + { + var outputStream = new MemoryStream(); + + outputStream.WriteByte(0x78); + outputStream.WriteByte(0xDA); + + inputStream.Position = 0; + using (var flateStream = new DeflateStream(outputStream, CompressionLevel.SmallestSize, true)) + { + inputStream.CopyTo(flateStream); + } + + outputStream.Position = 0; + return outputStream; + } + private void _throwWhenEndingWritten() { if (_endingWritten) throw new InvalidOperationException("Can't change document information when PDF trailer is written to the stream."); diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index f896a2f..9ba0585 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -14,8 +14,8 @@ - net6.0 + net8.0 false - \ No newline at end of file +