From 084fa4e767fd3fe1da2a2e0c912cd32b3683ba0b Mon Sep 17 00:00:00 2001 From: BobLd <38405645+BobLd@users.noreply.github.com> Date: Tue, 17 Sep 2024 07:02:29 +0100 Subject: [PATCH] Fix #878 --- .../Writer/OperationWriteHelperTests.cs | 231 ++++++++++++++++++ .../Operations/OperationWriteHelper.cs | 63 ++++- src/UglyToad.PdfPig/Writer/TokenWriter.cs | 6 +- 3 files changed, 289 insertions(+), 11 deletions(-) create mode 100644 src/UglyToad.PdfPig.Tests/Writer/OperationWriteHelperTests.cs diff --git a/src/UglyToad.PdfPig.Tests/Writer/OperationWriteHelperTests.cs b/src/UglyToad.PdfPig.Tests/Writer/OperationWriteHelperTests.cs new file mode 100644 index 000000000..22406fdbd --- /dev/null +++ b/src/UglyToad.PdfPig.Tests/Writer/OperationWriteHelperTests.cs @@ -0,0 +1,231 @@ +namespace UglyToad.PdfPig.Tests.Writer +{ + 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); + 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); + } + } + } + + [Fact] + public void WriteDoubleMinValue() + { + 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 WriteDoubleMaxValue() + { + 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); + } + } + } + } +} 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);