Skip to content

Adds support for Tiff's with jpeg compression - #1734

Merged
JimBobSquarePants merged 36 commits into
masterfrom
bp/tiffjpegcompression
Aug 26, 2021
Merged

JimBobSquarePants merged 36 commits into
masterfrom
bp/tiffjpegcompression

Conversation

@brianpopow

@brianpopow brianpopow commented Aug 12, 2021 •

Copy link
Copy Markdown
Collaborator

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 support for decoding and encoding of tiff's with jpeg compression. See adobe tech note 2 for details, spec pdf is in the tiff folder: ff8dfde

For this to work, I need to add a new method LoadTables to the JpegDecoderCore, which only loads quantization and/or Huffman tables. Those are only stored once in at tiff with jpeg compression, if JPEGTables are present in a Striped tiff (Tiff with pixel data is stored in multiple segments).

Also the JpegEncoder needs to store the data as RGB instead of YCbCr. There is now a new option for JpegSubsample to do that. note: this PR introduces additional enum values for JpegColorType for YCbCr with the subsampling rates and RGB (to encode the image as jpeg with RGB colorspace), which is a breaking change for the JpegEncoder options

note: #1732 is prerequisite for this PR

@br3aker

br3aker commented Aug 12, 2021

Copy link
Copy Markdown
Contributor

Is placing rgb as option to SubSampling enum a good idea? Maybe it's the time to separate subsampling from color space option?

@brianpopow

brianpopow commented Aug 12, 2021 •

Copy link
Copy Markdown
Collaborator Author

Is placing rgb as option to SubSampling enum a good idea? Maybe it's the time to separate subsampling from color space option?

yeah separating subsampling and color space is a good suggestion

@br3aker

br3aker commented Aug 12, 2021

Copy link
Copy Markdown
Contributor

Or we can just rename SubSampling to ColorSpace with following options:

  1. Grayscale
  2. YCbCr444
  3. YCbCr420
  4. Rgb

As it was decided (?) that the next release would be a major one I assume we can break it.

YCbCr can have other subsampling stuff like 422, 411, 410 even but I think it would be better to add them as options to 'ColorSpace' instead of 2 different options one of which would work only with YCbCr.

@brianpopow

Copy link
Copy Markdown
Collaborator Author

Or we can just rename SubSampling to ColorSpace with following options:

  1. Grayscale
  2. YCbCr444
  3. YCbCr420
  4. Rgb

As it was decided (?) that the next release would be a major one I assume we can break it.

YCbCr can have other subsampling stuff like 422, 411, 410 even but I think it would be better to add them as options to 'ColorSpace' instead of 2 different options one of which would work only with YCbCr.

That really sounds like the better solution, but I am undecided, if we really should introduce a breaking change for a Tiff related feature.

Comment thread src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs Outdated
Comment on lines +108 to +110
redBlock[i] = c.R;
greenBlock[i] = c.G;
blueBlock[i] = c.B;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why no Unsafe.Add here? So it's a bound check on each access.
Together with the previous suggestion, this should produce quite nice machine code*.

* except the movsxd from the int-indexing, which can be avoided by nint / IntPtr but I can't remember where we landed with that as older runtimes won't support this without hacks (sorry).

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.

I think we can support nint, we just need to change our build process. For reasons I cannot recall we're building the code with explicit target frameworks which I can't see a reason to do since nobody ever does this. We only need to test with individual frameworks and build things normally.

@JimBobSquarePants JimBobSquarePants Aug 26, 2021 •

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.

Just rereviewed this. redBlock etc are actually Block8x8F instances. There's no bounds checks there in release.

@gfoidl However, in the internal indexer we are creating the extra movsxd but I think we'll leave this as-is for now since passing nint would require a significant refactoring.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do you know if casting to nint is good?

(quote from the email GH sent me).

A direct cast

int a = 42;
nint b = (nint)a;

produces a movsxd. This can be avoided by casting to uint-first. So nint b = (nint)(uint)a;, then it's just a mov (which the cpu doesn't need to execute due register renaming, but it still needs to be fetched and decoded, so it hasn't zero-cost, but it's quite cheap).

So for the API one can keep int (to be CLS-compliant, etc.) and just perform the (nint)(uint) cast to get the best of both worlds.

Note: the JIT can't do this / isn't allowed to do this optimization for us, as int can have negative values. Here is a place where we can be smarter than the compiler, and say explicitely by the uint cast, that we only have >= 0 values.

Next week I'm out of office, but then I could have a skim over the indexers, etc. to try to remove the movsxd instructions.

@JimBobSquarePants JimBobSquarePants Aug 26, 2021 •

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.

I didn’t think of that double cast when experimenting earlier. Great stuff, thanks!

Comment thread src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs Outdated
@br3aker

br3aker commented Aug 12, 2021 •

Copy link
Copy Markdown
Contributor

That really sounds like the better solution, but I am undecided, if we really should introduce a breaking change for a Tiff related feature.

Different color spaces for jpeg were in plans for quite a while now #808 so it's good I think :)

This would also prepare the ground for remaining CMYK and YccK color spaces.

This can be introduced as a new property in non-breaking manner:

  1. Existing SubSample with Grayscale/YCbCr444/YCbCr420
  2. New ColorSpace with Grayscale/YCbCr444/YCbCr420/Rgb

Internal code would use new ColorSpace and Subsample would map to it. Marking Subsample obsolete and removing it somewhen in the next 2.X minor release.

@JimBobSquarePants

Copy link
Copy Markdown
Member

We don’t need to worry about breaking changes with Tiff at all. We haven’t shipped anything other than dev builds with it (and won’t) until 2.0

@br3aker

br3aker commented Aug 13, 2021

Copy link
Copy Markdown
Contributor

@JimBobSquarePants renaming/new ColorSpace property would alter public JpegEncoder interface, tiff just uses it internally via this commit.

@JimBobSquarePants

Copy link
Copy Markdown
Member

@br3aker I don't follow. We already have IJpegOptions.JpegColorType. Adding the new types as an when we introduce them would be fine.

@br3aker

br3aker commented Aug 13, 2021 •

Copy link
Copy Markdown
Contributor

@JimBobSquarePants my latest proposal was to remove the SubSampling parameter at all as it's only used for 1/3 of the implemented colors types (grayscale, ycbcr and rgb). As for my understanding SubSampling is more of a legacy thingy which was introduced when no other color spaces were implemented.

IMO it's more understandable to have a single parameter ColorSpace or existing JpegColorType with following options:

  1. Grayscale/Luminance
  2. Rgb
  3. YCbCr444
  4. YCbCr420
  5. (not implemented) Cmyk
  6. (not implemented) YccK
  7. (not implemented) YCbCr422
  8. (not implemented) YCbCr411
  9. Whatever else jpeg standard supports

While not removing parameter (marking it obsolete though) itself so it won't break anyone.

@JimBobSquarePants

Copy link
Copy Markdown
Member

I guess.... They're separate concerns but combining them makes sense in this instance.

While not removing parameter (marking it obsolete though) itself so it won't break anyone.

We'd just make the breaking change. It's 2.0, it's also free software - People can recompile.

@br3aker

br3aker commented Aug 13, 2021

Copy link
Copy Markdown
Contributor

Making breaking changes is not fun, I understand. But I believe it's for good here, possibility of Rgb with Ratio420 would be 'strange', to say the least :)

@JimBobSquarePants

Copy link
Copy Markdown
Member

Do you have any docs talking about subsampling with Rgb? I can't find anything to say anything other than YCbCr does it.

@br3aker

br3aker commented Aug 13, 2021 •

Copy link
Copy Markdown
Contributor

Do you have any docs talking about subsampling with Rgb? I can't find anything to say anything other than YCbCr does it.

Oh no, no, sorry for inconvenience. I meant right now it's possible to write something like:

var encoder1 = new JpegEncoder 
{
    ColorType = JpegColorType.Luminance,
    Subsample = JpegSubsample.Ratio420
};

// With this commit adding Rgb as option
var encoder2 = new JpegEncoder 
{
    ColorType = JpegColorType.Rgb,
    Subsample = JpegSubsample.Ratio420
};

Both of which make no sense as chroma sub-sampling only works with YCbCr as it clearly separates luminance from chrominance. Providing only color type would solve these.

P.S.
Above examples won't break anything as subsampling parameter is ignored for non-ycbcr types. My main concern is that this is possible to write in the first place. And you have a 2.0 as next release - good time to break it for simplification purposes IMO.

@JimBobSquarePants

Copy link
Copy Markdown
Member

Ah right yeah, that all makes sense and I concur. Let’s fix it properly

Comment thread src/ImageSharp/Formats/Jpeg/Components/Encoder/HuffmanScanEncoder.cs Outdated
@br3aker

br3aker commented Aug 16, 2021 •

Copy link
Copy Markdown
Contributor

@brianpopow did a bit of testing with rgb stuff and actually solved it:

First of all, actual solution: APP0 marker is conflicting with APP14 marker. Simply not writing it for rgb encoded data solves this issue.

Explanation (?): RGB is not JFIF compatible 'format' - there's no rgb mentions in the itu specs. Standard JPEG with YCbCr is a JPEG FIF file - it must contain SOI -> APP0 -> etc marker sequence. RGB (and I suppose Cmyk/YccK also) are not JPEG FIF compatible files, they are JPEG IF compatible files - they must contain SOI -> etc marker sequence.

As far as I know rgb jpegs are more of an Adobe experiment over the actual jpeg format so it explains a lot. Some software (if not most) uses APP0 to determine if image is a standard jpeg and I would dare to say it is kind of wrong - while it's technically correct as there's no format specification for anything APPX related, APP0 check limits actual format as only APP0 marker can contain image thumbnail.

tl;dr drop APP0 marker, we should separate private void WriteApplicationHeader(ImageMetadata meta) {} call into:
WriteStartOfImage - Writes SOI marker
WriteJfifApplicationHeader - Writes APP0 only for YCbCr encoding

Second interesting thing I found - libjpeg-turbo (and old libjpeg I guess) uses standard jpeg luminance quantization table and luminance huffman table for all rgb channels. While writing different tables/huffman for each channel is okay as long as correct indices for them are provided - we already do that so it's fine. It' still better to use same quantization table for each channel though :P

@brianpopow

Copy link
Copy Markdown
Collaborator Author

@brianpopow did a bit of testing with rgb stuff and actually solved it:

First of all, actual solution: APP0 marker is conflicting with APP14 marker. Simply not writing it for rgb encoded data solves this issue.
...

@br3aker thank you a lot for investigating this issue, its very helpful. I will try to change the encoder as you suggested.

@brianpopow brianpopow changed the title WIP: Adds support for Tiff's with jpeg compression Adds support for Tiff's with jpeg compression Aug 16, 2021
@JimBobSquarePants JimBobSquarePants added this to the 2.0.0 milestone Aug 19, 2021
@JimBobSquarePants JimBobSquarePants mentioned this pull request Aug 22, 2021
4 tasks done

@JimBobSquarePants JimBobSquarePants left a comment

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.

Looks great; Nice work! 👍

@JimBobSquarePants JimBobSquarePants added the breaking Signifies a binary breaking change. label Aug 26, 2021
@JimBobSquarePants
JimBobSquarePants merged commit 659daff into master Aug 26, 2021
@JimBobSquarePants
JimBobSquarePants deleted the bp/tiffjpegcompression branch August 26, 2021 02:09
{
// Table identifiers.
ReadOnlySpan<byte> headers = new byte[]
ReadOnlySpan<byte> headers = stackalloc byte[]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here stackalloc is actually a de-optimization.
See #1734 (comment) and https://vcsjones.dev/csharp-readonly-span-bytes-static/ for some background for this C#'s compiler optimization to refer to the static data directly without the need to allocate (either stack or heap) something.

This sharplab demonstrates the difference too.

Maybe it's best to annotate each place where we introduce that optimization with a comment like

// This uses a C#'s compiler optimization that refers to the static data segment of the assembly,
            // and doesn't incur any allocation at all.

to make it clear that the new byte[] is there intentional and doesn't allocate.

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.

I was under the impression that optimisation only worked for static instances?

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.

Just read the doco. Method body optimisation is a new one for me! I’ll revert

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Signifies a binary breaking change. formats:jpeg formats:tiff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants