Skip to content

Improve JPEG XL support #3712

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Nov 23, 2023
Merged

Conversation

DarthSim
Copy link
Contributor

Hey there 👋

Apple added JPEG XL support to macOS, iOS, and Safari, and lots of people want to try it. Unfortunately, JPEG XL support in libvips is pretty limited right now, and this PR fixes this.

jxlload, jxlsave: support EXIF and XMP metadata

JPEG XL has two file formats:

  1. A "naked" codestream that contains only image/animation data without any additional metadata.
  2. An ISOBMFF-based container that may contain additional metadata like EXIF and XMP.

Currently, jxlload doesn't extract EXIF/XMP from the container format, and jxlsave doesn't save files JXL in the container format. This PR adds extracting EXIF/XMP from ISOBMFF boxes and saving it to ISOBMFF boxes.

jxlload: close input on EOF instead of throwing an error

Currently, jxlload fails when meets the end of input. But in some cases, libjxl tries to read while the input is opened. So instead of returning an error on the end of input libvips should close the input.

jxlload: add animation support

Unfortunately, JPEG XL header doesn't have contain frames count so we have to subscribe to JXL_DEC_FRAME and count frames ourselves. Luckily, libjxl doesn't decode frames unless we subscribe to JXL_DEC_FULL_IMAGE.

The sequential decoding of frames is done in webpload manner: we decode a frame to a separate image when we reach it and read it line-by-line in vips_image_generate.

jxlsave: add animation support

This is done in a webpsave manner: we write image data in a memory buffer during vips_sink_disc and as soon as we've written a full frame, we add it to libjxl. Also, we process output after each frame to save memory.

jxlsave: lower min effort value to 1

The valid range for effort in libjxl is 1-9 but libvips currently allows a range of 3-9, not sure why

Copy link
Member

@jcupitt jcupitt left a comment

Choose a reason for hiding this comment

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

LGTM!

@jcupitt
Copy link
Member

jcupitt commented Nov 23, 2023

This looks great @DarthSim, thank you for doing this work.

I tested using libjxl 0.8.2:

  1. animated webp, low frame rate, transparency -> jxl -> animated webp
  2. animated GIF, high frame rate, no transparency -> jxl -> gif
  3. tiff with 2004 pixels/mm resolution -> jxl -> tif
  4. a few things with valgrind
  5. pytest and ninja test

And it all worked well.

@jcupitt jcupitt merged commit cca7fb0 into libvips:master Nov 23, 2023
jcupitt added a commit that referenced this pull request Nov 23, 2023
@kleisauke
Copy link
Member

The valid range for effort in libjxl is 1-9 but libvips currently allows a range of 3-9, not sure why

FWIW, I think this is due to that jxlsave was based upon libjxl v0.4 or earlier. The effort range was changed in libjxl v0.5 with commit libjxl/libjxl@8876a98.

PR #3774 backports commit 4caf924 to 8.15, which should be safe now that libjxl v0.6 is our minimum required version.

@AngelaDMerkel
Copy link

Have the jxlsave exif features been fully released yet? Looking at the release notes I don't see support included for those features yet and using jxlsave is not properly transferring metadata unfortunately

@jcupitt
Copy link
Member

jcupitt commented Dec 18, 2023

The revised JXL load and save will be in 8.16, so summer 2024, probably. You can build master yourself in the meantime, of course.

@AngelaDMerkel
Copy link

@jcupitt Thank you for the reply. I built the branch DarthSim:feature/jxl-better-support rather than the master, but that shouldn't make a difference right?

I see some improvements in the handling of metadata, but fields including make and model of the camera are not carried over when using jxlsave to convert from a tiff to a jxl file. Is there some planned improvement prior to release or is this the full functionality that can be expected?

@jcupitt
Copy link
Member

jcupitt commented Dec 18, 2023

You'd need to share a test file, but the usual problem is that libtiff does not store EXIF in a standard way. Instead of an EXIF block (like jpeg, webp, png, etc.), EXIF is broken out into a set of separate TIFF tags, making them very hard to work with. You'd probably be better off using XMP or IPTC. Additionally, most RAW cameras files will be loaded via imagemagick, which will not pass on many metadata fields.

Try vipsheader -a some-tiff-file.tif to see the set of fields that libvips has been able to discover.

@AngelaDMerkel
Copy link

Using vipsheader reveals that it's unable to see very much.

5119x15252 uchar, 3 bands, srgb, tiffload
width: 5119
height: 15252
bands: 3
format: uchar
coding: none
interpretation: srgb
xoffset: 0
yoffset: 0
xres: 11,811
yres: 11,811
filename: /path/to/file20210717 - _1.TIF
vips-loader: tiffload
n-pages: 1
icc-profile-data: 560 bytes of binary data
xmp-data: 11738 bytes of binary data
iptc-data: 7 bytes of binary data
photoshop-data: 5094 bytes of binary data
image-description: ZuidsandstraaT
resolution-unit: in
bits-per-sample: 8
orientation: 1

I have shared this file with Dropbox as that is easiest with a larger tiff file like I am working with.

exiftool shows an incredible amount of data by comparison. I won't add that as a code block would be over 100 lines.

@jcupitt
Copy link
Member

jcupitt commented Dec 18, 2023

exiftool will be unpacking the XMP, IPTC and PhotoShop data tags, so they are probably seeing the same number of things. You can use tiffinfo to see exactly what TIFF tags are set on the file.

You could look at the JXL and see if that has the XMP and IPTC data attached.

@jcupitt
Copy link
Member

jcupitt commented Dec 18, 2023

I tried with your file and I see:

$ vips copy 20210717\ -\ _1.TIF x.jxl
$ vipsheader -a x.jxl
x.jxl: 5119x15252 uchar, 3 bands, srgb, jxlload
width: 5119
height: 15252
bands: 3
format: uchar
coding: none
interpretation: srgb
xoffset: 0
yoffset: 0
xres: 11.811
yres: 11.811
filename: x.jxl
vips-loader: jxlload
icc-profile-data: 732 bytes of binary data
exif-data: 186 bytes of binary data
resolution-unit: in
exif-ifd0-Orientation: 1 (Top-left, Short, 1 components, 2 bytes)
exif-ifd0-XResolution: 300000/1000 (300.000, Rational, 1 components, 8 bytes)
exif-ifd0-YResolution: 300000/1000 (300.000, Rational, 1 components, 8 bytes)
exif-ifd0-ResolutionUnit: 2 (Inch, Short, 1 components, 2 bytes)
exif-ifd0-YCbCrPositioning: 1 (Centred, Short, 1 components, 2 bytes)
exif-ifd2-ExifVersion: Exif Version 2.1 (Exif Version 2.1, Undefined, 4 components, 4 bytes)
exif-ifd2-ComponentsConfiguration: Y Cb Cr - (Y Cb Cr -, Undefined, 4 components, 4 bytes)
exif-ifd2-FlashpixVersion: FlashPix Version 1.0 (FlashPix Version 1.0, Undefined, 4 components, 4 bytes)
exif-ifd2-ColorSpace: 65535 (Uncalibrated, Short, 1 components, 2 bytes)
exif-ifd2-PixelXDimension: 5119 (5119, Long, 1 components, 4 bytes)
exif-ifd2-PixelYDimension: 15252 (15252, Long, 1 components, 4 bytes)
xmp-data: 11738 bytes of binary data
orientation: 1
bits-per-sample: 8

So the XMP has made it over. The PhotoShop metadata is not supported by JXL. The colour profile is different, but I think that's expected to change, JXL has slightly funky colour handling (maybe we should check this). The IPTC has been removed, but it was probably empty. The EXIF was added by libvips and is the minimal set required by the standard.

If you extract the XMP you get:

<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 12.23'>
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>

 <rdf:Description rdf:about=''
  xmlns:aux='http://ns.adobe.com/exif/1.0/aux/'>
  <aux:Lens>Fujifilm EBC Fujinon SW 105mm f8</aux:Lens>
 </rdf:Description>

 <rdf:Description rdf:about=''
  xmlns:crs='http://ns.adobe.com/camera-raw-settings/1.0/'>
  <crs:AlreadyApplied>True</crs:AlreadyApplied>
  <crs:AutoLateralCA>0</crs:AutoLateralCA>
  <crs:Blacks2012>0</crs:Blacks2012>
  <crs:BlueHue>0</crs:BlueHue>
... a lot more

So you probably just need to extract that and put it through an XML parser.

@AngelaDMerkel
Copy link

@jcupitt Again, thank you for all your time.

The XMP is in fact successful at being passed over and includes the lens used, but Date/Time Original as well as the Make and Model fields would be useful if handled automatically through moving over exif data. If I were to convert tiff to png and then rely on cjxl would vips be likely to retain the fields I need up until that point?

Would there be future plans for better handling metadata copying between tiff and jxl or should I script an alternate solution?

@jcupitt
Copy link
Member

jcupitt commented Dec 19, 2023

You'll need to find out where this extra metadata is being held. Is it in the photoshop-data block? If it is, you're probably out of luck, since JXL does not (as far as I know) support this as it's an undocumented binary object read and written by photoshop.

Your image has an extra EXIF directory under the main image with some more metadata:

  RichTIFFIPTC Data: <present>, 7 bytes
  Photoshop Data: <present>, 5094 bytes
  EXIFIFDOffset: 0x45b2
  ICC Profile: <present>, 560 bytes
  GPSIFDOffset: 0x46f4
--- EXIF directory within directory 0 
TIFF Directory at offset 0x45b2 (17842)
  ExposureTime: 0.008000
  FNumber: 8.000000
  ISOSpeedRatings: 250
  ExifVersion: 0x30,0x32,0x33,0x31
  DateTimeOriginal: 2021:07:17 19:06:00
  OffsetTime: +02:00
  ShutterSpeedValue: 6.965784
  ApertureValue: 6.000000
  ExposureBiasValue: 0.000000
  LightSource: 0
  UserComment: 0x41,0x53,0x43,0x49,0x49,0x0,0x0,0x0,0x5a,0x75,0x69,0x64,0x73,0x61,0x6e,0x64,0x73,0x74,0x72,0x61,0x61,0x54
  ColorSpace: 65535
  PixelXDimension: 5119
  PixelYDimension: 15252
  LensMake: Fujifilm
  LensModel: EBC Fujinon SW 105mm f8
_TIFFreallocExt: Memory allocation of 358297968 bytes is beyond the 268435456 byte limit defined in open options.
20210717 - _1.TIF: Failed to allocate memory for ReadDirEntryArray (358297968 elements of 1 bytes each).
TIFFFetchNormalTag: Warning, Out of memory reading of "Adobe Photoshop Document Data Block"; tag ignored.
--- GPS directory within directory 0 
TIFF Directory at offset 0x46f4 (18164)
  VersionID: 2,3,0,0
  LatitudeRef: N
  Latitude: 51.000000,12.000000,33.192000
  LongitudeRef: E
  Longitude: 3.000000,13.000000,23.016000
_TIFFreallocExt: Memory allocation of 358297968 bytes is beyond the 268435456 byte limit defined in open options.
20210717 - _1.TIF: Failed to allocate memory for ReadDirEntryArray (358297968 elements of 1 bytes each).
TIFFFetchNormalTag: Warning, Out of memory reading of "Adobe Photoshop Document Data Block"; tag ignored.

But you can see libtiff is really struggling to read it. It's not really standard TIFF, so you're probably out of luck there too.

Maybe use exiftool on the original, dump the JSON into a file somewhere, then use that for the metadata for the JXL?

@AngelaDMerkel
Copy link

AngelaDMerkel commented Dec 20, 2023

I tested this on another sample TIFF and the data I need is really spread out. There is more than one TIFF directory it seems, but none of what I would consider essential data is contained in the Photoshop block.

TIFF Directory at offset 0x8 (8)
  Subfile Type: (0 = 0x0)
  Image Width: 9190 Image Length: 7547
  Resolution: 300, 300 pixels/inch
  Bits/Sample: 16
  Compression Scheme: None
  Photometric Interpretation: RGB color
  Samples/Pixel: 3
  Rows/Strip: 7547
  Planar Configuration: single image plane
  ImageDescription: Building 
  Make: Voigtländer
  Model: Bessa III

and another TIFF block

