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
+