File Coverage

blib/lib/Chart/EPS_graph.pm
Criterion Covered Total %
statement 21 263 7.9
branch 0 84 0.0
condition 0 18 0.0
subroutine 7 25 28.0
pod 0 18 0.0
total 28 408 6.8


line stmt bran cond sub pod time code
1             # $Source: /usr/pkg/lib/perl5/site_perl/5.8.0/Chart/EPS_graph.pm $
2             # $Date: 2006-08-19 $
3              
4             package Chart::EPS_graph;
5              
6 1     1   39657 use strict;
  1         3  
  1         40  
7 1     1   5 use warnings;
  1         2  
  1         32  
8 1     1   7 use Carp qw(carp croak);
  1         6  
  1         69  
9 1     1   5 use Config;
  1         2  
  1         42  
10 1     1   8629 use IPC::Open3;
  1         15532  
  1         97  
11 1     1   1149 use English qw(-no_match_vars);
  1         8593  
  1         8  
12 1     1   1737 use Chart::EPS_graph::PS;
  1         4  
  1         3764  
13             require File::Find;
14              
15             our ($VERSION) = '$Revision: 0.01 $' =~ m{ \$Revision: \s+ (\S+) }xm;
16              
17             my $EMPTY = q{};
18              
19             # The test module is stand-alone OO and makes own object.
20             sub full_test {
21 0     0 0   my ($ignored, $dir_path) = @_;
22 0           require Chart::EPS_graph::Test;
23 0           return Chart::EPS_graph::Test->full_test($dir_path);
24             }
25              
26             # Yack at user about what goes on.
27             sub verbose {
28 0 0   0 0   ref( my $self = shift ) or croak 'Oops! Method verbose() is instance, not class.';
29 0 0         print $_[0] if $_[1] <= $self->{verbosity};
30 0           return 'Pthhht! to Perl::Critic';
31             }
32              
33 0     0 0   sub version { return $VERSION }
34              
35             # Create new object.
36             sub new {
37 0 0   0 0   ref( my $class = shift ) and croak 'Oops! Method new() is class, not instance.';
38 0           my $self = {};
39 0           for (keys %Chart::EPS_graph::PS::ps_defaults){
40 0           $self->{$_} = $Chart::EPS_graph::PS::ps_defaults{$_}
41             }
42 0 0         if ( $_[0] ) { $self->{pg_width} = shift }
  0            
43 0 0         if ( $_[0] ) { $self->{pg_height} = shift }
  0            
44 0           $self->{ps_header} = $Chart::EPS_graph::PS::ps_header;
45 0           $self->{ps_header} =~
46             s/BoundingBox:.*\n/BoundingBox: 0 0 $self->{pg_width} $self->{pg_height}\n/m;
47 0           $self->{names} = [];
48 0           $self->{data} = [];
49 0           $self->{y1} = [];
50 0           $self->{y2} = [];
51 0           $self->{shown} = [];
52 0           $self->{not_shown} = [];
53 0           $self->{close_gap} = 0;
54 0           $self->{x_is_zeroth} = 1;
55 0           $self->{x_scale} = 1;
56 0           $self->{ps_path} = $EMPTY;
57 0           $self->{verbosity} = 1; # 0 = Quiet, 1 = Info, 2 = Diagnostic.
58 0           bless $self, $class;
59 0           return $self;
60             }
61              
62             # Allow user to change defaults and input data.
63             sub set {
64 0 0   0 0   ref( my $self = shift ) or croak 'Oops! Method set() is instance, not class.';
65 0           my %user_defs = @_;
66 0           while ( my ($key, $value) = each %user_defs ) {
67 0 0         if ( exists $self->{$key}) {$self->{$key} = $value }
  0            
68 0           else { carp "Oops! Key '$key' non-existant in hash '$self'.\n" }
69             }
70 0           return 'Pthhht! to Perl::Critic';
71             }
72              
73             # Make custom adjustments to default Prolog defs.
74             sub ps_defs_insert {
75 0 0   0 0   ref( my $self = shift ) or croak 'Oops! Method ps_defs_insert() is instance, not class.';
76 0           $self->chans_elect();
77 0           my $str = ();
78              
79 0           $str .= "/bg_color ($self->{bg_color}) def \n";
80 0           $str .= "/fg_color ($self->{fg_color}) def \n";
81 0           $str .= '/web_colors [ ';
82 0           for ( @{$self->{web_colors}} ) { $str .= "/$_ " }
  0            
  0            
83 0           $str .= "] def \n";
84              
85 0           $str .= "/font_name /$self->{font_name} def \n";
86 0           $str .= "/font_size $self->{font_size} def \n";
87              
88              
89             # Set not-to-be-shown labels as empty strings.
90 0           $str .= "/label_top ($self->{label_top}) def \n";
91 0           $str .= "/label_y1 ($self->{label_y1}) def \n";
92 0           $str .= "/label_y1_2 ($self->{label_y1_2}) def \n";
93 0           $str .= "/label_y2 ($self->{label_y2}) def \n";
94 0           $str .= "/label_y2_2 ($self->{label_y2_2}) def \n";
95 0           $str .= "/label_x ($self->{label_x}) def \n";
96 0           $str .= "/label_x_2 ($self->{label_x_2}) def \n";
97              
98 0           $str .= '/fake_col_zero_flag ';
99 0 0         $str .= $self->{x_is_zeroth} ? 'false' : 'true';
100 0           $str .= " def \n";
101              
102 0           $str .= "/fake_col_zero_scale $self->{x_scale} def \n";
103              
104 0           $str .= "/data_sets 1 def \n";
105              
106             # This is an ugly, ex-post-facto hack.
107             # When chans skipped, patch up the bottom string to cover up remove gap in
108             # channel ID's. In short, make legend ID match the gap-free curve ID.
109 0 0         if ($self->{close_gap}) {
110 0           $self->{shown} = [ @{ $self->{y1} }, @{ $self->{y2} } ];
  0            
  0            
111 0           my @gap_free = gap_free_skip( $self->{shown}, $self->{not_shown} );
112 0           for my $i ( 0 .. $#gap_free ) {
113 0           $self->{label_x_proc} =~ s/ $self->{shown}->[$i] / $gap_free[$i] /m;
114 0           $self->{label_x_proc} =~ s/ $self->{shown}->[$i] show_color_id/ $gap_free[$i] show_color_id/m;
115             }
116             }
117              
118 0           $str .= $self->{label_x_proc}; # List of chans shown
119              
120             # An array of data chans embeded in PostScript but whose curves are not to be shown
121             # and their colors skipped over by those curves which are shown.
122 0           $str .= '/not_shown [ ';
123 0 0         $str .= join q{ }, @{$self->{not_shown}} unless $self->{close_gap}; # 'Pthhht!'
  0            
124 0           $str .= " ] def \n";
125              
126             # Y2 axis (re-)numbered for PostScript.
127 0           $str .= '/y2 [';
128 0 0         if ($self->{close_gap}) {
129 0           $str .= join q{ }, gap_free_skip( $self->{y2}, $self->{not_shown} );
130             }
131             else {
132 0           $str .= join q{ }, @{ $self->{y2} };
  0            
133             }
134              
135 0           $str .= "] def \n"; # Y2 axis
136 0           return $str;
137             }
138              
139             # Given two arrays retrun a copy of 2nd array after decrementing its elements
140             # for each lesser element of 1st array. Used to provide PostScript with /column_arrays
141             # named 0 thru N with no gaps when chans have been skipped over to graph.
142             sub gap_free_skip {
143 0     0 0   my ( $shown_aref, $not_shown_aref ) = @_; # Local, not part of $self->{foo} hash!
144 0           my @gap_free = @$shown_aref;
145 0           for my $i ( 0 .. $#gap_free ) {
146 0           for my $j (@$not_shown_aref) {
147              
148             # Index decremented for each gap beneath it.
149 0 0         if ( $j < $shown_aref->[$i] ) { --$gap_free[$i] }
  0            
150             }
151             }
152 0           return @gap_free;
153             }
154              
155             # Assign an arrow character from Symbol font to
156             # indicate which Y axis ought be read from.
157             sub y_arrow {
158 0 0   0 0   ref( my $self = shift ) or croak 'Oops! Method y_arrow() is instance, not class.';
159 0           my $i = shift;
160 0           my $arrow = $EMPTY;
161 0           my @y2 = @{$self->{y2}};
  0            
162 0 0         if (@y2) {
163 0           $arrow = '\254';
164 0 0         for (@y2) { $arrow = '\256' if $_ == $i}
  0            
165             }
166 0           return $arrow;
167             }
168              
169             # PostScript strings are ( ) delimited!
170             sub ps_str_esc {
171 0 0   0 0   ref( my $self = shift ) or croak 'Oops! Method ps_str_esc() is instance, not class.';
172 0           $_[0] =~ s/\(/\\(/gm;
173 0           $_[0] =~ s/\)/\\)/gm;
174 0           $self->verbose( "ps_str_esc(): $_[0] \n", 2);
175 0           return $_[0];
176             }
177              
178             # Build label for chans shown, list of those not to show.
179             sub chans_elect {
180 0 0   0 0   ref( my $self = shift ) or croak 'Oops! Method chans_elect() is instance, not class.';
181              
182 0           for ( @{ $self->{names} } ) { $_ = $self->ps_str_esc($_) }
  0            
  0            
183              
184 0           my @ps_string_list = qw(
185             label_top
186             label_x
187             label_x_2
188             label_y1
189             label_y1_2
190             label_y2
191             label_y2_2
192             );
193              
194 0           for (@ps_string_list) { $self->{$_} = $self->ps_str_esc($self->{$_}) }
  0            
195              
196             # Collect list of shown-channel names
197             # Prettify them into a graph legend good for B&W, not just color.
198 0           for (@{ $self->{y1} }, @{ $self->{y2} }) {
  0            
  0            
199 0           my $arrow = $self->y_arrow($_);
200 0           $self->{label_x_proc} .= " ($arrow$_) $_ show_color_id ($self->{names}->[$_] ) show ";
201             }
202              
203             # Determine list of channels not to show.
204 0           for ( 1 .. $#{$self->{names}} ) {
  0            
205              
206             # Mitigate an RE between Perl & PostScript by swaping all
207             # escaped-for-PostScript string delimiters with Perl RE dots.
208 0           my $re = $self->{names}->[$_];
209 0           $re =~ tr/\\()/.../;
210              
211 0 0         push @{$self->{not_shown}}, $_ unless $self->{label_x_proc} =~ m/$re/m;
  0            
212              
213             # Problems may still exist between Perl RE's and PostScript syntax.
214             # Diagnose these via CLI if in doubt by setting $verbosity = 2.
215 0           $self->verbose(
216             "RE Check 1: \n\t" . $self->{label_x_proc}
217             . "\n\t =~ \n\t$re" . "\n"
218             , 2
219             );
220 0           $self->verbose(
221             'RE Check 2: not_shown = '
222 0           . join(', ', @{$self->{not_shown}}) . "\n\n"
223             , 2
224             );
225             }
226              
227 0           $self->{label_x_proc} = "/label_x_proc { $self->{label_x_proc} } def \n";
228 0           $self->verbose( "LABEL X PROC: $self->{label_x_proc} \n", 2);
229 0           return 'Pthhht! to Perl::Critic';
230             }
231              
232             # Output data in PostScript file format.
233             sub write_eps {
234 0 0   0 0   ref( my $self = shift ) or croak 'Oops! Method write_eps() is instance, not class.';
235 0           $self->{ps_path} = qq|$_[0]|;
236 0           local $OUTPUT_AUTOFLUSH = 1; # from 'use ENGLISH'
237              
238 0           my ( $ps_user_defs ) = $self->ps_defs_insert();
239              
240 0 0         if ( open my $fh, '>', "$self->{ps_path}" ) {
241              
242             # Embed filename sans path in PostScript header.
243 0           $self->{ps_header} =~ s/%%Title:/%%Title: ($self->{ps_path})/m;
244              
245             # Embed document font resources.
246 0           my $doc_rsrcs = "font Symbol $self->{font_name}";
247 0           $self->{ps_header} =~ s/%%DocumentResources:/%%DocumentResources: $doc_rsrcs/m;
248              
249             # IMPORTANT NOTE: Know that Perl::Critic errs about the package vars
250             # named like "$Chart::EPS_graph::PS::ps_foo" below. They are needed!
251              
252 0           print {$fh} $self->{ps_header};
  0            
253 0           print {$fh} $Chart::EPS_graph::PS::ps_web_colors_dict;
  0            
254 0           print {$fh} $Chart::EPS_graph::PS::ps_prolog_generic;
  0            
255 0           print {$fh} $Chart::EPS_graph::PS::ps_prolog_graphing;
  0            
256 0           print {$fh} $Chart::EPS_graph::PS::ps_prolog_data_arrays;
  0            
257 0           print {$fh} $Chart::EPS_graph::PS::ps_prolog_drawing;
  0            
258 0           print {$fh} $ps_user_defs;
  0            
259 0           print {$fh} "/pg_width $self->{pg_width} def \n";
  0            
260 0           print {$fh} "/pg_height $self->{pg_height} def \n";
  0            
261 0           for ( $self->chans_pl2ps() ) { print {$fh} $_ }
  0            
  0            
262 0           print {$fh} $Chart::EPS_graph::PS::ps_tail;
  0            
263 0           close $fh;
264 0           $self->verbose("Okay, EPS file written to path '$self->{ps_path}' \n", 1);
265             }
266 0           else { print qq|Oops! Cannot write to $self->{ps_path}: $!\n| }
267 0           return 'Pthhht! to Perl::Critic';
268             }
269              
270             # Convert kept Perl $self->{data} to sequential PostScript /column_arrays.
271             sub chans_pl2ps {
272 0 0   0 0   ref( my $self = shift ) or croak 'Oops! Method chans_pl2ps() is instance, not class.';
273 0           my @graph_ps;
274 0 0         my $k = $self->{x_is_zeroth} ? 0 : 1; # Preserve /channel_array-0 for X axis only.
275              
276             # Write data from all chans into PostScript arrays
277 0           for my $i ( 0 .. $#{$self->{data}} ) {
  0            
278 0           my $array_ps = ' [ ';
279 0           for ( 0 .. $#{ $self->{data}->[0] } ) {
  0            
280 0           $array_ps .= sprintf '%.3e ', $self->{data}->[$i][$_];
281             }
282 0           $array_ps .= " ] def \n\n";
283              
284             # If skipping chans, is this chan among those not shown?
285 0           my $flg = 0;
286 0 0         if ($self->{close_gap}) {
287 0           for (@{$self->{not_shown}}) {
  0            
288 0 0         if ( $_ == $i ) { $flg = 1; last; }
  0            
  0            
289             }
290             }
291              
292             # Renumber chans so that PostScript progs /column_array's will
293             # be innumerated in sequence with no gaps.
294 0 0 0       if ( $i == 0 || $flg == 0 ) {
295 0           $array_ps = "/column_array-$k $array_ps";
296 0           push @graph_ps, $array_ps;
297 0           ++$k;
298             }
299             }
300 0           push @graph_ps, "\n true \n";
301 0           return @graph_ps;
302             }
303              
304             # Narrow the search of directories made by win32_seek.
305             # If executable in dir named by numerical version, reduce the number of
306             # searched directories by matching to a regex.
307             sub single_dir {
308 0     0 0   my ($parent, $re) = @_;
309 0           my @fewer;
310 0   0       opendir DIR, $parent || croak "Can't open $parent at sub single_dir().";
311 0           my @files = readdir DIR;
312 0           closedir DIR;
313 0 0         for (@files) { push @fewer, $_ if $_ =~ m/$re/m}
  0            
314 0 0         return scalar @fewer == 1 ? $fewer[0] : 0;
315             }
316              
317             # On Win32 different veresions may be located variously.
318             # Not knowing which version user has, we mush seek it.
319             sub win32_seek {
320 0     0 0   our ($reg_ex, $start_path) = @_;
321 0           our $cmd_exe = $EMPTY;
322              
323             sub seek_exe {
324 0 0   0 0   if (m/$reg_ex/m) { $cmd_exe = qq|"$File::Find::name"| }
  0            
325 0           return 'Pthhht! to Perl::Critic';
326             }
327              
328             # Hunt for its full path.
329 0           File::Find::find(\&seek_exe, $start_path);
330              
331 0           $cmd_exe = qq|$cmd_exe|;
332 0           $cmd_exe =~ s/\\/\//gm;
333 0           return $cmd_exe;
334             }
335              
336             # On Win32 different veresions may be located variously.
337             sub win32_run_gs {
338 0 0   0 0   ref( my $self = shift ) or croak 'Oops! Method win32_run_gs() is instance, not class.';
339 0           my $gs_args = shift;
340 0           $self->verbose("Busy hunting for Ghostscript's executable...\n", 1);
341 0           my $cmd = win32_seek('gswin32\.exe$','C:/Program Files/gs/');
342 0           $self->verbose("Path to GhostScript: $cmd \n", 2);
343 0 0         if ($cmd =~ m/gswin32/m) {
344 0           `$cmd $gs_args`; # 'Pthhht!'
345             } else {
346 0           carp 'Oops! Could not find Ghostscript. Is it installed? '
347             }
348 0           return 'Pthhht! to Perl::Critic';
349             }
350              
351             # On Win32 different versions may be located variously.
352             sub win32_run_gimp {
353 0 0   0 0   ref( my $self = shift ) or croak 'Oops! Method win32_run_gimp() is instance, not class.';
354 0           my $path = 'C:/Program Files/';
355 0           $self->verbose("Busy hunting for The GIMP's executable...\n", 1);
356 0           my $sub_dir = single_dir($path, 'GIMP-');
357 0 0         $path .= "$sub_dir/" if $sub_dir;
358 0           $self->verbose("Search path to The GIMP: $path \n", 2);
359 0           my $cmd = win32_seek('gimp-[2-9]\.[2-9]+\.exe$', $path);
360 0           $self->verbose("Path to The GIMP: $cmd \n", 2);
361 0 0         if ($cmd =~ m/gimp-/m) {
362 0           system qq|start $cmd $cmd|, qq|"$self->{ps_path}"|
363             }
364 0           else { carp 'Oops! Could not find The GIMP. Is it installed? '}
365 0           return 'Pthhht! to Perl::Critic';
366             }
367              
368             # Cause *.eps graph to be converted/displayed in by separate program.
369             # Configured so may be called by Chart::EPS_graph::Test.pm
370             sub display {
371 0 0   0 0   ref( my $self = shift ) or croak 'Oops! Method display() is instance, not class.';
372 0           my $cmd;
373 0           my $gs_args = q{ }
374             . '-dSAFER -dBATCH -dNOPAUSE -sDEVICE=png16m '
375             . "-dDEVICEWIDTHPOINTS=$self->{pg_width} "
376             . "-dDEVICEHEIGHTPOINTS=$self->{pg_height} "
377             . '-dTextAlphaBits=2 -dGraphicsAlphaBits=4 -r96x96 '
378             . qq|-sOutputFile="$self->{ps_path}.png" |
379             . qq|"$self->{ps_path}" |;
380 0           $self->verbose("Ghostscript args:\n$gs_args\n", 2);
381              
382             # Test OS: if not Windoze do as for UNIX-like.
383 0 0         if ( $Config::Config{'osname'} =~ m/Win32/im ) {
384 0 0 0       if (!$_[0] || $_[0] =~ m/EPS/im) { # Default argument.
    0          
    0          
385 0           $cmd = qq|"gsview32.exe"|; # Perl::Critic errs about quotes here.
386 0           system qq|start $cmd $cmd|, qq|"$self->{ps_path}"|;
387             }
388 0           elsif ($_[0] =~ m/^GIMP$/im) { $self->win32_run_gimp() }
389 0           elsif ($_[0] =~ m/^GS$/im) { $self->win32_run_gs($gs_args) }
390             else {
391 0           carp 'Oops! Sub display() expects "EPS", "GIMP", "GS" or nothing'
392             . "not '$_[0]'.\n"
393             }
394             }
395             else {
396              
397             # UNIX users get different choices.
398 0 0 0       if (!$_[0] || $_[0] =~ m/EPS/im) { $cmd = 'gv --spartan ' } # Default
  0 0 0        
    0 0        
399 0           elsif ($_[0] && $_[0] =~ m/GIMP/im) { $cmd = 'gimp ' }
400 0           elsif ($_[0] && $_[0] =~ m/GS/im) { $cmd = 'gs' . $gs_args }
401             else {
402 0           carp 'Oops! Sub display() expects "EPS", "GIMP", "GS" or nothing'
403             . " not '$_[0]'.\n";
404             }
405              
406 0           $cmd .= qq|$self->{ps_path}|;
407              
408 0           `$cmd &` # Advice by Perl::Critic on backticks crashes program here.
409             }
410 0           return 'Pthhht! to Perl::Critic';
411             }
412              
413             1;
414              
415             __END__