TIFF Directory at offset 0x96ae8 (617192)
  ExposureTime: 0.008000
  FNumber: 11.000000
  ISOSpeedRatings: 200
  ExifVersion: 0x30,0x32,0x33,0x32
  DateTimeOriginal: 2023:08:09 19:46:00
  ComponentsConfiguration: 0x1,0x2,0x3,0x0
  ShutterSpeedValue: 6.965784
  ApertureValue: 6.918863
  LightSource: 1
  FocalLength: 80.000000
  UserComment: 0x41,0x53,0x43,0x49,0x49,0x0,0x0,0x0,0x42,0x75,0x69,0x6c,0x64,0x69,0x6e,0x67,0x20
  FlashpixVersion: 0x30,0x31,0x30,0x30
  ColorSpace: 65535
  LensMake: Voigtländer
  LensModel: Heliar 80mm f3.5

and yet another

IFF Directory at offset 0x96c10 (617488)
  VersionID: 2,3,0,0
  LatitudeRef: N
  Latitude: 41.000000,21.000000,9.211320
  LongitudeRef: E
  Longitude: 2.000000,6.000000,43.180560

All the data relevant to me is contained in these three TIFF blocks and theoretically should make its way over. When I use vips copy to write a PNG from this sample TIFF file all the correct metadata is carried over without issue except some mangling of the umlaut characters and their diacritics. Doing vips copy to a JXL however seems to leave all the metadata out.

Maybe use exiftool on the original, dump the JSON into a file somewhere, then use that for the metadata for the JXL?

This worked for me. Outputting a JSON and then writing that to the JXL is successful. This strikes me as odd because using exiftool -TagsFromFile source.tiff "-all:all>all:all" test.jxl fails to properly transfer metadata. It's possible I am making some mistake. In either case using exiftool this way is a solution but much more cumbersome than it would be, ideally. I've included the JSON produced by exiftool from my source tiff to hopefully save you some time.

@jcupitt
Copy link
Member

jcupitt commented Dec 20, 2023

When I use vips copy to write a PNG from this sample TIFF file all the correct metadata is carried over without issue except some mangling of the umlaut characters and their diacritics. Doing vips copy to a JXL however seems to leave all the metadata out.

Are you sure? I see:

$ vips copy 20210717\ -\ _1.TIF x.png
$ vipsheader -a x.png
x.png: 5119x15252 uchar, 3 bands, srgb, pngload
width: 5119
height: 15252
bands: 3
format: uchar
coding: none
interpretation: srgb
xoffset: 0
yoffset: 0
xres: 11.811
yres: 11.811
filename: x.png
vips-loader: pngload
icc-profile-data: 560 bytes of binary data
xmp-data: 11738 bytes of binary data
bits-per-sample: 8
exif-data: 180 bytes of binary data
resolution-unit: in
exif-ifd0-Orientation: 1 (Top-left, Short, 1 components, 2 bytes)
exif-ifd0-XResolution: 300000/1000 (300.000, Rational, 1 components, 8 bytes)
exif-ifd0-YResolution: 300000/1000 (300.000, Rational, 1 components, 8 bytes)
exif-ifd0-ResolutionUnit: 2 (Inch, Short, 1 components, 2 bytes)
exif-ifd0-YCbCrPositioning: 1 (Centred, Short, 1 components, 2 bytes)
exif-ifd2-ExifVersion: Exif Version 2.1 (Exif Version 2.1, Undefined, 4 components, 4 bytes)
exif-ifd2-ComponentsConfiguration: Y Cb Cr - (Y Cb Cr -, Undefined, 4 components, 4 bytes)
exif-ifd2-FlashpixVersion: FlashPix Version 1.0 (FlashPix Version 1.0, Undefined, 4 components, 4 bytes)
exif-ifd2-ColorSpace: 65535 (Uncalibrated, Short, 1 components, 2 bytes)
exif-ifd2-PixelXDimension: 5119 (5119, Long, 1 components, 4 bytes)
exif-ifd2-PixelYDimension: 15252 (15252, Long, 1 components, 4 bytes)
orientation: 1

And with JXL:

