# Copyright 2010, 2011, 2012 Kevin Ryde # This file is part of Image-Base-Other. # # Image-Base-Other is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 3, or (at your option) any later # version. # # Image-Base-Other is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. # # You should have received a copy of the GNU General Public License along # with Image-Base-Other. If not, see . package Image::Base::Text; # maybe one day 5.005 for 4-arg substr() replacing # 5.6 for easier clean open() use 5.006; use strict; use Carp; use Text::Tabs (); use List::Util 'min','max'; use vars '$VERSION', '@ISA'; $VERSION = 9; use Image::Base 1.12; # version 1.12 for ellipse() $fill @ISA = ('Image::Base'); # uncomment this to run the ### lines #use Smart::Comments; use constant default_colour_to_character => { 'black' => ' ', 'clear' => ' ', '#000000' => ' ', '#000000000000' => ' ', other => '*', }; sub new { my ($class, %param) = @_; if (ref $class) { # clone by copying fields and data array my $self = bless { %$class }, $class; $self->{'-rows_array'} = [ @{$class->{'-rows_array'}} ]; return $self; } my $self = bless { -rows_array => [], -width => 0, -colour_to_character => $class->default_colour_to_character, }, $class; if (defined (my $filename = delete $param{'-file'})) { $self->load($filename); } $self->set (%param); return $self; } sub _get { my ($self, $key) = @_; # ### Image-Base-Text _get(): $key if ($key eq '-height') { return scalar @{$self->{'-rows_array'}}; } return $self->SUPER::_get ($key); } sub set { my ($self, %param) = @_; ### set(): \%param if (defined (my $width = delete $param{'-width'})) { foreach my $row (@{$self->{'-rows_array'}}) { if (length($row) < $width) { $row .= ' ' x ($width - length($row)); } else { substr($row,$width) = ''; } } # ready for -height to use $self->{'-width'} = $width; } if (defined (my $height = delete $param{'-height'})) { my $rows_array = $self->{'-rows_array'}; if (@$rows_array >= $height) { ### rows_array shorten splice @$rows_array, $height; } else { ### rows_array extend by: ($height - scalar(@$rows_array)) my $row = ' ' x $self->{'-width'}; push @$rows_array, ($row) x ($height - scalar(@$rows_array)); } } %$self = (%$self, %param); } sub load { my ($self, $filename) = @_; ### Image-Base-Text load() if (@_ == 1) { $filename = $self->get('-file'); } else { $self->set('-file', $filename); } ### $filename open my $fh, '<', $filename or croak "Cannot open $filename: $!"; $self->load_fh ($fh); close $fh or croak "Error closing $filename: $!"; } # these undocumented yet ... sub load_fh { my ($self, $fh) = @_; ### Image-Base-Text load_fh(): $fh $self->load_lines (map {chomp; $_} <$fh>); } sub load_string { my ($self, $str) = @_; ### Image-Base-Text load_string(): $str # split my @lines = split /\n/, $str, -1; if (@lines && $lines[-1] eq '') { # drop the empty element after the last newline, but keep a non-empty # final element from chars without a final newline pop @lines; } $self->load_lines (@lines); } sub load_lines { my ($self, @rows_array) = @_; ### load_lines: @rows_array my $width = 0; foreach my $row (@rows_array) { $row = Text::Tabs::expand ($row); if ($width < length($row)) { $width = length($row); } } $self->{'-rows_array'} = \@rows_array; $self->set (-width => $width); # pad out shorter lines } sub save { my ($self, $filename) = @_; ### Image-Base-Text save(): @_ if (@_ == 2) { $self->set('-file', $filename); } else { $filename = $self->get('-file'); } ### $filename my $fh; (open $fh, '>', $filename and $self->save_fh($fh) and close $fh) or croak "Error writing $filename: $!"; } # these undocumented yet ... sub save_fh { my ($self, $fh) = @_; my $rows_array = $self->{'-rows_array'}; local $, = "\n"; return print $fh @$rows_array,(@$rows_array ? '' : ()); } sub save_string { my ($self) = @_; my $rows_array = $self->{'-rows_array'}; return join ("\n", @$rows_array, (@$rows_array ? '' : ())); } #------------------------------------------------------------------------------ # drawing sub xy { my ($self, $x, $y, $colour) = @_; ### Image-Base-Text xy(): @_[1 .. $#_] # clip to width,height return if ($x < 0 || $x >= $self->{'-width'} || $y < 0 || $y >= @{$self->{'-rows_array'}}); my $rows_array = $self->{'-rows_array'}; if (@_ == 3) { return $self->character_to_colour (substr ($rows_array->[$y], $x, 1)); } else { substr ($rows_array->[$y], $x, 1) = $self->colour_to_character($colour); } } sub line { my ($self, $x1,$y1, $x2,$y2, $colour) = @_; if ($y1 == $y2) { ### horizontal line by substr() block store ... my $rows_array = $self->{'-rows_array'}; return if $y1 < 0 || $y1 > $#$rows_array; # entirely outside if ($x1 > $x2) { ($x1,$x2) = ($x2,$x1) } # x1 smaller my $xmax = $self->{'-width'}-1; return if $x2 < 0 || $x1 > $xmax; # entirely outside $x1 = max($x1,0); $x2 = min($x2,$xmax); my $x_width = $x2-$x1+1; substr($rows_array->[$y1], $x1, $x_width, $self->colour_to_character($colour) x $x_width); } else { shift->SUPER::line(@_); } } # rectangle() can do a substr() block store on each filled row (either all # if $fill, or the top and bottom if not). # sub rectangle { my ($self, $x1,$y1, $x2,$y2, $colour, $fill) = @_; ### Image-Base-Text xy(): @_[1,$#_] my $rows_array = $self->{'-rows_array'}; unless ($x2 >= 0 && $y2 >= 0 && $x1 < $self->{'-width'} && $y1 <= $#$rows_array) { ### entirely outside 0,0,width,height ... return; } my $x1_clip = max($x1,0); my $x2_clip = min($x2,$self->{'-width'}-1); my $char = $self->colour_to_character($colour); my $x_width = $x2_clip - $x1_clip + 1; my $repl = $char x $x_width; if ($fill) { foreach my $y (max($y1,0) .. min($y2,$#$rows_array)) { substr ($rows_array->[$y], $x1_clip, $x_width) = $repl; } } else { ### top, if in range ... if ($y1 >= 0) { substr ($rows_array->[$y1], $x1_clip, $x_width) = $repl; } $y1++; if ($y2 >= $y1) { if ($y2 <= $#$rows_array) { ### bottom in range and not same as top ... substr ($rows_array->[$y2], $x1_clip, $x_width) = $repl; } ### sides, if any ... $y2--; if ($y2 >= $y1) { my $y1_clip = max($y1,0); my $y2_clip = min($y2,$#$rows_array); if ($x1 == $x1_clip) { foreach my $y ($y1_clip .. $y2_clip) { substr ($rows_array->[$y], $x1, 1) = $char; } } if ($x2 == $x2_clip) { foreach my $y ($y1_clip .. $y2_clip) { substr ($rows_array->[$y], $x2, 1) = $char; } } } } } } sub colour_to_character { my ($self, $colour) = @_; ### colour_to_character(): $colour if (defined (my $char = $self->{'-colour_to_character'}->{$colour})) { return $char; } if (length($colour) == 1) { return $colour; } if (defined (my $char = $self->{'-colour_to_character'}->{'other'})) { return $char; } croak "Unknown colour: $colour"; } sub character_to_colour { my ($self, $char) = @_; if (length ($char) == 0) { return undef; } if (defined (my $colour = $self->{'-character_to_colour'}->{$char})) { return $colour; } return $char; } 1; __END__ =for stopwords filename undef Ryde resizes PerlIO =head1 NAME Image::Base::Text -- draw in a plain text file or grid =head1 SYNOPSIS use Image::Base::Text; my $image = Image::Base::Text->new (-width => 70, -height => 20); $image->rectangle (5,5, 65,15, '*'); $image->save ('/some/filename.txt'); =head1 CLASS HIERARCHY C is a subclass of C, Image::Base Image::Base::Text =head1 DESCRIPTION C extends C to create or update text files treated as grids of characters, or just to create a grid of characters in memory. Colours for drawing can be a single character to set in the image, or there's an experimental C<-colour_to_character> attribute to map names to characters. Currently black, #000000, #000000000000 and clear all become spaces and anything else becomes a "*". Perhaps that will change. Perl wide characters can be used, in new enough Perl, though currently there's nothing to set input or output encoding for file read/write (making it fairly useless, unless perhaps you've got global PerlIO layers setup). =head1 FUNCTIONS See L for the behaviour common to all Image-Base classes. =over 4 =item C<$image = Image::Base::Text-Enew (key=Evalue,...)> Create and return an image object. A image can be started with C<-width> and C<-height>, $image = Image::Base::Text->new (-width => 70, -height => 20); Or an existing file can be read, $image = Image::Base::Text->new (-file => '/my/filename.txt'); =item C<$new_image = $image-Enew (key=Evalue,...)> Create and return a cloned copy of C<$image>. The optional parameters are applied to the new image as per C. =item C<$image-Eload ()> =item C<$image-Eload ($filename)> Read a text file into C<$image>, either from the current C<-file> option, or set that option to C<$filename> and read from there. Tab characters in the file are expanded to spaces per C. Its C<$Text::Tabs::tabstop> controls the width of each tab. C<-height> is set to the number of lines in the file, possibly zero. C<-width> is set to the widest line in the file and other lines are padded with spaces to that width as necessary. =item C<$image-Esave ()> =item C<$image-Esave ($filename)> Save the image to a text file, either the current C<-file> option, or set that option to C<$filename> and save to there. Trailing spaces are included in the output so that the width is represented in the file, and to keep it a rectangular grid. Tabs are not used in the output. =back =head1 ATTRIBUTES =over =item C<-width> (integer) =item C<-height> (integer) Setting these resizes an image, either truncating or extending. When extending the new area is initialized to space characters. =back =head1 SEE ALSO L, L, L =head1 HOME PAGE http://user42.tuxfamily.org/image-base-other/index.html =head1 LICENSE Image-Base-Other is Copyright 2010, 2011, 2012 Kevin Ryde Image-Base-Other is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. Image-Base-Other is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Image-Base-Other. If not, see . =cut