2023-03-12 00:33:17 +00:00
/*
* Copyright ( c ) 2023 , Nico Weber < thakis @ chromium . org >
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
# include <LibCore/ArgsParser.h>
# include <LibCore/File.h>
# include <LibCore/MappedFile.h>
ICC+image: Add conversion between color spaces for images :^)
For now, only for color spaces that are supported by Profile::to_pcs()
and Profile::from_pcs(), which currently means that all matrix profiles
(but not LUT profiles) in the source color space work, and that
matrix profiles with parametric curves in the destination color
space work.
This adds Profile::convert_image(Bitmap, source_profile), and
adds a `--convert-to-color-profile file.icc` flag to `image`.
It only takes a file path, so to use it with the built-in
sRGB profile, you have to write it to a file first:
% Build/lagom/icc -n sRGB --reencode-to serenity-sRGB.icc
`image` by default writes the source image's color profile
to the output image, and most image viewers display images
looking at the profile.
For example, take `Seven_Coloured_Pencils_(rg-switch_sRGB).jpg`
from https://commons.wikimedia.org/wiki/User:Colin/BrowserTest.
It looks normal in image viewers because they apply the unusual
profile embedded in the profile. But if you run
% Build/lagom/image -o huh.png --strip-color-profile \
'Seven_Coloured_Pencils_(rg-switch_sRGB).jpeg'
and then look at huh.png, you can see how the image's colors
look like when interpreted as sRGB (which is the color space
PNG data is in if the PNG doesn't store an embedded profile).
If you now run
% Build/lagom/image -o wow.png \
--convert-to-color-profile serenity-sRGB.icc --strip-color-profile \
'Seven_Coloured_Pencils_(rg-switch_sRGB).jpeg'
this will convert that image to sRGB, but then not write
the profile to the output image (verify with `Build/lagom/icc wow.png`).
It will look correct in image viewers, since they display PNGs without
an embedded color profile as sRGB.
(This works because 'Seven_Coloured_Pencils_(rg-switch_sRGB).jpeg'
contains a matrix profile, and Serenity's built-in sRGB profile
uses a matrix profile with a parametric curve.)
2023-05-01 19:42:29 +00:00
# include <LibGfx/ICC/Profile.h>
2023-03-21 18:58:06 +00:00
# include <LibGfx/ImageFormats/BMPWriter.h>
# include <LibGfx/ImageFormats/ImageDecoder.h>
# include <LibGfx/ImageFormats/PNGWriter.h>
# include <LibGfx/ImageFormats/PortableFormatWriter.h>
# include <LibGfx/ImageFormats/QOIWriter.h>
2023-03-12 00:33:17 +00:00
ErrorOr < int > serenity_main ( Main : : Arguments arguments )
{
Core : : ArgsParser args_parser ;
StringView in_path ;
args_parser . add_positional_argument ( in_path , " Path to input image file " , " FILE " ) ;
StringView out_path ;
args_parser . add_option ( out_path , " Path to output image file " , " output " , ' o ' , " FILE " ) ;
2023-05-24 13:00:50 +00:00
bool no_output = false ;
args_parser . add_option ( no_output , " Do not write output (only useful for benchmarking image decoding) " , " no-output " , { } ) ;
2023-05-06 11:09:41 +00:00
int frame_index = 0 ;
args_parser . add_option ( frame_index , " Which frame of a multi-frame input image (0-based) " , " frame-index " , { } , " INDEX " ) ;
2023-06-13 23:33:26 +00:00
bool move_alpha_to_rgb = false ;
args_parser . add_option ( move_alpha_to_rgb , " Copy alpha channel to rgb, clear alpha " , " move-alpha-to-rgb " , { } ) ;
2023-05-06 11:09:41 +00:00
bool ppm_ascii = false ;
2023-03-13 02:30:40 +00:00
args_parser . add_option ( ppm_ascii , " Convert to a PPM in ASCII " , " ppm-ascii " , { } ) ;
2023-06-13 23:26:01 +00:00
bool strip_alpha = false ;
args_parser . add_option ( strip_alpha , " Remove alpha channel " , " strip-alpha " , { } ) ;
2023-03-16 08:10:48 +00:00
StringView assign_color_profile_path ;
args_parser . add_option ( assign_color_profile_path , " Load color profile from file and assign it to output image " , " assign-color-profile " , { } , " FILE " ) ;
ICC+image: Add conversion between color spaces for images :^)
For now, only for color spaces that are supported by Profile::to_pcs()
and Profile::from_pcs(), which currently means that all matrix profiles
(but not LUT profiles) in the source color space work, and that
matrix profiles with parametric curves in the destination color
space work.
This adds Profile::convert_image(Bitmap, source_profile), and
adds a `--convert-to-color-profile file.icc` flag to `image`.
It only takes a file path, so to use it with the built-in
sRGB profile, you have to write it to a file first:
% Build/lagom/icc -n sRGB --reencode-to serenity-sRGB.icc
`image` by default writes the source image's color profile
to the output image, and most image viewers display images
looking at the profile.
For example, take `Seven_Coloured_Pencils_(rg-switch_sRGB).jpg`
from https://commons.wikimedia.org/wiki/User:Colin/BrowserTest.
It looks normal in image viewers because they apply the unusual
profile embedded in the profile. But if you run
% Build/lagom/image -o huh.png --strip-color-profile \
'Seven_Coloured_Pencils_(rg-switch_sRGB).jpeg'
and then look at huh.png, you can see how the image's colors
look like when interpreted as sRGB (which is the color space
PNG data is in if the PNG doesn't store an embedded profile).
If you now run
% Build/lagom/image -o wow.png \
--convert-to-color-profile serenity-sRGB.icc --strip-color-profile \
'Seven_Coloured_Pencils_(rg-switch_sRGB).jpeg'
this will convert that image to sRGB, but then not write
the profile to the output image (verify with `Build/lagom/icc wow.png`).
It will look correct in image viewers, since they display PNGs without
an embedded color profile as sRGB.
(This works because 'Seven_Coloured_Pencils_(rg-switch_sRGB).jpeg'
contains a matrix profile, and Serenity's built-in sRGB profile
uses a matrix profile with a parametric curve.)
2023-05-01 19:42:29 +00:00
StringView convert_color_profile_path ;
args_parser . add_option ( convert_color_profile_path , " Load color profile from file and convert output image from current profile to loaded profile " , " convert-to-color-profile " , { } , " FILE " ) ;
2023-04-30 21:29:48 +00:00
bool strip_color_profile = false ;
2023-03-16 08:02:31 +00:00
args_parser . add_option ( strip_color_profile , " Do not write color profile to output " , " strip-color-profile " , { } ) ;
2023-03-12 00:33:17 +00:00
args_parser . parse ( arguments ) ;
2023-05-24 13:00:50 +00:00
if ( out_path . is_empty ( ) ^ no_output ) {
warnln ( " exactly one of -o or --no-output is required " ) ;
2023-03-12 00:33:17 +00:00
return 1 ;
}
auto file = TRY ( Core : : MappedFile : : map ( in_path ) ) ;
auto decoder = Gfx : : ImageDecoder : : try_create_for_raw_bytes ( file - > bytes ( ) ) ;
2023-03-16 08:21:01 +00:00
if ( ! decoder ) {
warnln ( " Failed to decode input file '{}' " , in_path ) ;
return 1 ;
}
2023-03-12 00:33:17 +00:00
2023-05-06 11:09:41 +00:00
auto frame = TRY ( decoder - > frame ( frame_index ) ) . image ;
2023-06-13 23:26:01 +00:00
2023-06-13 23:33:26 +00:00
if ( move_alpha_to_rgb ) {
switch ( frame - > format ( ) ) {
case Gfx : : BitmapFormat : : Invalid :
case Gfx : : BitmapFormat : : Indexed1 :
case Gfx : : BitmapFormat : : Indexed2 :
case Gfx : : BitmapFormat : : Indexed4 :
case Gfx : : BitmapFormat : : Indexed8 :
warnln ( " Can't --strip-alpha with indexed or invalid bitmaps " ) ;
return 1 ;
case Gfx : : BitmapFormat : : RGBA8888 :
// No image decoder currently produces bitmaps with this format.
// If that ever changes, preferrably fix the image decoder to use BGRA8888 instead :)
// If there's a good reason for not doing that, implement support for this, I suppose.
warnln ( " Can't --strip-alpha not implemented for RGBA8888 " ) ;
return 1 ;
case Gfx : : BitmapFormat : : BGRA8888 :
case Gfx : : BitmapFormat : : BGRx8888 :
// FIXME: If BitmapFormat::Gray8 existed (and image encoders made use of it to write grayscale images), we could use it here.
for ( auto & pixel : * frame ) {
u8 alpha = pixel > > 24 ;
pixel = 0xff000000 | ( alpha < < 16 ) | ( alpha < < 8 ) | alpha ;
}
}
}
2023-06-13 23:26:01 +00:00
if ( strip_alpha ) {
switch ( frame - > format ( ) ) {
case Gfx : : BitmapFormat : : Invalid :
case Gfx : : BitmapFormat : : Indexed1 :
case Gfx : : BitmapFormat : : Indexed2 :
case Gfx : : BitmapFormat : : Indexed4 :
case Gfx : : BitmapFormat : : Indexed8 :
warnln ( " Can't --strip-alpha with indexed or invalid bitmaps " ) ;
return 1 ;
case Gfx : : BitmapFormat : : RGBA8888 :
// No image decoder currently produces bitmaps with this format.
// If that ever changes, preferrably fix the image decoder to use BGRA8888 instead :)
// If there's a good reason for not doing that, implement support for this, I suppose.
warnln ( " Can't --strip-alpha not implemented for RGBA8888 " ) ;
return 1 ;
case Gfx : : BitmapFormat : : BGRA8888 :
case Gfx : : BitmapFormat : : BGRx8888 :
frame - > strip_alpha_channel ( ) ;
}
}
2023-03-15 10:13:34 +00:00
Optional < ReadonlyBytes > icc_data = TRY ( decoder - > icc_data ( ) ) ;
2023-03-12 00:33:17 +00:00
2023-03-16 08:10:48 +00:00
RefPtr < Core : : MappedFile > icc_file ;
if ( ! assign_color_profile_path . is_empty ( ) ) {
icc_file = TRY ( Core : : MappedFile : : map ( assign_color_profile_path ) ) ;
icc_data = icc_file - > bytes ( ) ;
}
ICC+image: Add conversion between color spaces for images :^)
For now, only for color spaces that are supported by Profile::to_pcs()
and Profile::from_pcs(), which currently means that all matrix profiles
(but not LUT profiles) in the source color space work, and that
matrix profiles with parametric curves in the destination color
space work.
This adds Profile::convert_image(Bitmap, source_profile), and
adds a `--convert-to-color-profile file.icc` flag to `image`.
It only takes a file path, so to use it with the built-in
sRGB profile, you have to write it to a file first:
% Build/lagom/icc -n sRGB --reencode-to serenity-sRGB.icc
`image` by default writes the source image's color profile
to the output image, and most image viewers display images
looking at the profile.
For example, take `Seven_Coloured_Pencils_(rg-switch_sRGB).jpg`
from https://commons.wikimedia.org/wiki/User:Colin/BrowserTest.
It looks normal in image viewers because they apply the unusual
profile embedded in the profile. But if you run
% Build/lagom/image -o huh.png --strip-color-profile \
'Seven_Coloured_Pencils_(rg-switch_sRGB).jpeg'
and then look at huh.png, you can see how the image's colors
look like when interpreted as sRGB (which is the color space
PNG data is in if the PNG doesn't store an embedded profile).
If you now run
% Build/lagom/image -o wow.png \
--convert-to-color-profile serenity-sRGB.icc --strip-color-profile \
'Seven_Coloured_Pencils_(rg-switch_sRGB).jpeg'
this will convert that image to sRGB, but then not write
the profile to the output image (verify with `Build/lagom/icc wow.png`).
It will look correct in image viewers, since they display PNGs without
an embedded color profile as sRGB.
(This works because 'Seven_Coloured_Pencils_(rg-switch_sRGB).jpeg'
contains a matrix profile, and Serenity's built-in sRGB profile
uses a matrix profile with a parametric curve.)
2023-05-01 19:42:29 +00:00
if ( ! convert_color_profile_path . is_empty ( ) ) {
if ( ! icc_data . has_value ( ) ) {
warnln ( " No source color space embedded in image. Pass one with --assign-color-profile. " ) ;
return 1 ;
}
auto source_icc_file = icc_file ;
auto source_icc_data = icc_data . value ( ) ;
icc_file = TRY ( Core : : MappedFile : : map ( convert_color_profile_path ) ) ;
icc_data = icc_file - > bytes ( ) ;
auto source_profile = TRY ( Gfx : : ICC : : Profile : : try_load_from_externally_owned_memory ( source_icc_data ) ) ;
auto destination_profile = TRY ( Gfx : : ICC : : Profile : : try_load_from_externally_owned_memory ( icc_file - > bytes ( ) ) ) ;
TRY ( destination_profile - > convert_image ( * frame , * source_profile ) ) ;
}
2023-03-16 08:02:31 +00:00
if ( strip_color_profile )
icc_data . clear ( ) ;
2023-05-24 13:00:50 +00:00
if ( no_output )
return 0 ;
2023-05-01 01:13:50 +00:00
auto output_stream = TRY ( Core : : File : : open ( out_path , Core : : File : : OpenMode : : Write ) ) ;
auto buffered_stream = TRY ( Core : : OutputBufferedFile : : create ( move ( output_stream ) ) ) ;
2023-03-12 00:33:17 +00:00
ByteBuffer bytes ;
if ( out_path . ends_with ( " .bmp " sv , CaseSensitivity : : CaseInsensitive ) ) {
2023-03-15 11:09:32 +00:00
bytes = TRY ( Gfx : : BMPWriter : : encode ( * frame , { . icc_data = icc_data } ) ) ;
2023-03-12 00:33:17 +00:00
} else if ( out_path . ends_with ( " .png " sv , CaseSensitivity : : CaseInsensitive ) ) {
2023-03-15 10:13:34 +00:00
bytes = TRY ( Gfx : : PNGWriter : : encode ( * frame , { . icc_data = icc_data } ) ) ;
2023-03-13 02:22:43 +00:00
} else if ( out_path . ends_with ( " .ppm " sv , CaseSensitivity : : CaseInsensitive ) ) {
2023-03-13 02:30:40 +00:00
auto const format = ppm_ascii ? Gfx : : PortableFormatWriter : : Options : : Format : : ASCII : Gfx : : PortableFormatWriter : : Options : : Format : : Raw ;
2023-05-01 01:13:50 +00:00
TRY ( Gfx : : PortableFormatWriter : : encode ( * buffered_stream , * frame , { . format = format } ) ) ;
return 0 ;
2023-03-12 00:33:17 +00:00
} else if ( out_path . ends_with ( " .qoi " sv , CaseSensitivity : : CaseInsensitive ) ) {
2023-03-12 13:05:25 +00:00
bytes = TRY ( Gfx : : QOIWriter : : encode ( * frame ) ) ;
2023-03-12 00:33:17 +00:00
} else {
2023-03-13 02:22:43 +00:00
warnln ( " can only write .bmp, .png, .ppm, and .qoi " ) ;
2023-03-12 00:33:17 +00:00
return 1 ;
}
2023-05-01 01:13:50 +00:00
TRY ( buffered_stream - > write_until_depleted ( bytes ) ) ;
2023-03-12 00:33:17 +00:00
return 0 ;
}