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
2 changes: 1 addition & 1 deletion Examples/Excel/Example-ExcelAdvanced.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ New-OfficeExcel -Path $path {
ExcelComment -Cell 'C2' -Text 'Review this value'

if (Test-Path $imagePath) {
ExcelImage -Path $imagePath -Cell 'I8' -Width 120 -Height 90 | Out-Null
ExcelImage -Path $imagePath -Range 'I8:J12' -Name 'OfficeIMOLogo' -AltText 'OfficeIMO logo' | Out-Null
}
}

Expand Down
39 changes: 39 additions & 0 deletions Examples/Excel/Example-ExcelPictures.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
$modulePath = if ($env:PSWRITEOFFICE_MODULE_MANIFEST) {
$env:PSWRITEOFFICE_MODULE_MANIFEST
} else {
(Join-Path $PSScriptRoot '..\..\PSWriteOffice.psd1')
}
if (-not (Get-Module -Name PSWriteOffice)) { Import-Module $modulePath -ErrorAction Stop }

$documents = Join-Path $PSScriptRoot '..\Documents'
New-Item -Path $documents -ItemType Directory -Force | Out-Null

$path = Join-Path $documents 'Excel-Pictures.xlsx'
$officeimoRoot = Join-Path $PSScriptRoot '..\..\..\OfficeIMO'
$imageCandidates = @(
(Join-Path (Join-Path $officeimoRoot 'Assets') 'OfficeIMO.png')
(Join-Path (Join-Path $officeimoRoot 'OfficeIMO.Tests\Images') 'EvotecLogo.png')
)
$imagePath = $imageCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
if (-not $imagePath) {
throw 'Could not find a sample image. Run this example from an EvotecIT checkout with OfficeIMO next to PSWriteOffice.'
}

New-OfficeExcel -Path $path {
ExcelSheet 'Pictures' {
ExcelCell -Address 'A1' -Value 'Range anchored image'
ExcelCell -Address 'E1' -Value 'Scaled image'
ExcelCell -Address 'E8' -Value 'Rotated image'

ExcelImage -Path $imagePath -Range 'A2:C12' -Name 'HeaderLogo' -AltText 'Company logo pinned to A2 through C12' -Placement MoveAndSize
ExcelImage -Path $imagePath -Address 'E2' -ScalePercent 20 -Name 'ScaledLogo' -AltText 'Logo scaled to 20 percent'
ExcelImage -Path $imagePath -Address 'E9' -Width 120 -Height 48 -RotationDegrees 12 -Name 'RotatedLogo' -AltText 'Logo rotated by 12 degrees'

ExcelColumn -ColumnName A -Width 18
ExcelColumn -ColumnName B -Width 18
ExcelColumn -ColumnName C -Width 18
ExcelColumn -ColumnName E -Width 24
}
} | Out-Null

Write-Host "Workbook saved to $path"
185 changes: 154 additions & 31 deletions Sources/PSWriteOffice/Cmdlets/Excel/AddOfficeExcelImageCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,24 @@

namespace PSWriteOffice.Cmdlets.Excel;

/// <summary>Adds an image anchored to a worksheet cell.</summary>
/// <summary>Adds an image anchored to a worksheet cell or range.</summary>
/// <example>
/// <summary>Insert an image from disk at B2.</summary>
/// <summary>Insert a scaled image from disk at B2.</summary>
/// <prefix>PS&gt; </prefix>
/// <code>ExcelSheet 'Data' { Add-OfficeExcelImage -Address 'B2' -Path .\logo.png -WidthPixels 120 -HeightPixels 40 }</code>
/// <para>Anchors the image to cell B2.</para>
/// <code>ExcelSheet 'Data' { Add-OfficeExcelImage -Address 'B2' -Path .\logo.png -ScalePercent 20 -Name Logo -AltText 'Company logo' }</code>
/// <para>Anchors the image to cell B2 and sizes it to 20 percent of the original image dimensions.</para>
/// </example>
/// <example>
/// <summary>Insert an image from a URL.</summary>
/// <summary>Pin an image to a worksheet range.</summary>
/// <prefix>PS&gt; </prefix>
/// <code>ExcelSheet 'Data' { Add-OfficeExcelImage -Row 1 -Column 1 -Url 'https://example.org/logo.png' }</code>
/// <para>Downloads and anchors the image to cell A1.</para>
/// <code>ExcelSheet 'Data' { Add-OfficeExcelImage -Range 'A1:C15' -Path .\logo.png -Name HeaderLogo -Placement MoveAndSize }</code>
/// <para>Uses Excel's two-cell anchor so the picture moves and resizes with the cells in A1:C15.</para>
/// </example>
/// <example>
/// <summary>Insert and rotate an image from a URL.</summary>
/// <prefix>PS&gt; </prefix>
/// <code>ExcelSheet 'Data' { Add-OfficeExcelImage -Row 1 -Column 1 -Url 'https://example.org/logo.png' -WidthPixels 96 -HeightPixels 32 -RotationDegrees 12 }</code>
/// <para>Downloads, sizes, rotates, and anchors the image to cell A1.</para>
/// </example>
[Cmdlet(VerbsCommon.Add, "OfficeExcelImage", DefaultParameterSetName = ParameterSetContextPath)]
[Alias("ExcelImage")]
Expand Down Expand Up @@ -63,16 +69,27 @@

/// <summary>A1-style cell address (e.g., A1, C5).</summary>
[Parameter]
[Alias("Cell")]
public string? Address { get; set; }

/// <summary>A1-style range (for example, A1:C15) for a two-cell anchor that can move and resize with cells.</summary>
[Parameter]
public string? Range { get; set; }

/// <summary>Image width in pixels.</summary>
[Parameter]
[Alias("Width")]
public int WidthPixels { get; set; } = 96;

/// <summary>Image height in pixels.</summary>
[Parameter]
[Alias("Height")]
public int HeightPixels { get; set; } = 32;

/// <summary>Percentage of the original image size. Cannot be combined with WidthPixels or HeightPixels.</summary>
[Parameter]
public double? ScalePercent { get; set; }

/// <summary>Horizontal offset in pixels from the cell origin.</summary>
[Parameter]
public int OffsetXPixels { get; set; }
Expand All @@ -81,35 +98,67 @@
[Parameter]
public int OffsetYPixels { get; set; }

/// <summary>Horizontal offset in pixels for the range end marker when using Range.</summary>
[Parameter]
public int EndOffsetXPixels { get; set; }

/// <summary>Vertical offset in pixels for the range end marker when using Range.</summary>
[Parameter]
public int EndOffsetYPixels { get; set; }

/// <summary>Optional drawing name used by Excel's selection pane.</summary>
[Parameter]
public string? Name { get; set; }

/// <summary>Optional alternative text description for accessibility.</summary>
[Parameter]
public string? AltText { get; set; }

/// <summary>Optional alternative text title.</summary>
[Parameter]
public string? Title { get; set; }

/// <summary>Marks the image as decorative by clearing alternative text metadata.</summary>
[Parameter]
public SwitchParameter Decorative { get; set; }

/// <summary>Do not lock the image aspect ratio in Excel.</summary>
[Parameter]
public SwitchParameter NoLockAspectRatio { get; set; }

/// <summary>How a range-anchored image behaves when cells move or resize.</summary>
[Parameter]
public ExcelImagePlacement Placement { get; set; } = ExcelImagePlacement.MoveAndSize;

Check failure on line 131 in Sources/PSWriteOffice/Cmdlets/Excel/AddOfficeExcelImageCommand.cs

View workflow job for this annotation

GitHub Actions / Ubuntu

The type or namespace name 'ExcelImagePlacement' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 131 in Sources/PSWriteOffice/Cmdlets/Excel/AddOfficeExcelImageCommand.cs

View workflow job for this annotation

GitHub Actions / Ubuntu

The type or namespace name 'ExcelImagePlacement' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 131 in Sources/PSWriteOffice/Cmdlets/Excel/AddOfficeExcelImageCommand.cs

View workflow job for this annotation

GitHub Actions / Ubuntu

The type or namespace name 'ExcelImagePlacement' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 131 in Sources/PSWriteOffice/Cmdlets/Excel/AddOfficeExcelImageCommand.cs

View workflow job for this annotation

GitHub Actions / Ubuntu

The type or namespace name 'ExcelImagePlacement' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 131 in Sources/PSWriteOffice/Cmdlets/Excel/AddOfficeExcelImageCommand.cs

View workflow job for this annotation

GitHub Actions / Windows

The type or namespace name 'ExcelImagePlacement' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 131 in Sources/PSWriteOffice/Cmdlets/Excel/AddOfficeExcelImageCommand.cs

View workflow job for this annotation

GitHub Actions / Windows

The type or namespace name 'ExcelImagePlacement' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 131 in Sources/PSWriteOffice/Cmdlets/Excel/AddOfficeExcelImageCommand.cs

View workflow job for this annotation

GitHub Actions / Windows

The type or namespace name 'ExcelImagePlacement' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 131 in Sources/PSWriteOffice/Cmdlets/Excel/AddOfficeExcelImageCommand.cs

View workflow job for this annotation

GitHub Actions / Windows

The type or namespace name 'ExcelImagePlacement' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 131 in Sources/PSWriteOffice/Cmdlets/Excel/AddOfficeExcelImageCommand.cs

View workflow job for this annotation

GitHub Actions / macOS

The type or namespace name 'ExcelImagePlacement' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 131 in Sources/PSWriteOffice/Cmdlets/Excel/AddOfficeExcelImageCommand.cs

View workflow job for this annotation

GitHub Actions / macOS

The type or namespace name 'ExcelImagePlacement' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 131 in Sources/PSWriteOffice/Cmdlets/Excel/AddOfficeExcelImageCommand.cs

View workflow job for this annotation

GitHub Actions / macOS

The type or namespace name 'ExcelImagePlacement' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 131 in Sources/PSWriteOffice/Cmdlets/Excel/AddOfficeExcelImageCommand.cs

View workflow job for this annotation

GitHub Actions / macOS

The type or namespace name 'ExcelImagePlacement' could not be found (are you missing a using directive or an assembly reference?)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Update OfficeIMO.Excel package for placement APIs

In a normal package-based build (UseOfficeIMOProjectReferences is false unless a full sibling OfficeIMO checkout exists), PSWriteOffice.csproj still pins OfficeIMO.Excel 0.6.47, but this change now references ExcelImagePlacement and the new image placement overloads that are not available from that package. A clean CI/release checkout without the local OfficeIMO PR branch will fail to compile before tests run; please bump the package dependency or otherwise gate these APIs in the same change.

Useful? React with 👍 / 👎.


/// <summary>Clockwise image rotation in degrees.</summary>
[Parameter]
public double RotationDegrees { get; set; }

/// <summary>Emit the worksheet after inserting the image.</summary>
[Parameter]
public SwitchParameter PassThru { get; set; }

/// <inheritdoc />
protected override void ProcessRecord()
{
if (WidthPixels <= 0 || HeightPixels <= 0)
{
throw new PSArgumentException("WidthPixels and HeightPixels must be greater than zero.");
}
ValidateImageOptions();

var sheet = ResolveSheet();
var (row, column) = ExcelHostExtensions.ResolveCellAddress(Row, Column, Address);

if (ParameterSetName == ParameterSetContextPath || ParameterSetName == ParameterSetDocumentPath)
{
var resolved = SessionState.Path.GetUnresolvedProviderPathFromPSPath(Path);
AddLocalImage(sheet, row, column, resolved);
AddLocalImage(sheet, resolved);
}
else
{
if (TryGetLocalFilePath(Url, out var localPath))
{
AddLocalImage(sheet, row, column, localPath);
AddLocalImage(sheet, localPath);
}
else
{
sheet.AddImageFromUrlAt(row, column, Url, WidthPixels, HeightPixels, OffsetXPixels, OffsetYPixels);
AddRemoteImage(sheet, Url);
}
}

Expand All @@ -135,15 +184,102 @@
return context.RequireSheet();
}

private void AddLocalImage(ExcelSheet sheet, int row, int column, string path)
private void AddLocalImage(ExcelSheet sheet, string path)
{
if (!File.Exists(path))
{
throw new FileNotFoundException($"Image file '{path}' was not found.", path);
}

var bytes = File.ReadAllBytes(path);
sheet.AddImageAt(row, column, bytes, GetContentType(path), WidthPixels, HeightPixels, OffsetXPixels, OffsetYPixels);
ExcelImage image;
if (!string.IsNullOrWhiteSpace(Range))
{
image = sheet.AddImageFromFileToRange(Range!, path, OffsetXPixels, OffsetYPixels, EndOffsetXPixels, EndOffsetYPixels,
Name, Decorative.IsPresent ? null : AltText, Title, !NoLockAspectRatio.IsPresent, Placement, RotationDegrees);
}
else
{
var (row, column) = ExcelHostExtensions.ResolveCellAddress(Row, Column, Address);
var (width, height) = ResolveCellImageSize();
image = sheet.AddImageFromFile(row, column, path, width, height, ScalePercent, OffsetXPixels, OffsetYPixels,
Name, Decorative.IsPresent ? null : AltText, Title, !NoLockAspectRatio.IsPresent, RotationDegrees);
}

if (Decorative.IsPresent)
{
image.Decorative();
}
}

private void AddRemoteImage(ExcelSheet sheet, string url)
{
ExcelImage? image;
if (!string.IsNullOrWhiteSpace(Range))
{
image = sheet.AddImageFromUrlToRange(Range!, url, OffsetXPixels, OffsetYPixels, EndOffsetXPixels, EndOffsetYPixels,
Name, Decorative.IsPresent ? null : AltText, Title, !NoLockAspectRatio.IsPresent, Placement, RotationDegrees);
}
else
{
var (row, column) = ExcelHostExtensions.ResolveCellAddress(Row, Column, Address);
var (width, height) = ResolveCellImageSize();
image = sheet.AddImageFromUrl(row, column, url, width, height, ScalePercent, OffsetXPixels, OffsetYPixels,
Name, Decorative.IsPresent ? null : AltText, Title, !NoLockAspectRatio.IsPresent, RotationDegrees);
}

if (Decorative.IsPresent)
{
image?.Decorative();
}
}

private void ValidateImageOptions()
{
bool widthBound = MyInvocation.BoundParameters.ContainsKey(nameof(WidthPixels));
bool heightBound = MyInvocation.BoundParameters.ContainsKey(nameof(HeightPixels));
bool rangeBound = !string.IsNullOrWhiteSpace(Range);

if (WidthPixels <= 0 || HeightPixels <= 0)
{
throw new PSArgumentException("WidthPixels and HeightPixels must be greater than zero.");
}

if (ScalePercent.HasValue && (ScalePercent.Value <= 0 || double.IsNaN(ScalePercent.Value) || double.IsInfinity(ScalePercent.Value)))
{
throw new PSArgumentException("ScalePercent must be a positive finite number.");
}

if (ScalePercent.HasValue && (widthBound || heightBound))
{
throw new PSArgumentException("ScalePercent cannot be combined with WidthPixels or HeightPixels.");
}

if (rangeBound && (Row.HasValue || Column.HasValue || !string.IsNullOrWhiteSpace(Address)))
{
throw new PSArgumentException("Use either Range or Row/Column/Address, not both.");
}

if (rangeBound && (ScalePercent.HasValue || widthBound || heightBound))
{
throw new PSArgumentException("Range determines the image size. Do not combine Range with ScalePercent, WidthPixels, or HeightPixels.");
}

if (Decorative.IsPresent && (!string.IsNullOrWhiteSpace(AltText) || !string.IsNullOrWhiteSpace(Title)))
{
throw new PSArgumentException("Decorative images cannot also define AltText or Title.");
}
}

private (int? Width, int? Height) ResolveCellImageSize()
{
if (ScalePercent.HasValue)
{
return (null, null);
}

bool widthBound = MyInvocation.BoundParameters.ContainsKey(nameof(WidthPixels));
bool heightBound = MyInvocation.BoundParameters.ContainsKey(nameof(HeightPixels));
return (widthBound ? WidthPixels : 96, heightBound ? HeightPixels : 32);
}

private static bool TryGetLocalFilePath(string url, out string path)
Expand All @@ -157,17 +293,4 @@
path = uri.LocalPath;
return !string.IsNullOrWhiteSpace(path);
}

private static string GetContentType(string path)
{
var ext = System.IO.Path.GetExtension(path)?.ToLowerInvariant();
return ext switch
{
".jpg" or ".jpeg" => "image/jpeg",
".gif" => "image/gif",
".bmp" => "image/bmp",
".tif" or ".tiff" => "image/tiff",
_ => "image/png"
};
}
}
Loading
Loading