package OpenOffice::OOCBuilder; # Copyright 2004, 2007 Stefan Loones # More info can be found at http://www.maygill.com/oobuilder # # This library is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. use 5.008; # lower versions not tested use strict; use warnings; no warnings 'uninitialized'; # don't want this, because we use strict use OpenOffice::OOBuilder; our (@ISA); @ISA=qw(OpenOffice::OOBuilder); my $VERSION=sprintf("%d.%02d", q$Revision: 0.9 $ =~ /(\d+)\.(\d+)/); my ($MAXC, $MAXR, $MAXSHEETS, @TYPES); $MAXC=256; # is column IV $MAXR=32000; $MAXSHEETS=64; # - possible types ($TYPES[0] is default type) @TYPES=('standard', 'text', 'float', 'formula'); # TODO push & pop cell locations (incl sheetnb) - to make formulas easier to construct # create tags for cell locations # cell-format ? (seems with numeric styles, not possible in cell directly) # - Object constructor # sub new { my ($class, $self); $class=shift; $self=$class->SUPER::new('sxc'); # - active data $self->{actsheet}=1; $self->{act}{1}{c}=1; # {act}{sheetnb}{c}= $self->{act}{1}{r}=1; # - general data (parameters) $self->{cpars}{sheets}=1; $self->{cpars}{autoc}=0; $self->{cpars}{autor}=0; # - data $self->{cdata} = undef; # {cdata}{sheetnb}{}{} $self->{sheetname}= undef; # {sheetname}{sheetnb}=name $self->{cstyle} = undef; # {cstyle}{sheetnb}{}{} $self->{colwidth} = undef; # {colwidth}{sheetnb}{c} $self->{rowheight}= undef; # {rowheight}{sheetnb}{r} # - defaults (specific ooc - see other defaults in parent class oooBuilder.pm) $self->{defcolwidth} = '0.8925inch'; # ** $self->{defrowheight} = '0.8925inch'; return $self; } # - - End new (Object constructor) sub add_sheet { my ($self); $self=shift; if ($self->{cpars}{sheets}<$MAXSHEETS) { ++$self->{cpars}{sheets}; } 1; } sub goto_sheet { my ($self, $sheet)=@_; if ($sheet > $self->{cpars}{sheets}) { $self->{actsheet}=$self->{cpars}{sheets}; } elsif ($sheet < 1) { $self->{actsheet}=1; } else { $self->{actsheet}=$sheet; } 1; } sub set_sheet_name { my ($self, $name, $sheet)=@_; # TODO process name: check valid characters and length ?! if ($name) { $sheet=$self->{actsheet} if (! $sheet); if ($sheet>0 && $sheet <=$self->{cpars}{sheets}) { $self->{sheetname}{$sheet}=$name; } } 1; } sub set_colwidth { my ($self, $c, $width)=@_; $c=$self->_check_column ($c); # TODO do we need to check $width ? $self->{colwidth}{$self->{actsheet}}{$c}=$width; 1; } sub set_rowheight { my ($self, $r, $height)=@_; $r=$self->_check_row ($r); $self->{rowheight}{$self->{actsheet}}{$r}=$height; 1; } sub goto_xy { my ($self, $c, $r)=@_; $c=$self->_check_column ($c); $self->{act}{$self->{actsheet}}{c}=$c; $r=$self->_check_row ($r); $self->{act}{$self->{actsheet}}{r}=$r; 1; } sub goto_cell { my ($self, $cell)=@_; $cell=uc($cell); $cell=~ s/^([A-Z]+)([0-9]+)/$1$2/; $self->goto_xy ($1, $2); 1; } sub get_column { my $self=shift; return $self->_convert_column ($self->{act}{$self->{actsheet}}{c}); } sub get_x { my $self=shift; return $self->{act}{$self->{actsheet}}{c}; } sub get_row { my $self=shift; return $self->{act}{$self->{actsheet}}{r}; } sub get_y { my $self=shift; return $self->{act}{$self->{actsheet}}{r}; } sub get_xy { my $self=shift; return ($self->{act}{$self->{actsheet}}{c}, $self->{act}{$self->{actsheet}}{r}); } sub get_cell_id { my $self=shift; my $cell=$self->_convert_column ($self->{act}{$self->{actsheet}}{c}); return $cell . $self->{act}{$self->{actsheet}}{r}; } # - PublicMethod: set_data : set_data in active sheet/cell, with active style # API: set_data ($data, $type, $format) # $type && $format can be ommitted # sub set_data { my ($self, $data, $type, $format)=@_; return $self->set_data_sheet_xy($self->{actsheet}, $self->{act}{$self->{actsheet}}{c}, $self->{act}{$self->{actsheet}}{r}, $data, $type, $format); } sub set_data_xy { my ($self, $c, $r, $data, $type, $format)=@_; return $self->set_data_sheet_xy($self->{actsheet}, $c, $r, $data, $type, $format); } sub set_data_sheet_xy { my ($self, $sheet, $c, $r, $data, $type, $format)=@_; # - check sheet if ($sheet != $self->{actsheet}) { $self->goto_sheet ($sheet); $sheet=$self->{actsheet}; } # - check cell if ($c ne $self->{act}{$sheet}{c} || $r != $self->{act}{$sheet}{r}) { $self->goto_xy ($c, $r); $c=$self->{act}{$sheet}{c}; $r=$self->{act}{$sheet}{r}; } # - check type my ($ok); if ($type) { $type=lc($type); foreach (@TYPES) { if ($type eq $_) { $ok=1; last; } } } $type=$TYPES[0] if (! $ok); # take $TYPES[0] as default type # - check format # TODO # - check data $data=$self->encode_data ($data) if ($data); # - store (ATTENTION $r before $c because of the way we need to generate xml) $self->{cdata}{$sheet}{$r}{$c}{type}=$type; $self->{cdata}{$sheet}{$r}{$c}{format}=$format if ($format); $self->{cdata}{$sheet}{$r}{$c}{data}=$data; $self->{cdata}{$sheet}{$r}{$c}{style}=$self->{actstyle}; $self->cell_update if ($self->{cpars}{autoc} || $self->{cpars}{autor}); 1; } # - - End set_data_sheet_xy sub set_auto_xy { my ($self, $c, $r)=@_; $self->{cpars}{autoc}=$c; $self->{cpars}{autor}=$r; 1; } sub get_auto_x { my $self=shift; return $self->{cpars}{autoc}; } sub get_auto_y { my $self=shift; return $self->{cpars}{autor}; } sub cell_update { my $self=shift; if ($self->{cpars}{autoc}) { if ($self->{cpars}{autoc}>0) { $self->move_cell('right',$self->{cpars}{autoc}); } else { $self->move_cell('left',abs($self->{cpars}{autoc})); } } if ($self->{cpars}{autor}) { if ($self->{cpars}{autor}>0) { $self->move_cell('down',$self->{cpars}{autor}); } else { $self->move_cell('up',abs($self->{cpars}{autor})); } } 1; } sub move_cell { my ($self, $direction, $number)=@_; $number=1 if (! $number); $direction=lc($direction); if ($direction eq 'left') { $self->{act}{$self->{actsheet}}{c}-=$number; } elsif ($direction eq 'right') { $self->{act}{$self->{actsheet}}{c}+=$number; } elsif ($direction eq 'down') { $self->{act}{$self->{actsheet}}{r}+=$number; } elsif ($direction eq 'up') { $self->{act}{$self->{actsheet}}{r}-=$number; } else { # TODO direction unknown } $self->_cell_check; 1; } # - generate ooc specific, then call parent to complete generation sub generate { my ($self, $tgtfile)=@_; my ($subGetMaxRange); $subGetMaxRange=sub { my ($hr, $max, @keys); $hr=shift; @keys=sort {$a <=> $b} (keys(%$hr)); return (pop(@keys)); }; # - Build content.xml $self->{contentxml}=q{ }; # Styles will be done later, because they depend on the content # TODO $self->{rowheight}{$self->{actsheet}}{c}=$height; still to implement # Beginning of document content my ($content); $content=q{}; my ($sheet, $sheetname, $c, $columns, $r, $rows, $type, $format, $data); my ($style, $stylexml); my (%cellstyleids, $cellmaxid, %cellstylexml); my (%colstyleids, $colmaxid, %colstylexml, $colwidth); my ($colid, $prevcolid, $width, $t); my (%rowstyleids, $rowmaxid, %rowstylexml); $cellmaxid=0; $colmaxid=$rowmaxid=1; $colstyleids{$self->{defcolwidth}}='co1'; for (1 .. $self->{cpars}{sheets}) { $sheet=$_; if ($self->{sheetname}{$sheet}) { $sheetname=$self->{sheetname}{$sheet}; } else { $sheetname="Sheet$sheet"; } $content.=qq{}; foreach $c (sort {$a <=> $b} keys(%{$self->{colwidth}{$sheet}})) { $width=$self->{colwidth}{$sheet}{$c}; if (! $colstyleids{$width} && $width) { ++$colmaxid; $colstyleids{$width}=qq{co$colmaxid}; } } if ($self->{colwidth}{$sheet}{1}) { $prevcolid=$colstyleids{$self->{colwidth}{$sheet}{1}}; } else { $prevcolid='co1'; } $t=1; for ($c=2;$c<=256;++$c) { if ($self->{colwidth}{$sheet}{$c}) { $colid=$colstyleids{$self->{colwidth}{$sheet}{$c}}; } else { $colid='co1'; } if ($colid eq $prevcolid) { ++$t; } else { if ($t>1) { $content.=qq{}; $t=1; } else { $content.=qq{}; } $prevcolid=$colid } } if ($t>1) { $content.=qq{}; } else { $content.=qq{}; } $rows=&$subGetMaxRange ($self->{cdata}{$sheet}); for (1 .. $rows) { $r=$_; # TODO row style ? $content.=q{}; $columns=&$subGetMaxRange ($self->{cdata}{$sheet}{$r}); for (1 .. $columns) { $c=$_; $type=$self->{cdata}{$sheet}{$r}{$c}{type}; $format=$self->{cdata}{$sheet}{$r}{$c}{format}; $data=$self->{cdata}{$sheet}{$r}{$c}{data}; $style=$self->{cdata}{$sheet}{$r}{$c}{style}; if ($style eq $self->{defstyle} || ! $style) { $stylexml=''; } else { if (! exists($cellstyleids{$style})) { ++$cellmaxid; $cellstyleids{$style}=qq{ce$cellmaxid}; $cellstylexml{$cellstyleids{$style}}=qq{ table:style-name="$cellstyleids{$style}"}; } $stylexml=$cellstylexml{$cellstyleids{$style}}; } if ($type eq 'standard' || $type eq 'text') { $content.=qq{$data}; } elsif ($type eq 'float') { $content.= qq{ $data}; } elsif ($type eq 'formula') { $content.= qq{ }; } elsif ($type eq 'others') { # TODO } else { $content.=q{}; } } $content.=q{}; } $content.=q{}; } # - Process used fonts and used cell styles my ($bold, $italic, $underline, $align, $txtcolor, $bgcolor, $font, $size); my ($defbold, $defitalic, $defunderline, $defalign, $deftxtcolor, $defbgcolor); my ($deffont, $defsize, %usedfonts, $xml, %stylexml); ($defbold, $defitalic, $defunderline, $defalign, $deftxtcolor, $defbgcolor, $deffont, $defsize)=split(/#/, $self->{defstyle}); foreach $style (keys(%cellstyleids)) { ($bold, $italic, $underline, $align, $txtcolor, $bgcolor, $font, $size)= split(/#/, $style); $xml= qq{ }; $stylexml{$cellstyleids{$style}}=$xml; } # - Fonts $usedfonts{$deffont}=1; $self->{contentxml}.=q{}; foreach $font (sort(keys(%usedfonts))) { $self->{contentxml}.=$self->{availfonts}{$font}; } $self->{contentxml}.=q{}; # - col styles $self->{contentxml}.=qq{}; foreach $width (keys(%colstyleids)) { $colstylexml{$colstyleids{$width}}= qq{ }; } foreach $colid (sort(keys(%colstylexml))) { $self->{contentxml}.=$colstylexml{$colid}; } # TODO look at row styles ? # qq{ # # # # }; # - cell styles foreach $style (sort(keys(%stylexml))) { $self->{contentxml}.=$stylexml{$style}; } $self->{contentxml}.=qq{$content}; $self->SUPER::generate ($tgtfile); 1; } # - * - PrivateMethods sub _check_column { my ($self, $c)=@_; if ($c =~ /[A-Za-z]/) { # - convert to number my (@char, $char, $multi, $newx); $c=~ s/[^A-Za-z]//g; # we don't want anything else when using letters @char=split(//,uc($c)); $multi=1; $newx=0; while (@char) { $char=pop(@char); $newx+=$multi*(ord($char)-64); $multi*=26; } $c=$newx; } $c=1 if ($c<1); $c=$MAXC if ($c>$MAXC); return $c; } sub _convert_column { my ($self, $col)=@_; my $cell; while ($col>26) { my $div=int($col/26); $cell.=chr($div+64); $col-=$div*26; } $cell.=chr($col+64) if ($col>0); return $cell; } sub _check_row { my ($self, $r)=@_; $r=1 if ($r<1); $r=$MAXR if ($r>$MAXR); return $r; } sub _cell_check { my ($self); $self=shift; $self->{actsheet}=1 if ($self->{actsheet}<1); $self->{actsheet}=$MAXSHEETS if ($self->{actsheet}>$MAXSHEETS); my $sheet=$self->{actsheet}; # only for readability $self->{act}{$sheet}{c}=1 if ($self->{act}{$sheet}{c}<1); $self->{act}{$sheet}{r}=1 if ($self->{act}{$sheet}{r}<1); $self->{act}{$sheet}{c}=$MAXC if ($self->{act}{$sheet}{c}>$MAXC); $self->{act}{$sheet}{r}=$MAXR if ($self->{act}{$sheet}{r}>$MAXR); 1; } 1; __END__ =head1 NAME OpenOffice::OOCBuilder - Perl OO interface for creating OpenOffice Spreadsheets =head1 SYNOPSIS use OpenOffice::OOCBuilder; $sheet=OpenOffice::OOCBuilder->new(); This constructor will call the constructor of OOBuilder. =head1 DESCRIPTION OOCBuilder is a Perl OO interface to create OpenOffice spreadsheets. Documents can be created with multiple sheets, different styles, cell types, formulas, column widths and so on. =head1 METHODS new Create a new spreadsheet object add_sheet Add a new sheet within the document. Active sheet is not changed. You need to call goto_sheet (sheetnumber) to change the active sheet. goto_sheet ($sheetnumber) Set $sheetnumber as active sheet. set_sheet_name ($name, $sheetnumber) Set the name of the sheet. If $sheetnumber is ommitted (or 0), the name of the active sheet is set. set_colwidth ($c, $width) Set the column width for the specified column ($c). The column can be a number or letter(s). set_rowheight ($r, $height) Set the row height for the specified row ($r). goto_xy ($c, $r) Set the active cell to ($c, $r). The column can be specified by number or letter(s). goto_cell ($cell_id) Set the active cell to ($cell_id). This way you can use spreadsheet notations (i.e. A5 or BA401 and so on) get_column Returns the active column in letters. get_x Returns the active column as a number (starting at 1). get_row Returns the row as a number (starting at 1) get_y Returns the row as a number (starting at 1). Same as get_row. get_xy Returns the column and row as two numbers in a list. get_cell_id Returns the cell id in the form A1, AB564, and so on. Especially handy to create formulas. When you are at the start position, memorise the cell_id. See example2.pl in the examples directory. set_data ($data, $type, $format) Set data in the active cell within the active sheet. If type is ommitted the standard type is taken. set_data_xy ($c, $r, $data, $type, $format) Same as set_data, but now with column and row to set the data in. The column can be specified as a number or with letter(s). set_data_sheet_xy ($sheet, $c, $r, $data, $type, $format) Same as set_data, but now with sheet, column and row to set the data in. The column can be specified as a number or with letter(s). set_auto_xy ($x, $y) When entering data in a cell, we move to another cell if auto_x or y is set. X value: 0: no movement, negative: move left, positive: move right. Y valye: 0: no movement, negative: move up, positive: move down. get_auto_x Returns the auto_x value. get_auto_y Returns the auto_y value. cell_update This method is called always when entering data in a cell. If auto_x or auto_y is set, if will move to another active cell. You can also use this method to move to another cell without entering data in the previous cell. move_cell ($direction, $number) Move to cell in $direction where $direction is 'left', 'right', 'up' or 'down'. If you ommit $number, the move will be one row or column. generate ($tgtfile) Generates the sxc file. $tgtfile is the name of the target file without extension. If no name is supplied, the default name will be used, which is oo_doc. The target directory is '.', you can set this by calling the OOBuilder method set_builddir ($builddir). Setting the style and meta data See OpenOffice::OOBuilder, because these methods are directly inherited from the base class. =head1 EXAMPLES Look at the examples directory supplied together with this distribution. =head1 SEE ALSO L - the base class http://www.maygill.com/oobuilder Bug reports and questions can be sent to . Attention: make sure the word is in the subject or body of your e-mail. Otherwhise your e-mail will be taken as spam and will not be read. =head1 AUTHOR Stefan Loones =head1 COPYRIGHT AND LICENSE Copyright 2004, 2005 by Stefan Loones This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut