Skip to content

Make the library fully cross-platform: remove GDI+ (System.Drawing.Common), CI on Linux - #6

Merged
davidnmbond merged 4 commits into
mainfrom
feature/cross-platform-drawing
Oct 12, 2026
Merged

davidnmbond merged 4 commits into
mainfrom
feature/cross-platform-drawing

Conversation

@davidnmbond

Copy link
Copy Markdown

Summary

Removes every dependency on the GDI+ parts of System.Drawing so the library and its whole test suite run on Linux, and puts CI back on ubuntu-latest. System.Drawing.Color/Point/Size (plain structs in the shared framework) are still used.

  • System.Drawing.Common package reference removed; all Image/Bitmap/Graphics/Font/Imaging use and all [SupportedOSPlatform("windows")] / OperatingSystem checks removed.
  • Pictures: new managed ImageHeaderReader reads width/height/DPI from PNG, JPEG, GIF, BMP, TIFF, EMF, WMF headers and sniffs the content type. File bytes are stored verbatim (no re-encoding to JPEG). Loading a picture from a package no longer throws off Windows.
  • Autofit: TextMeasurer uses SkiaSharp (MIT) + SkiaSharp.NativeAssets.Linux. No ImageSharp. If the native library cannot load, AutoFitColumns leaves widths unchanged (as the old GDI-missing path did) and now restores DoAdjustDrawings.
  • NBGV 10.1 -> 11.0; breaking changes in PackageReleaseNotes.

Breaking API changes

Removed:

  • ExcelDrawings.AddPicture(string, Image) and (string, Image, Uri)
  • ExcelPicture.Image, ExcelPicture.ImageFormat
  • ExcelBackgroundImage.Image
  • ExcelVmlDrawingPicture.Image
  • ExcelHeaderFooterText.InsertPicture(Image, PictureAlignment)
  • ExcelFont.SetFromFont(Font), ExcelTextFont.SetFromFont(Font), ExcelFontXml.SetFromFont(Font)

Added / changed:

  • ExcelDrawings.AddPicture(string, byte[]), (string, byte[], Uri), (string, Stream), (string, Stream, Uri); the FileInfo overloads remain (read bytes)
  • ExcelPicture.ImageBytes (get/set), ExcelPicture.ContentType now public
  • ExcelBackgroundImage.ImageBytes, SetFromStream; SetFromFile kept
  • ExcelVmlDrawingPicture.ImageBytes
  • ExcelHeaderFooterText.InsertPicture(byte[], ...), InsertPicture(Stream, ...); FileInfo overload kept
  • Autofit widths may differ slightly from GDI+ (Skia measures without GDI's padding; a missing font falls back to the platform default).

Tests

  • Deleted: none.
  • Ported: SetFromFont(new Font(..)) call sites to property setters (DrawingTest, WorkSheetTests, SampleApp); Resources.Test1 is now byte[]; SetBackground uses ImageBytes; Issue100/Issue10 (ignored) use bytes.
  • AddPictureFromBytes_RoundTripsThroughASavedPackage: platform branch removed; now reopens and reads the picture back via ImageBytes.
  • New: header-reader tests (PNG/JPEG/GIF/BMP/truncated), AddPicture(byte[]) auto-size, Stream and FileInfo overloads.
  • SampleApp: removed the Windows-only shell icon extraction. EPPlusExcelCalculationSample (WinForms, not in the solution) is untouched.
  • Test-bug fixes (root causes of the old CI exclusions):
    • DateFunctionsWorkWithDifferentCultureDateFormats_*: exception on the worker thread crashed the host; now captured and asserted on the test thread, and the culture is created without user overrides.
    • RunDrawingTests / SetBackground: TestBase extracted clipart using the wrong resource-name prefix; fixed. Also path separators (\) in TestBase and several tests replaced with Path.Combine.
    • RunWorksheetTests: assertions compared against 15-digit literals; use the full round-trip values.
    • Issuer246: assertion arguments/expected text depended on OS culture data; now computed from the sv-SE long date.
    • Issue333: sets sv-SE explicitly (the format is the culture short date).
    • New TestAssemblySetup fixes the suite to en-US.
  • All --filter exclusions removed from CI (--blame-hang-timeout 5m and the Codacy upload are kept). Locally on Windows the entire suite passes with no filter: 1176 passed, 214 skipped (pre-existing [Ignore]s).

CI

build job is ubuntu-latest; no Windows runner anywhere.

🤖 Generated with Claude Code

davidnmbond and others added 4 commits October 12, 2026 15:16
…p text measurement

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…; add header reader tests

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d precision test bugs; release notes, NBGV 11.0

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@codacy-production

codacy-production Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 2 high · 4 medium · 1 minor

Alerts:
⚠ 7 issues (≤ 0 issues of at least minor severity)

Results:
7 new issues

Category Results
BestPractice 1 high
ErrorProne 1 high
CodeStyle 1 minor
Complexity 4 medium

View in Codacy

🟢 Metrics 54 complexity · -3 duplication

Metric Results
Complexity 54
Duplication -3

View in Codacy

🟢 Coverage 73.40% diff coverage · +9.61% coverage variation

Metric Results
Coverage variation ✅ +9.61% coverage variation (-1.00%)
Diff coverage ✅ 73.40% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (85c2320) 47155 25865 54.85%
Head commit (bce00dd) 47272 (+117) 30473 (+4608) 64.46% (+9.61%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#6) 406 298 73.40%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.

Run reviewer

TIP This summary will be updated as you push new changes.

@codacy-production codacy-production Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

⚠️ Resolve issues before merging

The Linux CI command now runs previously excluded tests, including documented failures and host-crashing culture tests, so the pipeline may fail for reasons unrelated to this change. The culture test also leaks state when assertions fail.

Image-header parsing remains a high-risk, uncovered area: TIFF/EMF/WMF scenarios and malformed-offset handling are not sufficiently tested. The autofit fallback also leaves DoAdjustDrawings unrestored on an early return.

About this PR

  • Verify that active tests use platform-neutral temporary paths and that CI explicitly runs the complete unfiltered suite on ubuntu-latest.

Test suggestions

  • Add, save, reopen, and read picture bytes through the public API on a non-Windows platform
  • Validate PNG, JPEG, GIF, BMP, and truncated image headers
  • Validate TIFF, EMF, and WMF dimensions, resolution, and content types
  • Exercise Stream and FileInfo picture overloads and verify bytes remain unchanged
  • Exercise background-image byte storage and replacement/removal
  • Exercise header/footer and VML picture byte APIs
  • Verify autofit with SkiaSharp and the unavailable-native-library fallback
  • Verify autofit restores DoAdjustDrawings on every exit path
  • Copy worksheets containing pictures without image decoding
  • Add malformed-offset and unsupported-TIFF-field tests for ImageHeaderReader
Prompt proposal for missing tests
Consider implementing these tests if applicable:
1. Validate TIFF, EMF, and WMF dimensions, resolution, and content types
2. Exercise background-image byte storage and replacement/removal
3. Exercise header/footer and VML picture byte APIs
4. Verify autofit with SkiaSharp and the unavailable-native-library fallback
5. Verify autofit restores DoAdjustDrawings on every exit path
6. Add malformed-offset and unsupported-TIFF-field tests for ImageHeaderReader

TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback

Comment on lines +202 to +252
private static (int, int, float, float) ReadTiff(ReadOnlySpan<byte> b)
{
var little = b[0] == 0x49;
ushort U16(ReadOnlySpan<byte> s, int o) => little ? BinaryPrimitives.ReadUInt16LittleEndian(s[o..]) : BinaryPrimitives.ReadUInt16BigEndian(s[o..]);
uint U32(ReadOnlySpan<byte> s, int o) => little ? BinaryPrimitives.ReadUInt32LittleEndian(s[o..]) : BinaryPrimitives.ReadUInt32BigEndian(s[o..]);
double Value(ReadOnlySpan<byte> s, int entry)
{
var type = U16(s, entry + 2);
switch (type)
{
case 3:
return U16(s, entry + 8); // SHORT
case 4:
return U32(s, entry + 8); // LONG
case 5: // RATIONAL
var at = (int)U32(s, entry + 8);
return (double)U32(s, at) / Math.Max(1U, U32(s, at + 4));
default:
return 0;
}
}

var ifd = (int)U32(b, 4);
var count = U16(b, ifd);
double width = 0, height = 0, xres = 0, yres = 0;
var unit = 2;
for (var i = 0; i < count; i++)
{
var entry = ifd + 2 + (i * 12);
switch (U16(b, entry))
{
case 256:
width = Value(b, entry);
break;
case 257:
height = Value(b, entry);
break;
case 282:
xres = Value(b, entry);
break;
case 283:
yres = Value(b, entry);
break;
case 296:
unit = (int)Value(b, entry);
break;
}
}

var scale = unit == 3 ? 2.54 : 1;
return ((int)width, (int)height, (float)(xres * scale), (float)(yres * scale));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 HIGH RISK

Add focused tests for TIFF, EMF, and WMF dimensions and DPI, truncated and malformed offsets, and unsupported TIFF field types. Add an explicit TIFF switch default that deliberately ignores optional fields or rejects unsupported required data with InvalidDataException.

See Coverage in Codacy
See Issue in Codacy

Comment thread .github/workflows/ci.yml
Comment on lines 35 to 38
dotnet test PanoramicData.EPPlus.Test/PanoramicData.EPPlus.Test.csproj
--configuration Release --no-build
--collect "Code Coverage;Format=Cobertura" --results-directory ./coverage
--blame-hang-timeout 5m

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 HIGH RISK

Retain or replace exclusions for known failing tests, or fix those tests before removing the filter; otherwise this command re-enables documented failures in the Linux CI run.

{
throw new Exception("Name already exists in the drawings collection");
}
throw new Exception("AddPicture: ImageFile can't be null");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 HIGH RISK

Suggestion: Use ArgumentNullException for a null FileInfo argument.

See Issue in Codacy

Comment on lines 127 to +181
@@ -165,15 +175,15 @@ public ExcelVmlDrawingPicture InsertPicture(FileInfo PictureFile, PictureAlignme

var contentType = ExcelPicture.GetContentType(PictureFile.Extension);
var uriPic = XmlHelper.GetNewUri(_ws._package.Package, "/xl/media/" + PictureFile.Name[..^PictureFile.Extension.Length] + "{0}" + PictureFile.Extension);
var imgBytes = ImageCompat.GetImageAsByteArray(Picture);

var ii = _ws.Workbook._package.AddImage(imgBytes, uriPic, contentType);

return AddImage(Picture, id, ii);
return AddImage(header, id, ii);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM RISK

Add cross-platform tests for InsertPicture(byte[]), InsertPicture(Stream), and InsertPicture(FileInfo), including dimensions, invalid/truncated data, duplicate-section validation, and verification that stored package bytes remain unchanged.

See Coverage in Codacy

//No usable font engine (native SkiaSharp library missing): leave the widths as they are.
_worksheet.Drawings.AdjustWidth(drawWidths);
_worksheet._package.DoAdjustDrawings = doAdjust;
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM RISK

Restore DoAdjustDrawings before returning from this path, as in the normal completion path.

Comment on lines +2115 to 2119
System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo("sv-SE", false);
var sv = new CultureInfo("sv-SE", false);
Assert.AreEqual(new DateTime(2018, 12, 31).ToString("D", sv), ws.Cells["A1"].Text);
Assert.AreEqual(ws.GetValue<DateTime>(1, 1), new DateTime(2018, 12, 31));
System.Threading.Thread.CurrentThread.CurrentCulture = pCulture;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM RISK

Restore the previous culture in a finally block surrounding the assertions.

Create(node, imageBytes, contentType, "/xl/media/image{0}" + GetExtension(contentType), widthPixels, heightPixels, ImageHeaderInfo.DefaultDpi, ImageHeaderInfo.DefaultDpi, hyperlink);
}

private void Create(XmlNode node, byte[] imageBytes, string contentType, string uriTemplate, int widthPixels, int heightPixels, float horizontalDpi, float verticalDpi, Uri hyperlink)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM RISK

Suggestion: Consider introducing an image metadata/options object to replace the positional parameter list.

See Issue in Codacy

@davidnmbond
davidnmbond merged commit 22a7e64 into main Oct 12, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant