File Coverage

blib/lib/App/WIoZ.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1 1     1   712 use strict;
  1         1  
  1         28  
2 1     1   5 use warnings;
  1         1  
  1         43  
3             package App::WIoZ;
4             {
5             $App::WIoZ::VERSION = '0.004';
6             }
7              
8             #use feature 'say';
9 1     1   381 use Moose;
  0            
  0            
10             use Color::Mix;
11             use Cairo;
12             use Math::PlanePath::HilbertCurve;
13             use Graphics::ColorNames;
14             use App::WIoZ::Point;
15             use App::WIoZ::Word;
16              
17             # ABSTRACT: App::WIoZ create a SVG or PNG image of a word cloud from a simple text file
18              
19             =head1 NAME - App::WIoZ
20              
21             App::WIoZ - a perl word cloud generator
22              
23             =head1 VERSION
24              
25             version 0.004
26              
27             =head1 DESCRIPTION
28              
29             App::WIoZ can create a SVG or PNG image of a word cloud from a simple text file with C<word;weight>.
30              
31             App::WIoZ is an acronym for "Words for Io by Zeus", look for the Correggio painting to watch the cloud.
32              
33             App::WIoZ is based on C<Wordle> strategy and C<yawc> perl clone.
34              
35             Usage:
36              
37             my $File = 'words.txt';
38              
39             my $wioz = App::WIoZ->new(
40             font_min => 18, font_max => 64,
41             set_font => "DejaVuSans,normal,bold",
42             filename => "testoutput",
43             basecolor => '226666'); # violet
44              
45             if (-f $File) {
46             my @words = $wioz->read_words($File);
47             $wioz->do_layout(@words);
48             }
49             else {
50             $wioz->chg_font("LiberationSans,normal,bold");
51             $wioz->update_colors('testoutput.sl.txt');
52             }
53              
54             watch C<doc/freq.pl> to create a C<words.txt> file.
55              
56             =head1 STATUS
57              
58             App::WIoZ is actually a POC to play with Moose, Cairo or Math::PlanePath.
59              
60             The use of an Hilbert curve to manage free space is for playing with Math::PlanePath modules.
61              
62             Performance can be improved in free space matching, or in spiral strategy to find free space.
63              
64             Max and min font sizes can certainly be computed.
65              
66             Feel free to clone this project on GitHub.
67              
68             =head1 SETTINGS
69              
70             =head2 height
71              
72             image height, default to 600
73              
74             =cut
75              
76             has 'height' => (
77             is => 'ro', isa => 'Int', default => 600
78             );
79              
80             =head2 width
81              
82             image width, default to 800
83              
84             =cut
85              
86             has 'width' => (
87             is => 'ro', isa => 'Int', default => 800
88             );
89              
90             has 'center' => (
91             is => 'ro', isa => 'App::WIoZ::Point',
92             lazy => 1,
93             default => sub {
94             my $self = shift;
95             return App::WIoZ::Point->new(
96             x => int($self->width/2),
97             y => int($self->height/2));
98             }
99             );
100              
101             =head2 font_min, font_max
102              
103             required min and max font size
104              
105             =cut
106              
107             has ['font_min','font_max'] => (
108             is => 'ro', required => 1, isa => 'Int'
109             );
110              
111             =head2 set_font, chg_font, font
112              
113             accessors for font name, type and weight
114              
115             C<set_font> : set font in new WIoZ object, default is C<'LiberationSans,normal,bold'>
116              
117             C<chg_font> : change font
118              
119             C<font> : read font object
120              
121             Usage :
122              
123             $wioz = App::WIoZ->new( font_min => 18, font_max => 64,
124             set_font => 'DejaVuSans,normal,bold');
125            
126             $fontname = $wioz->font->{font};
127             $wioz->chg_font('LiberationSans,normal,bold');
128              
129              
130             =cut
131              
132             has 'font' => (
133             isa => 'HashRef',
134             is => 'ro', lazy => 1,
135             writer => 'chg_font',
136             builder => '_set_font'
137             );
138              
139             # for font builder
140             has 'set_font' => ( is => 'rw',isa => 'Str' );
141              
142             sub _set_font {
143             my ($self,$font) = @_;
144             my ($fname,$ftype,$fweight) = split ',', ($self->set_font || ',,');
145             return ( { font => $fname || 'LiberationSans',
146             type => $ftype || 'normal',
147             weight => $fweight || 'bold' });
148             };
149              
150             # for font change
151             around 'chg_font' => sub {
152             my ($next,$self,$font) = @_;
153             my ($fname,$ftype,$fweight) = split ',', $font;
154             $self->$next( {font => $fname, type => $ftype, weight => $fweight});
155             };
156              
157             has 'backcolor' => (
158             is => 'ro', isa => 'Str',
159             default => 'white'
160             );
161              
162             has 'cr' => (
163             is => 'rw', isa => 'Cairo::Context',
164             lazy => 1, builder => '_create_cr'
165             );
166              
167             has 'surface' => (
168             is => 'rw', isa => 'Cairo::ImageSurface',
169             );
170              
171             has 'svgsurface' => (
172             is => 'rw', isa => 'Cairo::SvgSurface',
173             );
174              
175             =head2 filename
176              
177             file name output, extension C<.png> or C<.svg> will be added
178              
179             =cut
180              
181             has 'filename' => (
182             is => 'rw', isa => 'Str',
183             );
184              
185             =head2 svg
186              
187             produce a svg output, default value
188              
189             set to 0 to write a png
190              
191             =cut
192              
193             has 'svg' => (
194             is => 'ro', isa => 'Int', default => 1
195             );
196              
197             has 'fcurve' => (
198             is => 'rw', isa => 'Math::PlanePath',
199             );
200              
201             =head2 scale
202              
203             Scale for the Hilbert Curve granularity default to 10
204              
205             Higer value produces better speed but more words recovery.
206              
207             =cut
208              
209             has 'scale' => (
210             is =>'ro', isa => 'Int', default => 10 # 20 better
211             );
212              
213             has 'cused' => (
214             is => 'rw', isa => 'ArrayRef[Int]', default => sub {[]}
215             );
216              
217             =head2 basecolor
218              
219             Base color for color theme, default to 882222
220              
221             =cut
222              
223             has 'basecolor' => (
224             is =>'ro', isa => 'Str', default => '882222'
225             );
226              
227             =head1 METHODS
228              
229             =cut
230              
231             sub _create_cr {
232             my $self = shift;
233             my $scale = $self->scale;
234             my $hilbert = Math::PlanePath::HilbertCurve->new;
235             $self->fcurve($hilbert);
236             my $cr;
237              
238             if ($self->svg) {
239             my $svgsurface = Cairo::SvgSurface->create ($self->filename.'.svg', $self->width, $self->height);
240             $self->svgsurface($svgsurface);
241             $cr = Cairo::Context->create($svgsurface);
242             }
243             else {
244             my $surface = Cairo::ImageSurface->create ('argb32', $self->width, $self->height);
245             $self->surface($surface);
246             $cr = Cairo::Context->create($surface);
247             };
248              
249             $cr->save;
250             $cr->rectangle (0, 0, $self->width, $self->height);
251             my $po = Graphics::ColorNames->new;
252             my @rgb = $po->rgb($self->backcolor);
253             $cr->set_source_rgb ($rgb[0]/255.0, $rgb[1]/255.0, $rgb[2]/255.0);
254             $cr->fill;
255             $cr->restore;
256             return $cr;
257             };
258              
259             =head2 read_words
260              
261             read words form file : C<word;weight>
262              
263             Usage:
264             my @words = $wioz->read_words($File);
265              
266             =cut
267              
268             sub read_words {
269             my ($self, $filename) = @_;
270             my ($weight_min, $weight_max) = (1000000000, 0);
271             my @res = ();
272             my $fh;
273             open $fh, '<:utf8', $filename;
274             my @L = <$fh>;
275             close $fh;
276             foreach my $l (@L) {
277             my ($t,$n) = split /;/,$l;
278             if ( $t && $n ) {
279             $t =~ s/\s*$//g; $n =~ s/\s*$//g;
280             #$all_weight += $n;
281             $weight_max = $n if ( $n >$weight_max );
282             $weight_min = $n if ( $n <$weight_min );
283             my $w = new App::WIoZ::Word(text => $t, weight => $n, font => $self->font);
284             push @res, $w;
285             } else {
286             warn "error line: $_";
287             }
288             }
289             # set initial size and color
290             my @color = Color::Mix->new->analogous($self->basecolor, 12, 12);
291             foreach my $v (@res) {
292             $v->size( (($v->weight - $weight_min) / ($weight_max - $weight_min)) *
293             ($self->font_max - $self->font_min) +
294             $self->font_min );
295             $v->color($color[int(rand(12))]);
296             }
297             return @res;
298             }
299              
300              
301             =head2 update_colors
302              
303             Read words position from file and update colors.
304              
305             Usage:
306              
307             $wioz->update_colors("file.sl.txt");
308              
309             =cut
310              
311             sub update_colors{
312             my ($self, $filename) = @_;
313              
314             open my $fh, '<:utf8', $filename or die $filename . ' : ' .$!;
315             my @L = <$fh>;
316             close $fh;
317              
318             my @color = Color::Mix->new->analogous($self->basecolor, 12, 12);
319              
320             # reset background
321             $self->cr->rectangle (0, 0, $self->width, $self->height);
322             my $po = Graphics::ColorNames->new;
323             my @rgb = $po->rgb($self->backcolor);
324             $self->cr->set_source_rgb ($rgb[0]/255.0, $rgb[1]/255.0, $rgb[2]/255.0);
325             $self->cr->fill;
326              
327             foreach my $l (@L) {
328             my ($show,$text,$size,$x,$y,$angle) = split /\t/,$l;
329             #say "$text - $size - $angle";
330             my $w = App::WIoZ::Word->new(text => $text, size => $size, angle => $angle, show => $show, color => $color[int(rand(12))], font => $self->font);
331             my $newc = App::WIoZ::Point->new( x => $x, y => $y);
332             $w->update_size($self,$size);
333             $w->update_c($newc);
334             $self->_show_word($w);
335             }
336             $self->_save_to_png if (!$self->svg);
337             }
338              
339             =head2 do_layout
340              
341             Compute words position, save result to svg or png image, save in C<filename.sl.txt> words positions to update colors.
342              
343             Usage :
344             $wioz->do_layout(@words);
345              
346             =cut
347              
348             sub do_layout {
349             my ($self,@words) = @_;
350             my $c = 0;
351             my $current = undef;
352             my @dx = (1, 1, 0, 0,-1,-1,-1,-1, 0, 0, 1, 1);
353             my @dy = (0, 1, 1, 1, 1, 0, 0,-1,-1,-1,-1, 0);
354              
355             #foreach my $w (@words) {
356             foreach my $w (sort {$b->weight cmp $a->weight} @words) {
357             # init
358             $w->show(1);
359             $w->update_size($self,$w->size) if (!$w->height && !$w->width);
360             $current = $w if (! $current);
361              
362             # process
363             my $inside;
364             my @ranges;
365              
366             my ($x1, $y1) = my ($x, $y) = (int($self->width/2), int($self->height/2));
367             my $step = $self->scale;
368             my $dir = 0;
369             my $i = 0;
370             do {
371             # spiral
372             my $newc = App::WIoZ::Point->new( x => int($x), y => int($y));
373             $x1 = $x1 + $dx[$i%12] * $step;
374             $y1 = $y1 + $dy[$i%12] * $step;
375             $x = $x1; $y = $y1;
376             $step += 2 ;
377             $w->update_c($newc);
378             # is in free space
379             $inside = ($w->p->x > 0 && $w->p->x <= $self->width &&
380             $w->p2->x > 0 && $w->p2->x <= $self->width &&
381             $w->p->y > 0 && $w->p->y <= $self->height &&
382             $w->p2->y > 0 && $w->p2->y <= $self->height) || 0;
383             @ranges = $w->is_free($self) if $inside;
384             # try some other strategy
385             $i++;
386             if ($i>60 || !$inside) {
387             $i = 10;
388             $step=$self->scale;
389             my ($xt,$yt) = $self->_random_point($current->width,$current->height);
390             ($x1, $y1) = ($x, $y) = ($current->p->x + $xt,$current->p->y - $yt);
391             if ( ! $dir ) {
392             $dir = 1;
393             #say ' revert : '.$w->text;
394             my @rdx = reverse @dx;
395             my @rdy = reverse @dy;
396             @dx = @rdx; @dy = @rdy;
397             }
398             else {
399             $dir = 0;
400             if ($w->size - 1 <= 5) {
401             #say ' no place for : '.$w->text;
402             $w->show(0);
403             next;
404             }
405             #say ' decrease : '.$w->text;
406             $w->update_size($self,$w->size - 1);
407             }
408             };
409             } while ( ! $inside || scalar @ranges == 1 );
410              
411             # register used space
412             map { if ($_) {push @{ $self->cused }, $_} } @ranges;
413              
414             # show
415             $self->_show_word($w) if ($w->show);
416              
417             #$c++; last if $c > 2;
418             }
419              
420             $self->_save_to_png if (!$self->svg);
421              
422             $self->_save_layout(@words);
423              
424             }
425              
426             sub _save_to_png {
427             my $self = shift;
428             $self->surface->write_to_png ($self->filename . '.png');
429             }
430              
431             # Save words position to a file. Usefull to update colors.
432             sub _save_layout {
433             my ($self, @words) = @_;
434             my $fh;
435             open $fh, '>:utf8', $self->filename . '.sl.txt';
436             foreach my $w (@words) {
437             print $fh $w->show."\t".$w->text."\t".$w->size."\t".$w->c->x."\t".$w->c->y."\t".$w->angle."\n";
438             }
439             close $fh;
440             }
441              
442              
443             sub _show_word {
444             my ($self,$w) = @_;
445              
446             $self->cr->select_font_face(
447             $w->font->{font},$w->font->{type},$w->font->{weight});
448             $self->cr->set_font_size($w->size);
449             my $po = Graphics::ColorNames->new;
450             my @rgb = $po->rgb($w->color);
451             $self->cr->set_source_rgb ($rgb[0]/255.0, $rgb[1]/255.0, $rgb[2]/255.0);
452             #say ' '.$w->text.' '.$w->color;
453             if ($w->angle < 0) {
454             $self->cr->save;
455             $self->cr->move_to($w->p->x+$w->width,$w->p->y);
456             $self->cr->rotate($w->angle);
457             $self->cr->show_text($w->text);
458             $self->cr->restore;
459             }
460             else {
461             $self->cr->move_to($w->p->x,$w->p->y);
462             $self->cr->show_text($w->text);
463             }
464              
465             }
466              
467             sub _random_point {
468             my ($self,$width, $height) = @_;
469             my $x = rand( $width * 0.8 ) + $width * 0.1 ;
470             my $y = rand( $height * 0.8 ) + $height * 0.1 ;
471             return ($x, $y);
472             }
473              
474             =head1 Git
475              
476             L<https://github.com/yvesago/WIoZ/>
477              
478             =head1 AUTHORS
479              
480             Yves Agostini, C<< <yveago@cpan.org> >>
481              
482             =head1 LICENSE AND COPYRIGHT
483              
484             Copyright 2013 - Yves Agostini
485              
486             This program is free software and may be modified or distributed under the same terms as Perl itself.
487              
488             =cut
489              
490             1;