Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
179581e
Added migration for SVG width/height
markusobviuse Mar 24, 2026
354a751
#22114 worked on SVG width height implementation
markusobviuse Mar 24, 2026
d50d141
#22244 Code style fixes
enkelmedia Mar 25, 2026
8a0dd86
#22244 XmlReaderSettings and using
enkelmedia Mar 25, 2026
fe50b97
#22244 Cleanup
enkelmedia Mar 25, 2026
7c1671c
#22244 Correction if statement
enkelmedia Mar 25, 2026
deef6d6
#22244 Refactor log message
enkelmedia Mar 25, 2026
22135be
#22244 Correction if statment
enkelmedia Mar 25, 2026
2ce4b26
#22244 Cleanup
enkelmedia Mar 25, 2026
5528991
#22244 Cleanup
enkelmedia Mar 25, 2026
185659f
#22244 Code style adjustments
enkelmedia Mar 25, 2026
4c226a5
#22244 Adjust if statement
enkelmedia Mar 25, 2026
c35dc48
#22244 Adjust documentation comments
enkelmedia Mar 25, 2026
361f3c6
#22244 Fix log comment
markusobviuse Mar 25, 2026
50dfc18
#22244 Fallback to viewbox if width height attribute has other unit t…
markusobviuse Mar 25, 2026
5dbf0bd
#22244 Refactoring SVG parser, no support for decimals
markusobviuse Mar 25, 2026
fdf109c
#22244 Migration, consistent logging
markusobviuse Mar 25, 2026
ac3d587
#22244 Create vector umbracoWidth and umbracoHeight during clean install
markusobviuse Mar 25, 2026
221dd93
#22244 Remove SupportedImageType from ISvgDimensionsExtractor
markusobviuse Mar 25, 2026
880f9d2
#22244 pass culture and segment to SetValue
markusobviuse Mar 25, 2026
c34f620
Merge branch 'main' into v17/feature/22114-svg-width-height
AndyButland Mar 26, 2026
780373d
Add DtdProcessing.Prohibit security hardening to SvgDimensionExtractor.
AndyButland Mar 26, 2026
2027b08
Addressed some code styling and robustness of the migration and extra…
AndyButland Mar 26, 2026
e872f79
Add further unit tests.
AndyButland Mar 26, 2026
a39dbf7
Add logging to notification handler. Skip when properties don't exist…
AndyButland Mar 26, 2026
f174d1d
Add unit tests for media saving handler.
AndyButland Mar 26, 2026
70ce78e
Move the dimensions extractor implementation into infrastructure.
AndyButland Mar 26, 2026
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
18 changes: 18 additions & 0 deletions src/Umbraco.Core/Media/ISvgDimensionExtractor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Drawing;

namespace Umbraco.Cms.Core.Media;

/// <summary>
/// Extracts image dimensions from an SVG stream.
/// </summary>
public interface ISvgDimensionExtractor
{
/// <summary>
/// Gets the dimensions.
/// </summary>
/// <param name="stream">The stream.</param>
/// <returns>
/// The dimensions of the image if the stream was parsable; otherwise, <c>null</c>.
/// </returns>
public Size? GetDimensions(Stream stream);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Examine;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
Expand All @@ -13,7 +14,6 @@
using Umbraco.Cms.Core.DistributedLocking;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Handlers;
using Umbraco.Cms.Core.HealthChecks.NotificationMethods;
using Umbraco.Cms.Core.HostedServices;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Install;
Expand Down Expand Up @@ -49,6 +49,7 @@
using Umbraco.Cms.Infrastructure.Mail;
using Umbraco.Cms.Infrastructure.Mail.Interfaces;
using Umbraco.Cms.Infrastructure.Manifest;
using Umbraco.Cms.Infrastructure.Media;
using Umbraco.Cms.Infrastructure.Migrations;
using Umbraco.Cms.Infrastructure.Migrations.Install;
using Umbraco.Cms.Infrastructure.Persistence;
Expand All @@ -64,7 +65,6 @@
using Umbraco.Cms.Infrastructure.Serialization;
using Umbraco.Cms.Infrastructure.Services.Implement;
using Umbraco.Extensions;
using Microsoft.Extensions.DependencyInjection.Extensions;
using IScopeProvider = Umbraco.Cms.Infrastructure.Scoping.IScopeProvider;

namespace Umbraco.Cms.Infrastructure.DependencyInjection;
Expand Down Expand Up @@ -228,6 +228,7 @@

builder.Services.AddSingleton<UploadAutoFillProperties>();
builder.Services.AddSingleton<IImageDimensionExtractor, NoopImageDimensionExtractor>();
builder.Services.AddSingleton<ISvgDimensionExtractor, SvgDimensionExtractor>();

Check warning on line 231 in src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Large Method

AddCoreInitialServices increases from 124 to 125 lines of code, threshold = 70. Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.
builder.Services.AddSingleton<IImageUrlGenerator, NoopImageUrlGenerator>();

builder.Services.AddSingleton<ICronTabParser, NCronTabParser>();
Expand Down Expand Up @@ -403,6 +404,7 @@
.AddNotificationHandler<MediaMovedNotification, FileUploadContentDeletedNotificationHandler>()
.AddNotificationHandler<MemberDeletedNotification, FileUploadContentDeletedNotificationHandler>()
.AddNotificationHandler<MediaSavingNotification, FileUploadMediaSavingNotificationHandler>()
.AddNotificationHandler<MediaSavingNotification, SvgFileUploadMediaSavingNotificationHandler>()

Check warning on line 407 in src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Large Method

AddCoreNotifications increases from 105 to 106 lines of code, threshold = 70. Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.
.AddNotificationHandler<ContentCopiedNotification, ImageCropperPropertyEditor>()
.AddNotificationHandler<ContentDeletedNotification, ImageCropperPropertyEditor>()
.AddNotificationHandler<MediaDeletedNotification, ImageCropperPropertyEditor>()
Expand Down
164 changes: 164 additions & 0 deletions src/Umbraco.Infrastructure/Media/SvgDimensionExtractor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
using System.Drawing;
using System.Globalization;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Media;

namespace Umbraco.Cms.Infrastructure.Media;

/// <inheritdoc />
public class SvgDimensionExtractor : ISvgDimensionExtractor
{
private readonly ILogger<SvgDimensionExtractor> _logger;

/// <summary>
/// Initializes a new instance of the <see cref="SvgDimensionExtractor"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
public SvgDimensionExtractor(ILogger<SvgDimensionExtractor> logger)
=> _logger = logger;

/// <inheritdoc />
public Size? GetDimensions(Stream stream)
{
if (stream.CanRead is false)
{
return null;
}

long? originalPosition = null;

if (stream.CanSeek)
{
originalPosition = stream.Position;
stream.Position = 0;
}

try
{
return ReadDimensions(stream);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to extract dimensions from SVG stream.");
return null;
}
finally
{
if (originalPosition.HasValue)
{
stream.Position = originalPosition.Value;
}
}
}

private static Size? ReadDimensions(Stream stream)
{
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
};
using var reader = XmlReader.Create(stream, settings);
var document = XDocument.Load(reader);

XElement? root = document.Root;

if (root is null)
{
return null;
}

var widthAttributeValue = root.Attribute("width")?.Value;
var heightAttributeValue = root.Attribute("height")?.Value;

Size? size = null;

if (widthAttributeValue is not null && heightAttributeValue is not null)
{
size = ParseWidthHeightAttributes(widthAttributeValue, heightAttributeValue);
}

// Fall back to viewbox.
size ??= ParseViewBox(root);

return size;
}

private static Size? ParseViewBox(XElement root)
{
var viewBox = root.Attribute("viewBox")?.Value;

if (string.IsNullOrWhiteSpace(viewBox))
{
return null;
}

var parts = viewBox.Split([' ', ',', '\t', '\r', '\n'], StringSplitOptions.RemoveEmptyEntries);

if (parts.Length != 4)
{
return null;
}

if (double.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out var width) is false)
{
return null;
}

if (double.TryParse(parts[3], NumberStyles.Float, CultureInfo.InvariantCulture, out var height) is false)
{
return null;
}

if (width < 0 || height < 0)
{
return null;
}

return new Size(
(int)Math.Round(width),
(int)Math.Round(height));
}

private static Size? ParseWidthHeightAttributes(string widthAttributeValue, string heightAttributeValue)
{
if (TryExtractNumericFromValue(widthAttributeValue, out var widthValue)
&& TryExtractNumericFromValue(heightAttributeValue, out var heightValue))
{
return new Size(widthValue, heightValue);
}

return null;
}

/// <summary>
/// Extract a "pixel" value from the width / height attributes.
/// </summary>
private static bool TryExtractNumericFromValue(string attributeValue, out int value)
{
if (int.TryParse(attributeValue, out int onlyNumbersValue) && onlyNumbersValue > 0)
{
value = onlyNumbersValue;
return true;
}

value = 0;

var input = attributeValue.Trim();

if (input.EndsWith("px", StringComparison.OrdinalIgnoreCase))
{
input = input[..^2].Trim();
}

if (int.TryParse(input, out var numericValue) && numericValue > 0)
{
value = numericValue;
return true;
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1947,6 +1947,44 @@ private void CreatePropertyTypeData()
Description = null,
Variations = (byte)ContentVariation.Nothing,
});
_database.Insert(
Constants.DatabaseSchema.Tables.PropertyType,
"id",
false,
new PropertyTypeDto
{
Id = 53,
UniqueId = new Guid("5BC7E468-C53E-41A6-A522-2723F3B94514"),
DataTypeId = Constants.DataTypes.LabelPixels,
ContentTypeId = 1037,
PropertyTypeGroupId = 55,
Alias = Constants.Conventions.Media.Width,
Name = "Width",
SortOrder = 0,
Mandatory = false,
ValidationRegExp = null,
Description = null,
Variations = (byte)ContentVariation.Nothing,
});
_database.Insert(
Constants.DatabaseSchema.Tables.PropertyType,
"id",
false,
new PropertyTypeDto
{
Id = 54,
UniqueId = new Guid("9E4C2B59-6BC6-4648-BB71-B0F45DDBC274"),
DataTypeId = Constants.DataTypes.LabelPixels,
ContentTypeId = 1037,
PropertyTypeGroupId = 55,
Alias = Constants.Conventions.Media.Height,
Name = "Height",
SortOrder = 0,
Mandatory = false,
ValidationRegExp = null,
Description = null,
Variations = (byte)ContentVariation.Nothing,
});
}

// Membership property types.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@

// To 17.4.0
To<V_17_4_0.AddContentVersionDateIndex>("{D4E5F6A7-B8C9-4D0E-A1F2-3B4C5D6E7F80}");
To<V_17_4_0.AddDimensionsToSvg>("{72970B86-59D8-403C-B322-FFF43F9DB199}");

Check warning on line 172 in src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Large Method

UmbracoPlan increases from 80 to 81 lines of code, threshold = 70. Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.

// To 18.0.0
// TODO (V18): Enable on 18 branch
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Core.Strings;

namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_17_4_0;

/// <summary>
/// Migration that adds umbracoWidth and umbracoHeight to Vector Graphics Media Type.
/// </summary>
public class AddDimensionsToSvg : AsyncMigrationBase
Comment thread
enkelmedia marked this conversation as resolved.
{
private readonly ILogger<AddDimensionsToSvg> _logger;
private readonly IMediaTypeService _mediaTypeService;
private readonly IDataTypeService _dataTypeService;
private readonly IShortStringHelper _shortStringHelper;

private readonly Guid _labelPixelsDataTypeKey = new(Constants.DataTypes.Guids.LabelPixels);

/// <summary>
/// Initializes a new instance of the <see cref="AddDimensionsToSvg"/> class.
/// </summary>
/// <param name="context">The <see cref="IMigrationContext"/> for the migration operation.</param>
/// <param name="logger">The <see cref="ILogger{AddDimensionsToSvg}"/> instance for logging.</param>
/// <param name="mediaTypeService">The <see cref="IMediaTypeService"/> for managing media types.</param>
/// <param name="dataTypeService">The <see cref="IDataTypeService"/> for managing data types.</param>
/// <param name="shortStringHelper">The <see cref="IShortStringHelper"/> for working with strings.</param>
public AddDimensionsToSvg(
IMigrationContext context,
ILogger<AddDimensionsToSvg> logger,
IMediaTypeService mediaTypeService,
IDataTypeService dataTypeService,
IShortStringHelper shortStringHelper)
: base(context)
{
_logger = logger;
_mediaTypeService = mediaTypeService;
_dataTypeService = dataTypeService;
_shortStringHelper = shortStringHelper;
}

/// <inheritdoc/>
protected override async Task MigrateAsync()
{
IMediaType? vectorGraphicsMediaType = _mediaTypeService.Get(Constants.Conventions.MediaTypes.VectorGraphicsAlias);

if (vectorGraphicsMediaType is null)
{
_logger.LogInformation("No standard Vector Graphics Media Type configured, ignore adding width/height.");
return;
}

IDataType? labelPixelDataType = await _dataTypeService.GetAsync(_labelPixelsDataTypeKey);

if (labelPixelDataType is null)
{
_logger.LogInformation("No Label Pixel Data Type configured, ignore adding width/height.");
return;
}

PropertyGroup? propertyGroup = vectorGraphicsMediaType.PropertyGroups.FirstOrDefault();
if (propertyGroup is null)
{
_logger.LogWarning("Vector Graphics Media Type has no property groups, skipping adding width/height.");
return;
}

int highestSort = vectorGraphicsMediaType.PropertyTypes.Any()
? vectorGraphicsMediaType.PropertyTypes.Max(x => x.SortOrder)
: 0;

// Add new properties (AddPropertyType handles duplicates, so the migration is idempotent).
vectorGraphicsMediaType.AddPropertyType(new PropertyType(_shortStringHelper, labelPixelDataType, Constants.Conventions.Media.Width)
{
Name = "Width",
SortOrder = highestSort + 1,
PropertyGroupId = new Lazy<int>(()=> propertyGroup.Id),
});
vectorGraphicsMediaType.AddPropertyType(new PropertyType(_shortStringHelper, labelPixelDataType, Constants.Conventions.Media.Height)
{
Name = "Height",
SortOrder = highestSort + 2,
PropertyGroupId = new Lazy<int>(()=> propertyGroup.Id),
});

Attempt<ContentTypeOperationStatus> attempt = await _mediaTypeService.UpdateAsync(vectorGraphicsMediaType, Constants.Security.SuperUserKey);
if (attempt.Success is false)
{
if (attempt.Exception is not null)
{
_logger.LogError(attempt.Exception, "Failed to update media type '{Alias}' during migration.", vectorGraphicsMediaType.Alias);
}
else
{
_logger.LogWarning("Failed to update media type '{Alias}' during migration. Status: {ResultStatus}", vectorGraphicsMediaType.Alias, attempt.Result);
}
}
}
}
Loading
Loading