I finally found a solution that uses the graphics engine GDI+ to do this accurately. avg. char width * number of chars
doesn't cut it ever.
There's a caveat here in that you will need a couple of libraries if your target platform is linux - one for GDI+ interop and the other to add the open microsoft web fonts.
It uses a base unit of pixel, considering excel fonts are pt
and column width INCHES! (lol)
Here is the method. It takes a JSchema to map to columns but you get the gist.
private static Columns CreateColumnDefs(JSchema jsonSchema)
{
const double cellPadding = .4;
const double minCellWidth = 10;
var columnDefs = new Columns();
var columnIndex = 0U;
// set up graphics for calculating column width based on heading text width
var bmp = new Bitmap(1, 1);
// todo: ensure linux host has gdi and "Microsoft core fonts" libs installed
using var graphics = Graphics.FromImage(bmp);
graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
graphics.PageUnit = GraphicsUnit.Pixel;
// excel fonts are in points - Arial 10pt matches the excel default of Calibri 11pt pretty well...
using var font = new Font("Arial", 10F, FontStyle.Bold, GraphicsUnit.Point);
// currently only handles 1 level (no nested objects)
foreach (var (_, value) in jsonSchema.Properties)
{
var pixelWidth = graphics.MeasureString(value.Title, font).Width;
// see: https://mcmap.net/q/662572/-formula-to-convert-net-pixels-to-excel-width-in-openxml-format/7902415
var openXmlWidth = (pixelWidth - 12 + 5) / 7d + 1; // from pixels to inches.
columnIndex++;
var column = new Column
{
BestFit = true,
CustomWidth = true,
Width = Math.Max(openXmlWidth + cellPadding, minCellWidth),
Min = columnIndex,
Max = columnIndex
};
columnDefs.Append(column);
}
return columnDefs;
}
Edit
To run on linux install e.g.:
RUN apt update && apt-get install -y libgdiplus
(I didn't need any extra font libs, so perhaps system font works)