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

Normalize and cleanup encoders #2269

Merged
merged 25 commits into from
Oct 28, 2022
Merged

Conversation

JimBobSquarePants
Copy link
Member

@JimBobSquarePants JimBobSquarePants commented Oct 16, 2022

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

There's no point trying to copy the options pattern that we introduced in #2180 as the requirements are different.

  • We DON'T want to allow passing specific DECODERS to the General API.
  • We DO want to allow passing specific ENCODERS to the General API.

Instead, I've refactored the encoders to do the following

  • Introduced 2 new abstract types ImageEncoder and QuantizingImageEncoder with shared, preconfigured options properties. Two new shared options properties SkipMetadata and PixelSamplingStrategy have been introduced into the respective types.
  • Inlined the individual encoder options interfaces into the concrete implementations and removed any duplication (no more separate mutable options types)
  • Made the individual encoder options immutable.
  • Refactored each encoder to allow skipping metadata on encode - This allows us to encode images without metadata while preserving the metadata in the decoded image.
  • Refactored each QuantizingImageEncoder instance to use the sampling strategy from the options. This applies to local and global palette generation.
  • General cleanup in files I was altering.

Copy link
Contributor

@br3aker br3aker left a comment

Choose a reason for hiding this comment

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

Don't have time for a proper look but it definitely looks interesting.

src/ImageSharp/Formats/Jpeg/JpegEncoder.cs Outdated Show resolved Hide resolved
Copy link
Contributor

@gfoidl gfoidl left a comment

Choose a reason for hiding this comment

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

Just eye-rolled product code changes.

@antonfirsov
Copy link
Member

Will take a look at this within a day or two!

Copy link
Member

@antonfirsov antonfirsov left a comment

Choose a reason for hiding this comment

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

My review assumes that we are OK with the general design direction, pointing out a few minor issues, while the PR looks mostly good.

However:
While I do understand the differences in the requirements, I'm still not happy with the decoder-encoder API asymmetry. It feels weird, and I still think we can do better. We should re-evaluate the tradeoffs of different decoder/encoder design strategies.

That should be another iteration/discussion though, since this PR contains way too many changes better to be merged sooner than later.

Comment on lines 11 to 33
public interface IEncoderOptions
{
/// <summary>
/// Gets a value indicating whether to ignore decoded metadata when encoding.
/// </summary>
bool SkipMetadata { get; init; }
}

/// <summary>
/// Defines the contract for encoder options that allow color palette generation via quantization.
/// </summary>
public interface IQuantizingEncoderOptions : IEncoderOptions
{
/// <summary>
/// Gets the quantizer used to generate the color palette.
/// </summary>
IQuantizer Quantizer { get; init; }

/// <summary>
/// Gets the <see cref="IPixelSamplingStrategy"/> used for quantization when building color palettes.
/// </summary>
IPixelSamplingStrategy PixelSamplingStrategy { get; init; }
}
Copy link
Member

@antonfirsov antonfirsov Oct 26, 2022

Choose a reason for hiding this comment

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

I can't find any code utilizing these abstractions in a meaningful way. Wouldn't it be better to drop them?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, happy to use the base classes instead.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

/// <summary>
/// The base class for all image encoders that allow color palette generation via quantization.
/// </summary>
public abstract class QuantizingImageEncoder : ImageEncoder, IQuantizingEncoderOptions
Copy link
Member

@antonfirsov antonfirsov Oct 26, 2022

Choose a reason for hiding this comment

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

[Note on asymmetry, not to be addressed.] Also weird that we have subclassing while with decoders the structure is flat.

Copy link
Member Author

Choose a reason for hiding this comment

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

I actually want to subclass in the decoders. Just trying to figure out a nice way.

@@ -181,7 +173,7 @@ public new ImageFrameCollection<TPixel> Frames
/// <exception cref="ArgumentOutOfRangeException">Thrown when the provided (x,y) coordinates are outside the image boundary.</exception>
public TPixel this[int x, int y]
{
[MethodImpl(InliningOptions.ShortMethod)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
Copy link
Member

Choose a reason for hiding this comment

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

Any reason for the InliningOptions -> MethodImplOptions changes?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we should be using the custom methods more sparingly on areas we're actively profiling. (I recall you saying similar a few years back now ;))

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense for these 2 methods actually.

{
var encoder = new BmpEncoderCore(this, image.GetMemoryAllocator());
BmpEncoderCore encoder = new(this, image.GetMemoryAllocator());
Copy link
Member

Choose a reason for hiding this comment

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

It's easy to overlook important semantic changes if there are so many style refactorings in the same PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, but every time I open a file just now there's a heap of warnings from the new rules. I'm adopting the boy scout rule.

Comment on lines +106 to +107
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image.Frames.RootFrame);
quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds());
Copy link
Member

Choose a reason for hiding this comment

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

Do I understand it correctly that this introduces an optimization for single-frame gifs? Can't remember why didn't I add it originally single-frame, but would be nice to see a performance comparison. I assume there is no impact on the output quality?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah. It is but it only affects 4K or greater images by default. I didn't introduce a change to the default dimension limit as I didn't want to affect quality on smaller images.

@@ -101,4 +103,55 @@ Buffer2DRegion<TPixel> GetRow(int pos)
}
}
}

/// <inheritdoc />
public IEnumerable<Buffer2DRegion<TPixel>> EnumeratePixelRegions<TPixel>(ImageFrame<TPixel> frame)
Copy link
Member

Choose a reason for hiding this comment

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

The multi-frame overload has some unit tests, might be worth to test this one too.

Copy link
Member Author

Choose a reason for hiding this comment

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

Will do!

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

@JimBobSquarePants JimBobSquarePants merged commit b404826 into main Oct 28, 2022
@JimBobSquarePants JimBobSquarePants deleted the js/encoder-normalization branch October 28, 2022 11:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants