I have been experimenting with ImageMagick for quite some time now. Its one of those great libraries one couldn't manage without and the best part is it just works. I had to write an implementation on top of ImageMagick, to draw styled text on image. Which i thought was pretty straight forward, and started to dig on the api docs of ImageMagick. In the end i finally found that, there was no implementation or function in api to wrap have different style of text in the same line. No big deal right, but then there are lot of subtleties which one will not realize until he/she is deep in it.
I then went over my normal ritual of getting the pseudo code(comments, i cant work without them). First i thought the only thing i ll have to do would be to wrap the text, and then set the height which is maximum for any style. There also was a straight forward api function call in ImageMagick (QueryMultilineFontMetrics). So i started not knowing what i was getting into. The wrap logic was something like,
1. you get the width of all the visible characters by making the api call,
2. now get all the words in the sentence/ paragraph
3. for each of those word add the width for each character and try to figure out whether it has surpassed the line width or not.
4. if it has start over on the next line, calculate the height needed.
To describe the above solution in one word, it is specious and it didnt work. Some of the characters were going outside the boundary. I didnt know a thing about typography and typefaces which in a way increased my curiosity. Here I was coding, without really knowing any fundamentals about the internals. I decided to give programming a pass and started reading about typeface and things related with it.
I have been using computer for more than a decade now, but one thing that has never interested me is fonts. I was finally coming to terms with my demons. As i read more about fonts, it became clear that there was a lot of thought, logic and research gone into it, so much so that it was a science as well as creative. I feel it is one of those things which stares right on our face, but we just dont realize it. The fonts are mainly categorized into monospaced (ones we see in code editors) i.e. all characters are equally spaced and have equal widths and what we normally write in paper is proportional typeface i.e. 'i' has a smaller width compared to 'W'. There is something else apart from these in the font, which created the bug in my code. They are the small pauses between two characters when they are rendered. My algorithm didnt include kerning(this is what the that hole is called) for calculations and resulted in text bleeding out. I had to slightly change my approach, and had to include this along, but since the font could be proportional or monospaced you dont really know that width. The only way you can do that is by making the query fonts call for the whole word, and use it for all the calculations. Apart from that i also found that QueryFontMetrics was trimming the space, and it had to be found out in a round about way.
use strict;
use warnings;
use Image::Magick;
my ($my_styles);
$my_styles = {
style1 => {
font => "Arial",
color => "#000000",
size => "20"
},
style2 => {
font => "Arial",
color => "#00ff00",
size => "25"
}
};
#returns the image magick object after rendering the text, with given height and width.
#it truncates the paragraph after reaching the height limit.
#wraps the text, and also can support multiple styles in the same line
sub draw_styled_text {
my ($text, $height, $width, $default_style) = @_;
my $im = Image::Magick->new();
my $last_pos = 0;
my ($x, $y) = (0, 0);
my %lines;
$im->Set(
size => $width . "x" . $height
);
$im->Read('xc:none');
#format
#sentence%%$$style_name$$sentence%%sentence
#for each styled sentence
my @sentences = split /\%\%/, $text;
for my $sentence (@sentences) {
my ($style_name, $style_details);
$style_name = $default_style;
if ($sentence =~ /\$\$(.*)\$\$([\w\W]*)/) {
#get the style name,
$style_name = $1;
$sentence = $2;
}
#check length of the string
unless (length $sentence) {
next;
}
#get the style details,
$style_details = get_style_details($style_name);
$im->Set(
font => $style_details->{'font'},
pointsize => $style_details->{'pointsize'}
);
#wrap text
my ($new_text, $line_height);
my $space_width = get_space_metrics($im);
#annotate trims text. to avoid that check the no. of spaces in the begining and in the end.
#space in the begining of the string
if ($sentence =~ /^( +).*/) {
$last_pos = $last_pos + ($space_width * length($1));
$x = $last_pos;
}
($new_text, $last_pos, $line_height) = Wrap($sentence, $im, $width, $last_pos);
#space at the end of the string.
if ($sentence =~ /.*( +)$/) {
$last_pos = $last_pos + ($space_width * length($1));
}
#for each line
for my $line (split /\n/, $new_text) {
$y = $line_height unless ($y);
my $arr;
if (defined $lines{$y}) {
$arr = $lines{$y};
} else {
$arr = ();
}
#write the line
push @$arr, {
x => $x,
y => $y,
text => $line,
color => $style_details->{'color'},
font => $style_details->{'font'},
pointsize => $style_details->{'pointsize'},
line_height => $line_height
};
$lines{$y} = $arr;
$x = 0;
$y = $y + $line_height;
}
$x = $last_pos;
if ($new_text !~ /.*\n$/) {
$y = $y - $line_height;
} else {
$x = $last_pos = 0;
}
}
my $adjustment_y = 0;
for my $y(sort {$a <=> $b} keys %lines) {
my $arr = $lines{$y};
my $max_height = 0;
my $considered_height = 0;
for my $line (@$arr) {
if ($max_height < $line->{'line_height'}) {
$max_height = $line->{'line_height'};
}
unless ($considered_height) {
$considered_height = $line->{'line_height'};
}
}
if ($considered_height != $max_height) {
$adjustment_y += $max_height - $considered_height;
}
$y += $adjustment_y;
for my $line (@$arr) {
$im->Annotate(
font => $line->{'font'},
fill => $line->{'color'},
pointsize => $line->{'pointsize'},
text => $line->{'text'},
x => $line->{'x'},
y => $y,
);
}
}
#return
return $im;
}
#im doesnt support space
sub get_space_metrics {
my ($im) = @_;
my $val = 0;
#first get for a
my $a_width = ($im->QueryFontMetrics(text => "a"))[4];
my $a_with_space_width = ($im->QueryFontMetrics(text => "a a"))[4];
$val = $a_with_space_width - (2 * $a_width);
return $val;
}
sub Wrap {
my ($text, $img, $maxwidth, $lastpos) = @_;
my (@newtext, $pos);
$pos = $lastpos || 0;
my (@words, @lines, @char, $i, $word);
@char = split //, $text;
$i = 0;
$word = "";
for my $c (@char) {
$i++;
#get the word
if ($c eq " " || $c eq "-" || $c eq "\n") {
push @words, $word;
} else {
$word = $word . $c;
if ($i >= scalar @char) {
push @words, $word;
} else {
next;
}
}
$word = "";
#measure the length of current line
my @metrics = $img->QueryFontMetrics(text => join("", @words));
if (($pos + $metrics[4]) > $maxwidth) {
$word = pop @words;
push @lines, join("", @words), "\n";
$pos = 0;
@words = ();
#check whether the size of the word is greater than max width
if ($word && ($img->QueryFontMetrics(text => $word))[4] > $maxwidth) {
#force a line break and -
my $splitword = "";
for my $ch (split //, $word) {
#check whether the set is greater than max width
if (($img->QueryFontMetrics(text => "$splitword$ch-"))[4] > $maxwidth) {
push @lines, "$splitword-", "\n";
$splitword = "";
} else {
$splitword .= $ch;
}
}
if ($splitword) {
$word = $splitword;
}
}
#push it to the current line
if ($word) {
push @words, $word;
$word = "";
}
}
#add the white space
if (scalar @words) {
$words[-1] .= $c if ($c eq " " || $c eq "-");
if ($c eq "\n") {
push @lines, join("", @words), "\n";
@words = ();
}
}
}
#push the last remaining words
push @lines, join("", @words);
my $str = join "", @lines;
my @arr = split /\n/, $str;
#try to measure the width of the last line
my @metrics = $img->QueryMultilineFontMetrics(text => $arr[-1]);
$pos = $metrics[4];
my $line_height = $metrics[5];
unless (scalar @arr > 1) {
$pos += $lastpos;
}
return ($str, $pos, $line_height);
}
sub get_style_details {
my ($style_name) = @_;
return $my_styles->{$style_name} if defined $my_styles->{$style_name};
die "style doesnt exist";
}
P.S.ttt...:
I couldnt find a lot of solutions in web to suite my requirements, and that's one of the reason why i am sharing the solution. The code though is not neat, and there could be other optimizations that can be done, but at the very least it works and can be used as a base for further refinements.
download the source code here:
Perl WordWrap for ImageMagick