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

Refactored Mutate/Clone APIs #275

Merged
merged 40 commits into from
Sep 14, 2017
Merged

Refactored Mutate/Clone APIs #275

merged 40 commits into from
Sep 14, 2017

Conversation

tocsoft
Copy link
Member

@tocsoft tocsoft commented Jul 12, 2017

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 matches the existing coding patterns and practise as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

This is a rather significatant change to our public APIs the proposal is thus.

Instead of Image<TPixel> having a range of extension methods on it to do all sorts of mutations that return the save instance I've introduced IImageOperations<TPixel> which is used to queue/indirecty apply IImageProcessors to the source image.

The way you get a handle on an IImageOperations<TPixel> is by using the only 2 remaining extension APIs from Image<TPixel> (should theses be moved directly onto Image<TPixel> instead of being extension methds?).

The driver for this is I wanted the mutateing APIs to be obvious that they are a fluent chaining/mutating set of operations that return the source image again instead of returning a image each time.

Additionally I wanted to enable a version of all the ImageOperations that did return a new instance for those times you wanted to generate multiple outputs from the same input.

So some code examples.

So to perform a simple set of operation you now do this:

image.Mutate(x=>x.Grayscale().Resize(10,10).Pixelatte()); // returns void
image.Save("mutatedImage.png");

this will mutate the pixel data stored in image first making it grayscale, then resizing it, and finaly pixelatting it.

now to do all that again by not alter the source image would be a simple as

using(var new_image = image.Clone(x=>x.Grayscale().Resize(10,10).Pixelatte())) // returns `Image`
{
    // deal with clone in here
    new_image.Save("newImage.png");
}

image.Save("origionalImage.png");

second example has left image in exactly the same unaltered state is was in before starting, and instead has created a new image new_image with all the mutations applied.

Fixes #265

@codecov-io
Copy link

codecov-io commented Jul 12, 2017

Codecov Report

Merging #275 into master will increase coverage by 0.21%.
The diff coverage is 96.62%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #275      +/-   ##
==========================================
+ Coverage   86.71%   86.92%   +0.21%     
==========================================
  Files         671      716      +45     
  Lines       29912    30507     +595     
  Branches     2138     2151      +13     
==========================================
+ Hits        25937    26517     +580     
- Misses       3316     3325       +9     
- Partials      659      665       +6
Impacted Files Coverage Δ
src/ImageSharp.Drawing/Paths/DrawPath.cs 66.66% <ø> (ø) ⬆️
src/ImageSharp.Drawing/Paths/DrawPathCollection.cs 37.5% <ø> (ø) ⬆️
...geSharp/PixelFormats/PackedPixelConverterHelper.cs 0% <ø> (ø) ⬆️
src/ImageSharp.Drawing/Paths/DrawBeziers.cs 33.33% <ø> (ø) ⬆️
src/ImageSharp.Drawing/Paths/FillRectangle.cs 100% <ø> (ø) ⬆️
src/ImageSharp.Drawing/Text/TextGraphicsOptions.cs 81.25% <ø> (-1.11%) ⬇️
src/ImageSharp.Drawing/Paths/DrawRectangle.cs 66.66% <ø> (ø) ⬆️
src/ImageSharp.Drawing/Paths/FillPaths.cs 100% <ø> (ø) ⬆️
src/ImageSharp.Drawing/Text/DrawText.Path.cs 100% <ø> (ø) ⬆️
src/ImageSharp.Drawing/Text/DrawText.cs 100% <ø> (ø) ⬆️
... and 238 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a710ccc...9b34d09. Read the comment docs.

/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to rotate, flip, or both.</param>
/// <param name="operations">The operations to perform on the source.</param>
public static void Mutate<TPixel>(this Image<TPixel> source, Action<IImageOperations<TPixel>> operations)
Copy link
Member Author

Choose a reason for hiding this comment

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

Question : should theses be extension methods or exposed instance methods of Image<TPixel>?

The more I think about it I feel these should actually be moved onto Image directly thus exposing this APIs without needing to add the using statements.

Copy link
Member

Choose a reason for hiding this comment

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

We should be consistent:

  • Either by redesigning our whole API to be fully extension method focused, so not only Mutate(), but all the methods/properties producing derived results from a narrow core API, should be extension methods. So even image.Bounds would become an image.Bounds() extension method.
  • Or just turn both Mutate(operations) and Clone(operations) to instance methods as well

Copy link
Member Author

Choose a reason for hiding this comment

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

Bounds() should be an extension method I feel, as its only really used as a helper for image operations. (in fact it should probably just be internal)

We should also probably move out all all the overloads of Save() into extension methods too Image would just have Save(Stream, Encoder).

Also things like To<TPixel2>() could also probably be a candidate for changing.

And now the ApplyProcessor() is basically redundant.

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.

Made a quick review focusing on core stuff.

Haven't checked the concrete processor extension methods. It should be done probably by someone who has better understanding of the processors ;)

where TPixel : struct, IPixel<TPixel>;
}

/// <summary>
Copy link
Member

Choose a reason for hiding this comment

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

The naming seems inconsistent here.

It should be probably IImageOperationsFactory + CreateImageOperations(), or IImageOperationsProvider + GetImageOperations().

/// </summary>
/// <param name="processors">Processors to apply</param>
/// <returns>this </returns>
public IImageOperations<TPixel> ApplyProcessors(IEnumerable<IImageProcessor<TPixel>> processors)
Copy link
Member

Choose a reason for hiding this comment

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

This method is unused.

where TPixel : struct, IPixel<TPixel>
{
Guard.NotNull(operations, nameof(operations));
var generated = new Image<TPixel>(source);
Copy link
Member

@antonfirsov antonfirsov Jul 14, 2017

Choose a reason for hiding this comment

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

For most processors we (eg. Resize()) we are allocating a new Width*Height image area twice!

So the issue Eric Mellino pointed out on gitter is still here, having two allocations is unnecessary. We should find a way to avoid this during this API validation process. I'm not saying that we should refactor everything now, but we need to make sure it is possible with the new API!

Copy link
Member Author

Choose a reason for hiding this comment

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

The internal parts of the should be refactorable to make it possible to prevent the double allocation.

Copy link
Member

@antonfirsov antonfirsov Jul 14, 2017

Choose a reason for hiding this comment

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

I'm not sure if it's possible without changing IImageProcessor<TPixel> design.

The processors doing allocation of a new image buffer should be able to take the source and destination image buffers from the outside instead. It would make SwapPixelBuffers() unnecessary:
https://github.com/SixLabors/ImageSharp/blob/master/src/ImageSharp/Processing/Processors/Transforms/ResizeProcessor.cs#L97

@JimBobSquarePants thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

I may play around with this, making a proposal in the weekend.

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 was planning to create a new internal interface ICloneableImageProcessor<T> : IImageProcessor<TPixel> (or similare ) then in the DefaultImageOperator check if the passed in IImageProcessor is in fact an ICloneableImageProcessor and if it is then instead of calling IImageProcessor.Apply() then it instead call IImageProcessor.CloneAndApply() which returns the cloned image. All it would require is the DefaultImageOperator cloning the source image lazerly the first time we encounter an IImageProcesssor and its not an ICloneableImageProcessor and if it is on then it will retain the cloned image and just call Apply with it on all remaining IImageProcesssor

Copy link
Member Author

@tocsoft tocsoft Jul 14, 2017

Choose a reason for hiding this comment

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

feel free to have a go... basically my ideal situation (for v1) is to do it without having to change the public API any, even if that means that only our internal processors have to extra power of generating the clone.

@antonfirsov
Copy link
Member

@JimBobSquarePants @tocsoft
finding a clean & maintainable solution for the double cloning issue seems really hard, I'm struggling since hours with no success. One thing seems pretty sure: the IImageProcessor<T> API has to be changed to manage the copying family of processors in a clean way.

I don't think any of our users are implementing their own processors though, so probably a minor change on that API somewhere between the beta and the RC should not be an issue. Thoughts? :)

@tocsoft
Copy link
Member Author

tocsoft commented Jul 15, 2017

@antonfirsov c421e6d is basically what I was thinking with regards the fix for double cloning... none of those changes actually alter the public API... so it is possible without having to change IImageProcessor<> true it doesn't expose the ability to third party developers but that can be done an a version or 2 once other devs start wanting to make processors for it.

/// An interface to queue up image operations.
/// </summary>
/// <typeparam name="TPixel">The pixel format</typeparam>
public interface IImageProcessorApplicator<TPixel>
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we find a less verbose name for this? How about IImageProcessingContext<TPixel>? //image.Mutate(ctx => ctx.Resize(..) )

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 like IImageProcessingContext

/// Encapsulates methods to alter the pixels of an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
public interface ICloningImageProcessor<TPixel> : IImageProcessor<TPixel>
Copy link
Member

Choose a reason for hiding this comment

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

Wasn't your intention to internalize this?

Copy link
Member Author

Choose a reason for hiding this comment

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

yep i was, must have missed it

/// Allows the application of processors to images.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract class CloneingImageProcessor<TPixel> : IImageProcessor<TPixel>, ICloningImageProcessor<TPixel>
Copy link
Member

Choose a reason for hiding this comment

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

Typo: Cloneing...

Can't we avoid code duplication by subclassing ImageProcessor<TPixel>?

Copy link
Member Author

Choose a reason for hiding this comment

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

there is no duplication really, they both follow the same patterns but the code is different, one takes 2 images the other takes 1. Basically CloneingImageProcessor is for image processors that require a 2nd buffer to apply changes to before overwriting the pixel data in the original. ImageProcessor if for processors that don't require the 2nd buffer and can apply there changes directly to the source.

@JimBobSquarePants
Copy link
Member

Run makes me think of Task which doesn't feel contextually correct. Apply makes me think of JavaScript's function.Apply which would be contextually correct.

@antonfirsov
Copy link
Member

@JimBobSquarePants @tocsoft tried to clarify the avatar example by adding some docs, comments + a variant, that does the trick without Apply().

@JimBobSquarePants if you say that users who are not messed with 200 git changes+flu should understand all this, I believe you! 😄
Everything looks good to me now!

@tocsoft
Copy link
Member Author

tocsoft commented Aug 15, 2017

Those updates look good to me 👍

I like that we're showing both methods as its demonstrating that we aren't prescribing doing it one way or the other.

@antonfirsov
Copy link
Member

Here is the only problem left:

  • On one hand, we should merge this ASAP to unblock upcoming PR-s.
  • On the other hand: this is a big change, and we need to explain it to our users first, together with the reasons driving to this change. I'm afraid, we should write a blog post before merging.

@tocsoft
Copy link
Member Author

tocsoft commented Aug 15, 2017

I could use this as the base branch for doing the package renames/namespace changes. That should cause less disruption on people who inadvertently update to latest as they would be having to to pull in a different package anyway.

Also means there is only one large braking change instead of 2.

@tocsoft
Copy link
Member Author

tocsoft commented Aug 15, 2017

obviously would still require a blog post but it would be a single one with API, package name in one.

@antonfirsov
Copy link
Member

👍 Good thinking!

@JimBobSquarePants JimBobSquarePants mentioned this pull request Aug 19, 2017
4 tasks
@tocsoft tocsoft merged commit 9b34d09 into master Sep 14, 2017
@tocsoft tocsoft deleted the tocsoft/mutate-api branch September 14, 2017 18:22
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.

4 participants