Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
414 changes: 414 additions & 0 deletions plan/remaining-standards-test-roadmap.md

Large diffs are not rendered by default.

49 changes: 46 additions & 3 deletions scripts/capture_w3c_chrome_overrides.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,12 @@ const mimeTypes = new Map([
['.jpg', 'image/jpeg'],
['.jpeg', 'image/jpeg'],
['.js', 'application/javascript; charset=utf-8'],
['.otf', 'font/otf'],
['.png', 'image/png'],
['.svg', 'image/svg+xml; charset=utf-8'],
['.txt', 'text/plain; charset=utf-8'],
['.ttf', 'font/ttf'],
['.woff', 'font/woff'],
]);

async function readAnimationSeekOverrides()
Expand Down Expand Up @@ -103,6 +106,29 @@ function getContentType(filePath)
return mimeTypes.get(path.extname(filePath).toLowerCase()) ?? 'application/octet-stream';
}

function tryGetW3CFontResourceFallback(normalizedPath)
{
const svgWoffsDir = path.join(
repoRoot,
'externals',
'W3C_SVG_11_TestSuite',
'W3C_SVG_11_TestSuite',
'svg',
'woffs');
if (!normalizedPath.startsWith(`${svgWoffsDir}${path.sep}`))
{
return null;
}

return path.join(
repoRoot,
'externals',
'W3C_SVG_11_TestSuite',
'W3C_SVG_11_TestSuite',
'resources',
path.basename(normalizedPath));
}

function createStaticServer(rootPath)
{
return http.createServer(async (req, res) =>
Expand All @@ -122,10 +148,27 @@ function createStaticServer(rootPath)
return;
}

const stats = await fs.stat(normalizedPath);
let stats;
let resolvedPath = normalizedPath;
try
{
stats = await fs.stat(resolvedPath);
}
catch
{
const fallbackPath = tryGetW3CFontResourceFallback(resolvedPath);
if (!fallbackPath)
{
throw new Error('Not Found');
}

resolvedPath = fallbackPath;
stats = await fs.stat(resolvedPath);
}

const filePath = stats.isDirectory()
? path.join(normalizedPath, 'index.html')
: normalizedPath;
? path.join(resolvedPath, 'index.html')
: resolvedPath;

const body = await fs.readFile(filePath);
res.writeHead(200, { 'Content-Type': getContentType(filePath) });
Expand Down
10 changes: 6 additions & 4 deletions src/Svg.Animation/Animation/SvgAnimationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1289,7 +1289,7 @@ private static List<AnimationBinding> DiscoverBindings(SvgDocument sourceDocumen
continue;
}

if (ShouldIgnoreBrowserUnsupportedAnimateColorBinding(animation, target, attributeName!))
if (ShouldIgnorePaintServerDefinitionAnimateColorBinding(animation, target, attributeName!))
{
continue;
}
Expand All @@ -1300,13 +1300,15 @@ private static List<AnimationBinding> DiscoverBindings(SvgDocument sourceDocumen
return bindings;
}

private static bool ShouldIgnoreBrowserUnsupportedAnimateColorBinding(
private static bool ShouldIgnorePaintServerDefinitionAnimateColorBinding(
SvgAnimationElement animation,
SvgElement target,
string attributeName)
{
// Current browser snapshots do not apply deprecated animateColor to inherited
// paint-server color state declared in defs, while regular animate still applies.
// Direct SVG 1.1 animateColor interpolation is supported. This guard is
// limited to inherited paint-server color state in defs, where current
// browser snapshots keep referenced gradients stable while regular
// numeric animation on the same subtree still applies.
if (animation is not SvgAnimateColor ||
!IsInheritedPaintServerColorAttribute(attributeName) ||
!IsInsideDefinitions(target))
Expand Down
110 changes: 110 additions & 0 deletions src/Svg.Custom/Compatibility/SvgCssCompatibilityProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1498,6 +1498,116 @@ private static string ExpandImportedStyles(IReadOnlyCollection<SvgCssStyleSource
return builder.ToString();
}

internal static IEnumerable<SvgCssStyleSource> EnumerateExpandedStyleSources(
IReadOnlyCollection<SvgCssStyleSource> sources,
SvgDocument svgDocument,
SvgDocumentLoadOptions? loadOptions)
{
if (sources.Count == 0)
{
yield break;
}

var mediaContext = ResolveMediaContext(svgDocument);
foreach (var source in sources)
{
foreach (var expanded in EnumerateExpandedStyleSource(
source.Content,
source.BaseUri,
source.BaseUri,
mediaContext,
loadOptions,
CreateImportChain(),
allowLeadingImports: true))
{
yield return expanded;
}
}
}

private static IEnumerable<SvgCssStyleSource> EnumerateExpandedStyleSource(
string cssText,
Uri? baseUri,
Uri? policyBaseUri,
CssMediaContext mediaContext,
SvgDocumentLoadOptions? loadOptions,
HashSet<string> importChain,
bool allowLeadingImports)
{
var index = 0;
var isInLeadingImportSection = allowLeadingImports;

while (TryReadNextTopLevelStatement(cssText, ref index, out var statement))
{
var atRuleKind = GetAtRuleKind(cssText, statement);
if (isInLeadingImportSection && atRuleKind == CssAtRuleKind.Import)
{
if (TryParseKnownImportRule(cssText, statement, out var href, out var mediaCondition) &&
ShouldApplyMediaForCurrentContext(mediaCondition, mediaContext))
{
var imported = TryLoadImportedStylesheet(href, baseUri, policyBaseUri, loadOptions, importChain);
if (imported is not null)
{
try
{
foreach (var expanded in EnumerateExpandedStyleSource(
imported.Content,
imported.BaseUri,
policyBaseUri,
mediaContext,
loadOptions,
importChain,
allowLeadingImports: true))
{
yield return expanded;
}
}
finally
{
importChain.Remove(imported.BaseUri!.AbsoluteUri);
}
}
}

continue;
}

if (atRuleKind == CssAtRuleKind.Media)
{
isInLeadingImportSection = false;
if (TryGetMediaRuleParts(cssText, statement, out var mediaCondition, out var nestedCssText) &&
ShouldApplyMediaForCurrentContext(mediaCondition, mediaContext))
{
foreach (var expanded in EnumerateExpandedStyleSource(
nestedCssText,
baseUri,
policyBaseUri,
mediaContext,
loadOptions,
importChain,
allowLeadingImports: false))
{
yield return expanded;
}
}

continue;
}

if (atRuleKind != CssAtRuleKind.Charset)
{
isInLeadingImportSection = false;
}

if (atRuleKind == CssAtRuleKind.Import || statement.Length <= 0)
{
continue;
}

yield return new SvgCssStyleSource(cssText.Substring(statement.Start, statement.Length), baseUri);
Comment thread
wieslawsoltes marked this conversation as resolved.
}
}

internal static HashSet<string> CreateImportChain()
{
// Import cycle detection should compare the fully resolved URI text exactly. Folding case
Expand Down
2 changes: 2 additions & 0 deletions src/Svg.Custom/Compatibility/SvgDocument.DynamicStyles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ internal void CopyCompatibilityStyleStateTo(SvgDocument target)

internal bool HasCompatibilityStyleSources => _compatibilityStyleSources is { Count: > 0 };

internal IReadOnlyList<SvgCssStyleSource>? CompatibilityStyleSources => _compatibilityStyleSources;

internal bool UpdateCompatibilityStyleAttribute(SvgElement element, string name, string? value)
{
if (!SvgStyleAttributeNames.Contains(name))
Expand Down
5 changes: 5 additions & 0 deletions src/Svg.Custom/Painting/SvgPaintServerFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
return SvgPaintServer.None;
else if (colorValue.Equals("currentColor", StringComparison.OrdinalIgnoreCase))
// Keep the parse-time document for consistency with url(...) paint servers.
return new SvgDeferredPaintServer(document, "currentColor");

Check warning on line 49 in src/Svg.Custom/Painting/SvgPaintServerFactory.cs

View workflow job for this annotation

GitHub Actions / Build (macos-latest)

'SvgDeferredPaintServer.SvgDeferredPaintServer(SvgDocument, string)' is obsolete: 'Will be removed.'

Check warning on line 49 in src/Svg.Custom/Painting/SvgPaintServerFactory.cs

View workflow job for this annotation

GitHub Actions / Build (ubuntu-latest)

'SvgDeferredPaintServer.SvgDeferredPaintServer(SvgDocument, string)' is obsolete: 'Will be removed.'

Check warning on line 49 in src/Svg.Custom/Painting/SvgPaintServerFactory.cs

View workflow job for this annotation

GitHub Actions / Build MAUI (macos-latest)

'SvgDeferredPaintServer.SvgDeferredPaintServer(SvgDocument, string)' is obsolete: 'Will be removed.'

Check warning on line 49 in src/Svg.Custom/Painting/SvgPaintServerFactory.cs

View workflow job for this annotation

GitHub Actions / Build MAUI (macos-latest)

'SvgDeferredPaintServer.SvgDeferredPaintServer(SvgDocument, string)' is obsolete: 'Will be removed.'

Check warning on line 49 in src/Svg.Custom/Painting/SvgPaintServerFactory.cs

View workflow job for this annotation

GitHub Actions / Build (windows-latest)

'SvgDeferredPaintServer.SvgDeferredPaintServer(SvgDocument, string)' is obsolete: 'Will be removed.'

Check warning on line 49 in src/Svg.Custom/Painting/SvgPaintServerFactory.cs

View workflow job for this annotation

GitHub Actions / Pack MAUI (macos-latest)

'SvgDeferredPaintServer.SvgDeferredPaintServer(SvgDocument, string)' is obsolete: 'Will be removed.'

Check warning on line 49 in src/Svg.Custom/Painting/SvgPaintServerFactory.cs

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

'SvgDeferredPaintServer.SvgDeferredPaintServer(SvgDocument, string)' is obsolete: 'Will be removed.'

Check warning on line 49 in src/Svg.Custom/Painting/SvgPaintServerFactory.cs

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

'SvgDeferredPaintServer.SvgDeferredPaintServer(SvgDocument, string)' is obsolete: 'Will be removed.'

Check warning on line 49 in src/Svg.Custom/Painting/SvgPaintServerFactory.cs

View workflow job for this annotation

GitHub Actions / Test (macos-latest)

'SvgDeferredPaintServer.SvgDeferredPaintServer(SvgDocument, string)' is obsolete: 'Will be removed.'

Check warning on line 49 in src/Svg.Custom/Painting/SvgPaintServerFactory.cs

View workflow job for this annotation

GitHub Actions / Test (macos-latest)

'SvgDeferredPaintServer.SvgDeferredPaintServer(SvgDocument, string)' is obsolete: 'Will be removed.'

Check warning on line 49 in src/Svg.Custom/Painting/SvgPaintServerFactory.cs

View workflow job for this annotation

GitHub Actions / Test (windows-latest)

'SvgDeferredPaintServer.SvgDeferredPaintServer(SvgDocument, string)' is obsolete: 'Will be removed.'

Check warning on line 49 in src/Svg.Custom/Painting/SvgPaintServerFactory.cs

View workflow job for this annotation

GitHub Actions / Test (windows-latest)

'SvgDeferredPaintServer.SvgDeferredPaintServer(SvgDocument, string)' is obsolete: 'Will be removed.'

Check warning on line 49 in src/Svg.Custom/Painting/SvgPaintServerFactory.cs

View workflow job for this annotation

GitHub Actions / Pack (ubuntu-latest)

'SvgDeferredPaintServer.SvgDeferredPaintServer(SvgDocument, string)' is obsolete: 'Will be removed.'

Check warning on line 49 in src/Svg.Custom/Painting/SvgPaintServerFactory.cs

View workflow job for this annotation

GitHub Actions / Pack (ubuntu-latest)

'SvgDeferredPaintServer.SvgDeferredPaintServer(SvgDocument, string)' is obsolete: 'Will be removed.'
else if (colorValue.Equals("context-fill", StringComparison.OrdinalIgnoreCase))
return new SvgContextPaintServer(SvgContextPaintKind.Fill);
else if (colorValue.Equals("context-stroke", StringComparison.OrdinalIgnoreCase))
Expand Down Expand Up @@ -171,6 +171,11 @@
return true;
}

if (SvgSystemColorResolver.TryGetColor(value, out color))
{
return true;
}

if (TryParseHexColorWithAlpha(value, out color) ||
TryParseCssFunctionalColor(value, out color))
{
Expand Down
142 changes: 142 additions & 0 deletions src/Svg.Custom/Painting/SvgSystemColorProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright (c) Wiesław Šoltés. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Threading;

namespace Svg
{
public interface ISvgSystemColorProvider
{
bool TryGetColor(string name, out Color color);
}

public sealed class SvgDictionarySystemColorProvider : ISvgSystemColorProvider
{
private readonly IReadOnlyDictionary<string, Color> _colors;

public SvgDictionarySystemColorProvider(IDictionary<string, Color> colors)
{
if (colors == null)
{
throw new ArgumentNullException(nameof(colors));
}

_colors = new Dictionary<string, Color>(colors, StringComparer.OrdinalIgnoreCase);
}

public bool TryGetColor(string name, out Color color)
{
if (name == null)
{
color = Color.Empty;
return false;
}

return _colors.TryGetValue(name.Trim(), out color);
}
}

public sealed class SvgFixedSystemColorProvider : ISvgSystemColorProvider
{
public static SvgFixedSystemColorProvider Instance { get; } = new SvgFixedSystemColorProvider();

private static readonly IReadOnlyDictionary<string, Color> s_colors = new Dictionary<string, Color>(StringComparer.OrdinalIgnoreCase)
{
["ActiveBorder"] = FromRgb(0xD4, 0xD0, 0xC8),
["ActiveCaption"] = FromRgb(0x0A, 0x24, 0x6A),
["AppWorkspace"] = FromRgb(0x80, 0x80, 0x80),
["Background"] = FromRgb(0x3A, 0x6E, 0xA5),
["ButtonFace"] = FromRgb(0xD4, 0xD0, 0xC8),
["ButtonHighlight"] = FromRgb(0xFF, 0xFF, 0xFF),
["ButtonShadow"] = FromRgb(0x80, 0x80, 0x80),
["ButtonText"] = FromRgb(0x00, 0x00, 0x00),
["CaptionText"] = FromRgb(0xFF, 0xFF, 0xFF),
["GrayText"] = FromRgb(0x80, 0x80, 0x80),
["Highlight"] = FromRgb(0x0A, 0x24, 0x6A),
["HighlightText"] = FromRgb(0xFF, 0xFF, 0xFF),
["InactiveBorder"] = FromRgb(0xD4, 0xD0, 0xC8),
["InactiveCaption"] = FromRgb(0x80, 0x80, 0x80),
["InactiveCaptionText"] = FromRgb(0xD4, 0xD0, 0xC8),
["InfoBackground"] = FromRgb(0xFF, 0xFF, 0xE1),
["InfoText"] = FromRgb(0x00, 0x00, 0x00),
["Menu"] = FromRgb(0xD4, 0xD0, 0xC8),
["MenuText"] = FromRgb(0x00, 0x00, 0x00),
["Scrollbar"] = FromRgb(0xD4, 0xD0, 0xC8),
["ThreeDDarkShadow"] = FromRgb(0x40, 0x40, 0x40),
["ThreeDFace"] = FromRgb(0xD4, 0xD0, 0xC8),
["ThreeDHighlight"] = FromRgb(0xE3, 0xE3, 0xE3),
["ThreeDLightShadow"] = FromRgb(0xFF, 0xFF, 0xFF),
Comment thread
wieslawsoltes marked this conversation as resolved.
["ThreeDShadow"] = FromRgb(0x80, 0x80, 0x80),
["Window"] = FromRgb(0xFF, 0xFF, 0xFF),
["WindowFrame"] = FromRgb(0x00, 0x00, 0x00),
["WindowText"] = FromRgb(0x00, 0x00, 0x00)
};

private SvgFixedSystemColorProvider()
{
}

public bool TryGetColor(string name, out Color color)
{
if (name == null)
{
color = Color.Empty;
return false;
}

return s_colors.TryGetValue(name.Trim(), out color);
}

private static Color FromRgb(int red, int green, int blue)
=> Color.FromArgb(255, red, green, blue);
}

public static class SvgSystemColorResolver
{
private static readonly AsyncLocal<ISvgSystemColorProvider> s_scopedProvider = new AsyncLocal<ISvgSystemColorProvider>();
private static ISvgSystemColorProvider s_defaultProvider = SvgFixedSystemColorProvider.Instance;

public static ISvgSystemColorProvider DefaultProvider
{
get { return s_defaultProvider; }
set { s_defaultProvider = value ?? throw new ArgumentNullException(nameof(value)); }
}

public static bool TryGetColor(string name, out Color color)
{
var provider = s_scopedProvider.Value ?? s_defaultProvider;
return provider.TryGetColor(name, out color);
}

public static IDisposable PushProvider(ISvgSystemColorProvider provider)
{
var previous = s_scopedProvider.Value;
s_scopedProvider.Value = provider;
return new Scope(previous);
}

private sealed class Scope : IDisposable
{
private readonly ISvgSystemColorProvider _previous;
private bool _disposed;

public Scope(ISvgSystemColorProvider previous)
{
_previous = previous;
}

public void Dispose()
{
if (_disposed)
{
return;
}

s_scopedProvider.Value = _previous;
_disposed = true;
}
}
}
}
6 changes: 6 additions & 0 deletions src/Svg.Model/ISvgAssetLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public interface ISvgImageAlphaProvider
bool TryGetImageAlpha(SKImage image, out int width, out int height, out byte[] alpha);
}

public interface ISvgDocumentFontLoader
{
void ClearDocumentFonts();
IDisposable PushDocumentFonts(SvgDocument document);
}

public interface ISvgTextReferenceRenderingOptions
{
bool EnableTextReferences { get; }
Expand Down
Loading
Loading