$ vips copy 20210717\ -\ _1.TIF x.jxl
$ vipsheader -a x.jxl
x.jxl: 5119x15252 uchar, 3 bands, srgb, jxlload
width: 5119
height: 15252
bands: 3
format: uchar
coding: none
interpretation: srgb
xoffset: 0
yoffset: 0
xres: 11.811
yres: 11.811
filename: x.jxl
vips-loader: jxlload
icc-profile-data: 732 bytes of binary data
exif-data: 186 bytes of binary data
resolution-unit: in
exif-ifd0-Orientation: 1 (Top-left, Short, 1 components, 2 bytes)
exif-ifd0-XResolution: 300000/1000 (300.000, Rational, 1 components, 8 bytes)
exif-ifd0-YResolution: 300000/1000 (300.000, Rational, 1 components, 8 bytes)
exif-ifd0-ResolutionUnit: 2 (Inch, Short, 1 components, 2 bytes)
exif-ifd0-YCbCrPositioning: 1 (Centred, Short, 1 components, 2 bytes)
exif-ifd2-ExifVersion: Exif Version 2.1 (Exif Version 2.1, Undefined, 4 components, 4 bytes)
exif-ifd2-ComponentsConfiguration: Y Cb Cr - (Y Cb Cr -, Undefined, 4 components, 4 bytes)
exif-ifd2-FlashpixVersion: FlashPix Version 1.0 (FlashPix Version 1.0, Undefined, 4 components, 4 bytes)
exif-ifd2-ColorSpace: 65535 (Uncalibrated, Short, 1 components, 2 bytes)
exif-ifd2-PixelXDimension: 5119 (5119, Long, 1 components, 4 bytes)
exif-ifd2-PixelYDimension: 15252 (15252, Long, 1 components, 4 bytes)
xmp-data: 11738 bytes of binary data
orientation: 1
bits-per-sample: 8

You can see they are almost identical.

@jcupitt
Copy link
Member

jcupitt commented Dec 20, 2023

What are you using to view the metadata on the JXL? Perhaps it does not support XMP in JXL?

@AngelaDMerkel
Copy link

AngelaDMerkel commented Dec 20, 2023

After writing the extracted json to JXL I can read from the JXL using exiftool and get the following result:

ExifTool Version Number         : 12.60
File Name                       : test.jxl
Directory                       : JXL Test
File Size                       : 5.8 MB
File Modification Date/Time     : 2023:10:14 17:35:17+02:00
File Access Date/Time           : 2023:12:20 15:31:34+01:00
File Inode Change Date/Time     : 2023:12:20 15:31:32+01:00
File Permissions                : -rw-r--r--
File Type                       : JXL
File Type Extension             : jxl
MIME Type                       : image/jxl
Major Brand                     : JPEG XL Image (.JXL)
Minor Version                   : 0.0.0
Compatible Brands               : jxl
Exif Byte Order                 : Big-endian (Motorola, MM)
Subfile Type                    : Full-resolution image
Image Width                     : 9190
Image Height                    : 7547
Bits Per Sample                 : 16 16 16
Compression                     : Uncompressed
Photometric Interpretation      : RGB
Image Description               : Building
Make                            : Voigtländer
Camera Model Name               : Bessa III
Samples Per Pixel               : 3
Rows Per Strip                  : 7547
X Resolution                    : 300
Y Resolution                    : 300
Resolution Unit                 : inches
Y Cb Cr Positioning             : Centered
Exposure Time                   : 1/125
F Number                        : 11.0
ISO                             : 200
Exif Version                    : 0232
Date/Time Original              : 2023:08:09 19:46:00
Components Configuration        : Y, Cb, Cr, -
Shutter Speed Value             : 1/125
Aperture Value                  : 11.0
Light Source                    : Daylight
Focal Length                    : 80.0 mm
User Comment                    : Building
Flashpix Version                : 0100
Color Space                     : Uncalibrated
Lens Make                       : Voigtländer
Lens Model                      : Heliar 80mm f3.5
GPS Version ID                  : 2.3.0.0
GPS Latitude Ref                : North
GPS Longitude Ref               : East
XMP Toolkit                     : Image::ExifTool 12.60
Lens                            : Voigtländer Heliar 80mm f3.5
Date/Time Modified              : 2023:08:09 19:46
Aperture                        : 11.0
Image Size                      : 9190x7547
Megapixels                      : 69.4
Shutter Speed                   : 1/125
GPS Latitude                    : 41 deg 21' 9.21" N
GPS Longitude                   : 2 deg 6' 43.18" E
Focal Length                    : 80.0 mm
GPS Position                    : 41 deg 21' 9.21" N, 2 deg 6' 43.18" E
Light Value                     : 12.9
Lens ID                         : Heliar 80mm f3.5

When I use vipsheader -a I get the same garbled mess as you see:

9190x7547 ushort, 3 bands, rgb16, jxlload
width: 9190
height: 7547
bands: 3
format: ushort
coding: none
interpretation: rgb16
xoffset: 0
yoffset: 0
xres: 11,811
yres: 11,811
filename: JXL Test/test.jxl
vips-loader: jxlload
icc-profile-data: 536 bytes of binary data
exif-data: 186 bytes of binary data
resolution-unit: in
exif-ifd0-Orientation: 1 (Top-left, Short, 1 components, 2 bytes)
exif-ifd0-XResolution: 300000/1000 (300,000, Rational, 1 components, 8 bytes)
exif-ifd0-YResolution: 300000/1000 (300,000, Rational, 1 components, 8 bytes)
exif-ifd0-ResolutionUnit: 2 (Inch, Short, 1 components, 2 bytes)
exif-ifd0-YCbCrPositioning: 1 (Centred, Short, 1 components, 2 bytes)
exif-ifd2-ExifVersion: Exif Version 2.1 (Exif Version 2.1, Undefined, 4 components, 4 bytes)
exif-ifd2-ComponentsConfiguration: Y Cb Cr - (Y Cb Cr -, Undefined, 4 components, 4 bytes)
exif-ifd2-FlashpixVersion: FlashPix Version 1.0 (FlashPix Version 1.0, Undefined, 4 components, 4 bytes)
exif-ifd2-ColorSpace: 65535 (Uncalibrated, Short, 1 components, 2 bytes)
exif-ifd2-PixelXDimension: 9190 (9190, Long, 1 components, 4 bytes)
exif-ifd2-PixelYDimension: 7547 (7547, Long, 1 components, 4 bytes)
xmp-data: 2958 bytes of binary data
orientation: 1
bits-per-sample: 16

What would be the cause of the discrepancy?

What are you using to view the metadata on the JXL? Perhaps it does not support XMP in JXL?

After writing the JSON to the JXL files I can view it using either exiftool or Photoshop/Lightroom. Lightroom has been successful in reading the metadata from the JXL after it's been written to from the JSON.

@jcupitt
Copy link
Member

jcupitt commented Dec 20, 2023

vipsheader -a is not a garbled mess, it's just showing what's in the file heh. But PNG and JXL are definitely showing the same metadata, except for the ICC profile, I hope you agree.

As I said, libvips is not reading those strange extra TIFF directories, because that style of TIFF metadata is hard to read correctly. I think adding support for this would be out of scope for libvips. Just finding a specification of each tag and the expected value would be tricky, if it's even documented anywhere.

@AngelaDMerkel
Copy link

not a garbled mess, it's just showing what's in the file heh.

Maybe that was a bit of hyperbole. We agree that the PNG and JXL show the same metadata when using vipsheader -a. If this is out of scope, then I understand. It would be a very nice feature, but otherwise I will use exiftool to try to address this in addition to vips.

Thank you for all your help.

@sergeevabc
Copy link

sergeevabc commented Dec 20, 2023

@DarthSim, is there any news about improving lossless mode?

@DarthSim
Copy link
Contributor Author

@sergeevabc I answered in the discussion

@SimplyCorbett
Copy link

Might be the wrong place but any news about progressive image loading with jxl?

@jcupitt
Copy link
Member

jcupitt commented Oct 11, 2024

@SimplyCorbett if you mean loading an image in chunks rather than all in one go, it's not in libjxl yet. It's on the roadmap for 1.0, but no one's implemented it, as far as I know.

Chunked jxlsave is in a libvips branch right now, it'll be merged early next year (at a guess).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants