Skip to content

Add progressive JPEG encoder#2740

Merged
JimBobSquarePants merged 16 commits into
SixLabors:mainfrom
ardabada:progressive-jpeg-encoder
Oct 12, 2024
Merged

Add progressive JPEG encoder#2740
JimBobSquarePants merged 16 commits into
SixLabors:mainfrom
ardabada:progressive-jpeg-encoder

Conversation

@ardabada
Copy link
Copy Markdown
Contributor

@ardabada ardabada commented May 21, 2024

Prerequisites

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

Description

This PR adds progressive JPEG encoder (see #10 and #449).
Implementation adapted from https://github.com/vstroebel/jpeg-encoder

No tests added yet. Restart interval also should be added.
Please take a look if it makes sense to you.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 21, 2024

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
2 out of 3 committers have signed the CLA.

✅ ardabada
✅ JimBobSquarePants
❌ Alexandr Ivanov


Alexandr Ivanov seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@JimBobSquarePants
Copy link
Copy Markdown
Member

Wow! Thanks @ardabada

Tests are failing just now due to a minor StyleCop issue (trailing space) would it be possible to fix that and add some additional unit tests to verify the encoded output?

@br3aker would you be able to help review this? It reuses most of the work you implemented.

@ardabada
Copy link
Copy Markdown
Contributor Author

Hi @JimBobSquarePants, I've added a test. The test suite seems quite complicated to me, and I need more time to understand how it works before I can write more complex, byte-level tests.

I've also split the WriteBlock method into two separate methods: WriteDc and WriteAcBlock. This change was necessary because the progressive encoder requires different values in the for loop when writing AC components. Additionally, I've updated the final if statement for writing the end of the block to check runLength > 0 instead of lastValuableIndex, since runLength is reset when encountering a non-zero coefficient. I hope this makes sense. CC @br3aker

I'd like to add a restart interval as well, but it seems to require more changes. WriteMarker is currently in JpegEncoderCore, and it should be called from HuffmanScanEncoder, which isn't an ideal solution. Therefore, I will postpone this for now.

@JimBobSquarePants
Copy link
Copy Markdown
Member

Hi @JimBobSquarePants, I've added a test. The test suite seems quite complicated to me, and I need more time to understand how it works before I can write more complex, byte-level tests.

I've also split the WriteBlock method into two separate methods: WriteDc and WriteAcBlock. This change was necessary because the progressive encoder requires different values in the for loop when writing AC components. Additionally, I've updated the final if statement for writing the end of the block to check runLength > 0 instead of lastValuableIndex, since runLength is reset when encountering a non-zero coefficient. I hope this makes sense. CC @br3aker

I'd like to add a restart interval as well, but it seems to require more changes. WriteMarker is currently in JpegEncoderCore, and it should be called from HuffmanScanEncoder, which isn't an ideal solution. Therefore, I will postpone this for now.

Thanks for the updates! I've no issue with duplicate WriteMarker calls for now if you find it easier to go that way..

I'll pull down your code ASAP and have a good read through. Maybe I can help write tests.

@JimBobSquarePants
Copy link
Copy Markdown
Member

Hi @ardabada apologies for the slow response. The code all looks great so far!

I think you can either

  1. Add a duplicate WriteMarkerHeader method to the HuffmanScanEncoder.
  2. Make WriteMarkerHeader static and internal passing the stream as a parameter.

I'm happy with whatever approach you take.

For tests I would keep it high level and simply encode/verify the output against expected results.

[WithFile(TestImages.Png.BikeGrayscale, nameof(LuminanceEncodingSetups), PixelTypes.L8)]
[WithFile(TestImages.Jpeg.Baseline.Cmyk, nameof(CmykEncodingSetups), PixelTypes.Rgb24)]
[WithFile(TestImages.Jpeg.Baseline.Ycck, nameof(YcckEncodingSetups), PixelTypes.Rgb24)]
public void EncodeProgressive_DefaultNumberOfScans<TPixel>(TestImageProvider<TPixel> provider, JpegEncodingColor colorType, int quality, float tolerance)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you can add an additional test setting the restart interval this would be good to merge! 👍

@JimBobSquarePants
Copy link
Copy Markdown
Member

JimBobSquarePants commented Jul 26, 2024

@ardabada I pulled down your code and added the following test. It appears that writing anything other than the default value causes our decoder to fail.

[Theory]
[WithFile(TestImages.Png.CalliphoraPartial, nameof(NonSubsampledEncodingSetups), PixelTypes.Rgb24)]
[WithFile(TestImages.Png.CalliphoraPartial, nameof(SubsampledEncodingSetups), PixelTypes.Rgb24)]
[WithFile(TestImages.Png.BikeGrayscale, nameof(LuminanceEncodingSetups), PixelTypes.L8)]
[WithFile(TestImages.Jpeg.Baseline.Cmyk, nameof(CmykEncodingSetups), PixelTypes.Rgb24)]
[WithFile(TestImages.Jpeg.Baseline.Ycck, nameof(YcckEncodingSetups), PixelTypes.Rgb24)]
public void EncodeProgressive_CustomNumberOfScans<TPixel>(TestImageProvider<TPixel> provider, JpegEncodingColor colorType, int quality, float tolerance)
where TPixel : unmanaged, IPixel<TPixel>
{
    using Image<TPixel> image = provider.GetImage();

    JpegEncoder encoder = new()
    {
        Quality = quality,
        ColorType = colorType,
        Progressive = true,
        RestartInterval = 7
    };
    string info = $"{colorType}-Q{quality}";

    using MemoryStream ms = new();
    image.SaveAsJpeg(ms, encoder);
    ms.Position = 0;

    // TEMP: Save decoded output as PNG so we can do a pixel compare.
    using Image<TPixel> image2 = Image.Load<TPixel>(ms);
    image2.DebugSave(provider, testOutputDetails: info, extension: "png");

    ImageComparer comparer = new TolerantImageComparer(tolerance);
    image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "jpg");
}

Here's an encoded jpeg which seems to be decodable by browsers, Windows, and System.Drawing.
EncodeProgressive_CustomNumberOfScans_Rgb24_CalliphoraPartial_Rgb-Q80

And here's how our decoder sees it.
EncodeProgressive_CustomNumberOfScans_Rgb24_CalliphoraPartial_Rgb-Q40

I did find an issue with DRI marker writing where we were writing too many bytes to the stream (see fixed version below)

/// <summary>
/// Writes the DRI marker
/// </summary>
/// <param name="restartInterval">Numbers of MCUs between restart markers.</param>
/// <param name="buffer">Temporary buffer.</param>
private void WriteDri(int restartInterval, Span<byte> buffer)
{
    if (restartInterval <= 0)
    {
        return;
    }

    this.WriteMarkerHeader(JpegConstants.Markers.DRI, 4, buffer);

    buffer[1] = (byte)(restartInterval & 0xff);
    buffer[0] = (byte)((restartInterval >> 8) & 0xff);
    this.outputStream.Write(buffer, 0, 2); // See explicit offset and length.
}

However, I think the issue is with the HuffmanScanDecoder. I've done some debugging and it's finding the markers well enough, perhaps something is not getting reset properly?

@br3aker If you have any time to help out here it would be greatly appreciated.

@ardabada
Copy link
Copy Markdown
Contributor Author

ardabada commented Aug 2, 2024

Hi, @JimBobSquarePants, sorry for such long silence. I am currently looking into the JpegBitReader, looks like bitstream adjustment to start on the next byte boundary is not handled properly. As a suggestion, we can take restart interval in a separate PR

@JimBobSquarePants
Copy link
Copy Markdown
Member

Hi, @JimBobSquarePants, sorry for such long silence. I am currently looking into the JpegBitReader, looks like bitstream adjustment to start on the next byte boundary is not handled properly. As a suggestion, we can take restart interval in a separate PR

No worries at all and thanks for replying. I'd like to get the bug in the reader fixed if possible before merging so that we don't forget. Would you be happy to investigate?

@ardabada
Copy link
Copy Markdown
Contributor Author

ardabada commented Oct 8, 2024

Hi @JimBobSquarePants. PR updated. Looks like decoder is fixed now.
However i don't really like the duplicated ifs for restart intervals, are you ok with such approach or it's better to keep it in separate methods and track restarts to go in a field?

@JimBobSquarePants
Copy link
Copy Markdown
Member

Legend! Thanks for fixing it. I’ll pull down and review ASAP

Copy link
Copy Markdown
Member

@JimBobSquarePants JimBobSquarePants left a comment

Choose a reason for hiding this comment

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

This is fantastic thank you. I feature I've wanted for many years!

this.FlushToStream();
}

if (this.restartInterval > 0)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Multiple if is fine here. Libjpeg turbo does the same.

This was referenced May 12, 2026
WarperSan added a commit to WarperSan/ThunderPipe that referenced this pull request May 15, 2026
Updated [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp)
from 3.1.12 to 4.0.0.

<details>
<summary>Release notes</summary>

_Sourced from [SixLabors.ImageSharp's
releases](https://github.com/SixLabors/ImageSharp/releases)._

## 4.0.0

## What's Changed
* Update to net8 by @​stefannikolei in
SixLabors/ImageSharp#2583
* Handle dedup of local palette of 256 length - Main by
@​JimBobSquarePants in SixLabors/ImageSharp#2607
* Replace custom Crc32 by @​JimBobSquarePants in
SixLabors/ImageSharp#2611
* Sync 3.1 DrawImage fixes by @​tocsoft in
SixLabors/ImageSharp#2612
* Fix handling gif encoding for global palettes - Main by
@​JimBobSquarePants in SixLabors/ImageSharp#2615
* Bump actions/setup-dotnet from 3 to 4 by @​dependabot[bot] in
SixLabors/ImageSharp#2613
* Adjusted the casing of the Webp format name by @​jscarle in
SixLabors/ImageSharp#2623
* Fix Paeth Filter decode on platforms that do not support Ssse3 - Main
by @​JimBobSquarePants in
SixLabors/ImageSharp#2620
* Fix WebP animation speed bug by @​marklagendijk in
SixLabors/ImageSharp#2624
* Promote PixelTypeInfo to Pixel by @​stefannikolei in
SixLabors/ImageSharp#2601
* TGA: Treat 32 bit True Color images always as transparent by
@​brianpopow in SixLabors/ImageSharp#2643
* Modernize and optimize pixel format operations across platforms. by
@​JimBobSquarePants in SixLabors/ImageSharp#2645
* Cleanup SimdUtils by @​JimBobSquarePants in
SixLabors/ImageSharp#2654
* Bump actions/cache from 3 to 4 by @​dependabot[bot] in
SixLabors/ImageSharp#2648
* Bump codecov/codecov-action from 3 to 4 by @​dependabot[bot] in
SixLabors/ImageSharp#2657
* Bump NuGet/setup-nuget from 1 to 2 by @​dependabot[bot] in
SixLabors/ImageSharp#2658
* Add v3.1.x fixes #​2673 and #​2674 into main. by @​JimBobSquarePants
in SixLabors/ImageSharp#2675
* Add fixes 2668, 2676, and 2677 to main by @​JimBobSquarePants in
SixLabors/ImageSharp#2678
* Merge 2681 to v4 Main by @​JimBobSquarePants in
SixLabors/ImageSharp#2690
* Add JPEG COM marker support by @​RobertMut in
SixLabors/ImageSharp#2641
* Bump actions/upload-artifact from 3 to 4 by @​dependabot[bot] in
SixLabors/ImageSharp#2625
* Only exit JPEG scan decoding after multiple EOF hits by
@​JimBobSquarePants in SixLabors/ImageSharp#2701
* V4 Ensure VP8X alpha flag is updated correctly. by @​JimBobSquarePants
in SixLabors/ImageSharp#2703
* Fix animated png handling (issue #​2708) by @​SpaceCheetah in
SixLabors/ImageSharp#2710
* Merge latest release from v3 by @​JimBobSquarePants in
SixLabors/ImageSharp#2720
* Fix MacOS jobs by @​antonfirsov in
SixLabors/ImageSharp#2728
* Fix async-over-sync issue in Image.DecodeAsync() by @​kroymann in
SixLabors/ImageSharp#2725
* Fix overflow in MemoryAllocator.Create(options) by @​antonfirsov in
SixLabors/ImageSharp#2730
* GifDecoder: Limit lzw bits to a maximum of 12 bits by @​brianpopow in
SixLabors/ImageSharp#2744
* GifDecoder : Allow skipping bad metadata using identify by
@​JimBobSquarePants in SixLabors/ImageSharp#2749
* Add ICO and CUR file decoder. by @​frg2089 in
SixLabors/ImageSharp#2579
* v4 - Fix off-by-one error when centering a transform. by
@​JimBobSquarePants in SixLabors/ImageSharp#2761
* v4 Fix 2758 by @​JimBobSquarePants in
SixLabors/ImageSharp#2764
* Simplify Color Space Conversion APIs by @​JimBobSquarePants in
SixLabors/ImageSharp#2739
* Webp: Fix Issue 2763 by @​brianpopow in
SixLabors/ImageSharp#2767
* V4 Correctly break during Png decoding by @​JimBobSquarePants in
SixLabors/ImageSharp#2773
* V4 : Fix filtering on PNG encode. by @​JimBobSquarePants in
SixLabors/ImageSharp#2778
* Fix #​2779 buffer overrun by @​KirillAldashkin in
SixLabors/ImageSharp#2780
* Fix ImageMetadata docs typo by @​lofcz in
SixLabors/ImageSharp#2781
* Add API for metadata conversion between formats. by
@​JimBobSquarePants in SixLabors/ImageSharp#2751
* Tiff decoder: Fix issue 2679 by @​brianpopow in
SixLabors/ImageSharp#2789
* Replace PngCrcChunkHandling by @​JimBobSquarePants in
SixLabors/ImageSharp#2786
* Add tagname to debugger visualization for Exif- and Iptc-values, to
facilitate easier debugging and discovery by @​lassevk in
SixLabors/ImageSharp#2787
* V4 - Correctly handle transform spaces when building transform
matrices. by @​JimBobSquarePants in
SixLabors/ImageSharp#2795
* Allow decoding Tiff of different frame size. by @​JimBobSquarePants in
SixLabors/ImageSharp#2788
* Add progressive JPEG encoder by @​ardabada in
SixLabors/ImageSharp#2740
* Fix using dither in BmpEncoder when bit per pixel is <= 4 by @​mistoll
in SixLabors/ImageSharp#2819
* Add QuadDistortion to ProjectiveTransformBuilder by @​Socolin in
SixLabors/ImageSharp#2748
* WEBP : Use Correct Width With AlphaDecoder by @​JimBobSquarePants in
SixLabors/ImageSharp#2823
 ... (truncated)

Commits viewable in [compare
view](SixLabors/ImageSharp@v3.1.12...v4.0.0).
</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: WarperSan <leumas.ecole@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants