Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1500,6 +1500,18 @@ var images = new[]
};
MiniExcel.AddPicture(path, images);
```

To embed an image **inside** a cell (Excel 365 "Place in Cell"), set `ImgType = XlsxImgType.PlaceInCell`. The image becomes the cell value and resizes with the cell. This requires Microsoft 365; older Excel versions show `#VALUE!`. `WidthPx`, `HeightPx`, and `Location` are ignored in this mode:

```csharp
MiniExcel.AddPicture(path, new MiniExcelPicture
{
ImageBytes = File.ReadAllBytes("logo.png"),
CellAddress = "C3",
ImgType = XlsxImgType.PlaceInCell,
});
```

![Image](https://github.com/user-attachments/assets/19c4d241-9753-4ede-96c8-f810c1a22247)

#### 8. Get Sheets Dimension
Expand Down
12 changes: 12 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1666,6 +1666,18 @@ var images = new[]
};
MiniExcel.AddPicture(path, images);
```

若要真正**嵌入单元格**(Excel 365「Place in Cell」),设置 `ImgType = XlsxImgType.PlaceInCell`。图片会作为单元格值,并随单元格缩放。需要 Microsoft 365;旧版 Excel 会显示 `#VALUE!`。此模式下 `WidthPx` / `HeightPx` / `Location` 无效:

```csharp
MiniExcel.AddPicture(path, new MiniExcelPicture
{
ImageBytes = File.ReadAllBytes("logo.png"),
CellAddress = "C3",
ImgType = XlsxImgType.PlaceInCell,
});
```

![Image](https://github.com/user-attachments/assets/19c4d241-9753-4ede-96c8-f810c1a22247)

#### 8. Get Sheets Dimension
Expand Down
14 changes: 13 additions & 1 deletion README_V2.md
Original file line number Diff line number Diff line change
Expand Up @@ -1790,9 +1790,21 @@ MiniExcelPicture[] images =
},
];

var templater = MiniExcel.Exporters.GetOpenXmlExporter();
var templater = MiniExcel.Templaters.GetOpenXmlTemplater();
templater.AddPicture(path, images);
```

To embed an image **inside** a cell (Excel 365 "Place in Cell"), set `ImgType = XlsxImgType.PlaceInCell`. The image becomes the cell value and resizes with the cell. This requires Microsoft 365; older Excel versions show `#VALUE!`. `WidthPx`, `HeightPx`, and `Location` are ignored in this mode:

```csharp
templater.AddPicture(path, new MiniExcelPicture
{
ImageBytes = File.ReadAllBytes("logo.png"),
CellAddress = "C3",
ImgType = XlsxImgType.PlaceInCell,
});
```

![Image](https://github.com/user-attachments/assets/19c4d241-9753-4ede-96c8-f810c1a22247)

#### 8. Get Sheets Dimensions
Expand Down
12 changes: 10 additions & 2 deletions src/MiniExcel.Core/Enums/XlsxImgType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@ public enum XlsxImgType
AbsoluteAnchor,

/// <summary>
/// The image is embedded in the cell and moves and scales with the cell (TwoCellAnchor).
/// Drawing anchored between two cells; moves and scales with the cells (TwoCellAnchor).
/// Still a floating drawing — not Excel 365 Place in Cell. Prefer <see cref="PlaceInCell"/> for true in-cell images.
/// </summary>
TwoCellAnchor
TwoCellAnchor,

/// <summary>
/// Excel 365 "Place in Cell": the image is stored as the cell value via richData (not a drawing).
/// Requires Microsoft 365; older Excel versions show #VALUE! in the cell.
/// WidthPx, HeightPx, and Location are ignored — size follows the cell.
/// </summary>
PlaceInCell
}
8 changes: 8 additions & 0 deletions src/MiniExcel.OpenXml/OpenXmlConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ public class OpenXmlConfiguration : MiniExcelBaseConfiguration
public int FreezeColumnCount { get; set; } = 0;
public bool EnableConvertByteArray { get; set; } = true;
public bool EnableWriteFilePath{ get; set; } = true;

/// <summary>
/// When true, <c>byte[]</c> image columns are written as Excel 365 Place in Cell
/// (embedded cell values via richData) instead of floating Drawing pictures.
/// Requires <see cref="EnableConvertByteArray"/> = true and <see cref="MiniExcelBaseConfiguration.FastMode"/> = true.
/// Needs Microsoft 365; older Excel may show <c>#VALUE!</c>.
/// </summary>
public bool EmbedImagesInCell { get; set; }
public bool IgnoreTemplateParameterMissing { get; set; } = true;
public bool EnableWriteNullValueCell { get; set; } = true;
public bool WriteEmptyStringAsNull { get; set; } = false;
Expand Down
41 changes: 26 additions & 15 deletions src/MiniExcel.OpenXml/Picture/OpenXmlPicture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,33 @@ public class MiniExcelPicture
public string? SheetName { get; set; }
public string? PictureType { get; set; }
public string? CellAddress { get; set; }


/// <summary>
/// Only takes effect when the image is in AbsoluteAnchor floating mode.
/// Ignored for <see cref="XlsxImgType.PlaceInCell"/>.
/// </summary>
public Point Location { get; set; }

/// <summary>
/// Drawing anchor mode, or <see cref="XlsxImgType.PlaceInCell"/> for Excel 365 in-cell images.
/// </summary>
public XlsxImgType ImgType { get; set; }

internal int ColumnNumber => CellReferenceConverter.TryParseCellReference(CellAddress, out var column, out _)
? column - 1
: throw new InvalidDataException($"Value {CellAddress} is not a valid cell reference.");

internal int RowNumber => CellReferenceConverter.TryParseCellReference(CellAddress, out var _, out var row)
? row - 1
: throw new InvalidDataException($"Value {CellAddress} is not a valid cell reference.");

/// <summary>
/// Only takes effect when the image is in AbsoluteAnchor floating mode
/// Image width in pixels for drawing anchors. Ignored for <see cref="XlsxImgType.PlaceInCell"/>.
/// </summary>
public Point Location { get; set; }
public XlsxImgType ImgType { get; set; }

internal int ColumnNumber => CellReferenceConverter.TryParseCellReference(CellAddress, out var column, out _)
? column - 1
: throw new InvalidDataException($"Value {CellAddress} is not a valid cell reference.");

internal int RowNumber => CellReferenceConverter.TryParseCellReference(CellAddress, out var _, out var row)
? row - 1
: throw new InvalidDataException($"Value {CellAddress} is not a valid cell reference.");


public int WidthPx { get; set; } = 80;

/// <summary>
/// Image height in pixels for drawing anchors. Ignored for <see cref="XlsxImgType.PlaceInCell"/>.
/// </summary>
public int HeightPx { get; set; } = 24;
}
}
38 changes: 29 additions & 9 deletions src/MiniExcel.OpenXml/Picture/OpenXmlPictureImplement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,44 @@ private static XmlNamespaceManager GetRNamespaceManager(XmlDocument doc)
[CreateSyncVersion]
public static async Task AddPictureAsync(Stream excelStream, CancellationToken cancellationToken = default, params MiniExcelPicture[] images)
{
// get sheets
var excelArchive = await OpenXmlZip.CreateAsync(excelStream, cancellationToken: cancellationToken).ConfigureAwait(false);
await using var disposableExcelArchive = excelArchive.ConfigureAwait(false);
if (images.Length == 0)
return;

var reader = await OpenXmlReader.CreateAsync(excelStream, null, false, cancellationToken).ConfigureAwait(false);
await using var dispoableReader = reader.ConfigureAwait(false);
var placeInCellImages = images.Where(i => i.ImgType == XlsxImgType.PlaceInCell).ToArray();
var drawingImages = images.Where(i => i.ImgType != XlsxImgType.PlaceInCell).ToArray();

// Read sheet list first (leaveOpen so callers can keep using MemoryStream)
List<SheetRecord> sheetEntries;
{
var excelArchive = await OpenXmlZip.CreateAsync(excelStream, leaveOpen: true, cancellationToken: cancellationToken)
.ConfigureAwait(false);
await using var disposableExcelArchive = excelArchive.ConfigureAwait(false);
using var reader = OpenXmlReader.CreateFromArchive(excelArchive, null);
var rels = await OpenXmlReader.GetWorkbookRelsAsync(excelArchive.EntryCollection, cancellationToken).ConfigureAwait(false);
sheetEntries = rels?.ToList() ?? [];
}

if (excelStream.CanSeek)
excelStream.Position = 0;

#if NET10_0_OR_GREATER
var archive = await ZipArchive.CreateAsync(excelStream, ZipArchiveMode.Update, true, null, cancellationToken).ConfigureAwait(false);
await using var disposableArchive = archive.ConfigureAwait(false);
#else
using var archive = new ZipArchive(excelStream, ZipArchiveMode.Update, true);
#endif
var rels = await OpenXmlReader.GetWorkbookRelsAsync(excelArchive.EntryCollection, cancellationToken).ConfigureAwait(false);
var sheetEntries = rels?.ToList() ?? [];

// Group images by sheet
var imagesBySheet = images.GroupBy(img => img.SheetName ?? sheetEntries[0].Name);
if (placeInCellImages.Length > 0)
{
await OpenXmlPlaceInCellImplement.AddAsync(archive, sheetEntries, placeInCellImages, cancellationToken)
.ConfigureAwait(false);
}

if (drawingImages.Length == 0)
return;

// Group drawing images by sheet
var imagesBySheet = drawingImages.GroupBy(img => img.SheetName ?? sheetEntries[0].Name);
foreach (var sheetGroup in imagesBySheet)
{
var sheetName = sheetGroup.Key;
Expand Down
Loading
Loading