Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace ImageExtensions.Save.tt with Source Generator #2237

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
7 changes: 7 additions & 0 deletions ImageSharp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tga", "Tga", "{5DFC394F-136
tests\Images\Input\Tga\targa_8bit_rle.tga = tests\Images\Input\Tga\targa_8bit_rle.tga
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Generators", "src\ImageSharp.Generators\ImageSharp.Generators.csproj", "{4743B273-473A-4D20-BE25-EC3D0B38E396}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -669,6 +671,10 @@ Global
{FC527290-2F22-432C-B77B-6E815726B02C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC527290-2F22-432C-B77B-6E815726B02C}.Release|Any CPU.Build.0 = Release|Any CPU
{4743B273-473A-4D20-BE25-EC3D0B38E396}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4743B273-473A-4D20-BE25-EC3D0B38E396}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4743B273-473A-4D20-BE25-EC3D0B38E396}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4743B273-473A-4D20-BE25-EC3D0B38E396}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -699,6 +705,7 @@ Global
{FC527290-2F22-432C-B77B-6E815726B02C} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
{670DD46C-82E9-499A-B2D2-00A802ED0141} = {E1C42A6F-913B-4A7B-B1A8-2BB62843B254}
{5DFC394F-136F-4B76-9BCA-3BA786515EFC} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
{4743B273-473A-4D20-BE25-EC3D0B38E396} = {815C0625-CD3D-440F-9F80-2D83856AB7AE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795}
Expand Down
165 changes: 165 additions & 0 deletions src/ImageSharp.Generators/ImageExtensionsSaveGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

namespace ImageSharp.Generators;

[Generator(LanguageNames.CSharp)]
internal class ImageExtensionsSaveGenerator : IIncrementalGenerator
{
private const string FileHeader = @"// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

// <auto-generated />";

private static readonly string[] ImageFormats =
Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't it be better to have some sort of attribute on the encoder type that can be used to generate the methods instead?

That would make it more generator worthy would it not @Sergio0694 ?

It would be amazing if we could add that to a base class (ImageEncoder See #2269) and have these methods generated automatically without us ever having to touch this file again.

Copy link
Author

Choose a reason for hiding this comment

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

Yup, this would be possible and probably a better suited scenario for a Source Generator. You'd still have the issues around local projects with SGs which @Sergio0694 mentioned in #2237 (comment) and would need to decide if this is okay for you.

Copy link
Member

Choose a reason for hiding this comment

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

Not to mention, using source generators with a local project reference is unsupported, and has broken tooling

This one? I'm still blown away by that. How does someone test such a thing if locally referencing the project is unsupported? It seems incredibly short sighted.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, to be honest I haven't noticed this as much. I rarely clean my repository like that.
I have multiple SGs in a local project at work and only get red squiggly lines when modifying the SG code, which rarely happens. Even then closing and reopening the solution fixes it. Other than that, all is good.

{
"Bmp",
"Gif",
"Jpeg",
"Pbm",
"Png",
"Tga",
"Webp",
"Tiff"
};

public void Initialize(IncrementalGeneratorInitializationContext context) => context.RegisterPostInitializationOutput(GenerateExtensionsMethods);

private static void GenerateExtensionsMethods(IncrementalGeneratorPostInitializationContext ctx)
{
StringBuilder stringBuilder = new(FileHeader);
stringBuilder.AppendLine();
stringBuilder.AppendLine("using SixLabors.ImageSharp.Advanced;");
foreach (string format in ImageFormats)
{
stringBuilder.Append("using SixLabors.ImageSharp.Formats.").Append(format).AppendLine(";");
}

stringBuilder.AppendLine();
stringBuilder.AppendLine("namespace SixLabors.ImageSharp;");
stringBuilder.AppendLine();
stringBuilder.AppendLine(@"/// <summary>
/// Extension methods for the <see cref=""Image""/> type.
/// </summary>
public static partial class ImageExtensions
{");

ctx.CancellationToken.ThrowIfCancellationRequested();

foreach (string format in ImageFormats)
{
string methods = $@"
/// <summary>
/// Saves the image to the given stream with the {format} format.
/// </summary>
/// <param name=""source"">The image this method extends.</param>
/// <param name=""path"">The file path to save the image to.</param>
/// <exception cref=""System.ArgumentNullException"">Thrown if the path is null.</exception>
public static void SaveAs{format}(this Image source, string path) => SaveAs{format}(source, path, null);

/// <summary>
/// Saves the image to the given stream with the {format} format.
/// </summary>
/// <param name=""source"">The image this method extends.</param>
/// <param name=""path"">The file path to save the image to.</param>
/// <exception cref=""System.ArgumentNullException"">Thrown if the path is null.</exception>
/// <returns>A <see cref=""Task""/> representing the asynchronous operation.</returns>
public static Task SaveAs{format}Async(this Image source, string path) => SaveAs{format}Async(source, path, null);

/// <summary>
/// Saves the image to the given stream with the {format} format.
/// </summary>
/// <param name=""source"">The image this method extends.</param>
/// <param name=""path"">The file path to save the image to.</param>
/// <param name=""cancellationToken"">The token to monitor for cancellation requests.</param>
/// <exception cref=""System.ArgumentNullException"">Thrown if the path is null.</exception>
/// <returns>A <see cref=""Task""/> representing the asynchronous operation.</returns>
public static Task SaveAs{format}Async(this Image source, string path, CancellationToken cancellationToken)
=> SaveAs{format}Async(source, path, null, cancellationToken);

/// <summary>
/// Saves the image to the given stream with the {format} format.
/// </summary>
/// <param name=""source"">The image this method extends.</param>
/// <param name=""path"">The file path to save the image to.</param>
/// <param name=""encoder"">The encoder to save the image with.</param>
/// <exception cref=""System.ArgumentNullException"">Thrown if the path is null.</exception>
public static void SaveAs{format}(this Image source, string path, {format}Encoder encoder) =>
source.Save(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder({format}Format.Instance));

/// <summary>
/// Saves the image to the given stream with the {format} format.
/// </summary>
/// <param name=""source"">The image this method extends.</param>
/// <param name=""path"">The file path to save the image to.</param>
/// <param name=""encoder"">The encoder to save the image with.</param>
/// <param name=""cancellationToken"">The token to monitor for cancellation requests.</param>
/// <exception cref=""System.ArgumentNullException"">Thrown if the path is null.</exception>
/// <returns>A <see cref=""Task""/> representing the asynchronous operation.</returns>
public static Task SaveAs{format}Async(this Image source, string path, {format}Encoder encoder, CancellationToken cancellationToken = default) =>
source.SaveAsync(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder({format}Format.Instance),
cancellationToken);

/// <summary>
/// Saves the image to the given stream with the {format} format.
/// </summary>
/// <param name=""source"">The image this method extends.</param>
/// <param name=""stream"">The stream to save the image to.</param>
/// <exception cref=""System.ArgumentNullException"">Thrown if the stream is null.</exception>
public static void SaveAs{format}(this Image source, Stream stream)
=> SaveAs{format}(source, stream, null);

/// <summary>
/// Saves the image to the given stream with the {format} format.
/// </summary>
/// <param name=""source"">The image this method extends.</param>
/// <param name=""stream"">The stream to save the image to.</param>
/// <param name=""cancellationToken"">The token to monitor for cancellation requests.</param>
/// <exception cref=""System.ArgumentNullException"">Thrown if the stream is null.</exception>
/// <returns>A <see cref=""Task""/> representing the asynchronous operation.</returns>
public static Task SaveAs{format}Async(this Image source, Stream stream, CancellationToken cancellationToken = default)
=> SaveAs{format}Async(source, stream, null, cancellationToken);

/// <summary>
/// Saves the image to the given stream with the {format} format.
/// </summary>
/// <param name=""source"">The image this method extends.</param>
/// <param name=""stream"">The stream to save the image to.</param>
/// <param name=""encoder"">The encoder to save the image with.</param>
/// <exception cref=""System.ArgumentNullException"">Thrown if the stream is null.</exception>
public static void SaveAs{format}(this Image source, Stream stream, {format}Encoder encoder)
=> source.Save(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder({format}Format.Instance));

/// <summary>
/// Saves the image to the given stream with the {format} format.
/// </summary>
/// <param name=""source"">The image this method extends.</param>
/// <param name=""stream"">The stream to save the image to.</param>
/// <param name=""encoder"">The encoder to save the image with.</param>
/// <param name=""cancellationToken"">The token to monitor for cancellation requests.</param>
/// <exception cref=""System.ArgumentNullException"">Thrown if the stream is null.</exception>
/// <returns>A <see cref=""Task""/> representing the asynchronous operation.</returns>
public static Task SaveAs{format}Async(this Image source, Stream stream, {format}Encoder encoder, CancellationToken cancellationToken = default) =>
source.SaveAsync(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder({format}Format.Instance),
cancellationToken);";
stringBuilder.AppendLine(methods);
}

stringBuilder.Append('}');

SourceText sourceText = SourceText.From(stringBuilder.ToString(), Encoding.UTF8);
ctx.AddSource("ImageExtensions.Save.g.cs", sourceText);
}
}
19 changes: 19 additions & 0 deletions src/ImageSharp.Generators/ImageSharp.Generators.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net7.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Keep up to date with .NET version of ImageSharp -->
<LangVersion>10</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
</ItemGroup>

<ItemGroup>
<Using Remove="SixLabors" />
</ItemGroup>

</Project>
Loading