Skip to content

Commit 94ca58a

Browse files
authored
Merge pull request #66 from synercoder/features/transparent-and-separation-images
Features/transparent and separation images
2 parents f862897 + d614af1 commit 94ca58a

File tree

15 files changed

+303
-90
lines changed

15 files changed

+303
-90
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ The PDF files created in this library are not fully PDF 1.7 compliant, because t
4141
This shortcoming shall be remedied when broader font support is implemented.
4242

4343
### Sample program images
44-
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/).
44+
The sample project called *Synercoding.FileFormats.Pdf.ConsoleTester* uses multiple images.
45+
Those images were taken from:
46+
- [Pexels.com](https://www.pexels.com/royalty-free-images/) and are licensed under the [Pexels License](https://www.pexels.com/photo-license/)
47+
- [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/)
4548

4649
## Sample usage
4750

samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using SixLabors.ImageSharp.PixelFormats;
12
using Synercoding.FileFormats.Pdf.Extensions;
23
using Synercoding.FileFormats.Pdf.LowLevel.Colors;
34
using Synercoding.FileFormats.Pdf.LowLevel.Colors.ColorSpaces;
@@ -63,7 +64,7 @@ public static void Main(string[] args)
6364
});
6465

6566
using (var forestStream = File.OpenRead("Pexels_com/android-wallpaper-art-backlit-1114897.jpg"))
66-
using (var forestImage = SixLabors.ImageSharp.Image.Load(forestStream))
67+
using (var forestImage = SixLabors.ImageSharp.Image.Load<Rgba32>(forestStream))
6768
{
6869
var scale = (double)forestImage.Width / forestImage.Height;
6970

@@ -81,7 +82,7 @@ public static void Main(string[] args)
8182
page.TrimBox = trimBox;
8283

8384
using (var barrenStream = File.OpenRead("Pexels_com/arid-barren-desert-1975514.jpg"))
84-
using (var barrenImage = SixLabors.ImageSharp.Image.Load(barrenStream))
85+
using (var barrenImage = SixLabors.ImageSharp.Image.Load<Rgba32>(barrenStream))
8586
{
8687
var scale = (double)barrenImage.Width / barrenImage.Height;
8788

@@ -175,7 +176,7 @@ public static void Main(string[] args)
175176
page.TrimBox = trimBox;
176177

177178
using (var forestStream = File.OpenRead("Pexels_com/android-wallpaper-art-backlit-1114897.jpg"))
178-
using (var forestImage = SixLabors.ImageSharp.Image.Load(forestStream))
179+
using (var forestImage = SixLabors.ImageSharp.Image.Load<Rgba32>(forestStream))
179180
{
180181
var scale = (double)forestImage.Width / forestImage.Height;
181182

@@ -187,7 +188,7 @@ public static void Main(string[] args)
187188
});
188189

189190
using (var blurStream = File.OpenRead("Pexels_com/4k-wallpaper-blur-bokeh-1484253.jpg"))
190-
using (var blurImage = SixLabors.ImageSharp.Image.Load(blurStream))
191+
using (var blurImage = SixLabors.ImageSharp.Image.Load<Rgba32>(blurStream))
191192
{
192193
var reusedImage = writer.AddImage(blurImage);
193194

@@ -226,6 +227,33 @@ public static void Main(string[] args)
226227
});
227228
});
228229
}
230+
231+
using (var pantherPngStream = File.OpenRead("FreePngImage_com/59872-jaguar-panther-royalty-free-cougar-black-cheetah.png"))
232+
using (var pantherImage = SixLabors.ImageSharp.Image.Load<Rgba32>(pantherPngStream))
233+
{
234+
var pantherImg = writer.AddImage(pantherImage);
235+
var transparentPanther = writer.AddSeparationImage(new Separation(LowLevel.PdfName.Get("White"), PredefinedColors.Yellow), pantherImage, GrayScaleMethod.AlphaChannel);
236+
237+
writer.AddPage(page =>
238+
{
239+
page.MediaBox = mediaBox;
240+
page.TrimBox = trimBox;
241+
242+
var scale = (double)transparentPanther.Width / transparentPanther.Height;
243+
var pantherSize = new Rectangle(0, 0, 216, 216 / scale, Unit.Millimeters);
244+
245+
page.Content.AddImage(pantherImage, pantherSize);
246+
247+
page.Content.WrapInState(pantherImage, (image, content) =>
248+
{
249+
content.SetExtendedGraphicsState(new ExtendedGraphicsState()
250+
{
251+
Overprint = true
252+
});
253+
content.AddImage(transparentPanther, pantherSize);
254+
});
255+
});
256+
}
229257
}
230258
}
231259
}

samples/Synercoding.FileFormats.Pdf.ConsoleTester/Synercoding.FileFormats.Pdf.ConsoleTester.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,10 @@
1515
</None>
1616
</ItemGroup>
1717

18+
<ItemGroup>
19+
<None Update="FreePngImage_com\*.png">
20+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
21+
</None>
22+
</ItemGroup>
23+
1824
</Project>

src/Synercoding.FileFormats.Pdf/Extensions/IPageContentContextExtensions.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using SixLabors.ImageSharp.PixelFormats;
12
using Synercoding.FileFormats.Pdf.Internals;
23
using Synercoding.FileFormats.Pdf.LowLevel.Colors.ColorSpaces;
34
using Synercoding.FileFormats.Pdf.LowLevel.Text;
@@ -61,7 +62,7 @@ public static IPageContentContext AddImage(this IPageContentContext context, Sys
6162
/// <returns>The same <paramref name="context"/> to enable chaining operations.</returns>
6263
public static IPageContentContext AddImage(this IPageContentContext context, System.IO.Stream stream)
6364
{
64-
using var image = SixLabors.ImageSharp.Image.Load(stream);
65+
using var image = SixLabors.ImageSharp.Image.Load<Rgba32>(stream);
6566

6667
return context.AddImage(image);
6768
}
@@ -73,7 +74,7 @@ public static IPageContentContext AddImage(this IPageContentContext context, Sys
7374
/// <param name="image">The image to place</param>
7475
/// <param name="matrix">The placement matrix to use</param>
7576
/// <returns>The same <paramref name="context"/> to enable chaining operations.</returns>
76-
public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image image, Matrix matrix)
77+
public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image<Rgba32> image, Matrix matrix)
7778
{
7879
return context.WrapInState((image, matrix), static (tuple, context) =>
7980
{
@@ -89,7 +90,7 @@ public static IPageContentContext AddImage(this IPageContentContext context, Six
8990
/// <param name="image">The image to place</param>
9091
/// <param name="rectangle">The rectangle of where to place the image.</param>
9192
/// <returns>The same <paramref name="context"/> to enable chaining operations.</returns>
92-
public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image image, Rectangle rectangle)
93+
public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image<Rgba32> image, Rectangle rectangle)
9394
=> context.AddImage(image, rectangle.AsPlacementMatrix());
9495

9596
/// <summary>
@@ -98,7 +99,7 @@ public static IPageContentContext AddImage(this IPageContentContext context, Six
9899
/// <param name="context">The context to add the image to.</param>
99100
/// <param name="image">The image to place</param>
100101
/// <returns>The same <paramref name="context"/> to enable chaining operations.</returns>
101-
public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image image)
102+
public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image<Rgba32> image)
102103
{
103104
var name = context.RawContentStream.Resources.AddImage(image);
104105

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace Synercoding.FileFormats.Pdf;
2+
3+
/// <summary>
4+
/// What method is used to generate a 1 component grayscale pixel byte array
5+
/// </summary>
6+
public enum GrayScaleMethod
7+
{
8+
/// <summary>
9+
/// Use the red channel
10+
/// </summary>
11+
RedChannel,
12+
/// <summary>
13+
/// Use the green channel
14+
/// </summary>
15+
GreenChannel,
16+
/// <summary>
17+
/// Use the blue channel
18+
/// </summary>
19+
BlueChannel,
20+
/// <summary>
21+
/// Use the alpha channel
22+
/// </summary>
23+
AlphaChannel,
24+
/// <summary>
25+
/// Use the average of the Red, Green and Blue channels.
26+
/// </summary>
27+
AverageOfRGBChannels
28+
}
Lines changed: 82 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,33 @@
11
using SixLabors.ImageSharp;
2+
using SixLabors.ImageSharp.PixelFormats;
23
using Synercoding.FileFormats.Pdf.LowLevel;
34
using Synercoding.FileFormats.Pdf.LowLevel.Colors.ColorSpaces;
5+
using Synercoding.FileFormats.Pdf.LowLevel.XRef;
6+
using System.IO.Compression;
47

58
namespace Synercoding.FileFormats.Pdf;
69

710
/// <summary>
811
/// Class representing an image inside a pdf
912
/// </summary>
10-
public sealed class Image : IDisposable
13+
public class Image : IDisposable
1114
{
12-
private bool _disposed;
15+
private protected bool _disposed;
1316

14-
internal Image(PdfReference id, SixLabors.ImageSharp.Image image)
15-
{
16-
Reference = id;
17-
18-
var ms = new MemoryStream();
19-
image.SaveAsJpeg(ms, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder()
20-
{
21-
Quality = 100,
22-
ColorType = SixLabors.ImageSharp.Formats.Jpeg.JpegEncodingColor.YCbCrRatio444
23-
});
24-
Width = image.Width;
25-
Height = image.Height;
26-
ColorSpace = DeviceRGB.Instance.Name;
27-
DecodeArray = new double[] { 0, 1, 0, 1, 0, 1 };
28-
ms.Position = 0;
29-
RawStream = ms;
30-
}
31-
32-
internal Image(PdfReference id, Stream jpgStream, int width, int height, ColorSpace colorSpace)
33-
{
34-
Reference = id;
35-
36-
Width = width;
37-
Height = height;
38-
RawStream = jpgStream;
39-
40-
var (csName, decodeArray) = colorSpace switch
41-
{
42-
DeviceCMYK cmyk => (cmyk.Name, new double[] { 0, 1, 0, 1, 0, 1, 0, 1 }),
43-
DeviceRGB rgb => (rgb.Name, new double[] { 0, 1, 0, 1, 0, 1 }),
44-
_ => throw new ArgumentOutOfRangeException(nameof(colorSpace), $"The provided color space {colorSpace} is currently not supported.")
45-
};
46-
47-
ColorSpace = csName;
48-
DecodeArray = decodeArray;
49-
}
50-
51-
internal Image(PdfReference id, Stream jpgStream, int width, int height, PdfName colorSpace, double[] decodeArray)
17+
internal Image(PdfReference id, Stream jpgStream, int width, int height, ColorSpace colorSpace, Image? softMask, params StreamFilter[] filters)
5218
{
5319
Reference = id;
5420

5521
Width = width;
5622
Height = height;
5723
RawStream = jpgStream;
5824
ColorSpace = colorSpace;
59-
DecodeArray = decodeArray;
25+
SoftMask = softMask;
26+
Filters = filters;
6027
}
6128

29+
internal Image? SoftMask { get; private set; }
30+
6231
internal Stream RawStream { get; private set; }
6332

6433
/// <summary>
@@ -79,12 +48,9 @@ internal Image(PdfReference id, Stream jpgStream, int width, int height, PdfName
7948
/// <summary>
8049
/// The name of the colorspace used in this <see cref="Image"/>
8150
/// </summary>
82-
public PdfName ColorSpace { get; }
51+
public ColorSpace ColorSpace { get; }
8352

84-
/// <summary>
85-
/// The decode array used in this <see cref="Image"/>
86-
/// </summary>
87-
public double[] DecodeArray { get; }
53+
internal StreamFilter[] Filters { get; } = Array.Empty<StreamFilter>();
8854

8955
/// <inheritdoc />
9056
public void Dispose()
@@ -95,4 +61,74 @@ public void Dispose()
9561
_disposed = true;
9662
}
9763
}
64+
65+
private static Stream _encodeToJpg(SixLabors.ImageSharp.Image image)
66+
{
67+
var ms = new MemoryStream();
68+
image.SaveAsJpeg(ms, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder()
69+
{
70+
Quality = 100,
71+
ColorType = SixLabors.ImageSharp.Formats.Jpeg.JpegEncodingColor.YCbCrRatio444
72+
});
73+
ms.Position = 0;
74+
75+
return ms;
76+
}
77+
78+
internal static Image Get(TableBuilder tableBuilder, Image<Rgba32> image)
79+
{
80+
return new Image(tableBuilder.ReserveId(), _encodeToJpg(image), image.Width, image.Height, DeviceRGB.Instance, GetMask(tableBuilder, image), StreamFilter.DCTDecode);
81+
}
82+
83+
internal static Image? GetMask(TableBuilder tableBuilder, Image<Rgba32> image)
84+
{
85+
var hasTrans = image.Metadata.TryGetPngMetadata(out var pngMeta)
86+
&&
87+
(
88+
pngMeta.ColorType == SixLabors.ImageSharp.Formats.Png.PngColorType.RgbWithAlpha
89+
|| pngMeta.ColorType == SixLabors.ImageSharp.Formats.Png.PngColorType.GrayscaleWithAlpha
90+
);
91+
92+
return hasTrans
93+
? new Image(tableBuilder.ReserveId(), AsImageByteStream(image, GrayScaleMethod.AlphaChannel), image.Width, image.Height, DeviceGray.Instance, null, StreamFilter.FlateDecode)
94+
: null;
95+
}
96+
97+
internal static Stream AsImageByteStream(Image<Rgba32> image, GrayScaleMethod grayScaleMethod)
98+
{
99+
using (var byteStream = new MemoryStream())
100+
{
101+
image.ProcessPixelRows(accessor =>
102+
{
103+
for (int y = 0; y < accessor.Height; y++)
104+
{
105+
var pixelRow = accessor.GetRowSpan(y);
106+
107+
// pixelRow.Length has the same value as accessor.Width,
108+
// but using pixelRow.Length allows the JIT to optimize away bounds checks:
109+
for (int x = 0; x < pixelRow.Length; x++)
110+
{
111+
// Get a reference to the pixel at position x
112+
ref Rgba32 pixel = ref pixelRow[x];
113+
114+
var pixelValue = grayScaleMethod switch
115+
{
116+
GrayScaleMethod.AlphaChannel => pixel.A,
117+
GrayScaleMethod.RedChannel => pixel.R,
118+
GrayScaleMethod.GreenChannel => pixel.G,
119+
GrayScaleMethod.BlueChannel => pixel.B,
120+
GrayScaleMethod.AverageOfRGBChannels => (byte)( ( pixel.R + pixel.G + pixel.B ) / 3 ),
121+
_ => throw new NotImplementedException()
122+
};
123+
124+
byteStream.WriteByte(pixelValue);
125+
}
126+
}
127+
});
128+
129+
byteStream.Position = 0;
130+
131+
return PdfWriter.FlateEncode(byteStream);
132+
}
133+
}
98134
}

src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/StreamFilterExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ public static PdfName ToPdfName(this StreamFilter streamFilter)
77
return streamFilter switch
88
{
99
StreamFilter.DCTDecode => PdfName.Get("DCTDecode"),
10+
StreamFilter.FlateDecode => PdfName.Get("FlateDecode"),
1011
_ => throw new NotImplementedException(),
1112
};
1213
}

src/Synercoding.FileFormats.Pdf/LowLevel/Internal/PageResources.cs

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using SixLabors.ImageSharp.PixelFormats;
12
using Synercoding.FileFormats.Pdf.Internals;
23
using Synercoding.FileFormats.Pdf.LowLevel.Colors.ColorSpaces;
34
using Synercoding.FileFormats.Pdf.LowLevel.Text;
@@ -56,25 +57,14 @@ public PdfName AddJpgUnsafe(System.IO.Stream jpgStream, int originalWidth, int o
5657
{
5758
var id = _tableBuilder.ReserveId();
5859

59-
var pdfImage = new Image(id, jpgStream, originalWidth, originalHeight, colorSpace);
60+
var pdfImage = new Image(id, jpgStream, originalWidth, originalHeight, colorSpace, null, StreamFilter.DCTDecode);
6061

6162
return AddImage(pdfImage);
6263
}
6364

64-
public PdfName AddJpgUnsafe(System.IO.Stream jpgStream, int originalWidth, int originalHeight, PdfName colorSpace, double[] decodeArray)
65+
public PdfName AddImage(SixLabors.ImageSharp.Image<Rgba32> image)
6566
{
66-
var id = _tableBuilder.ReserveId();
67-
68-
var pdfImage = new Image(id, jpgStream, originalWidth, originalHeight, colorSpace, decodeArray);
69-
70-
return AddImage(pdfImage);
71-
}
72-
73-
public PdfName AddImage(SixLabors.ImageSharp.Image image)
74-
{
75-
var id = _tableBuilder.ReserveId();
76-
77-
var pdfImage = new Image(id, image);
67+
var pdfImage = Image.Get(_tableBuilder, image);
7868

7969
return AddImage(pdfImage);
8070
}
@@ -108,7 +98,7 @@ internal PdfName AddSeparation(Separation separation)
10898

10999
var key = PREFIX_SEPARATION + System.Threading.Interlocked.Increment(ref _separationCounter).ToString().PadLeft(6, '0');
110100
var name = PdfName.Get(key);
111-
_separations[separation] = (name, _tableBuilder.ReserveId());
101+
_separations[separation] = (name, _tableBuilder.GetSeparationId(separation));
112102

113103
return name;
114104
}

0 commit comments

Comments
 (0)