Skip to content

Commit f85263b

Browse files
committed
Add .NET Framework compatibility polyfills and update existing methods to use them
1 parent 20edf6d commit f85263b

10 files changed

Lines changed: 280 additions & 41 deletions
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
#if NETFRAMEWORK
2+
// Polyfill types and extension methods for .NET Framework 4.6.2 compatibility.
3+
// These are automatically available on .NET 6+ and are excluded via #if.
4+
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Runtime.CompilerServices;
8+
using System.Text;
9+
10+
// ── IsExternalInit: required by C# 9 records and init-only properties ──
11+
namespace System.Runtime.CompilerServices
12+
{
13+
internal static class IsExternalInit { }
14+
}
15+
16+
// ── Index / Range: required by C# 8 range operators (e.g. str[1..], arr[^1]) ──
17+
namespace System
18+
{
19+
internal readonly struct Index : IEquatable<Index>
20+
{
21+
private readonly int _value;
22+
23+
public Index(int value, bool fromEnd = false)
24+
{
25+
_value = fromEnd ? ~value : value;
26+
}
27+
28+
public int Value => _value < 0 ? ~_value : _value;
29+
public bool IsFromEnd => _value < 0;
30+
31+
public static Index Start => new Index(0);
32+
public static Index End => new Index(~0);
33+
34+
public int GetOffset(int length)
35+
{
36+
var offset = _value;
37+
if (IsFromEnd) offset += length;
38+
return offset;
39+
}
40+
41+
public static implicit operator Index(int value) => new Index(value);
42+
public bool Equals(Index other) => _value == other._value;
43+
public override bool Equals(object? obj) => obj is Index other && Equals(other);
44+
public override int GetHashCode() => _value;
45+
public override string ToString() => IsFromEnd ? $"^{Value}" : Value.ToString();
46+
}
47+
48+
internal readonly struct Range : IEquatable<Range>
49+
{
50+
public Index Start { get; }
51+
public Index End { get; }
52+
53+
public Range(Index start, Index end) { Start = start; End = end; }
54+
55+
public static Range StartAt(Index start) => new Range(start, Index.End);
56+
public static Range EndAt(Index end) => new Range(Index.Start, end);
57+
public static Range All => new Range(Index.Start, Index.End);
58+
59+
public (int Offset, int Length) GetOffsetAndLength(int length)
60+
{
61+
var start = Start.GetOffset(length);
62+
var end = End.GetOffset(length);
63+
return (start, end - start);
64+
}
65+
66+
public bool Equals(Range other) => Start.Equals(other.Start) && End.Equals(other.End);
67+
public override bool Equals(object? obj) => obj is Range other && Equals(other);
68+
public override int GetHashCode() => Start.GetHashCode() * 31 + End.GetHashCode();
69+
public override string ToString() => $"{Start}..{End}";
70+
}
71+
}
72+
73+
// ── RuntimeHelpers.GetSubArray: required when using range operators on arrays ──
74+
namespace System.Runtime.CompilerServices
75+
{
76+
internal static class RuntimeHelpersPolyfill
77+
{
78+
// The compiler calls RuntimeHelpers.GetSubArray<T> for array[range].
79+
// We provide it here so that pattern compiles on .NET Framework.
80+
}
81+
}
82+
83+
namespace System
84+
{
85+
internal static class RuntimeHelpersEx
86+
{
87+
public static T[] GetSubArray<T>(T[] array, Range range)
88+
{
89+
var (offset, length) = range.GetOffsetAndLength(array.Length);
90+
var dest = new T[length];
91+
Array.Copy(array, offset, dest, 0, length);
92+
return dest;
93+
}
94+
}
95+
}
96+
97+
// ── Extension methods for .NET Framework ──
98+
namespace MiniSoftware
99+
{
100+
internal static class NetFxPolyfills
101+
{
102+
// string.Contains(string, StringComparison) — .NET Core 2.1+
103+
public static bool Contains(this string s, string value, StringComparison comparison)
104+
=> s.IndexOf(value, comparison) >= 0;
105+
106+
// string.Split(char, StringSplitOptions) — .NET Core 2.1+
107+
public static string[] Split(this string s, char separator, StringSplitOptions options)
108+
=> s.Split(new[] { separator }, options);
109+
110+
// string.StartsWith(char) — .NET Core only
111+
public static bool StartsWith(this string s, char c)
112+
=> s.Length > 0 && s[0] == c;
113+
114+
// string.EndsWith(char) — .NET Core only
115+
public static bool EndsWith(this string s, char c)
116+
=> s.Length > 0 && s[s.Length - 1] == c;
117+
118+
// Stream.Write(byte[]) — Stream.Write(ReadOnlySpan<byte>) is .NET Core 2.1+
119+
public static void Write(this Stream stream, byte[] buffer)
120+
=> stream.Write(buffer, 0, buffer.Length);
121+
122+
// KeyValuePair Deconstruct — .NET Core 2.0+
123+
public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value)
124+
{
125+
key = kvp.Key;
126+
value = kvp.Value;
127+
}
128+
129+
// Math.Clamp — .NET Core 2.0+
130+
public static int Clamp(int value, int min, int max) =>
131+
value < min ? min : value > max ? max : value;
132+
public static float Clamp(float value, float min, float max) =>
133+
value < min ? min : value > max ? max : value;
134+
public static double Clamp(double value, double min, double max) =>
135+
value < min ? min : value > max ? max : value;
136+
137+
// Array.Fill — .NET Core 2.0+
138+
public static void Fill<T>(T[] array, T value)
139+
{
140+
for (int i = 0; i < array.Length; i++) array[i] = value;
141+
}
142+
public static void Fill<T>(T[] array, T value, int startIndex, int count)
143+
{
144+
for (int i = startIndex; i < startIndex + count; i++) array[i] = value;
145+
}
146+
147+
// Encoding.Latin1 — .NET 5+
148+
public static Encoding Latin1 => Encoding.GetEncoding(28591);
149+
}
150+
}
151+
#endif
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System.Text;
2+
3+
namespace MiniSoftware;
4+
5+
/// <summary>
6+
/// Cross-platform compatibility helpers that work on both .NET Framework and .NET 6+.
7+
/// </summary>
8+
internal static class Compat
9+
{
10+
// ── Math.Clamp (introduced in .NET Core 2.0) ──
11+
public static int Clamp(int value, int min, int max) => Math.Max(min, Math.Min(value, max));
12+
public static float Clamp(float value, float min, float max) => Math.Max(min, Math.Min(value, max));
13+
public static double Clamp(double value, double min, double max) => Math.Max(min, Math.Min(value, max));
14+
15+
// ── MathF.Ceiling (introduced in .NET Core 2.0) ──
16+
public static float Ceiling(float value) => (float)Math.Ceiling(value);
17+
18+
// ── Encoding.Latin1 (property introduced in .NET 5) ──
19+
public static readonly Encoding Latin1 = Encoding.GetEncoding(28591);
20+
21+
// ── OperatingSystem.IsWindows / IsMacOS (introduced in .NET 5) ──
22+
public static bool IsWindows()
23+
{
24+
#if NETFRAMEWORK
25+
return true; // .NET Framework only runs on Windows
26+
#else
27+
return OperatingSystem.IsWindows();
28+
#endif
29+
}
30+
31+
public static bool IsMacOS()
32+
{
33+
#if NETFRAMEWORK
34+
return false;
35+
#else
36+
return OperatingSystem.IsMacOS();
37+
#endif
38+
}
39+
40+
// ── Array.Fill (introduced in .NET Core 2.0) ──
41+
public static void ArrayFill<T>(T[] array, T value)
42+
{
43+
for (int i = 0; i < array.Length; i++) array[i] = value;
44+
}
45+
46+
// ── HashCode.Combine (introduced in .NET Core 2.1) ──
47+
public static int HashCombine<T1, T2, T3>(T1 v1, T2 v2, T3 v3)
48+
{
49+
unchecked
50+
{
51+
int h = 17;
52+
h = h * 31 + (v1?.GetHashCode() ?? 0);
53+
h = h * 31 + (v2?.GetHashCode() ?? 0);
54+
h = h * 31 + (v3?.GetHashCode() ?? 0);
55+
return h;
56+
}
57+
}
58+
}

src/MiniPdf/DocxReader.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,7 @@ internal static DocxDocument Read(Stream stream)
685685
return null;
686686
}
687687
}
688-
else if (hasCrop && OperatingSystem.IsWindows())
688+
else if (hasCrop && Compat.IsWindows())
689689
{
690690
var cropped = TryCropImagePng(data, cropL, cropT, cropR, cropB);
691691
if (cropped != null)
@@ -719,7 +719,7 @@ internal static DocxDocument Read(Stream stream)
719719
private static byte[]? TryConvertMetafileToPng(byte[] sourceBytes, long widthEmu, long heightEmu,
720720
float cropL = 0, float cropT = 0, float cropR = 0, float cropB = 0)
721721
{
722-
if (!OperatingSystem.IsWindows())
722+
if (!Compat.IsWindows())
723723
return null;
724724

725725
try
@@ -742,8 +742,8 @@ internal static DocxDocument Read(Stream stream)
742742

743743
var targetHeight = 512;
744744
var targetWidth = (int)Math.Round(targetHeight * aspect);
745-
targetWidth = Math.Clamp(targetWidth, 32, 4096);
746-
targetHeight = Math.Clamp(targetHeight, 32, 4096);
745+
targetWidth = Compat.Clamp(targetWidth, 32, 4096);
746+
targetHeight = Compat.Clamp(targetHeight, 32, 4096);
747747

748748
using var bmp = new Bitmap(targetWidth, targetHeight, PixelFormat.Format32bppArgb);
749749
using (var g = Graphics.FromImage(bmp))
@@ -942,7 +942,7 @@ internal static DocxDocument Read(Stream stream)
942942
{
943943
var fmla = gd.Attribute("fmla")?.Value;
944944
if (fmla != null && fmla.StartsWith("val ") &&
945-
int.TryParse(fmla.AsSpan(4), out var v))
945+
int.TryParse(fmla.Substring(4), out var v))
946946
frameThicknessRatio = v / 100000f;
947947
}
948948
}

src/MiniPdf/DocxToPdfConverter.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -330,12 +330,12 @@ private static void RenderParagraph(RenderState state, DocxParagraph paragraph)
330330
if (runFs > maxFs) maxFs = runFs;
331331
}
332332
var autoHeight = maxFs * FontMetricsFactor * options.LineSpacing;
333-
lineHeight = MathF.Ceiling(autoHeight / gridPitch) * gridPitch;
333+
lineHeight = Compat.Ceiling(autoHeight / gridPitch) * gridPitch;
334334
}
335335
else if (paragraph.LineSpacingAbsolute && paragraph.LineSpacing > 0)
336336
{
337337
// Exact spacing: snap the exact value up to nearest grid line
338-
lineHeight = MathF.Ceiling(lineHeight / gridPitch) * gridPitch;
338+
lineHeight = Compat.Ceiling(lineHeight / gridPitch) * gridPitch;
339339
}
340340
}
341341

@@ -1185,7 +1185,7 @@ private static float[] CalculateTableColumnWidths(DocxTable table, float usableW
11851185
var maxCols = table.Rows.Count > 0 ? table.Rows.Max(r => r.Cells.Count) : 1;
11861186
var colWidth = usableWidth / maxCols;
11871187
var result = new float[maxCols];
1188-
Array.Fill(result, colWidth);
1188+
Compat.ArrayFill(result, colWidth);
11891189
return result;
11901190
}
11911191

@@ -1463,7 +1463,7 @@ private static float GetDynamicNumberedWrapScale(DocxParagraph paragraph)
14631463
scale -= 0.003f;
14641464

14651465
// Keep the adjustment bounded to avoid regressions in other numbered paragraphs.
1466-
return Math.Clamp(scale, 0.972f, 0.986f);
1466+
return Compat.Clamp(scale, 0.972f, 0.986f);
14671467
}
14681468

14691469
/// <summary>

src/MiniPdf/ExcelReader.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -620,9 +620,9 @@ private static PdfColor ApplyTint(PdfColor color, double tint)
620620
}
621621

622622
return new PdfColor(
623-
Math.Clamp(r, 0f, 1f),
624-
Math.Clamp(g, 0f, 1f),
625-
Math.Clamp(b, 0f, 1f));
623+
Compat.Clamp(r, 0f, 1f),
624+
Compat.Clamp(g, 0f, 1f),
625+
Compat.Clamp(b, 0f, 1f));
626626
}
627627

628628
private static float HueToRgb(float p, float q, float t)
@@ -2741,7 +2741,7 @@ private static (int col, int row) ParseCellAddress(string addr)
27412741
i++;
27422742
}
27432743
col--; // convert to 0-based
2744-
int.TryParse(addr.AsSpan(i), out var row);
2744+
int.TryParse(addr.Substring(i), out var row);
27452745
row--; // convert to 0-based
27462746
return (col, row);
27472747
}

src/MiniPdf/ExcelToPdfConverter.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2859,7 +2859,7 @@ private static bool IsDefaultSheetName(string name)
28592859
{
28602860
if (name.StartsWith("Sheet", StringComparison.OrdinalIgnoreCase) && name.Length <= 8)
28612861
{
2862-
return int.TryParse(name.AsSpan(5), out _);
2862+
return int.TryParse(name.Substring(5), out _);
28632863
}
28642864
return false;
28652865
}
@@ -3137,23 +3137,23 @@ private static float[] CalculateNaturalColumnWidths(ExcelSheet sheet, int maxCol
31373137
// honour it (spacer columns etc.). Only apply minColWidth for
31383138
// columns using the default/fallback width.
31393139
var floor = hasExplicitWidth ? 0f : minColWidth;
3140-
widths[i] = Math.Clamp(excelPts, floor, maxColWidth);
3140+
widths[i] = Compat.Clamp(excelPts, floor, maxColWidth);
31413141
}
31423142
else if (maxCols == 1)
31433143
{
31443144
// Single-column sheet: use content-based width so the column fills the page
31453145
// (LibreOffice expands 1-column sheets to page width).
31463146
var natural = colMaxWidthPts[i] + 2 * avgCharWidth;
31473147
natural = Math.Max(natural, 5 * avgCharWidth); // minimum 5 chars
3148-
widths[i] = Math.Clamp(natural, minColWidth, maxColWidth);
3148+
widths[i] = Compat.Clamp(natural, minColWidth, maxColWidth);
31493149
}
31503150
else
31513151
{
31523152
// No explicit column widths — use Excel's default column width (8.43
31533153
// char units) like LibreOffice does. Text that exceeds the column
31543154
// boundary is clipped in the rendering step (shouldClip logic).
31553155
var defaultPts = ExcelSheet.CharUnitsToPoints(8.43f);
3156-
widths[i] = Math.Clamp(defaultPts, minColWidth, maxColWidth);
3156+
widths[i] = Compat.Clamp(defaultPts, minColWidth, maxColWidth);
31573157
}
31583158
}
31593159

src/MiniPdf/MiniPdf.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@ public static class MiniPdf
1616
/// <param name="fontData">The raw bytes of the .ttf or .ttc font file.</param>
1717
public static void RegisterFont(string name, byte[] fontData)
1818
{
19+
#if NET6_0_OR_GREATER
1920
ArgumentNullException.ThrowIfNull(name);
2021
ArgumentNullException.ThrowIfNull(fontData);
22+
#else
23+
if (name is null) throw new ArgumentNullException(nameof(name));
24+
if (fontData is null) throw new ArgumentNullException(nameof(fontData));
25+
#endif
2126
lock (_registeredFonts)
2227
_registeredFonts.Add((name, fontData));
2328
}

src/MiniPdf/MiniPdf.csproj

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFrameworks>net6.0;net8.0;net9.0</TargetFrameworks>
3+
<TargetFrameworks>net462;net6.0;net8.0;net9.0</TargetFrameworks>
44
<LangVersion>latest</LangVersion>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
@@ -24,7 +24,14 @@
2424
<InternalsVisibleTo Include="MiniPdf.Tests" />
2525
</ItemGroup>
2626

27-
<ItemGroup>
27+
<ItemGroup Condition="'$(TargetFramework)' == 'net462'">
28+
<PackageReference Include="System.Memory" Version="4.5.5" />
29+
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
30+
<Reference Include="System.Drawing" />
31+
<Reference Include="System.IO.Compression" />
32+
</ItemGroup>
33+
34+
<ItemGroup Condition="'$(TargetFramework)' != 'net462'">
2835
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
2936
</ItemGroup>
3037

src/MiniPdf/PdfColor.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ namespace MiniSoftware;
2020
/// </summary>
2121
public PdfColor(float r, float g, float b)
2222
{
23-
R = Math.Clamp(r, 0f, 1f);
24-
G = Math.Clamp(g, 0f, 1f);
25-
B = Math.Clamp(b, 0f, 1f);
23+
R = Compat.Clamp(r, 0f, 1f);
24+
G = Compat.Clamp(g, 0f, 1f);
25+
B = Compat.Clamp(b, 0f, 1f);
2626
}
2727

2828
/// <summary>
@@ -84,7 +84,7 @@ public static PdfColor FromHex(string hex)
8484
/// <inheritdoc />
8585
public override bool Equals(object? obj) => obj is PdfColor c && Equals(c);
8686
/// <inheritdoc />
87-
public override int GetHashCode() => HashCode.Combine(R, G, B);
87+
public override int GetHashCode() => Compat.HashCombine(R, G, B);
8888
/// <summary>Equality operator.</summary>
8989
public static bool operator ==(PdfColor left, PdfColor right) => left.Equals(right);
9090
/// <summary>Inequality operator.</summary>

0 commit comments

Comments
 (0)