Thanks to @WumpusQ.Wumbley for pointing me in the right direction.
TL;DR: Use -type TrueColor
to remove colourmaps, either from Palette types of PseudoClass images.
A colourmap is not a PseudoClass
Reading the IM forums I finally (more-or-less) understood what is a colourmap in an image and how to deal with it. First of all there is a difference between DirectClass and PseudoClass:
- A DirectClass stores the colour values for every pixel.
- A PseudoClass uses a colour table and then stores offsets into that table.
This was the source of my confusion: a PseudoClass is not a colourmap, they are different things. And since IM will almost always convert a PseudoClass image into a DirectClass image I was baffled.
Major operations involving image modifications would generally upgrade PsuedoColor images (images using a color table) to a DirectColor (images using separate color values for every pixel) before the operation is applied.
source
(P.S. DirectClass and PseudoClass are synonyms to DirectColor and PseudoColor)
So, what is a Colourmap?
The colourmap is an index of colours against a pallette which is stored in the image. This is very similar to a PseudoClass since it also is a table of colours, the difference is that a PseudoClass will include only the colours present in the image and the colourmap will include all colours in a pallette. Note that a PseudoClass will have a colourmap section but this will be the colour table of the PseudoClass not a Palette type (yes, that is very confusing on IM's side).
The colourmap is defined by the -type
option to IM:
-type type
the image type.
Choose from: Bilevel, Grayscale, GrayscaleMatte,
Palette, PaletteMatte, TrueColor, TrueColorMatte,
ColorSeparation, or ColorSeparationMatte.
Normally, when a format supports different subformats such
as grayscale and truecolor, the encoder will try to choose an
efficient subformat. The -type option can be used to override
this behavior. For example, to prevent a JPEG from being written
in grayscale format even though only gray pixels are present, use.
convert bird.png -type TrueColor bird.jpg
Similarly, use -type TrueColorMatte to force the encoder to write
an alpha channel even though the image is opaque, if the output
format supports transparency.
Use -type optimize to ensure the image is written in the
smallest possible file size.
Note that the type is different from the colourspace. Most images will have a similar type an colourspace, e.g. Type: Grayscale
and Colorspace: Gray
, but that does not need to be the case. The image (in the question) that was uploaded to my server had a distinct type and colourspace.
The types from the man above can be understood as:
- Bilevel: Black and white (I do not think anyone still uses this)
- Grayscale: 0-255 are shades of Gray
- Palette: Uses a colourmap present in the image
- TrueColor: Uses one of the standard colour schemes (today most likely sRGB)
- ColorSeparation: Not sure, but I think it stores channels separately
- |one of the above|Matte: Forces the existence of an Alpha channel.
Note: By today's standards the difference between writing a grayscale image out in the sRGB colourspace or in the Gray colourspace should be almost negligible. (And using plain RGB instead of sRGB would also be negligible, and the image would often sdisplay badly. Don't use plain RGB, use sRGB.)
Back to the images
Now we can explain the decisions taken by IM, and we can argue that it is image1.png that is at fault, not IM. Let's again have a look at the images:
[grochmal@phoenix so]$ identify -verbose image1.png | grep -A 3 -e 'Color\|Type\|Class'
Class: DirectClass
Geometry: 95x74+0+0
Resolution: 28.35x28.35
Print size: 3.35097x2.61023
--
Type: PaletteAlpha
Endianess: Undefined
Colorspace: sRGB
Depth: 8-bit
Channel depth:
red: 8-bit
--
Colors: 96
Histogram:
1357: (217,217,217,255) #D9D9D9FF grey85
16: (217,217,218,255) #D9D9DAFF srgba(217,217,218,1)
[grochmal@phoenix so]$ identify -verbose image2.png | grep -A 3 -e 'Color\|Type\|Class'
Class: DirectClass
Geometry: 69x102+0+0
Resolution: 28.35x28.35
Print size: 2.43386x3.59788
--
Type: GrayscaleAlpha
Endianess: Undefined
Colorspace: sRGB
Depth: 8-bit
Channel depth:
gray: 8-bit
--
Colors: 188
Histogram:
1: ( 26, 26, 26,255) #1A1A1AFF graya(26,1)
1: ( 35, 35, 35,255) #232323FF graya(35,1)
We can see that image1.png has a PaletteAlpha
type (Or PaletteMatte
in IM terms) and image2 has a GrayscaleAlpha
type.
The fact that image1.png does not have a colourmap is plain wrong. So buggy image writer must have written that image as having a colourmap but not writing the colourmap into the image.
When IM sees that the image should have a colourmap it assigns one, based on a guess probably. The guess appears to be a grayscale colourmap which is added during the conversion.
(See the question to see the output of the images being guess-converted)
How to fix it?
Fortunately, we can help IM by telling it that we do not want to keep or guess the type of the image. We simply want an image that will use colours from a standard colourmap (sRGB or Gray for our purposes) instead of a Palette which we do not have anyway (since the buggy image writer did not write it into the image).
We use -type TrueColor
:
[grochmal@phoenix so]$ convert image1.png -strip -quality 80 -type TrueColor image1.jpg
[grochmal@phoenix so]$ convert image2.png -strip -quality 80 -type TrueColor image2.jpg
And the images come out the way we expect:
[grochmal@phoenix so]$ identify -verbose image1.jpg | grep -A 3 -e 'Color\|Type\|Class'
Class: DirectClass
Geometry: 95x74+0+0
Resolution: 28x28
Print size: 3.39286x2.64286
--
Type: Grayscale
Endianess: Undefined
Colorspace: sRGB
Depth: 8-bit
Channel depth:
gray: 8-bit
--
Colors: 37
Histogram:
1520: (217,217,217) #D9D9D9 gray(217)
95: (219,219,219) #DBDBDB gray(219)
[grochmal@phoenix so]$ identify -verbose image2.jpg | grep -A 3 -e 'Color\|Type\|Class'
Class: DirectClass
Geometry: 69x102+0+0
Resolution: 28x28
Print size: 2.46429x3.64286
--
Type: Grayscale
Endianess: Undefined
Colorspace: sRGB
Depth: 8-bit
Channel depth:
gray: 8-bit
--
Colors: 195
Histogram:
1: ( 14, 14, 14) #0E0E0E gray(14)
1: ( 37, 37, 37) #252525 gray(37)
Note that IM figured out that the images use a Grayscale of pixels and gave out a DirectClass,Grayscale,sRGB image, which is likely to be what we are after in almost all cases.
If you instead use -type Grayscale
you will get a PseudoClass,Graysclae,Gray image, which may save you a handful of bytes (perhaps 10-20k on a typical web image of 100-500k) at the cost of a less compatible format. For example, my pygtk
sometimes barfs on PseudoClass images, lack of backward compatibility I guess.
(226, 227, 227)
, 16 pixels with rgb values(217, 217, 218)
, etc. These slightly-off-gray colors prevent the use of a grayscale output format. – Jul 29 '17 at 22:50