Ashley Sheridan​.co.uk

CatImgPHP - Preview Images on the Command Line with PHP

Posted on

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:

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:

List of times in seconds for a run of the first iteration of my script
RunTime (in seconds)
11.458
21.474
31.549
41.458
51.639
61.437
71.481
81.419
91.498
101.449
Total1.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.

First attempt of command line image preview

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.

List of times in seconds for a run of the second iteration of my script
RunTime (in seconds)
11.195
21.164
31.216
41.126
51.156
61.191
71.18
81.201
91.158
101.178
Total1.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:

List of times in seconds for a run of the third iteration of my script
RunTime (in seconds)
10.06
20.063
30.061
40.067
50.062
60.066
70.061
80.063
90.062
100.062
Total0.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.

List of times in seconds for a run of the best quality iteration of my script
RunTime (in seconds)
10.109
20.118
30.111
40.112
50.114
60.112
70.111
80.11
90.11
100.111
Total0.1118
More detailed command line image preview

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:

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:

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.