diff --git a/src/UglyToad.PdfPig.Tests/Writer/OperationWriteHelperTests.cs b/src/UglyToad.PdfPig.Tests/Writer/OperationWriteHelperTests.cs new file mode 100644 index 000000000..4a715b903 --- /dev/null +++ b/src/UglyToad.PdfPig.Tests/Writer/OperationWriteHelperTests.cs @@ -0,0 +1,236 @@ +namespace UglyToad.PdfPig.Tests.Writer +{ + using System.Globalization; + using UglyToad.PdfPig.Graphics.Operations; + + public class OperationWriteHelperTests + { + [Fact] + public void WriteDouble0() + { + using (var memStream = new MemoryStream()) + { + OperationWriteHelper.WriteDouble(memStream, 0); + + // Read Test + memStream.Position = 0; + using (var streamReader = new StreamReader(memStream)) + { + var line = streamReader.ReadToEnd(); + Assert.Equal("0", line); + } + } + } + + [Fact] + public void WriteDouble5() + { + using (var memStream = new MemoryStream()) + { + OperationWriteHelper.WriteDouble(memStream, 5); + + // Read Test + memStream.Position = 0; + using (var streamReader = new StreamReader(memStream)) + { + var line = streamReader.ReadToEnd(); + Assert.Equal("5", line); + } + } + } + + [Fact] + public void WriteDoubleMinus5() + { + using (var memStream = new MemoryStream()) + { + OperationWriteHelper.WriteDouble(memStream, -5); + + // Read Test + memStream.Position = 0; + using (var streamReader = new StreamReader(memStream)) + { + var line = streamReader.ReadToEnd(); + Assert.Equal("-5", line); + } + } + } + + [Fact] + public void WriteDouble10() + { + using (var memStream = new MemoryStream()) + { + OperationWriteHelper.WriteDouble(memStream, 10); + + // Read Test + memStream.Position = 0; + using (var streamReader = new StreamReader(memStream)) + { + var line = streamReader.ReadToEnd(); + Assert.Equal("10", line); + } + } + } + + [Fact] + public void WriteDoubleMinus10() + { + using (var memStream = new MemoryStream()) + { + OperationWriteHelper.WriteDouble(memStream, -10); + + // Read Test + memStream.Position = 0; + using (var streamReader = new StreamReader(memStream)) + { + var line = streamReader.ReadToEnd(); + Assert.Equal("-10", line); + } + } + } + + [Fact] + public void WriteDouble1() + { + using (var memStream = new MemoryStream()) + { + OperationWriteHelper.WriteDouble(memStream, 0.00000001); + + // Read Test + memStream.Position = 0; + using (var streamReader = new StreamReader(memStream)) + { + var line = streamReader.ReadToEnd(); + Assert.Equal("0.00000001", line); + } + } + } + + [Fact] + public void WriteDouble1bis() + { + using (var memStream = new MemoryStream()) + { + OperationWriteHelper.WriteDouble(memStream, -0.00000001); + + // Read Test + memStream.Position = 0; + using (var streamReader = new StreamReader(memStream)) + { + var line = streamReader.ReadToEnd(); + Assert.Equal("-0.00000001", line); + } + } + } + + [Fact] + public void WriteDouble2() + { + using (var memStream = new MemoryStream()) + { + OperationWriteHelper.WriteDouble(memStream, .00000005100); + + // Read Test + memStream.Position = 0; + using (var streamReader = new StreamReader(memStream)) + { + var line = streamReader.ReadToEnd(); + Assert.Equal("0.000000051", line); + } + } + } + + [Fact] + public void WriteDouble2bis() + { + using (var memStream = new MemoryStream()) + { + OperationWriteHelper.WriteDouble(memStream, -.0000000510); + + // Read Test + memStream.Position = 0; + using (var streamReader = new StreamReader(memStream)) + { + var line = streamReader.ReadToEnd(); + Assert.Equal("-0.000000051", line); + } + } + } + + [Fact] + public void WriteDouble3() + { + using (var memStream = new MemoryStream()) + { + OperationWriteHelper.WriteDouble(memStream, 15001.98); + + // Read Test + memStream.Position = 0; + using (var streamReader = new StreamReader(memStream)) + { + var line = streamReader.ReadToEnd(); + var v = double.Parse(line, CultureInfo.InvariantCulture); + Assert.Equal(15001.98, v); + } + } + } + + [Fact] + public void WriteDouble4() + { + using (var memStream = new MemoryStream()) + { + OperationWriteHelper.WriteDouble(memStream, 10000.000); + + // Read Test + memStream.Position = 0; + using (var streamReader = new StreamReader(memStream)) + { + var line = streamReader.ReadToEnd(); + Assert.Equal("10000", line); + } + } + } + +#if NET + // See here why we are not running on framework - thanks @cremor + // https://stackoverflow.com/a/1658420/631802 + [Fact] + public void WriteMinValue() + { + string expected = "-340282346638528859811704183484516925440"; + using (var memStream = new MemoryStream()) + { + OperationWriteHelper.WriteDouble(memStream, -340282346638528859811704183484516925440d); + + // Read Test + memStream.Position = 0; + using (var streamReader = new StreamReader(memStream)) + { + var line = streamReader.ReadToEnd(); + Assert.Equal(expected, line); + } + } + } + + [Fact] + public void WriteMaxValue() + { + string expected = "340282346638528859811704183484516925440"; + using (var memStream = new MemoryStream()) + { + OperationWriteHelper.WriteDouble(memStream, 340282346638528859811704183484516925440d); + + // Read Test + memStream.Position = 0; + using (var streamReader = new StreamReader(memStream)) + { + var line = streamReader.ReadToEnd(); + Assert.Equal(expected, line); + } + } + } +#endif + } +} diff --git a/src/UglyToad.PdfPig/Graphics/Operations/OperationWriteHelper.cs b/src/UglyToad.PdfPig/Graphics/Operations/OperationWriteHelper.cs index d76dd2043..06deb4da6 100644 --- a/src/UglyToad.PdfPig/Graphics/Operations/OperationWriteHelper.cs +++ b/src/UglyToad.PdfPig/Graphics/Operations/OperationWriteHelper.cs @@ -2,26 +2,31 @@ { using PdfPig.Core; using System; + using System.Buffers; using System.Buffers.Text; + using System.Globalization; using System.IO; - using System.Text; using Util; internal static class OperationWriteHelper { private const byte Whitespace = (byte)' '; private const byte NewLine = (byte)'\n'; + private const byte Zero = (byte)'0'; + private const byte Point = (byte)'.'; + + private static readonly StandardFormat StandardFormatDouble = new StandardFormat('F', 9); public static void WriteText(this Stream stream, string text, bool appendWhitespace = false) { #if NET8_0_OR_GREATER - if (Ascii.IsValid(text)) + if (System.Text.Ascii.IsValid(text)) { Span buffer = text.Length <= 64 ? stackalloc byte[text.Length] : new byte[text.Length]; - Ascii.FromUtf16(text, buffer, out _); + System.Text.Ascii.FromUtf16(text, buffer, out _); stream.Write(buffer); } @@ -75,11 +80,57 @@ public static void WriteNewLine(this Stream stream) public static void WriteDouble(this Stream stream, double value) { - Span buffer = stackalloc byte[32]; // matches dotnet Number.CharStackBufferSize + int stackSize = 32; // matches dotnet Number.CharStackBufferSize + + bool success = TryWriteDouble(stream, value, stackSize); + while (!success && stackSize <= 1024) + { + stackSize *= 2; + success = TryWriteDouble(stream, value, stackSize); + } + + if (!success) + { + ReadOnlySpan buffer = System.Text.Encoding.UTF8.GetBytes(value.ToString("F9", CultureInfo.InvariantCulture)); + int lastIndex = GetLastSignificantDigitIndex(buffer, buffer.Length); + stream.Write(buffer.Slice(0, lastIndex)); + } + } + + private static bool TryWriteDouble(Stream stream, double value, int stackSize) + { + System.Diagnostics.Debug.Assert(stackSize <= 1024); + + Span buffer = stackalloc byte[stackSize]; + + if (Utf8Formatter.TryFormat(value, buffer, out int bytesWritten, StandardFormatDouble)) + { + int lastIndex = GetLastSignificantDigitIndex(buffer, bytesWritten); + stream.Write(buffer.Slice(0, lastIndex)); + return true; + } + + return false; + } + + private static int GetLastSignificantDigitIndex(ReadOnlySpan buffer, int bytesWritten) + { + int lastIndex = bytesWritten; + for (int i = bytesWritten - 1; i > 1; --i) + { + if (buffer[i] != Zero) + { + break; + } + lastIndex--; + } - Utf8Formatter.TryFormat(value, buffer, out int bytesWritten); + if (buffer[lastIndex - 1] == Point) + { + lastIndex--; + } - stream.Write(buffer.Slice(0, bytesWritten)); + return lastIndex; } public static void WriteNumberText(this Stream stream, int number, string text) diff --git a/src/UglyToad.PdfPig/Writer/TokenWriter.cs b/src/UglyToad.PdfPig/Writer/TokenWriter.cs index 25a5b7195..9e9cf1479 100644 --- a/src/UglyToad.PdfPig/Writer/TokenWriter.cs +++ b/src/UglyToad.PdfPig/Writer/TokenWriter.cs @@ -461,11 +461,7 @@ protected virtual void WriteNumber(NumericToken number, Stream outputStream) } else { - Span buffer = stackalloc byte[32]; // matches dotnet Number.CharStackBufferSize - - Utf8Formatter.TryFormat(number.Data, buffer, out int bytesWritten); - - outputStream.Write(buffer.Slice(0, bytesWritten)); + outputStream.WriteDouble(number.Data); } WriteWhitespace(outputStream);