Git Product home page Git Product logo

photosauce's Introduction

NuGet Build Status CI NuGet

PhotoSauce.MagicScaler

MagicScaler is a high-performance image processing pipeline for .NET, focused on making complex imaging tasks simple.

It implements best-of-breed algorithms, linear light processing, and sharpening for the best image resizing quality available.

Speed and efficiency are unmatched by anything else on the .NET platform.

Requirements

MagicScaler runs on Windows and Linux.

Linux hosting requires one or more of the cross-platform codec plugins available on nuget.org. Most common image formats are supported. Notable exceptions are support for BMP and TIFF images.

Usage

Image Resizing

MagicImageProcessor.ProcessImage(@"\img\big.jpg", @"\img\small.jpg", new ProcessImageSettings { Width = 400 });

The above example will resize big.jpg to a width of 400 pixels and save the output to small.jpg. The height will be set automatically to preserve the correct aspect ratio. Default settings are optimized for a balance of speed and image quality.

The MagicScaler pipleline is also customizable if you wish to use an alternate pixel source, capture the output pixels for additional processing, or add custom filtering.

See the full documentation for more details.

MagicScaler Performance

Benchmark results in this section come from the tests used in https://blogs.msdn.microsoft.com/dotnet/2017/01/19/net-core-image-processing/ -- updated to use current (Nov 2022) versions of the libraries and runtime. The original benchmark project is also on GitHub.

For these results, the benchmarks were modified to use a constant UnrollFactor so these runs more accurately report managed memory allocations and GC counts. By default, BenchmarkDotNet targets a run time in the range of 500ms-1s for each iteration. This means it executes slower benchmark methods using a smaller number of operations per iteration, and it can wildly under-report allocation and GCs, as those numbers are extrapolated from the limited iterations it runs. The constant UnrollFactor ensures all benchmarks' reported memory stats are based on the same run counts. The UnrollFactor used for each run is listed at the top of each set of results.

End-to-End Image Resizing

This is a semi-real-world image resizing benchmark, in which 12 JPEGs of approximately 1 megapixel each are resized to 150px wide thumbnails and saved back as JPEG. Not all libraries are supported on all platforms. See version notes below.

Windows x64

BenchmarkDotNet=v0.13.2.1974-nightly, OS=Windows 10 (10.0.19043.2006/21H1/May2021Update)
Intel Xeon W-11855M CPU 3.20GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK=7.0.100-rc.2.22477.23
  ShortRun : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2

Job=ShortRun  IterationCount=5  LaunchCount=1  UnrollFactor=32  WarmupCount=3

|                               Method |      Mean |     Error |   StdDev | Ratio | RatioSD |      Gen0 |      Gen1 |      Gen2 |  Allocated | Alloc Ratio |
|------------------------------------- |----------:|----------:|---------:|------:|--------:|----------:|----------:|----------:|-----------:|------------:|
|   *MagicScaler Load, Resize, Save(1) |  46.85 ms |  2.948 ms | 0.456 ms |  0.13 |    0.00 |         - |         - |         - |   42.37 KB |        4.65 |
| System.Drawing Load, Resize, Save(2) | 354.73 ms |  8.168 ms | 1.264 ms |  1.00 |    0.00 |         - |         - |         - |    9.11 KB |        1.00 |
|      ImageFlow Load, Resize, Save(3) | 226.48 ms |  2.284 ms | 0.353 ms |  0.64 |    0.00 |  968.7500 |  968.7500 |  968.7500 | 4627.13 KB |      508.17 |
|     ImageSharp Load, Resize, Save(4) | 115.90 ms | 12.847 ms | 3.724 ms |  0.33 |    0.04 |  156.2500 |   62.5000 |         - |  1532.8 KB |      168.34 |
|    ImageMagick Load, Resize, Save(5) | 345.99 ms | 14.847 ms | 3.856 ms |  0.98 |    0.01 |         - |         - |         - |   50.44 KB |        5.54 |
|      ImageFree Load, Resize, Save(6) | 212.10 ms |  2.055 ms | 0.318 ms |  0.60 |    0.00 | 6000.0000 | 6000.0000 | 6000.0000 |   91.95 KB |       10.10 |
|      SkiaSharp Load, Resize, Save(7) | 117.43 ms |  1.270 ms | 0.330 ms |  0.33 |    0.00 |         - |         - |         - |   84.19 KB |        9.25 |
|        NetVips Load, Resize, Save(8) | 100.92 ms |  4.383 ms | 0.678 ms |  0.28 |    0.00 |         - |         - |         - |   115.9 KB |       12.73 |

Windows Arm64 (Windows Dev Kit 2023)

BenchmarkDotNet=v0.13.2.1974-nightly, OS=Windows 11 (10.0.22621.674)
Snapdragon Compute Platform, 1 CPU, 8 logical and 8 physical cores
.NET SDK=6.0.402
  ShortRun : .NET 6.0.10 (6.0.1022.47605), Arm64 RyuJIT AdvSIMD

Job=ShortRun  IterationCount=5  LaunchCount=1  UnrollFactor=32  WarmupCount=3

|                               Method |      Mean |     Error |   StdDev | Ratio |     Gen0 |     Gen1 |  Allocated | Alloc Ratio |
|------------------------------------- |----------:|----------:|---------:|------:|---------:|---------:|-----------:|------------:|
|   *MagicScaler Load, Resize, Save(1) |  65.72 ms |  0.532 ms | 0.082 ms |  0.17 |        - |        - |   42.36 KB |        4.65 |
| System.Drawing Load, Resize, Save(2) | 386.78 ms |  2.359 ms | 0.613 ms |  1.00 |        - |        - |    9.11 KB |        1.00 |
|     ImageSharp Load, Resize, Save(4) | 287.06 ms |  3.125 ms | 0.812 ms |  0.74 | 375.0000 | 187.5000 | 1635.48 KB |      179.62 |
|    ImageMagick Load, Resize, Save(5) | 588.63 ms | 13.049 ms | 2.019 ms |  1.52 |        - |        - |   50.44 KB |        5.54 |
|      SkiaSharp Load, Resize, Save(7) | 158.27 ms |  1.816 ms | 0.472 ms |  0.41 |        - |        - |   82.32 KB |        9.04 |
|        NetVips Load, Resize, Save(8) | 136.21 ms |  6.125 ms | 1.591 ms |  0.35 |        - |        - |   115.9 KB |       12.73 |

Linux x64 (WSL2)

BenchmarkDotNet=v0.13.2.1974-nightly, OS=ubuntu 20.04
Intel Xeon W-11855M CPU 3.20GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK=6.0.402
  ShortRun : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2

Job=ShortRun  IterationCount=5  LaunchCount=1  UnrollFactor=32  WarmupCount=3

|                               Method |     Mean |    Error |   StdDev | Ratio | RatioSD |     Gen0 |     Gen1 |     Gen2 |  Allocated | Alloc Ratio |
|------------------------------------- |---------:|---------:|---------:|------:|--------:|---------:|---------:|---------:|-----------:|------------:|
|    MagicScaler Load, Resize, Save(1) |  99.8 ms |  1.91 ms |  0.47 ms |  0.37 |    0.01 |        - |        - |        - |   42.38 KB |        4.51 |
| System.Drawing Load, Resize, Save(2) | 271.7 ms | 12.34 ms |  3.20 ms |  1.00 |    0.00 |        - |        - |        - |     9.4 KB |        1.00 |
|      ImageFlow Load, Resize, Save(3) | 321.2 ms |  6.80 ms |  1.77 ms |  1.18 |    0.01 | 968.7500 | 968.7500 | 968.7500 | 4627.46 KB |      492.06 |
|     ImageSharp Load, Resize, Save(4) | 226.5 ms |  4.09 ms |  1.06 ms |  0.83 |    0.01 | 156.2500 |  62.5000 |        - | 1532.81 KB |      162.99 |
|    ImageMagick Load, Resize, Save(5) | 522.6 ms | 27.02 ms |  7.02 ms |  1.92 |    0.04 |        - |        - |        - |   50.84 KB |        5.41 |
|      SkiaSharp Load, Resize, Save(7) | 338.4 ms | 38.62 ms | 10.03 ms |  1.25 |    0.04 |        - |        - |        - |   84.48 KB |        8.98 |
|        NetVips Load, Resize, Save(8) | 380.5 ms | 11.04 ms |  2.87 ms |  1.40 |    0.02 |        - |        - |        - |  116.48 KB |       12.39 |

Linux Arm64 (Raspberry Pi 4b 2GB)

BenchmarkDotNet=v0.13.2.1974-nightly, OS=ubuntu 22.04
Unknown processor
.NET SDK=6.0.402
  ShortRun : .NET 6.0.10 (6.0.1022.47605), Arm64 RyuJIT AdvSIMD

Job=ShortRun  IterationCount=5  LaunchCount=1  UnrollFactor=8  WarmupCount=3

|                               Method |       Mean |    Error |  StdDev | Ratio |      Gen0 |     Gen1 |  Allocated | Alloc Ratio |
|------------------------------------- |-----------:|---------:|--------:|------:|----------:|---------:|-----------:|------------:|
|   *MagicScaler Load, Resize, Save(1) |   214.7 ms |  4.33 ms | 0.67 ms |  0.18 |         - |        - |   43.67 KB |        4.42 |
| System.Drawing Load, Resize, Save(2) | 1,205.9 ms | 26.54 ms | 6.89 ms |  1.00 |         - |        - |    9.87 KB |        1.00 |
|     ImageSharp Load, Resize, Save(4) |   997.0 ms | 18.08 ms | 4.70 ms |  0.83 | 1625.0000 | 375.0000 | 1635.48 KB |      165.72 |
|    ImageMagick Load, Resize, Save(5) | 1,688.5 ms | 12.84 ms | 3.34 ms |  1.40 |         - |        - |   51.47 KB |        5.21 |
|      SkiaSharp Load, Resize, Save(7) |   279.7 ms |  1.87 ms | 0.48 ms |  0.23 |  125.0000 |        - |   81.29 KB |        8.24 |
|        NetVips Load, Resize, Save(8) |   421.5 ms | 14.72 ms | 3.82 ms |  0.35 |  125.0000 |        - |  117.35 KB |       11.89 |

Versions Tested

  • (1) PhotoSauce.MagicScaler version 0.13.2 with PhotoSauce.NativeCodecs.Libjpeg version 2.1.4-preview1.
  • (2) System.Drawing.Common version 5.0.3.
  • (3) Imageflow.AllPlatforms Version 0.9.0.
  • (4) SixLabors.ImageSharp version 2.1.3.
  • (5) Magick.NET-Q8-AnyCPU version 12.2.0.
  • (6) FreeImage.Standard version 4.3.8.
  • (7) SkiaSharp version 2.88.3.
  • (8) NetVips version 2.2.0 with NetVips.Native (libvips) version 8.13.2.

Note that unmanaged memory usage is not measured by BenchmarkDotNet's MemoryDiagnoser, nor is managed memory allocated but never released to GC (e.g. pooled objects/buffers). See the MagicScaler Efficiency section for an analysis of total process memory usage for each library.

The performance numbers mostly speak for themselves, but some notes on image quality are warranted. The benchmark suite saves the output so that the visual quality of the output of each library can be compared in addition to the performance. See the MagicScaler Quality section below for details.

More Benchmarks

Benchmark environment:

BenchmarkDotNet=v0.13.2.1974-nightly, OS=Windows 10 (10.0.19043.2006/21H1/May2021Update)
Intel Xeon W-11855M CPU 3.20GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK=7.0.100-rc.2.22477.23
  ShortRun : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2

Parallel End-to-End Resizing

This benchmark is the same as the previous but uses Parallel.ForEach to run the 12 test images in parallel. It is meant to highlight cases where the libraries' performance doesn't scale up linearly with extra processors.

Job=ShortRun  IterationCount=5  LaunchCount=1  UnrollFactor=64  WarmupCount=3

|                                       Method |      Mean |     Error |    StdDev | Ratio | RatioSD |      Gen0 |      Gen1 |      Gen2 |  Allocated | Alloc Ratio |
|--------------------------------------------- |----------:|----------:|----------:|------:|--------:|----------:|----------:|----------:|-----------:|------------:|
|   *MagicScaler Load, Resize, Save - Parallel |  11.07 ms |  0.511 ms |  0.133 ms |  0.08 |    0.00 |         - |         - |         - |   71.35 KB |        1.95 |
| System.Drawing Load, Resize, Save - Parallel | 144.01 ms | 26.195 ms |  4.054 ms |  1.00 |    0.00 |         - |         - |         - |   36.62 KB |        1.00 |
|      ImageFlow Load, Resize, Save - Parallel |  51.70 ms |  5.179 ms |  0.801 ms |  0.36 |    0.01 |  984.3750 |  984.3750 |  984.3750 | 4628.02 KB |      126.39 |
|     ImageSharp Load, Resize, Save - Parallel |  26.25 ms | 32.435 ms |  5.019 ms |  0.18 |    0.03 |  171.8750 |   78.1250 |         - | 1564.91 KB |       42.74 |
|    ImageMagick Load, Resize, Save - Parallel | 149.86 ms | 27.115 ms |  7.042 ms |  1.03 |    0.05 |         - |         - |         - |   88.22 KB |        2.41 |
|      ImageFree Load, Resize, Save - Parallel |  63.07 ms | 41.212 ms | 10.703 ms |  0.45 |    0.09 | 3156.2500 | 3156.2500 | 3156.2500 |  117.06 KB |        3.20 |
|      SkiaSharp Load, Resize, Save - Parallel |  26.31 ms |  1.399 ms |  0.216 ms |  0.18 |    0.01 |   15.6250 |         - |         - |  110.13 KB |        3.01 |

The NetVips test hung during this benchmark and had to be excluded. Previous versions worked, so it is unclear whether the issue lies with BenchmarkDotNet or a change in NetVips.

Resize-Only Synthetic Benchmark

This benchmark creates a blank image of 1280x853 and resizes it to 150x99, throwing away the result. MagicScaler does very well on this one, but it isn't a real-world scenario, so take the results with a grain of salt.

Job=ShortRun  IterationCount=5  LaunchCount=1  UnrollFactor=256  WarmupCount=3

|                Method |        Mean |       Error |    StdDev | Ratio | RatioSD |     Gen0 |     Gen1 |     Gen2 | Allocated | Alloc Ratio |
|---------------------- |------------:|------------:|----------:|------:|--------:|---------:|---------:|---------:|----------:|------------:|
|   *MagicScaler Resize |    621.8 us |    57.39 us |  14.90 us |  0.07 |    0.00 |        - |        - |        - |    1385 B |       10.04 |
| System.Drawing Resize |  9,525.0 us |   619.59 us | 160.91 us |  1.00 |    0.00 |        - |        - |        - |     138 B |        1.00 |
|      ImageFlow Resize |  6,464.7 us |   237.70 us |  61.73 us |  0.68 |    0.01 |  11.7188 |        - |        - |  116005 B |      840.62 |
|     ImageSharp Resize |  2,299.1 us |    72.70 us |  18.88 us |  0.24 |    0.01 |        - |        - |        - |   10365 B |       75.11 |
|    ImageMagick Resize | 39,426.5 us | 2,093.00 us | 543.54 us |  4.14 |    0.10 |        - |        - |        - |    5338 B |       38.68 |
|      FreeImage Resize |  5,855.3 us |   324.42 us |  84.25 us |  0.61 |    0.01 | 500.0000 | 500.0000 | 500.0000 |     306 B |        2.22 |
|      SkiaSharp Resize |  1,560.8 us |    49.18 us |   7.61 us |  0.16 |    0.00 |        - |        - |        - |     489 B |        3.54 |
|        NetVips Resize |  9,412.2 us |   131.95 us |  34.27 us |  0.99 |    0.02 |        - |        - |        - |    3859 B |       27.96 |

MagicScaler Efficiency

Raw speed isn't the only important factor when evaluating performance. As demonstrated in the parallel benchmark results above, some libraries consume extra resources in order to produce a result quickly, at the expense of overall scalability. Particularly when integrating image processing into another application, like a CMS or an E-Commerce site, it is important that your imaging library not steal resources from the rest of the system. That applies to both processor time and memory.

BenchmarkDotNet does a good job of showing relative performance, and its managed memory diagnoser is quite useful for identifying excessive GC allocations, but its default configuration doesn't track actual processor usage or any memory that doesn't show up in GC collections. For example, when it reports a time of 100ms on a benchmark, was that 100ms of a single processor at 100%? More than one processor? Less than 100%? And what about memory allocated but never collected, like object caches and pooled arrays? And what about unmanaged memory? To capture these things, we must use different tools.

Because most of the libraries tested make calls to native libraries internally (ImageSharp is the only pure-managed library in the bunch), measuring only GC memory can be very misleading. And even ImageSharp's memory usage isn't accurately reflected in the BDN MemoryDiagnoser's numbers, because it holds allocated heap memory in the form of pooled objects and arrays (as does MagicScaler).

In order to accurately measure both CPU time and total memory usage, I devised a more real-world test. The 1-megapixel images in the benchmark test suite make for reasonable benchmark run times, but 1-megapixel is hardly representative of what we see coming from even smartphones now. In order to stress the libraries a bit more, I replaced the input images in the benchmark app's input folder with the Bee Heads album from the USGS Bee Inventory flickr. This collection contains 351 images (350 JPEG, 1 GIF), ranging in size from 2-22 megapixels, with an average of 13.4 megapixels. The total album is just over 2.5 GiB in size, and it can be downloaded directly from flickr.

I re-used the image resizing code from the benchmark app but processed the test images only once, using Parallel.ForEach to load up the system. Because of the test image set's size, startup and JIT overhead are overshadowed by the actual image processing, and although there may be some variation in times between runs, the overall picture is accurate and is more realistic than the BDN runs that cycle through the same small set of small images. Each library's test was run in isolation so memory stats would include only that library.

This table shows the actual CPU time and peak memory usage as captured by the Windows Performance Toolkit when running the modified benchmark app on .NET 6.0 x64.

Method Peak Memory VirtualAlloc Total CPU Time Clock Time
*MagicScaler Bee Heads 420 MB 2389 MB 37153 ms 3.99s
System.Drawing Bee Heads 1001 MB 36449 MB 156050 ms 39.58s
ImageSharp Bee Heads 1079 MB 1101 MB 96510 ms 10.76s
ImageFlow Bee Heads 1485 MB 28843 MB 209247 ms 22.61s
ImageMagick Bee Heads 878 MB 17426 MB 277780 ms 29.86s
FreeImage Bee Heads 1048 MB 16398 MB 198207 ms 21.77s
SkiaSharp Bee Heads 1273 MB 26371 MB 110919 ms 12.13s
NetVips Bee Heads 785 MB 3029 MB 43515 ms 5.01s

It's clear from the CPU time vs wall clock time that System.Drawing is spending a fair amount of its time idle. Its total consumed CPU time is middle of the pack, but its wall clock time shows it to be the slowest by far.

Earlier runs of this test showed extreme total memory use in NetVips and ImageSharp, but they've both brough their memory use way down. MagicScaler still manages clear wins in peak memory and CPU time.

MagicScaler Quality

The benchmark application detailed above saves the output from its end-to-end image resizing tests so they can be evaluated for file size and image quality. The images were contributed by @bleroy, who created the original benchmark. They were chosen because of their high-frequency detail which had been observed to be challenging for some image resampling algorithms. The images have a couple of other interesting properties as well.

First, 10 of the 12 images are saved in the Abobe RGB color space, with an embedded color profile. Second, they have a significant amount of metadata related to the source camera and their processing history. Third, one of the images is in the sRGB color space and contains an embedded profile for sRGB, which is relatively large. These factors combine to highlight some of the shortcomings in the tested libraries.

There is one design flaw in the benchmark app that makes it more difficult to judge the output quality, however. The tests were configured to save the thumbnails with a low JPEG quality value and to use 4:2:0 chroma subsampling. While these values are acceptable or even ideal for large images, they will cause compression artifacts in areas of small details. At thumbnail size, the only details are small details, so it is better to use a higher-quality JPEG setting for very small images. MagicScaler performs this adjustment automatically under its default settings, but those are overridden in the benchmark suite to be consistent with the other libraries.

Note that outside the ill-advised overrides to JPEG quality settings, the results discussed here are from MagicScaler's default settings. Not only is MagicScaler faster and more more efficient than the other libraries, it is also easier to use. You'll get better quality with its defaults than can be obtained with lots of extra code in other libraries.

Color Management

Handling images in a color space other than sRGB can be a challenge, and it's not something most developers are familiar with. Now that all iOS and most Android devices' camera apps default to the Display P3 color space and wide-gamut displays are common, this is a topic of increasing importance.

MagicScaler will automatically normalize the color space of images it processes to maximize compatibility with image consumers while preserving the original gamut of the image. Other software may not be color aware or may make it difficult to process in a color-correct manner.

Additional Info on Color Space Handling

There are essentially 3 ways to approach an image with an embedded color profile:

  1. Convert the image to sRGB. This has the advantage that any downstream software that reads the processed image does not need to be color managed to display it correctly. This was historically a problem with many apps, including popular web browsers (you can test yours here). The disadvantage is that it takes extra processing to do this conversion.
  2. Preserve the color space by embedding the ICC profile in the output image. This is basically the opposite of option 1). It's cheaper to do but may result in other software mangling the colors later. It also results in a larger file because the profile may be very large -- in the case of a thumbnail, it might double the file size or more.
  3. Ignore the color profile and treat the image as if it's encoded as sRGB. This option is absolutely incorrect and would put your software in the category of color manglers mentioned above.

The libraries tested in the benchmark have different capabilities, so the options available depend on the library.

  • System.Drawing supports options 1) and 3) but does 3) by default. The original version of the benchmark did it that way, until I submitted a PR later to correct it to do 1). This resulted in correct colors but a drop in speed.
  • ImageSharp can do 2) or 3) and does 2) by default. When it was originally integrated into the benchmark, however, it only did 3).
  • ImageMagick can do any of the options above but does 2) by default. However, it also preserves all other metadata by default, and the test images have quite a lot of metadata. This results in thumbnails that are extremely oversized and consist of roughly 90% metadata. For this reason, the ImageMagick tests were written to strip all metadata, resulting in behavior 3).
  • FreeImage works the same way as ImageMagick by default, and its test was implemented the same way. It could have done option 2), but it was implemented to do option 3).
  • Skia can be made to do option 1) with a lot of extra code. This was not done in the benchmark tests, resulting in behavior 3). Update: as of SkiaSharp 2.80, behavior 1) is now default, but with a marked reduction in speed.
  • MagicScaler can do any of the above options but does option 2) by default. I contributed the MagicScaler test in the benchmark myself, and it has been correct from the beginning, although it used behavior 1) by default initially.

The net result is that if you look at the sample image output in @bleroy's blog post, the MagicScaler output has different colors than all the others. 10 of the 12 images have washed-out colors in the output from the other libraries -- most apparent in the vibrant red, green, and blue hues, such as the snake or the Wild River ride on the back of what is now the MoPOP building. If you download the project today and run it, the outputs from System.Drawing (corrected by me), ImageSharp (fixed in the library), SkiaSharp (fixed in the library), and MagicScaler will all have correct colors, and the rest will be wrong.

These are common mistakes made by developers starting out with image processing, because it can be easy to miss the shift in colors and difficult to discover how to do the right thing.

Sample Images:
System.Drawing   MagicScaler    ImageSharp     Magick.NET       NetVips       FreeImage      SkiaSharp      ImageFlow  
System.Drawing MagicScaler ImageSharp MagickNET NetVips FreeImage SkiaSharp ImageFlow

The color difference between these should be obvious. Compared to the original image, it's easy to see which are correct (unless your browser is busted).

Gamma-Corrected Blending

Of the libraries originally tested in the benchmark, only MagicScaler performs the resampling step in linear light. ImageSharp, ImageMagick, and Vips are capable of processing in linear light but would require extra code to do so and would perform significantly worse. ImageFlow is a recent addition which also processes in linear light by default.

Sample Images:
System.Drawing   MagicScaler    ImageSharp     Magick.NET       NetVips       FreeImage      SkiaSharp      ImageFlow  
System.Drawing MagicScaler ImageSharp MagickNET NetVips FreeImage SkiaSharp ImageFlow

In addition to keeping the correct colors, MagicScaler does markedly better at preserving image highlights because of the linear light blending. Notice the highlights on the flowers are a better representation of those in the original image

High-Quality Resampling

Most imaging libraries have at least some capability to do high-quality resampling, but not all do. MagicScaler defaults to high-quality, but the other libraries in this test were configured for their best quality as well.

Sample Images:
System.Drawing   MagicScaler    ImageSharp     Magick.NET       NetVips       FreeImage      SkiaSharp      ImageFlow  
System.Drawing MagicScaler ImageSharp MagickNET NetVips FreeImage SkiaSharp ImageFlow

FreeImage and SkiaSharp have particularly poor image quality in this test, with output substantially more blurry than the others. ImageFlow benefits from its linear light processing but also produces blurrier output than others.

Note that when the benchmark and blog post were originally published, Skia supported multiple ways to resize images, which is why there are two Skia benchmark tests. Under the current version of Skia, those two versions have the same benchmark numbers and same output quality, because the Skia internals have been changed to unify its resizing code. The lower-quality version is all that's left.

And here's that original image for reference.

Sharpening

Finally, MagicScaler performs a post-resizing sharpening step to compensate for the natural blurring that occurs when an image is resized. Some of the other libraries would be capable of doing the same, but again, that would require extra code and would negatively impact the performance numbers.

Sample Images:
System.Drawing   MagicScaler    ImageSharp     Magick.NET       NetVips       FreeImage      SkiaSharp      ImageFlow  
System.Drawing MagicScaler ImageSharp MagickNET NetVips FreeImage SkiaSharp ImageFlow

The linear light blending combined with the sharpening work to preserve more details from this original image than the other libraries do. Again, some details are mangled by the poor JPEG settings, so MagicScaler's default settings would do even better.

Also of note is that ImageSharp's and NetVips' thumbnails for this image are roughly twice the size of the others because they have embedded the 3KiB sRGB color profile from the original image unnecessarily.

Versioning

This project is using semantic versioning. Releases without a Preview/RC tag are considered release quality and are safe for production use. The major version number will remain at 0, however, until the APIs are complete and stabilized.

Contributing

Contributions are welcome, but please open a new issue or discussion before submitting any pull requests that alter API or functionality. This will hopefully save any wasted or duplicate effort.

License

PhotoSauce is licensed under the MIT license.

photosauce's People

Contributors

d2phap avatar iamcarbon avatar rickbrew avatar saucecontrol avatar sewer56 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

photosauce's Issues

Is the resulting image size available somewhere?

Sorry, I couldn't find it. If I do this:

ProcessImageResult processImageResult = MagicImageProcessor.ProcessImage(sourceFileOrUri, fileStream, new ProcessImageSettings { Width = thumbnailSize.Width, Height = thumbnailSize.Height, ResizeMode = CropScaleMode.Max });

Is there any info on the resulting image size I can get anywhere?

Padding Filter

Our pipeline supports adding padding to any side. It would be nice to be able to provide this via a custom transform or the settings.

e.g.

pipeline.AddTransform(new PadTransform(top, right, bottom, left));

how resize image without save it

hi how can i resize image with this lib but i dont want to save it
this not work
var mag = MagicImageProcessor.ProcessImage(item, null, settings);

Best library to build a chart like an image

Hi,
We have to create a new chart with the attached template. could you please suggest me which library would be best to use for the chart. Quality of the image should not distort when it is viewed in laptop or ipad or ipod. please suggest a library. Earlier we were using system.drawing. But now we are trying the revamp the whole code to use a better approach. Please advise us.

C# should build this image using any library. All these has to be flexible. Not should be harded. Means chart is purely configurable. Based upon the configuration the rendering varies.
Also this should be an vector image format.

Improve safety of ReadOnlySpan<byte> overloads

e8358ff fixed possible access violations when using ProcessImage and BuildPipeline overloads that accept ReadOnlySpan<byte> image buffer inputs. The memory was previously pinned when loading the image but could still be accessed by a native codec after the pin was released.

ProcessImage now pins the memory for the duration of the call.

BuildPipeline now copies the memory referenced by the Span to unmanaged memory and holds that for the lifetime of the pipeline. This is required for safety because ownership and lifetime of the memory referenced by the Span cannot be determined. For example:

ProcessingPipeline createPipeline()
{
    Span<byte> buff = stackalloc byte[] {
        (byte)'G', (byte)'I', (byte)'F', (byte)'8', (byte)'9', (byte)'a',
        0x00, 0x01, 0x00, 0x01, 0x00, 0xff, 0x00, 0x2c, 0x00, 0x00, 0x00,
        0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x3b
     };

    return MagicImageProcessor.BuildPipeline(buff, new ProcessImageSettings { Width = 100 });
}

void KABOOM()
{
    using var pipeline = createPipeline();
    using var stream = new MemoryStream();

    // noooooooooo.gif
    pipeline.WriteOutput(stream);
}

The above might do something very bad in older versions but will be safe starting from v0.10.3

In order to prevent the defensive copy, you should switch calls to BuildPipeline to instead pass a MemoryStream wrapping managed memory or an UnmanagedMemoryStream wrapping unmanaged memory. The marshaling overhead associated with native codecs reading from a Stream is significantly reduced in v0.10.

The Span overload for BuildPipeline will likely be removed from future versions to prevent the unexpected copy, although I'm open to arguments for keeping it.

[Feature Request] Preserve source image colorspace and embedded ICC Profile

Are there some settings that can be used to mimic the behavior of scaling an image with GDI+?

I have tried using the below settings but high resolution images still produce thumbnails that differ from GDI+.

var settings = new ProcessImageSettings
{
    Width = 400,
    Height = 400,
    ResizeMode = CropScaleMode.Max,
    JpegQuality = 0,
    HybridMode = HybridScaleMode.Turbo,
    SaveFormat = FileFormat.Jpeg,
    Sharpen = false
};

My goal is to produce thumbnails that look the same to my image processing algorithm as it is much faster to run the algorithm on smaller images. Scaling using GDI+ produces very similar results with or without scaling. MagicScaler seems to produce slightly different images especially if the source image is of very high resolution, e.g. 15204x4942 or 8688x5792.

I have also tried using HybridScaleMode.Off but that was actually worse.

[Feature Request] Orientation

We've run into a few cases where we need to explicitly specify the orientation.

PROPOSAL:

ProcessorImageSettings {
   + Orientation Orientation { get; set; } 
}

// based on EXIF options
enum Orientation {
     Automatic, // default
     Horizontal,
     MirrorHorizontal,
     Rotate180 ,
     MirrorVertical,
     MirrorHorizontalAndRotate270,
     Rotate90 ,
     MirrorHorizontalAndRotate90,
     Rotate270
}

CropScaleMode.Max gives unexpected results (upscaling)

CropScaleMode.Max gives unexpected results (upscaling) when processing small images and only width or height is specified.

CropScaleMode.Max description:
"Preserve the aspect ratio of the input image. Reduce one or both of the output dimensions if necessary to preserve the ratio but never enlarge."

The following has been tested on version 0.10.3 and a small 100x66 jpeg image as source.

Example 1 - Both width and height is specified (OK)

MagicImageProcessor.ProcessImage(inputStream, outputStream, new ProcessImageSettings { Width = 200, Height = 200, ResizeMode = CropScaleMode.Max });

Expected: 100x66 (not enlarged)
Actual: 100x66 (not enlarged)

Example 2 - Only width is specified

MagicImageProcessor.ProcessImage(inputStream, outputStream, new ProcessImageSettings { Width = 200, ResizeMode = CropScaleMode.Max });

Expected: 100x66 (not enlarged)
Actual: 200x132 (enlarged)

Example 3 - Only height is specified

MagicImageProcessor.ProcessImage(inputStream, outputStream, new ProcessImageSettings { Height = 200, ResizeMode = CropScaleMode.Max });

Expected: 100x66 (not enlarged)
Actual: 303x200 (enlarged)

Have I misunderstood what CropScaleMode.Max is supposed to do?
Is there any alternative solution that prevents upscaling?

Also, it would be great if there was an alternative to CropScaleMode.Pad which doesn't upscale the image but only pads/fills the canvas. Is there any way that this can be achieved already?

[Feature Request] Linux support

is it possible to run this library under ubuntu?

Unfortunately, he receives a message under the implementation

`An unhandled exception occurred while processing the request.
TypeLoadException: Could not load type 'PhotoSauce.MagicScaler.Interop.WICImagingFactory2' from assembly 'PhotoSauce.MagicScaler, Version=0.8.4.0, Culture=neutral, PublicKeyToken=null'.
Unknown location

TypeInitializationException: The type initializer for 'PhotoSauce.MagicScaler.Interop.Wic' threw an exception.`

Can WebRSize work with remote URL's like Azure CDN?

In the configuration documents you say that an image folder is mandatory. Can we somehow leave the disk cache folder local and configure utilization of remote storage like Azure Blob Storage or any kind of CDN for image delivery?

If it is possible with the latest nuget release, what would the configuration look like? Basically what I want to achieve is something like this:

<img src="https://cdn.my.com/some/path/my-image?w=123" />

Thanks,
Drazen

ArgumentOutOfRangeException in ColorProfileReader.cs

ArgumentOutOfRangeException at

> src/MagicScaler/Core/ColorProfileReader.cs: 49
CreateDate = new DateTime(rdr.ReadBigEndianUInt16(), rdr.ReadBigEndianUInt16(), rdr.ReadBigEndianUInt16(), rdr.ReadBigEndianUInt16(), rdr.ReadBigEndianUInt16(), rdr.ReadBigEndianUInt16());

This is the image where the problem occurs.

magicscalerexception

I hope to be able to convert this image as well. Thank you.

Optimal approach to chain multiple transformations and persist output stream at each stage.

I'm looking at a better approach to persisting multiple image dimensions. We average about 100K ~12MP images daily, and keep a cached dimensions of each at 1200x, 600x and 240x. We are using MagicImageProcessor.Process to trasform these streams in memory. Is there a better approach that would use the PipelineProcess constructs? There aren't any examples, and the documentation leads me to believe that it composes multiple transforms but only one "output" stream at the end. Is that correct? Or is there a way to both capture the stream at each stage as well as let it become the source stream for an additional stage? And would any of it actually result in a more efficient (memory, compute) than our current approach of orchestrating the streams and multiple calls to Process? Many thanks. This library runs circles around the nightmare that was our old GDI approach.

Update example code on README.md

the code example on the readme is incorrect

the example code assumes the code is:

ProcessImage(string, FileStream, ProcessImageSettings)

but it is actually

ProcessImage(string, Stream, ProcessImageSettings)

as a first time user trying to figure it out, thats very frustrating

Exception trying to process any image

I've been trying to bootstrap a simple WebRSize site so that I can play with this project, as a potential replacement for our current imaging pipeline, but I'm having trouble actually processing images. I'm expecting (and hoping) that there's just a step missing somewhere in my setup, but I haven't found it yet.

Here's the exception stack trace:

[TypeInitializationException: Type constructor threw an exception.]
   PhotoSauce.Interop.Wic.IWICImagingFactory.CreateDecoderFromStream(IStream pIStream, Guid[] pguidVendor, WICDecodeOptions metadataOptions) +0
   PhotoSauce.MagicScaler.<>c.<Create>b__20_0(IStream stm) +72
   PhotoSauce.MagicScaler.WicImageContainer.createDecoder(Func`2 factory, T arg) +67
   PhotoSauce.MagicScaler.WicImageContainer.Create(Stream inStream, WicPipelineContext ctx) +200
   PhotoSauce.MagicScaler.ImageFileInfo.Load(Stream imgStream, DateTime lastModified) +485
   PhotoSauce.WebRSize.<<GetImageInfoAsync>b__0>d.MoveNext() +996
   System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +102
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +64
   System.Runtime.CompilerServices.TaskAwaiter`1.GetResult() +29
   PhotoSauce.WebRSize.<mapRequest>d__6.MoveNext() +2474
   System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) +102
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +64
   System.Web.TaskAsyncHelper.EndTask(IAsyncResult ar) +72
   System.Web.AsyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +429
   System.Web.HttpApplication.ExecuteStepImpl(IExecutionStep step) +50
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +163

Here's what I've done:

  • Created a new, empty ASP.NET Web Application project in VS 2017. Running on Windows 10, targeting .NET Framework 4.8.
  • Added WebRSize from NuGet.
  • Created an /images folder in my project, and dropped some sample image files in there.
  • F5 to run the app in IIS Express.
  • Fetch an image without processing (e.g. /images/foo.png) and it works.
  • Fetch any image with a w or h param to trigger processing, and get the stack trace above.

I've verified in applicationhost.config that the site is running in integrated mode, on a CLR v4 app pool.

Here's the WebRSize configuration from web.config:

  <webrsize>
    <diskCache enabled="false" path="/webrsizecache"/>
    <imageFolders>
      <add name="images" path="/images/" />
    </imageFolders>
  </webrsize>

As far as I can tell, WIC is built into Windows these days, so there's no installer I'm forgetting to run. But this really feels like a problem where there's just some piece missing from my project / environment. Any idea what's going on?

"PhotoSauce.MagicScaler.Interop.Wic" type initial value setting item throws an exception.

PhotoSauce.MagicScaler.WicImageDecoder.<>c.b__2_0(String fn)
PhotoSauce.MagicScaler.WicImageDecoder.createDecoder[T](Func`2 factory, T arg)
PhotoSauce.MagicScaler.WicImageDecoder.Load(String fileName, PipelineContext ctx)
PhotoSauce.MagicScaler.MagicImageProcessor.ProcessImage(String imgPath, Stream outStream, ProcessImageSettings settings)

int dHeight = 1920;
int dWidth = 1374;
int quality = 70; 
var settings = new ProcessImageSettings()
            {
                Width = dWidth,
                Height = dHeight,
                JpegQuality = quality
            };

            using (var output = new FileStream(dFile, FileMode.Create))
            {
                MagicImageProcessor.ProcessImage(sFile, output, settings);
            }

Work on win7 32-bit system
I can't understand the problem with this error

Exception when opening multipage tiff files on Windows Server 2012 R2 using `ProcessImage(Stream imgStream, Stream outStream, ProcessImageSettings settings)`

When opening multipage tiff files on a Windows Server 2012 R2, we receive an exception only when trying to open multipage tiff files. Single-page tiff files from the same source open fine, and both single and multi-page tiff files open with Windows Photo Viewer on the same server. Both files also open with TiffBitmapDecoder without issue.

The same tiff files can be opened on a Windows 10 1703 computer without issue using the same calling code.

Calling code:

        var source = FileStream.OpenRead("demo.tif");
        var settings = new ProcessImageSettings()
        {
            FrameIndex = page,
            // scale to thumbnail size
            Width = newImageSize.Width,
            Height = newImageSize.Height,
            // Crop is needed to compensate for the nonuniform pixel aspect ratio
            Crop = new gdi.Rectangle(0, 0, fullFrame.PixelWidth, fullFrame.PixelHeight),
            // Add the DPI metadata to the desired result
            DpiX = newImageDpi.X,
            DpiY = newImageDpi.Y,
            // Lanczos results in sharpness at the expense of ringing
            Interpolation = InterpolationSettings.Lanczos
        };

        settings.SaveFormat = FileFormat.Png;

        using (var msResult = new MemoryStream())
        {
            // Exception here.
            MagicImageProcessor.ProcessImage(source, msResult, settings);

            // SNIP: use msResult
        }

Exception:

System.IO.InvalidDataException: Image format not supported.  Please ensure the input file is an image and that a WIC codec capable of reading the image is installed. 
	---> System.Runtime.InteropServices.COMException: The component cannot be found. (Exception from HRESULT: 0x88982F50)

   at PhotoSauce.MagicScaler.Interop.IWICImagingFactory.CreateDecoderFromStream(IStream pIStream, Guid[] pguidVendor, WICDecodeOptions metadataOptions)

   at PhotoSauce.MagicScaler.WicDecoder.<>c__DisplayClass20_0.<.ctor>b__0()

   at PhotoSauce.MagicScaler.WicDecoder.checkDecoder(Func`1 factory)

   --- End of inner exception stack trace ---

   at PhotoSauce.MagicScaler.WicDecoder.checkDecoder(Func`1 factory)

   at PhotoSauce.MagicScaler.WicDecoder..ctor(Stream inFile, WicProcessingContext ctx)

   at PhotoSauce.MagicScaler.MagicImageProcessor.ProcessImage(Stream imgStream, Stream outStream, ProcessImageSettings settings)

Every multi-page tiff I have tried seems to trigger the issue, but the one I was using is http://s000.tinyupload.com/index.php?file_id=01002933363618854391

Fractional scaling impossible?

As a follow up to #42, I started yesterday cooking my own AVX2 image downsampler that uses SoA.

I still have a long way to go learning all the SIMD optimizations, but being a former Commodore Amiga assembly hacker, this is a lot of fun, and all in C#, amazing. I learned so much from your code!

Now when comparing my results with MagicScaler's, I noticed that I could only set the output image size in integer pixels.

So very smooth downscaling, in fractional pixel steps, doesn't seem possible?

Fractional scaling allows creating small thumbnails that retain the exact aspect ratio of the original, or to create a movie with very slowly scaling images with "pixel jumping".

This requires just a tiny change to the algoritm. But of course, this is not a feature people would need a lot ;-)

PS: I'm using the following MagicScaler settings for comparing:

new ProcessImageSettings
				{
					Width = dstWidth,
					Height = dstHeight,
					Sharpen = false,
					Interpolation = InterpolationSettings.Lanczos,
					HybridMode = HybridScaleMode.Off
				}

Using these settings, my current SoA SIMD implementation is a bit faster on my machine, but I guess magic scaler does a lot more internally. I also guess MagicScaler converts back to sRGB when outputting, something I don't do, since we need the images in a linear space. I also drop the alpha channel when processing an opaque image like JPEG.

Obviously when enabling hybrid scaling, and using the default interpolation setting, MagicScaler is 4 times faster, and because of the sharpen, actually looks better ;-) I'm using images like this one for testing

SoA for convolution?

First of all thanks for this amazing library! You really are an expert in this underestimated field, and sharing your knowledge in the form of code is invaluable.

I am contributing to SkiaSharp, and a high quality resampler is on my list. First I wanted to wrap the AVIR library, but your code seems very interesting too.

I was reading through your code, and was pleasantly surprised that you where already using the new SIMD intristics.

As far as I understand, you are using an AoS algorithm, working directly on the 4, 3 or 1 channel color structs, duplicating the kernel weights to that dimension

I was wandering, did you also try to implement an SoA version of this, as is done on the CoreCLR raytracer intrisric example?

Furthermore, I think bandwidth could be decreased by working on 16-bit floats as input, but at first sight .NET SIMD doesn't support that yet. Maybe 16-bit integers would also work...

Other thinks I would like to experiment with are streaming writes, more cache friendly image transposition, ...

I haven't worked with SIMD since SSE2, so my low level knowledge is very outdated, so sorry if none of this makes sense on modern CPUs ;-)

How to resize multiple image folder?

My project has many different image directories.
Ex:
/assets/images/product
/assets/images/gallery
/assets/images/user

How to configure to use? Because currently you only allow one specified directory.

PixelSource in processor has destination's dimensions

Hello, I'm trying to create an image minify function and I ran across this issue where imageProcessor.PixelSource.Width does not report original image size anymore.

Background: I want to write a function that creates thumbnails but never enlarges images. Given an NxN box it needs to resize image to fit (preserving aspect ratio), but keep images smaller than NxN pixels intact (pass-thru).

One way to accomplish this would be to create image processor and examine size of input image before processing it. Alas, I can't do that currently with single pass processing. Internal Context has original image dimensions, but it's not accessible. PixelSource looks like destination rather than source (it reports requested width, not original). I'm stuck.

Request: make PixelSource reflect original image properties. Add PixelDestination with resulting image size and other properties.

Thanks,
MK

ArgumentException when NearestNeighbor and scale-down

System.ArgumentException "Value does not fall within the expected range."
when NearestNeighbor and scale-down, maybe.

  • version: MagicScaler 0.8.0-beta3

  • StackTrace

   at PhotoSauce.MagicScaler.Interop.IWICBitmapFrameEncode.WriteSource(IWICBitmapSource pIBitmapSource, WICRect prc)
   at PhotoSauce.MagicScaler.WicEncoder.WriteSource(WicProcessingContext ctx) in C:\gitlocal\photosauce\src\MagicScaler\WIC\WicCodec.cs:line 166
   at PhotoSauce.MagicScaler.MagicImageProcessor.executePipeline(WicProcessingContext ctx, Stream ostm) in C:\gitlocal\photosauce\src\MagicScaler\Magic\MagicImageProcessor.cs:line 193
   at PhotoSauce.MagicScaler.MagicImageProcessor.ProcessImage(Stream imgStream, Stream outStream, ProcessImageSettings settings) in C:\gitlocal\photosauce\src\MagicScaler\Magic\MagicImageProcessor.cs:line 61
  • sample code
    // For example, inStream is #6 issue's picture.
    var setting = new ProcessImageSettings();
    setting.Width = 64;
    setting.Height = 64;
    setting.Interpolation = InterpolationSettings.NearestNeighbor;
    MagicImageProcessor.ProcessImage(inStream, outStream, setting); 

Add Metadata to ImageFileInfo & FrameInfo

It would be useful to be able query and enumerate over the metadata of an image from ImageFileInfo & FrameInfo without decoding the image.

Proposed API for discussion:

namespace PhotoSauce.MagicScaler 
{
  public class Metadata
  {
      public IEnumerable<ExifMetadataEntry> Exif { get; }
  }

  public readonly struct ExifMetadataEntry
  {
      public ExifTag Tag { get; }
      public object Value { get; }
  }

  public class WicMetadata : Metadata 
  {
       public WicMetadataEntry? QueryFirstOrDefault(string path);
  }

  public readonly struct WicMetadataEntry
  {
      public string Path { get; }
      public object Value { get; }
  }

   public class ImageFileInfo 
   {
        + public Metadata Metadata { get; }
   }

   public readonly struct FrameInfo
   {
        + public Metadata Metadata { get; }
   }
}

Example Usage:

var info = new ImageFileInfo(stream);

if (info.Metadata is WicMetadata wicMetadata) // when processed by WIC
{
     var gifDisposalMethod = wicMetadata.QueryFirstOrDefault("/grctlext/Disposal");
}

Example paths include:

/app0/{ushort=0}
/{ushort=40961}      
/grctlext/Disposal
/imgdesc/Top

Exif tag reference:

https://github.com/carbon/Media/blob/master/src/Carbon.Media.Metadata/Exif/ExifTag.cs

Please remove limitation on output stream

I am trying to write directly to Azure Blob storage write stream and am getting an error that:

Output Stream must allow Seek and Write_

There's no reason that you need to Seek when writing. Please remove this limitation to avoid having to buffer the write.

Smart crop

It would be nice to have a "smart crop" feature. The main function of those kind of apps is generating thumbnails, so the resize/crop part functionality is the most important I guess.
It would be nice to have a smart resize/crop feature that for example crops around where there is more "pixel density" in an image, or face recognition for example if there are people involved.

PS. your software and "server" are wonderful

Preserve color space and color profiles

Hello everyone,
I have an image in CMYK space. When I resize this image to a smaller size, it will be forced to RGB space and wrong color profiles.
I have already read this issue: #13, and my settings are:

MagicImageProcessor.ProcessImage(inputStream, output, new ProcessImageSettings
{
	Width = maxSize,
	Height = maxSize,
	ColorProfileMode = ColorProfileMode.Preserve,
	HybridMode = HybridScaleMode.FavorSpeed,
	ResizeMode = CropScaleMode.Max,
	SaveFormat = FileFormat.Jpeg,
	JpegQuality = quality
});

Color profile of my source image is "U.S. Web Coated (SWOP) v2" (CMYK). And although I have already installed it on my machine (https://i.imgur.com/ICpf01d.png), but my result image alway in uRGB.

My images:
Source: https://drive.google.com/open?id=1ciGXciq3GWzB8XAPriFcKwP3fuAgIuJF
Result: https://drive.google.com/open?id=1SuJQV53MNcHA3H86dgaq9-6dpBbQp8E_

Do you have any idea?

Input&output image information

It would be extremely handy to somehow retrieve the source and destination image information, like it's dimensions, color depth, etc. right after processing is finished.
Right now we have to query them both separately with System.Drawing.
I suppose this information is available anyway.
What do you think about it?

Ability to add custom Image Decoders

Proposed interface:

AddDecoder(IDecoder decoder)

interface IDecoder 
{
   bool CanDecode(ReadOnlySpan<byte> headerBytes);
   IPixelSource Decode(Stream data);
}

Questions:

  • Should pluggable decoders be prioritized over WIC?
  • Should we support color profiles & metadata?
  • Should we support any additional pixel formats within the decoded pixel source or should the decoder be responsible for converting to Bgra32, Bgr24, or Gray8 pixels?

[Feature Request] WebP image format support

I'd love to see some support for WebP image format. It's often used to serve smaller image sizes for browsers that support it. I have no idea how difficult it would be to add something like this. Any thoughts on this?

Also, related to this are image formats like JPEG 2000 (JP2) or JPEG XR (JXR). Any thoughts on supporting those?

Some links on the support of those formats in web browsers:

Support well-known wide gamut color space output

A lot has changed in color management since MagicScaler was initially designed, and now is a good time to revisit the original design choices.

For MagicScaler's initial design, the obvious option for dealing with images in various color spaces was to simply convert them all to sRGB. This was the most compatible option when considering inconsistencies in software support for color management, and it rarely made any visible difference to the viewer since the average user's display couldn't reproduce colors outside the sRGB gamut.

However two very important things have changed in recent years:

  1. Wide gamut displays are now commonplace. All IOS devices and most high-end Android devices now ship with displays that support the full DCI-P3-D65 gamut, and even mid-range PC and laptop displays now support a gamut larger than sRGB.
  2. Software/device support for color management has become much more widespread and consistent. Naturally, devices shipping with wide gamut displays also ship with software that supports color management. Modern web browsers and most other software now also support at least basic color management.

This test page shows some examples where the sRGB color space limits color reproduction in real-world images when shown on a wider-gamut display: https://webkit.org/blog-files/color-gamut/comparison.html

Now that software is more likely to support alternate color spaces correctly and displays are more likely to be able to show a wider gamut, I will be revising the way MagicScaler handles images encoded in alternate color spaces. Starting from v0.11, MagicScaler will output images in Adobe RGB or Diplay P3 instead of sRGB if the source uses a wider gamut color space. Its rules for substitution are as follows:

  • If the source is Adobe RGB, the output will also be Adobe RGB.
  • If the source is CMYK, the output will be Adobe RGB. Adobe RGB tends to match the gamut of CMYK printers more closely than either sRGB or Display P3.
  • If the source is Display P3 or any other color space with a wider gamut than sRGB, the output will be Display P3. P3 is the widest gamut supported by most current hardware, and color spaces with much wider gamuts (e.g. ProPhoto or Rec.2020) often result in posterization when used in 8-bit-per-channel images.
  • If the source is sRGB, the source has no color profile, or the output format does not support embedding a color profile, the output will be sRGB. This category still represents the largest amount of content on the web, and nothing will change for those images.

As time goes on, you can expect to see more Display P3 images in the wild, and MagicScaler will preserve the colors in those images accurately.

This new behavior will be enabled under both the (default) ColorProfileMode.Normalize and ColorProfileMode.NormalizeAndEmbed settings. For users who wish to or need to restore the old behavior of converting all output to sRGB, a new ColorProfileMode.ConvertToSrgb setting value will be introduced.

In order to maximize compatibility and minimize output file size impact, MagicScaler will embed compact ICC profiles compatible with the well-known color spaces it uses rather than the profile(s) from the source image. For more information, see my Compact ICC Profile Collection.

Trying to resize gif with animation

Hi I am trying to resize a gif while keeping the animation.

I get a resized image that is no longer animated.

How can this be fixed?

Kind Regards,
Lyubomir

Support YCbCr IPixelSources

We're decoding some images directly into YCbCr planes (via ffmpeg and libheif).

It would be handy to avoid the RGB conversions and pass these buffers in directly to MagicScaler.

e.g.

public class YCbCrPixelSource(IntPtr yPlanePointer, IntPtr cbPlanePointer, IntPtr crPlanePointer, ChromaSubsampling chromaSubsampling) { } 

Exception when running in container

Hi! I have created a image scaling application in .NET Core 2 which works just fine when running it like a console application. When i run it in Docker it throws errors:

"Could not load type 'PhotoSauce.MagicScaler.Interop.WICImagingFactory2' from assembly 'PhotoSauce.MagicScaler, Version=0.8.4.0, Culture=neutral, PublicKeyToken=null'."

Not sure if i am doing something wrong but i would assume that i would not need to modify my code to get it to work in a container?

EDIT: I have also tried the same code on Mac/Docker with the same result as with PC/Docker.

MagicScaler converts TIF to JPG when using FileFormat.Auto

Like the title says, when I'm resizing some TIF files I have they are converted to JPG in FileFormat.Auto mode. PNGs are not converted, they remain as PNG, so I assume my code/settings are OK.

Just tested Image.RawFormat output for the files before resizing:

[ImageFormat: b96b3cb1-0728-11d3-9d7b-0000f81ef32e] tif
[ImageFormat: b96b3caf-0728-11d3-9d7b-0000f81ef32e] png
[ImageFormat: b96b3cae-0728-11d3-9d7b-0000f81ef32e] jpg

File headers are also OK before resizing, but after resize the TIF headers have changed to JPG format, and the new file size of the TIFs also match what you would expect after a JPG conversion.

There were some updates of some associated MS system files on NuGet after I installed MagicScaler, I wonder if they can be the cause?

v.0.10 Change Log / Roadmap

Changes in v0.10

Breaking Changes

  • The behavior of CropScaleMode.Max has been changed so that it no longer upscales the source image if the target size is greater #30. The new CropScaleMode.Contain replaces the original functionality of Max. When downscaling, the two modes have identical behavior.

  • MagicImageProcessor.EnableSimd is now marked obsolete. Forcing the setting on or off has always added risk of serious performance consequences, and that will only become more true now that MagicScaler is taking advantage of .NET Core's first-class support for SIMD hardware intrinsics.

  • XMP Orientation tags are no longer read by default. Very little software supports reading Orientation from XMP tag data, and consequently it is very rarely used. Because XMP metadata can be very large in some images, parsing it to look for Orientation comes at a cost. Note that Exif Orientation is not affected by this change and is the most common/preferred way of tagging images that require Orientation correction. If you need to support XMP Orientation tags as well, you can force the old behavior back on with MagicImageProcessor.EnableXmpOrientation.

  • IPixelTransform and associated transform implementations have been moved to the PhotoSauce.MagicScaler.Transforms namespace.

  • ColorMatrixTransform now interprets matrices in RGB column-major order instead of BGR column-major order. The pre-defined matrices on the ColorMatrix class have been re-ordered to maintain their old behavior. If you have any custom matrices defined, you will need to swap the first and third rows and first and third columns to maintain compatibility. This change makes the matrix definitions more consistent with the way they are defined in other software and should improve usability going forward.

  • ImageFileInfo constructors have been replaced with a static factory method. new ImageFileInfo(path/stream/span) is replaced by ImageFileInfo.Load(path/stream/span). Overloads and parameters are unchanged.

  • ProcessImageSettings.IndexedColor and ProcessImageSettings.ScaleRatio properties have been removed. The values of these properties were only valid after settings were calculated from an input image, and their values can easily be determined from other available properties.

  • Pipeline instrumentation is no longer enabled by default. Because there is some overhead associated with the collection of PixelSourceStats, it pays to disable it when the stats are not being consumed. You can re-enable instrumentation with MagicImageProcessor.EnablePixelSourceStats if you are using it.

  • MagicImageProcessor.ExecutePipeline has been replaced with a new WriteOutput instance method on ProcessingPipeline. Additionally, ProcessingPipeline.AddTransform has been modified to return the ProcessingPipeline instance, so it can be used in a fluent manner. For example:

var settings = new ProcessImageSettings { SaveFormat = FileFormat.Png };

using var srcimage = new TestPatternPixelSource(640, 480, PixelFormats.Bgr24bpp);
using var pipeline = MagicImageProcessor.BuildPipeline(srcimage, settings);
using var outfile = File.Create(@"c:\img\sepiabars.png");

pipeline
    .AddTransform(new ColorMatrixTransform(ColorMatrix.Sepia))
    .AddTransform(new OrientationTransform(Orientation.Transpose))
    .WriteOutput(outfile);

Minor API Changes

  • Added CropBasis property to ProcessImageSettings, which allows the Crop to be expressed in dimensions other than the input image dimensions. For example CropBasis = new Size(100,100) makes the Crop offset and dimensions behave as percentages.

  • Modified the Crop settings behavior to allow for zero or negative dimensions. Zero width or height will be interpreted as all the remaining image. Negative width or height is interpreted as an offset from the right or bottom of the image. For example Crop = new Rectangle(100, 100, 0, 0) will crop the left and top 100 pixels from the image, keeping whatever remains. Crop = new Rectangle(100, 100, -100, -100) will crop 100 pixels from each side.

  • ImageFileInfo.Frames now returns IReadOnlyList<FrameInfo> instead of FrameInfo[].

  • ProcessImageSettings.HybridScaleRatio now returns int rather than double.

  • PixelSourceStats.PixelCount now returns long rather than int.

Performance Improvements

  • Lots of new SIMD implementations, using the new .NET Core x86 hardware intrinsics. Perf gains range from 15-600% in the pixel-mashing code. Real-world gains are limited by other parts of the pipeline and are more pronounced on larger images.

  • Reduced GC allocations for both internal pipeline components and interop marshaling. For a typical JPEG resize operation, GC allocations are down by ~75%.

  • Replaced WIC YCbCr<->BGR converters with faster and more flexible internal implementations.

  • Replaced WIC Fant hybrid scaling with faster internal box scaler.

  • Improved efficiency of internal pixel buffers by using all available space in the rented buffer rather than the minimum required/requested.

  • Orientation correction is now supported within the planar pipeline. Previously, orientation correction would force processing in RGB mode.

  • Orientation correction will now be delayed to post-crop/resize if it is profitable to do so.

  • BLAKE2 hashing is now used for color profile matching and for cache file naming. It is both faster and more memory efficient than the MD5 and SHA2 hashing previously used.

Image Quality Improvements

  • In planar mode, chroma interpolation now matches that used for luma. Previously, Hermite interpolation was used for scaling chroma planes when the luma plane was scaled with a more expensive filter. Although this gives good results on the majority of images, it could lead to quality loss when chroma has high-frequency data.

  • Images Exif-tagged with ColorProfile=Uncalibrated/InteropIndex=R03 are now interpreted as Adobe RGB, as is done in Photoshop and many other apps. Previously only the non-standard ColorProfile=Adobe RGB value was recognized.

New APIs

  • The new IImageContainer interface allows for definition of custom (non-WIC) image containers.

  • IImageFrame allows for user-defined image frames within an IImageContainer.

  • IYccImageFrame allows for user-defined planar image frames to be processed within the planar pipeline. #31

  • MagicImageProcessor.ProcessImage and MagicImageProcessor.BuildPipeline have new overloads that accept IImageContainer.

Non-behavioral Changes

  • The PhotoSauce license has been changed from Apache 2.0 to MIT. They are equally permissive licenses, but MIT is simpler.

  • The libraries are now strong named. #24

  • Moved documentation to DocFX https://docs.photosauce.net

  • SourceLink is now used for PDB symbol sources, replacing GitLink.

  • Added nullable annotations for a better C# 8 dev experience.

  • Dropped netcoreapp1.x and netstandard1.x support.

Internal Changes

  • Replaced WIC Flip, Rotate, and Crop transforms with internal implementations.

  • Removed System.Drawing.Color shims and replaced color string parsing with an internal implementation.

  • Added additional abstractions to WIC components as part of the larger effort toward a WIC-independent (and therefore Linux-compatible) pipeline.

Future Roadmap

These changes are slated for post v0.10.

  • Replace WIC LUT-based ICC profile processing with internal implementation. This will result in improved performance and image quality while removing yet another WIC dependency.

  • Add animated GIF support. Presently, only individual GIF frames can be extracted, and only if the frames don't depend on a previous frame.

  • Add support for plug-in codecs. Building on the IImageContainer support, codecs will be able to register with the pipeline for auto format detection and decoding as well as encoding. #22 #19

CropScaleMode.Max upscales the image ?

As I thought CropScaleMode.Max only downscales images if needed.
Looks like it also upscales so name is a little misleading.

Is there a mode that only downscales if needed?

How to determine future scaled image size

Suppose I have an image file and I need to scale it to some targetWidth (or targetHeight). There must be something like that in the code but I couldn't find it. I found how to get image size with ImageFileInfo. But there's definitely should be the code which calculates the resulting size of the image. Please give me a hint.

I can write the calculation myself but if there's existing one, I'd like to be on the same page.

ProcessImage w/ decoded source (Custom pixel accessor)

We do a lot of upstream processing of images (i.e. decoding, applying filters, etc).

It would be helpful to have an overload to ProcessImage that can provide decoded pixels directly to the processor.

For example:

interface IBitmapSource
{ 
  PixelFormat PixelFormat { get; }
  ColorPalette Palette { get; }
  Size Size { get; } 
  CopyPixels(Rectangle rect, Span<byte> buffer)
} 

public static void ProcessImage(IBitmapSource source, Stream output, ProcessImageSettings s);

[Feature Request] Ignore Exif Orientation

I'm sure this must be a config issue, but I can't seem to make the EXIF Orientation to be honored before scaling, or if not honored, at least copied to the output thumbnail.

Here is the invocation. This works great for all images without EXIF Orientation present. IrfanView confirms the EXIF Orientation is present on the full-size input.

ProcessImageResult processImageResult =MagicImageProcessor.ProcessImage(
imageFileStream,
thumbnailStream,
new PhotoSauce.MagicScaler.ProcessImageSettings
{
Width = thumbnailWidth,
Height = thumbnailHeight,
ResizeMode = CropScaleMode.Max,
});

Are there EXIF-only modes in MagicScaler?

This application is a server side thumbnail generator for a clients' image repository.
I have updated to 0.8.4 with no change.
Using .Net Framework 4.6.2.

BitsPerPixel

Hi,
Is there anyway i can get BitsPerPixel of the Image loaded and also applied on the output Image? Any help would be appreciated.

Thanks

Calling MagicImageProcessor.ProcessImage() a second time results in a COM error

The byte arrays are for the separate images and are not related (in case that's relevant).

The first run through the code seems to work perfectly. The second run through throws an InvalidComObjectException.

The error: System.Runtime.InteropServices.InvalidComObjectException: 'COM object that has been separated from its underlying RCW cannot be used.'

The stack trace from the call to MagicImageProcessor.ProcessImage():

   at System.StubHelpers.StubHelpers.GetCOMIPFromRCW(Object objSrc, IntPtr pCPCMD, IntPtr& ppTarget, Boolean& pfNeedsRelease)
   at PhotoSauce.Interop.Wic.IWICImagingFactory.CreateDecoderFromStream(IStream pIStream, Guid[] pguidVendor, WICDecodeOptions metadataOptions)
   at PhotoSauce.MagicScaler.WicImageDecoder.<>c.<Load>b__3_0(IStream stm)
   at PhotoSauce.MagicScaler.WicImageDecoder.createDecoder[T](Func`2 factory, T arg)
   at PhotoSauce.MagicScaler.WicImageDecoder.Load(Stream inStream, PipelineContext ctx)
   at PhotoSauce.MagicScaler.MagicImageProcessor.ProcessImage(Stream imgStream, Stream outStream, ProcessImageSettings settings)

The code used:

public (byte[] bytes, int width, int height) CompressBitmapImage(byte[] uncompressedBitmapBytes)
{
  byte[] compressedBytes = new byte[] { };

  using(var inputStream = new MemoryStream(uncompressedBitmapBytes))
  using(var outputStream = new MemoryStream())
  {
      var result = MagicImageProcessor.ProcessImage(inputStream, outputStream, new ProcessImageSettings() { SaveFormat=FileFormat.Jpeg, JpegQuality = 80 });
      outputStream.Position = 0;

      using (BinaryReader binaryReader = new BinaryReader(outputStream))
      {
          compressedBytes = binaryReader.ReadBytes((int)outputStream.Length);
      }

      return (compressedBytes, result.Settings.Width, result.Settings.Height);
  }
}

The intention of the code is to compress an image. What am I doing wrong?

This is running in a .NET Core 3.1 project under Windows 10.

Overlay/ blur partial part

Is there a way to apply a partial overlay or blue partial part in the image? I've a unique case where we have to hide/ cut out a specific area in an image. Currently we are using System.Drawing to draw an overlay with partial transparency. However, it'll be great if we can move away from it. May be gaussian blur can be used as a base as long as we can specify x/ y point and radius.

poor performance

Encouraged by the benchmark I tried MagicScaler using C# and Net 4.7.2.
By measuring time with Stopwatch, I got times worse by almost 40% than libvips CLI.

Image is JPG 24mpx.
Timings:
libvips: 270ms
MagicScaler: 380ms

The operation is simply resizing the image:

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
var settings = new ProcessImageSettings { Height=400 };
var outStream = new FileStream("out.jpg", FileMode.Create);
MagicImageProcessor.ProcessImage("in.jpg", outStream, settings);
outStream.Dispose();
stopwatch.Stop();

Am I doing something wrong?

Different result when resizing in another thread

We are using MagicScaler in a WPF application to resize images and encountered a strange problem:

Depending on whether the call to ProcessImage happens on the main thread or in a background thread*, the result image is slightly different. Visually they are equal, you cannot see any difference, but the file size is different by a few bytes (86 000 vs 85 728]. This wouldn't be a problem in itself, but what is really strange is that when we analyze the two images using the Cognitive Services from Microsoft, there is a face detected in the image generated in the main thread, but no face detected in the image generated in the background thread.

Do you have any explanation or idea what is going on here? I can provide the images if that helps.

* Actually, it's even more complicated: It depends on the thread the first call to ProcessImage the app makes happens on. All subsequent calls seem to somehow be executed on the same thread, no matter which thread calls it. Is that on purpose?

Dynamic specify Image paths

It would be nice to be able to specify image paths dynamically from code.
I'm trying to implement WebRSize in my custom CMS which has plugins and widgets which will have their own image folders.
I have global application variables and plugins variables read at application start as static variables, so for example I could pass the folders to the module from there.
At the moment it seems this functionality is not provided and the only way is static in web config right?

[Question] Is there a way to copy the Exif data from 1 image to the output of a transform?

Currently have a use case where I want to resize some images if they exceed a certain size, however, we would also like to preserve the exif metadata of the original image into the output of the new image.

I noticed in the documentation there was a MetadataNames property, however I'm struggling to find any examples of how to get this property to work. Is this type of functionality meant to be resolved using the above mentioned property? is there another way perhaps of achieving this?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.