Tags:
I was reading an article recently about the catimg
tool used to preview images on the command line. I saw this as a fun way to keep my PHP skills sharp.
The Inspiration
I installed the catimg
tool locally to test it out and see what kind of output it produced. It's included in most Linux distributions, and is available on MacOS via Homebrew, but Windows users will need to build it from source; I was in luck as my test machine was running Fedora.
From the output, it was fairly obvious what it was doing:
- Shell escape codes to select foreground and background colours
- Block drawing characters for the acual displayed "pixels"
Armed with this knowledge (educated guesses), I set about creating my own version with PHP.
A Note for Testing Times
All tests were performed on the same machine: a 4-core 3.3GHz Intel i3, with 8GB of RAM. On the software side, the OS was Fedora 25, running PHP 7.0.25, on Apache 2.4.
First Attempt
My initial try at this (seen in the Github first commit used only whole blocks of background colour.
I found all the console escape codes I'd need at the Bash tips: Colors and formatting (ANSI/VT100 Control sequences) and created a list of RGB colours from this helpful console colour cheat sheet.
A quick regular expression find/replace later, and I had several hundred lines of code similar to the following setting up my Colour
objects:
$colours = [
new Colour(0, 0, 0),
new Colour(128, 0, 0),
new Colour(0, 128, 0),
new Colour(128, 128, 0),
new Colour(0, 0, 128),
new Colour(128, 0, 128),
// several hundred lines more like this...
];
Overall, the script was fairly slow. My chosen source image was 1200×1600 pixels in size, and my console window was 89×58 characters in size. The script took an average of 1.49 seconds (basically forever in computing terms) over a run of 10 tests:
Run | Time (in seconds) |
---|---|
1 | 1.458 |
2 | 1.474 |
3 | 1.549 |
4 | 1.458 |
5 | 1.639 |
6 | 1.437 |
7 | 1.481 |
8 | 1.419 |
9 | 1.498 |
10 | 1.449 |
Total | 1.4862 |
The output was fairly basic, and very blocky, with each console character block representing a pixel of the scaled down preview image (scaled to fit into the window) which was why the image appears so distorted, as character blocks are roughly twice as high as they are wide.
The core of this initial attempt was the colour matching code:
public function get_closest_colour($colour_value)
{
$lowest_difference = null;
$matched_colour_index = null;
$colour_to_find = $this->get_colour_from_number($colour_value);
foreach($this->colours as $index => $colour)
{
$difference = sqrt(
pow($colour_to_find->r - $colour->r, 2) +
pow($colour_to_find->g - $colour->g, 2) +
pow($colour_to_find->b - $colour->b, 2)
);
if($difference == 0)
{
return $index;
}
if(is_null($lowest_difference) || $difference < $lowest_difference)
{
$lowest_difference = $difference;
$matched_colour_index = $index;
}
}
return $matched_colour_index;
}
I was a bit disappointed with the speed though, as it was over 25 times slower than the original C
version from which I was inspired, and I suspected the above code to be the main culprit.
Round Two: Avoiding Lookups for Already Matched Colours
The second attempt was aimed at improving the speed. I did this by generating a list of matched colours while the program was attempting to determine the closest colour from the 256-colour palette, and return matches from this list if the same colour was matched previously. This did improve the speed a little for my test image, shaving ³/â‚â‚€ of a second from the overall time. I would expect better benefits for images with larger blocks of colour, such as graphics with large colour blocks, graphs, charts, etc. Photos, like the one I used in my test (and what I would presume is the more popular use-case for a script such as this), would have the least benefit from this change.
Run | Time (in seconds) |
---|---|
1 | 1.195 |
2 | 1.164 |
3 | 1.216 |
4 | 1.126 |
5 | 1.156 |
6 | 1.191 |
7 | 1.18 |
8 | 1.201 |
9 | 1.158 |
10 | 1.178 |
Total | 1.1765 |
While this wasn't the part of the code that I felt was taking most of the tme, it felt like low-hanging fruit that I could tackle very simply.
if(isset($this->matched_colours[$colour_value]))
return $this->matched_colours[$colour_value];
Round 3: Utilising In-Built Colour Matching
As I'd earlier suspected, the main area where the script was spending its time was the colour matching method I wrote (see above). I recalled that the GD library offers a way to do this perfectly using paletted images, which is a happy coincidence as we are working with a palette rather than traditional "true colour".
The imagecolorclosest()
function is vastly faster than my original custom code, and only needs an initial image with the colours added to check against. The nice thing about this approach is that the image in memory only needs the colours added to the palette, it doesn't need them to be used within the image, so I passed in a 1×1 image and added the colours using the imagecolorallocate()
function.
public function get_closest_colour($colour_value)
{
$colour_to_find = $this->get_colour_from_number($colour_value);
return imagecolorclosest($this->palette_image, $colour_to_find->r, $colour_to_find->g, $colour_to_find->b);
}
This produced the greatest speed boost, and actually brought the whole thing in-line with the C
version. The C
version took an average 0.059 seconds, and this attempt at the PHP
one took an average of 0.0627 seconds; not far off at all. These are the times for 10 runs:
Run | Time (in seconds) |
---|---|
1 | 0.06 |
2 | 0.063 |
3 | 0.061 |
4 | 0.067 |
5 | 0.062 |
6 | 0.066 |
7 | 0.061 |
8 | 0.063 |
9 | 0.062 |
10 | 0.062 |
Total | 0.0627 |
Quality Improvements
Now that I'd got the speed to something more reasonable, I wanted to bring the quality of the preview image up to match catimg
. This I achieved by scanning two pixels at a time from adjoining rows, and outputting the second one using a foreground escape sequence and the half block drawing symbol ▄.
This did have the obvious downside of increasing the time, but overall it was still far faster than the first attempt.
Run | Time (in seconds) |
---|---|
1 | 0.109 |
2 | 0.118 |
3 | 0.111 |
4 | 0.112 |
5 | 0.114 |
6 | 0.112 |
7 | 0.111 |
8 | 0.11 |
9 | 0.11 |
10 | 0.111 |
Total | 0.1118 |
What's Next?
Initially I'd updated the readme.md
file with some possible ideas about where to take this script in the future if I had some time (and the inclination) to spend on it. I had 3 main ideas:
- Allow more detailed image drawing using block drawing characters ontop of the background colours
- Support for transparency
- Improve speed of renders
Of the two, I've achieved the first and last, and come as close to the original C
version as I believe is possible with vanilla PHP.
I would like to add to the list:
- Return cursor state to the command line default.
By this, I mean change the foreground and background colour back to what it was before the script was run. Right now it just sets it back to grey on black, which is the default colouring for a typical Linux and Windows terminal window. Mac users, on the other hand, well, their default terminal window is black on white, so using this would really cause some weirdness!
Can You Use It?
Absolutely! Just follow the installation instructions detailed in the Read Me.
If you have any questions, suggestions, or want to supply any improvements, please feel free to contact me or open a pull request on the GitHub repo.
Comments