saucecontrol / PhotoSauce

MagicScaler high-performance, high-quality image processing pipeline for .NET
http://photosauce.net/
MIT License
589 stars 49 forks source link

Default JPEG encoding options are not publicly accessible #133

Closed loudenvier closed 7 months ago

loudenvier commented 11 months ago

I know I can pass a JpegEncoderOptions object when calling MagicProcessor.ProcessImages, but I'm at a loss how to set default jpeg encoder options. Since the encoder is selected automatically depending on the source file extension or signature, I only want to control jpeg options and not force all files to be converted to jpeg. My current code is, sadly, forcing jpeg encoding for all files:

MagicImageProcessor.ProcessImage(filename, stm, new ProcessImageSettings {
    Width = o.Width ?? 0,
    Height = o.Height ?? 0,
    ResizeMode = CropScaleMode.Max,
    HybridMode = o.ScaleMode,
    EncoderOptions = new JpegEncoderOptions {
        Quality = 70,
}

I'm aware of CodecManager but I didn't find any example on how to simply setup some encoder specific default options, more precisely jpeg options. I know it's probably a silly question :-) but I'm not being able to solve it by myself.

saucecontrol commented 11 months ago

Howdy!

In general, you can change the default options for a codec just after registration, like this:

CodecManager.Configure(codecs => {
    // locate the desired codec
    var png = codecs.OfType<EncoderInfo>().First(c => c.MimeTypes.FirstOrDefault() == ImageMimeTypes.Png);

    // remove the default definition
    codecs.Remove(png);

    // replace it with a new one with different options
    codecs.Add(png with { DefaultOptions = new PngEncoderOptions(PngFilter.None, Interlace: false) });
});

However, it seems that I inadvertently left the encoder definition for JPEG defined as internal, so you won't be able to change JPEG options through the normal API. It is, however, possible to do in the current release through reflection, and this will be safe when done at app startup in CodecManager.Configure.

CodecManager.Configure(codecs => {
    // locate the JPEG codec
    var jpeg = codecs.OfType<IImageEncoderInfo>().First(c => c.MimeTypes.FirstOrDefault() == ImageMimeTypes.Jpeg);

    // change the default options on the existing definition
    var prop = jpeg.GetType().GetProperty(nameof(IImageEncoderInfo.DefaultOptions));
    prop!.SetValue(jpeg, new JpegEncoderOptions(Quality: 70, Subsample: ChromaSubsampleMode.Subsample420));
});

This is also possible with the libjpeg native plugin, which supports progressive encoding for smaller file sizes.

CodecManager.Configure(codecs => {
    // register the libjpeg plugin, replacing the Windows codec if present
    codecs.UseLibjpeg();

    // locate the JPEG codec
    var jpeg = codecs.OfType<IImageEncoderInfo>().First(c => c.MimeTypes.FirstOrDefault() == ImageMimeTypes.Jpeg);

    // change the default options on the existing definition
    var prop = jpeg.GetType().GetProperty(nameof(IImageEncoderInfo.DefaultOptions));
    prop!.SetValue(jpeg, new JpegOptimizedEncoderOptions(Quality: 70, Subsample: ChromaSubsampleMode.Subsample420, Progressive: JpegProgressiveMode.Semi));
});

I will plan on making the JPEG options public in a future release so that the reflection call isn't necessary.

loudenvier commented 11 months ago

Oh Internal classes, been there, done that :-) ... They have a tendency to come back and bite me in the butt... When I started "doing" Kotlin programming I found it so strange that everything defaults to public, but now I'm leaning towards this and only making private the really, really, really implementation details. If something needs to be visible inside other files in the assembly (internal) chances are they'll be needed somewhere else down the road :-)

But, anyways, reflection to the rescue. Everything now works like a charm! I'm using it on a simple Image Resizer app: https://github.com/loudenvier/imgsizer

(still need to upload the changes based on your help though)

saucecontrol commented 7 months ago

I've just published v0.14.1 with the fix, so the reflection workaround is no longer necessary. Something like this should work:

CodecManager.Configure(codecs => {
    // locate the JPEG codec
    var jpeg = codecs.OfType<EncoderInfo>().First(c => c.MimeTypes.FirstOrDefault() == ImageMimeTypes.Jpeg);

    // remove the default definition
    codecs.Remove(jpeg);

    // replace it with a new one with different options
    codecs.Add(jpeg with { DefaultOptions = new JpegEncoderOptions(Quality: 70, Subsample: ChromaSubsampleMode.Subsample420) });
});
loudenvier commented 7 months ago

Thank you for the update! I'll update my code accordingly!

Em qua., 6 de mar. de 2024, 04:16, Clinton Ingram @.***> escreveu:

I've just published v0.14.1 with the fix, so the reflection workaround is no longer necessary. Something like this should work:

CodecManager.Configure(codecs => { // locate the JPEG codec var jpeg = codecs.OfType().First(c => c.MimeTypes.FirstOrDefault() == ImageMimeTypes.Jpeg); // remove the default definition codecs.Remove(jpeg); // replace it with a new one with different options codecs.Add(jpeg with { DefaultOptions = new JpegEncoderOptions(Quality: 70, Subsample: ChromaSubsampleMode.Subsample420) });});

— Reply to this email directly, view it on GitHub https://github.com/saucecontrol/PhotoSauce/issues/133#issuecomment-1980229002, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAEIGLJPAKMR6B6E57F7QQLYW27EBAVCNFSM6AAAAAA62IV2VGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTSOBQGIZDSMBQGI . You are receiving this because you authored the thread.Message ID: @.***>