Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ private static void _writePdf(string fileName, bool enableSubsetting = true)
using (var pantherSixImage = SixLabors.ImageSharp.Image.Load<Rgba32>(pantherPngStream))
{
var pantherImg = writer.AddImage(pantherSixImage);
var transparentPanther = writer.AddSeparationImage(pantherSixImage, new Separation(PdfName.Get("White"), PredefinedColors.Yellow), GrayScaleMethod.AlphaChannel, [0, 1]);
var transparentPanther = writer.AddSeparationImage(pantherSixImage, new Separation(PdfName.Get("White"), PredefinedColors.Yellow), GrayScaleMethods.AlphaChannel, [0, 1]);

writer.AddPage(page =>
{
Expand Down
6 changes: 3 additions & 3 deletions src/Synercoding.FileFormats.Pdf/Content/ContentStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ public ContentStream EndText()
public ContentStream ShowTextTj(byte[] line)
{
InnerStream
.WriteStringLiteral(line)
.WriteStringHex(line)
.Space()
.Write("Tj")
.NewLine();
Expand All @@ -250,7 +250,7 @@ public ContentStream ShowTextTj(byte[] line)
public ContentStream MoveNextLineShowText(byte[] line)
{
InnerStream
.WriteStringLiteral(line)
.WriteStringHex(line)
.Space()
.Write("'")
.NewLine();
Expand Down Expand Up @@ -285,7 +285,7 @@ public ContentStream MoveNextLineShowText(byte[] line, double wordSpacing, doubl
.Space()
.Write(characterSpacing)
.Space()
.WriteStringLiteral(line)
.WriteStringHex(line)
.Space()
.Write("\"")
.NewLine();
Expand Down
90 changes: 82 additions & 8 deletions src/Synercoding.FileFormats.Pdf/Content/GrayScaleMethod.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,110 @@
using SixLabors.ImageSharp.PixelFormats;

namespace Synercoding.FileFormats.Pdf.Content;

/// <summary>
/// What method is used to generate a 1 component grayscale pixel byte array
/// </summary>
public enum GrayScaleMethod
public static class GrayScaleMethods
{
/// <summary>
/// Use the red channel
/// </summary>
RedChannel,
public static byte RedChannel(ref Rgba32 pixel) => pixel.R;
/// <summary>
/// Use the red channel
/// </summary>
public static byte RedChannel(ref Rgb24 pixel) => pixel.R;
/// <summary>
/// Use the green channel
/// </summary>
GreenChannel,
public static byte GreenChannel(ref Rgba32 pixel) => pixel.G;
/// <summary>
/// Use the green channel
/// </summary>
public static byte GreenChannel(ref Rgb24 pixel) => pixel.G;
/// <summary>
/// Use the blue channel
/// </summary>
public static byte BlueChannel(ref Rgba32 pixel) => pixel.B;
/// <summary>
/// Use the blue channel
/// </summary>
BlueChannel,
public static byte BlueChannel(ref Rgb24 pixel) => pixel.B;
/// <summary>
/// Use the alpha channel
/// </summary>
AlphaChannel,
public static byte AlphaChannel(ref Rgba32 pixel) => pixel.A;
/// <summary>
/// Use the average of the Red, Green and Blue channels.
/// </summary>
public static byte AverageOfRGBChannels(ref Rgba32 pixel) => (byte)( ( pixel.R + pixel.G + pixel.B ) / 3 );
/// <summary>
/// Use the average of the Red, Green and Blue channels.
/// </summary>
AverageOfRGBChannels,
public static byte AverageOfRGBChannels(ref Rgb24 pixel) => (byte)( ( pixel.R + pixel.G + pixel.B ) / 3 );
/// <summary>
/// The constants defined by ITU-R BT.601 are 0.299 red + 0.587 green + 0.114 blue.
/// </summary>
public static byte BT601(ref Rgba32 pixel) => (byte)( ( pixel.R * 0.299 ) + ( pixel.G * 0.587 ) + ( pixel.B * 0.114 ) );
/// <summary>
/// The constants defined by ITU-R BT.601 are 0.299 red + 0.587 green + 0.114 blue.
/// </summary>
BT601,
public static byte BT601(ref Rgb24 pixel) => (byte)( ( pixel.R * 0.299 ) + ( pixel.G * 0.587 ) + ( pixel.B * 0.114 ) );
/// <summary>
/// The constants defined by ITU-R BT.709 are 0.2126 red + 0.7152 green + 0.0722 blue.
/// </summary>
BT709,
public static byte BT709(ref Rgba32 pixel) => (byte)( ( pixel.R * 0.2126 ) + ( pixel.G * 0.7152 ) + ( pixel.B * 0.0722 ) );
/// <summary>
/// The constants defined by ITU-R BT.709 are 0.2126 red + 0.7152 green + 0.0722 blue.
/// </summary>
public static byte BT709(ref Rgb24 pixel) => (byte)( ( pixel.R * 0.2126 ) + ( pixel.G * 0.7152 ) + ( pixel.B * 0.0722 ) );

/// <summary>
/// Create a threshold method from an initial method. The threshold method will return the lowValue if the initial method returns a value less than the threshold, and the highValue otherwise.
/// </summary>
/// <param name="initialMethod">The initial method to use.</param>
/// <param name="threshold">The threshold value.</param>
/// <param name="lowValue">The value to return if the initial method returns a value less than the threshold.</param>
/// <param name="highValue">The value to return if the initial method returns a value greater than or equal to the threshold.</param>
/// <returns>The threshold method.</returns>
public static GrayScaleMethod32 Threshold(GrayScaleMethod32 initialMethod, byte threshold, byte lowValue = 0x00, byte highValue = 0xFF)
{
return (ref Rgba32 pixel) =>
{
var value = initialMethod(ref pixel);
return value < threshold ? lowValue : highValue;
};
}

/// <summary>
/// Create a threshold method from an initial method. The threshold method will return the lowValue if the initial method returns a value less than the threshold, and the highValue otherwise.
/// </summary>
/// <param name="initialMethod">The initial method to use.</param>
/// <param name="threshold">The threshold value.</param>
/// <param name="lowValue">The value to return if the initial method returns a value less than the threshold.</param>
/// <param name="highValue">The value to return if the initial method returns a value greater than or equal to the threshold.</param>
/// <returns>The threshold method.</returns>
public static GrayScaleMethod24 Threshold(GrayScaleMethod24 initialMethod, byte threshold, byte lowValue = 0x00, byte highValue = 0xFF)
{
return (ref Rgb24 pixel) =>
{
var value = initialMethod(ref pixel);
return value < threshold ? lowValue : highValue;
};
}
}

/// <summary>
/// Represents a method to convert a <see cref="Rgba32"/> pixel into a single byte.
/// </summary>
/// <param name="pixel">The pixel to convert</param>
/// <returns>The byte value the pixel represents.</returns>
public delegate byte GrayScaleMethod32(ref Rgba32 pixel);

/// <summary>
/// Represents a method to convert a <see cref="Rgb24"/> pixel into a single byte.
/// </summary>
/// <param name="pixel">The pixel to convert</param>
/// <returns>The byte value the pixel represents.</returns>
public delegate byte GrayScaleMethod24(ref Rgb24 pixel);
36 changes: 7 additions & 29 deletions src/Synercoding.FileFormats.Pdf/Content/PdfImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,14 @@ internal static PdfImage Get(TableBuilder tableBuilder, Image<Rgba32> image)
internal static PdfImage Get(TableBuilder tableBuilder, Image<Rgb24> image)
=> new PdfImage(tableBuilder.ReserveId(), _encodeToJpg(image), image.Width, image.Height, DeviceRGB.Instance, null, null, (PdfNames.DCTDecode, null));

internal static PdfImage GetSeparation(TableBuilder tableBuilder, Image<Rgb24> image, Separation separation, GrayScaleMethod grayScaleMethod, double[]? decodeArray = null)
internal static PdfImage GetSeparation(TableBuilder tableBuilder, Image<Rgb24> image, Separation separation, GrayScaleMethod24 grayScaleMethod, double[]? decodeArray = null)
{
using var grayScaleStream = AsGrayScaleByteStream(image, grayScaleMethod);

return new PdfImage(tableBuilder.ReserveId(), _flateEncode(grayScaleStream), image.Width, image.Height, separation, null, decodeArray, (PdfNames.FlateDecode, null));
}

internal static PdfImage GetSeparation(TableBuilder tableBuilder, Image<Rgba32> image, Separation separation, GrayScaleMethod grayScaleMethod, double[]? decodeArray = null)
internal static PdfImage GetSeparation(TableBuilder tableBuilder, Image<Rgba32> image, Separation separation, GrayScaleMethod32 grayScaleMethod, double[]? decodeArray = null)
{
using var grayScaleStream = AsGrayScaleByteStream(image, grayScaleMethod);

Expand All @@ -170,7 +170,7 @@ internal static PdfImage GetSeparation(TableBuilder tableBuilder, Image<Rgba32>
if (!_hasTransparancy(image))
return null;

using var grayScaleStream = AsGrayScaleByteStream(image, GrayScaleMethod.AlphaChannel);
using var grayScaleStream = AsGrayScaleByteStream(image, GrayScaleMethods.AlphaChannel);

return new PdfImage(tableBuilder.ReserveId(), _flateEncode(grayScaleStream), image.Width, image.Height, DeviceGray.Instance, null, null, (PdfNames.FlateDecode, null));
}
Expand Down Expand Up @@ -214,7 +214,7 @@ private static Stream _flateEncode(MemoryStream stream)
return new MemoryStream(bytes);
}

internal static MemoryStream AsGrayScaleByteStream(Image<Rgba32> image, GrayScaleMethod grayScaleMethod)
internal static MemoryStream AsGrayScaleByteStream(Image<Rgba32> image, GrayScaleMethod32 grayScaleMethod)
{
var byteStream = new MemoryStream();

Expand All @@ -231,17 +231,7 @@ internal static MemoryStream AsGrayScaleByteStream(Image<Rgba32> image, GrayScal
// 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 ),
GrayScaleMethod.BT601 => (byte)( ( pixel.R * 0.299 ) + ( pixel.G * 0.587 ) + ( pixel.B * 0.114 ) ),
GrayScaleMethod.BT709 => (byte)( ( pixel.R * 0.2126 ) + ( pixel.G * 0.7152 ) + ( pixel.B * 0.0722 ) ),
_ => throw new NotImplementedException()
};
var pixelValue = grayScaleMethod(ref pixel);

byteStream.WriteByte(pixelValue);
}
Expand All @@ -253,11 +243,8 @@ internal static MemoryStream AsGrayScaleByteStream(Image<Rgba32> image, GrayScal
return byteStream;
}

internal static MemoryStream AsGrayScaleByteStream(Image<Rgb24> image, GrayScaleMethod grayScaleMethod)
internal static MemoryStream AsGrayScaleByteStream(Image<Rgb24> image, GrayScaleMethod24 grayScaleMethod)
{
if (grayScaleMethod == GrayScaleMethod.AlphaChannel)
throw new ArgumentException($"Can not use alpha channel for images of pixel format {nameof(Rgb24)}.", nameof(grayScaleMethod));

var byteStream = new MemoryStream();

image.ProcessPixelRows(accessor =>
Expand All @@ -273,16 +260,7 @@ internal static MemoryStream AsGrayScaleByteStream(Image<Rgb24> image, GrayScale
// Get a reference to the pixel at position x
ref Rgb24 pixel = ref pixelRow[x];

var pixelValue = grayScaleMethod switch
{
GrayScaleMethod.RedChannel => pixel.R,
GrayScaleMethod.GreenChannel => pixel.G,
GrayScaleMethod.BlueChannel => pixel.B,
GrayScaleMethod.AverageOfRGBChannels => (byte)( ( pixel.R + pixel.G + pixel.B ) / 3 ),
GrayScaleMethod.BT601 => (byte)( ( pixel.R * 0.299 ) + ( pixel.G * 0.587 ) + ( pixel.B * 0.114 ) ),
GrayScaleMethod.BT709 => (byte)( ( pixel.R * 0.2126 ) + ( pixel.G * 0.7152 ) + ( pixel.B * 0.0722 ) ),
_ => throw new NotImplementedException()
};
var pixelValue = grayScaleMethod(ref pixel);

byteStream.WriteByte(pixelValue);
}
Expand Down
3 changes: 2 additions & 1 deletion src/Synercoding.FileFormats.Pdf/Generation/PageResources.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ public PdfName AddJpgUnsafe(Stream jpgStream, int originalWidth, int originalHei
return Add(pdfImage);
}

public PdfName Add(SixLabors.ImageSharp.Image<Rgba32> image, Separation separation, GrayScaleMethod grayScaleMethod = GrayScaleMethod.AverageOfRGBChannels)
public PdfName Add(SixLabors.ImageSharp.Image<Rgba32> image, Separation separation, GrayScaleMethod32? grayScaleMethod = null)
{
grayScaleMethod ??= GrayScaleMethods.AverageOfRGBChannels;
var pdfImage = PdfImage.GetSeparation(_tableBuilder, image, separation, grayScaleMethod);

return Add(pdfImage);
Expand Down
82 changes: 64 additions & 18 deletions src/Synercoding.FileFormats.Pdf/Generation/PdfStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,16 +259,7 @@ internal PdfStream WriteStringLiteral(string value)
: [.. Encoding.UTF8.Preamble, .. Encoding.UTF8.GetBytes(value)];

foreach (var b in bytes)
{
if (b == '(')
Write('\\').Write('(');
else if (b == ')')
Write('\\').Write(')');
else if (b == '\\')
Write('\\').Write('\\');
else
Write(b);
}
_writeLiteralByte(b);

WriteByte(0x29); // )

Expand All @@ -285,22 +276,77 @@ internal PdfStream WriteStringLiteral(byte[] encodedString)
WriteByte(0x28); // (

foreach (var b in encodedString)
_writeLiteralByte(b);

WriteByte(0x29); // )

return this;
}

/// <summary>
/// Write an encoded byte sequence to the stream as a PDF hexadecimal string.
/// </summary>
/// <remarks>
/// Hexadecimal strings (ISO 32000-1 §7.3.4.3) are the correct container for arbitrary
/// binary data such as CID-encoded show-text operands: they have no escape rules and
/// no end-of-line normalisation, so every byte round-trips exactly.
/// </remarks>
/// <param name="encodedString">The bytes to write.</param>
/// <returns>The <see cref="PdfStream"/> to support chaining operations.</returns>
internal PdfStream WriteStringHex(byte[] encodedString)
{
WriteByte(0x3C); // <

Span<byte> pair = stackalloc byte[2];
foreach (var b in encodedString)
{
pair[0] = _hexNibble(b >> 4);
pair[1] = _hexNibble(b & 0x0F);
Write(pair);
}

WriteByte(0x3E); // >

return this;
}

private void _writeLiteralByte(byte b)
{
switch (b)
{
if (b == '(')
case (byte)'(':
Write('\\').Write('(');
else if (b == ')')
break;
case (byte)')':
Write('\\').Write(')');
else if (b == '\\')
break;
case (byte)'\\':
Write('\\').Write('\\');
else
break;
case 0x0A:
Write('\\').Write('n');
break;
case 0x0D:
Write('\\').Write('r');
break;
case 0x09:
Write('\\').Write('t');
break;
case 0x08:
Write('\\').Write('b');
break;
case 0x0C:
Write('\\').Write('f');
break;
default:
WriteByte(b);
break;
}

WriteByte(0x29); // )

return this;
}

private static byte _hexNibble(int n)
=> (byte)( n < 10 ? ( '0' + n ) : ( 'A' + n - 10 ) );

/// <summary>
/// Write an array of numbers to the pdf stream
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Synercoding.FileFormats.Pdf/PackageDetails.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<Product>Synercoding.FileFormats.Pdf</Product>
<Title>Synercoding.FileFormats.Pdf</Title>
<Description>Contains classes which makes it easy to quickly create a pdf file.</Description>
<PackageReleaseNotes>Complete rewrite to add support for fonts, and with an read/edit future in mind.</PackageReleaseNotes>
<PackageReleaseNotes>Changed GrayScaleMethod from an enum to a delegate, giving users full control over how separation images are converted to grayscale (breaking change). Fixed issue #87 where CID text bytes containing 0x0D were incorrectly unescaped in string literals, causing parsers to misread them as 0x0A and render the wrong glyph; string literals now escape more characters and CID text in content streams is written as hex strings.</PackageReleaseNotes>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
Expand Down
6 changes: 4 additions & 2 deletions src/Synercoding.FileFormats.Pdf/PdfWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,9 @@ public PdfImage AddImage(SixLabors.ImageSharp.Image<Rgb24> image)
/// <param name="grayScaleMethod">The method to convert to grayscale.</param>
/// <param name="decodeArray">Optional decode array for the image.</param>
/// <returns>The added PDF image.</returns>
public PdfImage AddSeparationImage(SixLabors.ImageSharp.Image<Rgba32> image, Separation separation, GrayScaleMethod grayScaleMethod = GrayScaleMethod.AverageOfRGBChannels, double[]? decodeArray = null)
public PdfImage AddSeparationImage(SixLabors.ImageSharp.Image<Rgba32> image, Separation separation, GrayScaleMethod32? grayScaleMethod = null, double[]? decodeArray = null)
{
grayScaleMethod ??= GrayScaleMethods.AverageOfRGBChannels;
var pdfImage = PdfImage.GetSeparation(_objectWriter.TableBuiler, image, separation, grayScaleMethod, decodeArray);

_objectWriter.Write(pdfImage.ToStreamObject(_cachedResources));
Expand All @@ -193,8 +194,9 @@ public PdfImage AddSeparationImage(SixLabors.ImageSharp.Image<Rgba32> image, Sep
/// <param name="grayScaleMethod">The method to convert to grayscale.</param>
/// <param name="decodeArray">Optional decode array for the image.</param>
/// <returns>The added PDF image.</returns>
public PdfImage AddSeparationImage(SixLabors.ImageSharp.Image<Rgb24> image, Separation separation, GrayScaleMethod grayScaleMethod = GrayScaleMethod.AverageOfRGBChannels, double[]? decodeArray = null)
public PdfImage AddSeparationImage(SixLabors.ImageSharp.Image<Rgb24> image, Separation separation, GrayScaleMethod24? grayScaleMethod = null, double[]? decodeArray = null)
{
grayScaleMethod ??= GrayScaleMethods.AverageOfRGBChannels;
var pdfImage = PdfImage.GetSeparation(_objectWriter.TableBuiler, image, separation, grayScaleMethod, decodeArray);

_objectWriter.Write(pdfImage.ToStreamObject(_cachedResources));
Expand Down
Loading
Loading