Skip to content

Support Marpit image sizing directives (w:, h:, percentage)#140

Open
Copilot wants to merge 3 commits intomainfrom
copilot/support-marpit-image-sizing
Open

Support Marpit image sizing directives (w:, h:, percentage)#140
Copilot wants to merge 3 commits intomainfrom
copilot/support-marpit-image-sizing

Conversation

Copy link
Contributor

Copilot AI commented Mar 19, 2026

Marp decks commonly use ![w:200px](img.png), ![h:150px](img.png), and ![50%](img.png) sizing syntax. Previously these tokens were passed through as raw alt text and ignored by the renderer.

Changes

  • ImageElement model — adds ExplicitWidth, ExplicitHeight (layout units), and SizePercent (0–100) properties
  • MarpitImageSizingParser (new) — strips w:Npx, h:Npx, and N% tokens from alt text; converts CSS px → layout units at 0.75 (1 CSS px = 0.75 pt). Remaining text becomes the accessible description
  • MarpMarkdownParser — uses the new parser when building ImageElement; skips sizing parsing for bg-prefixed alt texts to avoid accidentally promoting ![bg 50%] images to slide backgrounds
  • LayoutEngine — new CreateImageFrame() uses explicit sizing for frame height allocation so subsequent elements don't overlap
  • CalculateImagePlacement — new explicit-sizing path: computes exact dimensions (aspect-ratio-preserving when only one axis given), centres within allocated frame; full-bleed / fit-to-frame logic unchanged
![w:400px](photo.png)          → 300 layout units wide, height from aspect ratio
![h:200px](photo.png)          → 150 layout units tall, width from aspect ratio
![w:400px h:200px](photo.png)  → 300 × 150 layout units (overrides aspect ratio)
![60%](photo.png)              → 60% of slide width (576 lu), height from aspect ratio
![w:300px My caption](photo.png) → 225 lu wide; AltText = "My caption"

Tests

  • Parser tests updated to assert sizing properties on ImageElement and token removal from alt text
  • Renderer tests upgraded to assert actual EMU geometry rather than just "renders without error"

⌨️ Start Copilot coding agent tasks without leaving your editor — available in VS Code, Visual Studio, JetBrains IDEs and Eclipse.

Co-authored-by: jongalloway <68539+jongalloway@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement Marpit image sizing directives and percentage-based sizing Support Marpit image sizing directives (w:, h:, percentage) Mar 19, 2026
Copilot AI requested a review from jongalloway March 19, 2026 20:00
@jongalloway jongalloway marked this pull request as ready for review March 23, 2026 07:19
@jongalloway jongalloway requested a review from Copilot March 23, 2026 07:19
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds end-to-end support for Marpit image sizing directives (w:/h: px and percent) by parsing sizing tokens into the slide model and teaching layout + renderer to honor them, with tests updated to assert geometry.

Changes:

  • Extend ImageElement with explicit width/height and percent sizing fields.
  • Parse Marpit sizing directives out of image alt text (while preserving remaining descriptive alt text) and propagate sizing into rendering.
  • Update layout heuristics and renderer placement logic; upgrade tests to validate actual shape extents.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/MarpToPptx.Core/Models/SlideDeck.cs Extends ImageElement model to carry explicit sizing and percent sizing information.
src/MarpToPptx.Core/Parsing/MarpitImageSizingParser.cs New parser that extracts/strips Marpit sizing tokens from alt text and converts to layout units.
src/MarpToPptx.Core/Parsing/MarpMarkdownParser.cs Integrates sizing parsing into image extraction while avoiding bg … alt-text edge cases.
src/MarpToPptx.Core/Layout/LayoutEngine.cs Allocates image frame height based on explicit sizing so following elements can be positioned appropriately.
src/MarpToPptx.Pptx/Rendering/OpenXmlPptxRenderer.cs Passes sizing through to image rendering and adds explicit-sizing logic to image placement calculation.
tests/MarpToPptx.Tests/ParserTests.cs Updates parser tests to assert sizing fields and token stripping behavior.
tests/MarpToPptx.Tests/PptxRendererTests.cs Updates renderer tests to assert actual EMU extents for sized images.

Comment on lines +3004 to +3006
// Centre the image within the allocated frame.
var cx = frame.X + Math.Max(0, (frame.Width - finalWidth) * xAlign);
var cy = frame.Y + Math.Max(0, (frame.Height - finalHeight) * yAlign);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

In the explicit/percent sizing branch, Math.Max(0, (frame.Width - finalWidth) * xAlign) prevents negative offsets when the requested size is larger than the allocated frame. That makes oversized explicit images always top/left anchored instead of honoring xAlign/yAlign (and contradicts the comment about centering). Consider removing the clamp so alignment works consistently even when finalWidth/Height exceed the frame.

Suggested change
// Centre the image within the allocated frame.
var cx = frame.X + Math.Max(0, (frame.Width - finalWidth) * xAlign);
var cy = frame.Y + Math.Max(0, (frame.Height - finalHeight) * yAlign);
// Centre the image within the allocated frame, honoring alignment even when oversized.
var cx = frame.X + (frame.Width - finalWidth) * xAlign;
var cy = frame.Y + (frame.Height - finalHeight) * yAlign;

Copilot uses AI. Check for mistakes.
{
if (!ImageMetadataReader.TryReadSize(imagePath, out var pw, out var ph) || pw <= 0 || ph <= 0)
{
// No size metadata: fall back to the frame dimensions.
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

When a sizing directive is present but ImageMetadataReader.TryReadSize fails, the code falls back to the frame size and ignores explicitWidth/explicitHeight entirely. For explicit width+height (and arguably for single-axis sizing), it would be better to still honor the requested dimensions even without metadata, instead of silently reverting to fit-to-frame behavior.

Suggested change
// No size metadata: fall back to the frame dimensions.
// No size metadata. If both dimensions are explicitly specified, honor them even
// without intrinsic image size; otherwise, fall back to frame sizing.
if (explicitWidth.HasValue && explicitHeight.HasValue)
{
var finalWidth = explicitWidth.Value;
var finalHeight = explicitHeight.Value;
var cxExplicit = frame.X + Math.max(0, (frame.Width - finalWidth) * xAlign);
var cyExplicit = frame.Y + Math.max(0, (frame.Height - finalHeight) * yAlign);
return (cxExplicit, cyExplicit, finalWidth, finalHeight);
}
// For single-axis or percentage sizing we cannot infer aspect ratio without metadata,
// so fall back to the frame dimensions.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +98
// Process percentage tokens (only when no explicit px dimensions are present).
if (explicitWidth is null && explicitHeight is null)
{
cleaned = PercentPattern().Replace(cleaned, match =>
{
if (double.TryParse(match.Groups[1].Value,
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out var pct))
{
sizePercent = pct;
}

return string.Empty;
});
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

PercentPattern tokens are only removed when no w:/h: tokens are present. This means input like "w:200px 50% My photo" will keep "50%" in the cleaned alt text even though it’s a sizing directive. Consider always stripping percentage tokens from the alt text, while still applying SizePercent only when no explicit dimensions were provided.

Suggested change
// Process percentage tokens (only when no explicit px dimensions are present).
if (explicitWidth is null && explicitHeight is null)
{
cleaned = PercentPattern().Replace(cleaned, match =>
{
if (double.TryParse(match.Groups[1].Value,
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out var pct))
{
sizePercent = pct;
}
return string.Empty;
});
}
// Process percentage tokens:
// - Always strip them from the alt text.
// - Only apply SizePercent when no explicit px dimensions are present.
cleaned = PercentPattern().Replace(cleaned, match =>
{
if (explicitWidth is null && explicitHeight is null &&
double.TryParse(match.Groups[1].Value,
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out var pct))
{
sizePercent = pct;
}
return string.Empty;
});

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +97
cleaned = PercentPattern().Replace(cleaned, match =>
{
if (double.TryParse(match.Groups[1].Value,
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out var pct))
{
sizePercent = pct;
}

return string.Empty;
});
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

SizePercent is documented as 0–100, but the parser currently accepts any numeric percentage (e.g., 250%) without clamping/validation. Either clamp to [0,100] (or reject out-of-range values) to match the model contract, or update the documentation to reflect that values outside the range are allowed.

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +81
// When an explicit height is given, honour it so subsequent elements are placed below.
// When only a width is given, estimate height from a 4:3 aspect ratio as a conservative
// placeholder (the renderer will compute the exact aspect-ratio-correct height).
// Percentage sizing uses the same estimation.
const double defaultHeight = 220;
double height;

if (image.ExplicitHeight.HasValue)
{
height = image.ExplicitHeight.Value;
}
else if (image.ExplicitWidth.HasValue)
{
// Estimate: width / (4/3) → preserve an approximate 4:3 ratio.
height = image.ExplicitWidth.Value * 0.75;
}
else if (image.SizePercent.HasValue)
{
// Estimate using the frame width (100%) scaled to the percentage.
var scaledWidth = width * (image.SizePercent.Value / 100.0);
height = scaledWidth * 0.75;
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

The 4:3 height estimate for width-only / percentage sizing can under-allocate space for portrait or very tall images, which can still cause later elements to overlap once the renderer computes the true aspect-ratio height. Consider using a more conservative heuristic, or (if feasible) deriving the estimated height from image metadata during layout, and adjust the comment so it doesn’t claim this is always conservative.

Copilot uses AI. Check for mistakes.
break;
case AudioElement audio:
AddAudio(context, frame, audio.Source, audio.AltText);
case AudioElement audio: AddAudio(context, frame, audio.Source, audio.AltText);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

This switch case appears to have accidental formatting (the AddAudio(...) call is on the same line as the case label with large spacing), which hurts readability and is inconsistent with the other cases. Please reformat to match the surrounding style.

Suggested change
case AudioElement audio: AddAudio(context, frame, audio.Source, audio.AltText);
case AudioElement audio:
AddAudio(context, frame, audio.Source, audio.AltText);

Copilot uses AI. Check for mistakes.
string? Caption = null,
double? ExplicitWidth = null,
double? ExplicitHeight = null,
double? SizePercent = null) : ISlideElement;
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

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

ImageElement is a public positional record; extending the primary constructor changes the generated ctor signature and is a binary-breaking change for any downstream assemblies compiled against the previous version (they’ll look for a 3-arg ctor that no longer exists). If binary compatibility matters, consider adding an explicit 3-parameter constructor that forwards to the new one, or switching these new values to init-only properties instead of primary-constructor parameters.

Suggested change
double? SizePercent = null) : ISlideElement;
double? SizePercent = null) : ISlideElement
{
// Backward-compatible constructor matching the original 3-parameter signature.
public ImageElement(string Source, string AltText, string? Caption = null)
: this(Source, AltText, Caption, null, null, null)
{
}
}

Copilot uses AI. Check for mistakes.
@jongalloway
Copy link
Owner

@copilot apply changes based on the comments in this thread

Copy link
Contributor Author

Copilot AI commented Mar 23, 2026

@copilot apply changes based on the comments in this thread

Applied all 7 review comments in commit ced2a78:

  • Centering clamp — removed Math.Max(0, ...) so xAlign/yAlign work correctly for oversized images too.
  • No-metadata fallback — when both w: and h: are specified but image metadata is unavailable, the explicit dimensions are now honored instead of falling back to the frame size.
  • Percentage token stripping% tokens are always stripped from alt text; SizePercent is only populated when no w:/h: px directives are present.
  • SizePercent clamping — the parsed percentage value is now clamped to [0, 100].
  • Height estimateCreateImageFrame now uses a 1:1 (square) estimate instead of 4:3, which is more conservative for common landscape images; comment updated to accurately describe the trade-off.
  • Binary compatibility — added a 3-parameter constructor to ImageElement forwarding to the full constructor.
  • Formatting — fixed the AudioElement switch case that had AddAudio(...) on the same line as the case label.

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.

Support Marpit image sizing directives and percentage-based sizing

3 participants