Skip to content

Commit 6c34a18

Browse files
committed
perf: 优化图片导出性能,修复测试断言问题
1. 使用 SKCodec 替代 Image.Load 读取图片元数据,跳过解码/编码 - ExcelPicture 新增原始字节构造函数,直接写入 Excel 包 - ExcelDrawings 新增 AddPictureFromBytes 方法 - ExcelPackage.AddImage 修复 uri=null 时 contentType 被硬编码为 image/jpeg 的问题 2. AddPictures 支持并行加载 + 应用层去重 - 同一 URL 只下载一次,通过 ConcurrentDictionary 缓存 - Parallel.ForEach 并行下载,串行写入 Excel 3. AutoFit 字体缓存,优化 Linux 性能 - ExcelRangeBase 预加载 SKTypeface 到缓存,避免每个 cell 都调用 fontconfig - SKTypeface 和 SKFont 在 finally 中统一释放 4. 修复测试断言问题 - .Equals() / .Any() 返回值未断言(空操作)→ 改为 .ShouldBe() / .ShouldBeTrue() - AttrsExport_Test 日期格式列号修正(D2/E2 而非 E2/F2) - ExportWithJPG_Test 补充断言 - AttrExportWithColAutoCenterData_Test 文件路径冲突修复 - RequiredIf_Test Culture 全局状态泄漏修复(try/finally) - ImportTestColumnIndex_Test 删除调试遗留代码 5. 新增 ImageOptimization_Tests(21 个测试) - IdentifyImage / DecodeBase64ToBytes / ReadImageBytes 单元测试 - AddPictureFromBytes / ExcelPicture 原始字节集成测试 - 端到端导出测试(多图/混合/去重/Base64) Refs #571 Refs #616(部分解决:主导出路径已用 SKCodec 替代,模板导出/导入/颜色系统仍需迁移)
1 parent c469dfd commit 6c34a18

9 files changed

Lines changed: 799 additions & 157 deletions

File tree

src/EPPlus/EPPlus/Drawing/ExcelDrawings.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,27 @@ public ExcelPicture AddPicture(string Name, Image image, IImageFormat format, Ur
352352
throw (new Exception("AddPicture: Image can't be null"));
353353
}
354354

355+
/// <summary>
356+
/// 从原始字节添加图片,配合 SKCodec 读取的元数据使用
357+
/// </summary>
358+
public ExcelPicture AddPictureFromBytes(string name, byte[] imageBytes,
359+
string contentType, int width, int height)
360+
{
361+
if (imageBytes == null || imageBytes.Length == 0)
362+
throw new ArgumentException("imageBytes 不能为空");
363+
364+
if (_drawingNames.ContainsKey(name))
365+
throw new Exception("Name already exists in the drawings collection");
366+
367+
XmlElement drawNode = CreateDrawingXml();
368+
drawNode.SetAttribute("editAs", "oneCell");
369+
ExcelPicture pic = new ExcelPicture(this, drawNode, imageBytes, contentType, width, height);
370+
pic.Name = name;
371+
_drawings.Add(pic);
372+
_drawingNames.Add(name, _drawings.Count - 1);
373+
return pic;
374+
}
375+
355376
/// <summary>
356377
/// Add a picure to the worksheet
357378
/// </summary>

src/EPPlus/EPPlus/Drawing/ExcelPicture.cs

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,58 @@ private void AddNewPicture(byte[] img, string relID)
166166
//_drawings._pics.Add(newPic);
167167
}
168168

169+
/// <summary>
170+
/// 从原始字节构造,跳过 Image.Load 和 GetImageAsByteArray
171+
/// </summary>
172+
internal ExcelPicture(ExcelDrawings drawings, XmlNode node,
173+
byte[] imageBytes, string contentType, int width, int height)
174+
: base(drawings, node, "xdr:pic/xdr:nvPicPr/xdr:cNvPr/@name")
175+
{
176+
XmlElement picNode = node.OwnerDocument.CreateElement("xdr", "pic", ExcelPackage.schemaSheetDrawings);
177+
node.InsertAfter(picNode, node.SelectSingleNode("xdr:to", NameSpaceManager));
178+
_hyperlink = null;
179+
picNode.InnerXml = PicStartXml();
180+
181+
node.InsertAfter(node.OwnerDocument.CreateElement("xdr", "clientData", ExcelPackage.schemaSheetDrawings),
182+
picNode);
183+
184+
var package = drawings.Worksheet._package.Package;
185+
Image = null;
186+
_contentType = contentType;
187+
string relID = SavePictureDirectly(imageBytes, contentType);
188+
189+
node.SelectSingleNode("xdr:pic/xdr:blipFill/a:blip/@r:embed", NameSpaceManager).Value = relID;
190+
_height = height;
191+
_width = width;
192+
EditAs = eEditAs.OneCell;
193+
SetPixelWidth(width, 96f);
194+
SetPixelHeight(height, 96f);
195+
package.Flush();
196+
}
197+
198+
private string SavePictureDirectly(byte[] imageBytes, string contentType)
199+
{
200+
var ii = _drawings._package.AddImage(imageBytes, null, contentType);
201+
202+
ImageHash = ii.Hash;
203+
if (_drawings._hashes.ContainsKey(ii.Hash))
204+
{
205+
var relID = _drawings._hashes[ii.Hash];
206+
var rel = _drawings.Part.GetRelationship(relID);
207+
UriPic = UriHelper.ResolvePartUri(rel.SourceUri, rel.TargetUri);
208+
return relID;
209+
}
210+
211+
UriPic = ii.Uri;
212+
ImageHash = ii.Hash;
213+
214+
RelPic = _drawings.Part.CreateRelationship(UriHelper.GetRelativeUri(_drawings.UriDrawing, UriPic),
215+
Packaging.TargetMode.Internal, ExcelPackage.schemaRelationships + "/image");
216+
217+
_drawings._hashes.Add(ii.Hash, RelPic.Id);
218+
return RelPic.Id;
219+
}
220+
169221
#endregion
170222

171223
private string SavePicture(Image image, IImageFormat format)
@@ -246,7 +298,7 @@ private string PicStartXml()
246298
internal string ImageHash { get; set; }
247299

248300
/// <summary>
249-
/// The Image
301+
/// The Image (may be null when constructed from raw bytes via AddPictureFromBytes)
250302
/// </summary>
251303
public Image Image { get; private set; }
252304

@@ -256,7 +308,12 @@ private string PicStartXml()
256308
/// </summary>
257309
public IImageFormat ImageFormat { get; private set; }
258310

259-
internal string ContentType => ImageFormat?.DefaultMimeType;
311+
/// <summary>
312+
/// 从原始字节构造时,直接存储 content type 字符串
313+
/// </summary>
314+
private string _contentType;
315+
316+
internal string ContentType => _contentType ?? ImageFormat?.DefaultMimeType;
260317

261318
/// <summary>
262319
/// Set the size of the image in percent from the orginal size
@@ -267,6 +324,7 @@ public override void SetSize(int Percent)
267324
{
268325
if (Image == null)
269326
{
327+
// 原始字节构造的图片,使用已存储的 _width/_height 和标准 DPI
270328
base.SetSize(Percent);
271329
}
272330
else
@@ -348,9 +406,9 @@ public override void Dispose()
348406
{
349407
base.Dispose();
350408
_hyperlink = null;
351-
Image.Dispose();
409+
Image?.Dispose();
352410
Image = null;
353-
ImageFormat = JpegFormat.Instance;
411+
_contentType = null;
354412
}
355413
}
356414
}

src/EPPlus/EPPlus/ExcelPackage.cs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,8 +440,10 @@ internal ImageInfo AddImage(byte[] image, Uri uri, string contentType)
440440
Packaging.ZipPackagePart imagePart;
441441
if (uri == null)
442442
{
443-
uri = GetNewUri(Package, "/xl/media/image{0}.jpg");
444-
imagePart = Package.CreatePart(uri, "image/jpeg", CompressionLevel.None);
443+
var effectiveContentType = string.IsNullOrEmpty(contentType) ? "image/jpeg" : contentType;
444+
var extension = GetExtensionFromContentType(effectiveContentType);
445+
uri = GetNewUri(Package, "/xl/media/image{0}" + extension);
446+
imagePart = Package.CreatePart(uri, effectiveContentType, CompressionLevel.None);
445447
}
446448
else
447449
{
@@ -455,6 +457,23 @@ internal ImageInfo AddImage(byte[] image, Uri uri, string contentType)
455457
}
456458
return _images[hash];
457459
}
460+
461+
/// <summary>
462+
/// 根据 content type 返回对应的文件扩展名
463+
/// </summary>
464+
private static string GetExtensionFromContentType(string contentType)
465+
{
466+
switch (contentType)
467+
{
468+
case "image/png": return ".png";
469+
case "image/gif": return ".gif";
470+
case "image/bmp": return ".bmp";
471+
case "image/webp": return ".webp";
472+
case "image/tiff": return ".tiff";
473+
default: return ".jpg";
474+
}
475+
}
476+
458477
internal ImageInfo LoadImage(byte[] image, Uri uri, Packaging.ZipPackagePart imagePart)
459478
{
460479
#if (Core)

src/EPPlus/EPPlus/ExcelRangeBase.cs

Lines changed: 64 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -918,28 +918,57 @@ public void AutoFitColumns(double minimumWidth, double maximumWidth)
918918
}
919919

920920
var styles = _worksheet.Workbook.Styles;
921-
foreach (var cell in this)
921+
922+
// 字体缓存:避免每个 cell 都调用 SKTypeface.FromFamilyName(Linux 上 fontconfig 很慢)
923+
var fontCache = new Dictionary<(string family, int style, float size), SKFont>();
924+
var typefaces = new List<SKTypeface>();
925+
var fallbackFont = new SKFont(SKTypeface.Default);
926+
try
922927
{
923-
if (_worksheet.Column(cell.Start.Column).Hidden) //Issue 15338
928+
for (int fi = 0; fi < styles.Fonts.Count; fi++)
924929
{
925-
continue;
926-
}
930+
var excelFontXml = styles.Fonts[fi];
931+
var skFontStyle = SKFontStyle.Normal;
932+
if (excelFontXml.Bold && excelFontXml.Italic)
933+
skFontStyle = SKFontStyle.BoldItalic;
934+
else if (excelFontXml.Italic)
935+
skFontStyle = SKFontStyle.Italic;
936+
else if (excelFontXml.Bold)
937+
skFontStyle = SKFontStyle.Bold;
927938

928-
if (cell.Merge || cell.Style.WrapText)
929-
{
930-
continue;
939+
var cacheKey = (excelFontXml.Name, skFontStyle.GetHashCode(), excelFontXml.Size);
940+
if (!fontCache.ContainsKey(cacheKey))
941+
{
942+
var fontTypeface = SKTypeface.FromFamilyName(excelFontXml.Name, skFontStyle);
943+
if (fontTypeface != null && fontTypeface.FamilyName == excelFontXml.Name)
944+
{
945+
typefaces.Add(fontTypeface);
946+
fontCache[cacheKey] = new SKFont(fontTypeface, excelFontXml.Size);
947+
}
948+
}
931949
}
932950

933-
if (cell.Address.StartsWith("I"))
951+
foreach (var cell in this)
934952
{
935-
}
953+
if (_worksheet.Column(cell.Start.Column).Hidden) //Issue 15338
954+
{
955+
continue;
956+
}
936957

937-
var ind = styles.CellXfs[cell.StyleID].Indent;
938-
var textForWidth = cell.TextForWidth;
939-
var t = textForWidth + (ind > 0 && !string.IsNullOrEmpty(textForWidth) ? new string('_', ind) : "");
940-
if (t.Length > 32000) t = t.Substring(0, 32000); //Issue
941-
using (var font = GetAvailableFont(t))
942-
{
958+
if (cell.Merge || cell.Style.WrapText)
959+
{
960+
continue;
961+
}
962+
963+
if (cell.Address.StartsWith("I"))
964+
{
965+
}
966+
967+
var ind = styles.CellXfs[cell.StyleID].Indent;
968+
var textForWidth = cell.TextForWidth;
969+
var t = textForWidth + (ind > 0 && !string.IsNullOrEmpty(textForWidth) ? new string('_', ind) : "");
970+
if (t.Length > 32000) t = t.Substring(0, 32000); //Issue
971+
var font = GetAvailableFont(t, fontCache, fallbackFont);
943972
var normalSize =
944973
Convert.ToSingle(ExcelWorkbook.GetWidthPixels(font.Typeface.FamilyName, font.Size));
945974
var measureWidthInPoints = font.MeasureText(t, out SKRect rect);
@@ -972,42 +1001,45 @@ public void AutoFitColumns(double minimumWidth, double maximumWidth)
9721001
widthInPixels > maximumWidth ? maximumWidth : widthInPixels;
9731002
}
9741003
}
975-
}
9761004

977-
_worksheet.Drawings.AdjustWidth(drawWidths);
978-
_worksheet._package.DoAdjustDrawings = doAdjust;
1005+
_worksheet.Drawings.AdjustWidth(drawWidths);
1006+
_worksheet._package.DoAdjustDrawings = doAdjust;
1007+
}
1008+
finally
1009+
{
1010+
// 统一 dispose 所有缓存的字体和 typeface 对象
1011+
foreach (var kv in fontCache)
1012+
kv.Value?.Dispose();
1013+
foreach (var tf in typefaces)
1014+
tf?.Dispose();
1015+
fallbackFont?.Dispose();
1016+
}
9791017
}
9801018

981-
private SKFont GetAvailableFont(string text)
1019+
private SKFont GetAvailableFont(string text,
1020+
Dictionary<(string family, int style, float size), SKFont> fontCache,
1021+
SKFont fallbackFont)
9821022
{
9831023
var isAllAsciiSymbolOrChars =
984-
!text.Any(c => c < 20 || c > 126); // use FangSong to measure the text that is not in english
1024+
!text.Any(c => c < 20 || c > 126);
9851025
var styles = _worksheet.Workbook.Styles;
9861026
for (int i = isAllAsciiSymbolOrChars ? 0 : 1; i < styles.Fonts.Count; i++)
9871027
{
9881028
var skFontStyle = SKFontStyle.Normal;
9891029
var excelFontXml = styles.Fonts[i];
9901030
if (excelFontXml.Bold && excelFontXml.Italic)
991-
{
9921031
skFontStyle = SKFontStyle.BoldItalic;
993-
}
9941032
else if (excelFontXml.Italic)
995-
{
9961033
skFontStyle = SKFontStyle.Italic;
997-
}
9981034
else if (excelFontXml.Bold)
999-
{
10001035
skFontStyle = SKFontStyle.Bold;
1001-
}
10021036

1003-
var fontTypeface = SKTypeface.FromFamilyName(excelFontXml.Name, skFontStyle);
1004-
if (fontTypeface.FamilyName == excelFontXml.Name)
1005-
{
1006-
return new SKFont(fontTypeface, excelFontXml.Size);
1007-
}
1037+
var cacheKey = (excelFontXml.Name, skFontStyle.GetHashCode(), excelFontXml.Size);
1038+
if (fontCache.TryGetValue(cacheKey, out var cachedFont))
1039+
return cachedFont;
10081040
}
10091041

1010-
return new SKFont(SKTypeface.Default);
1042+
return fallbackFont;
10111043
}
10121044

10131045
private void SetMinWidth(double minimumWidth, int fromCol, int toCol)

0 commit comments

Comments
 (0)