# -*- perl -*- # Formatter.pm $Id: Formatter.pm,v 1.11.2.2 2000/01/30 13:56:23 jens Exp $ # (C) Copyright Jens G Jensen # This file is part of epsmerge and is distributed under GNU GPL package Formatter; use strict; sub new { my $self = shift; $self = ref($self) || $self; return bless { }, $self; } # Input: # A CompositeCell reference (whose Cells should be formatted) # [optional, possibly] A boundingbox (4 integers) # Output: # String (error message) # or Reference to array of actual boundingbox # or Nothing sub format { Options->new()->getopts('-io-')->message("Non-overloaded Formatter::format called", 'W'); } # Normally, I'd use POSIX for these but I am not sure that everybody has POSIX sub ceil { no integer; my $num = int $_[1]; return $num == $_[1] ? $num : $num+1; } sub floor { int $_[1] } package CellFormatter; use integer; use Cell; use Options; use vars ('@ISA'); @ISA = ('Formatter'); sub format { use integer; my $self = shift; my $ccell = shift; my $opt = Options->new(); return "CellFormatter::format: must have a proper boundingbox" if @_ < 4; $self->{bbox} = [ splice @_, 0, 4 ]; my $cells = $ccell->cells(); my $numcells = @$cells; my ($x, $y) = $opt->getopts('x', 'y'); $self->find_max_dim( $cells ) if $opt->getopts('prs'); my ($w, $h); if( $x ) { if( $y ) { unless( $numcells <= $x * $y ) { return "Can't fit $numcells subcells into just ${x}x$y boxes!\n"; } } else { for( $y = $numcells / $x; $numcells > $x * $y ; ++$y) { }; } } else { if( $y ) { for( $x = $numcells / $y; $numcells > $x * $y ; ++$x) { }; } else { if($numcells == 1) { $x = $y = 1; } else { ($x, $y, $w, $h) = $self->_format( $ccell ); } } } if($opt->getopts('x') == 0 || $opt->getopts('y') == 0) { $opt->setopts('x' => $x, 'y' => $y); } unless($w && $h) { use integer; $w = ( $self->{'bbox'}[2] - $self->{'bbox'}[0] - ($x-1) * $opt->getopts('xcs') ) / $x; $h = ( $self->{'bbox'}[3] - $self->{'bbox'}[1] - ($y-1) * $opt->getopts('ycs') ) / $y; } if( $w <= 0 || $h <= 0 ) { return "Can't format with these margins and spacings; no room for cells!"; } my ($xc, $yc, $col, $row, $indx); $indx = 0; # hm -- some code (almost-)duplication... if( $opt->getopts('rmo') ) { # 123/456/789 format $yc = $self->{'bbox'}[3] - $h; FORM: for($row=0; $row<$y; ++$row) { $xc = $self->{'bbox'}[0]; for($col=0; $col<$x; ++$col) { my @box = $self->_newbox( $cells->[ $indx ], $xc, $yc, $w, $h); my $err = $cells->[ $indx ++ ]->write( @box ); Options->new()->getopts('-io-')->message($err, 'W') if defined $err && !ref $err; $xc += $w + $opt->getopts('xcs'); last FORM if $indx == $numcells; } $yc -= $h + $opt->getopts('ycs'); } } else { # 147/258/369 format $xc = $self->{'bbox'}[0]; FORM: for($col=0; $col<$x; ++$col) { $yc = $self->{'bbox'}[3] - $h; for($row=0; $row<$y; ++$row) { my @box = $self->_newbox( $cells->[ $indx ], $xc, $yc, $w, $h); my $err = $cells->[ $indx ++ ]->write( @box ); Options->new()->getopts('-io-')->message($err, 'W') if defined $err && !ref $err; $yc -= $h + $opt->getopts('ycs'); last FORM if $indx == $numcells; } $xc += $w + $opt->getopts('xcs'); } } return; } # Return the required boundingbox for the current cell sub _newbox { my ($self, $cell, $llx, $lly, $w, $h) = @_; my $opt = Options->new(); use integer; if( $opt->getopts('prs') ) { if( $opt->getopts('par') ) { print STDERR "Warning: -prs taking precedence over -par\n"; $opt->setopts( par => 0 ); } my @dim = $cell->dim(); die "Internal error: found a cell without dimensions: " . ref $cell unless @dim; my ($oldw, $oldh) = ($w, $h); $w = $w * $dim[0] / $self->{'maxw'}; $h = $h * $dim[1] / $self->{'maxh'}; $llx += ($oldw - $w) / 2; $lly += ($oldh - $h) / 2; } elsif( $opt->getopts('par') ) { my @dim = $cell->dim(); die "Internal error: found a cell without dimensions: " . ref $cell unless @dim; no integer; my $ar = $dim[1] / $dim[0]; if( $h/$w < $ar ) { my $oldw = $w; $w = $h / $ar; $llx += ($oldw - $w) / 2; } else { my $oldh = $h; $h = $ar * $w; $lly += ($oldh - $h) / 2; } } # force rounding to integer return ($llx+0, $lly+0, $llx+$w, $lly+$h); } # This finds the max width and height ever occuring among the cells # and stores this data in the object; used only for -prs option # Could transfer to CompositeCell. sub find_max_dim { my $self = shift; my ($maxw, $maxh) = (0, 0); foreach (@{$_[0]}) { my @dim = $_->dim(); if( @dim ) { $maxw = $dim[0] if $dim[0] > $maxw; $maxh = $dim[1] if $dim[1] > $maxh; } else { Options->new->getopts('-io-')->message("Internal error: found a cell without dimensions: " . ref $_, 'F'); } } $self->{maxw} = $maxw; $self->{maxh} = $maxh; } # Calculate modified width and height # Input: reference to Options class (from where spacings will be extracted) sub _modwh { my ($self, $opt) = @_; my ($w, $h) = ($self->{bbox}->[2] - $self->{bbox}->[0], $self->{bbox}->[3] - $self->{bbox}->[1]); $w += $opt->getopts('xcs'); $h += $opt->getopts('ycs'); if( $w <= 0 || $h <= 0) { return "Not enough room for formatting with these margins and spacings\n"; } return ($w, $h); } # Given cell references, calculate suggested format given # that we somehow try to preserve their aspect ratio sub _format { my ($self, $ccell) = @_; my $opt = Options->new(); my ($modw, $modh) = $self->_modwh($opt); my $n = @{$ccell->cells()}; # plain average; experience must show if this is OK no integer; my $avg_aspect_ratio = 0; foreach ( @{$ccell->cells()} ) { $avg_aspect_ratio += $_->asprat(); } $avg_aspect_ratio /= $n; my $x; # hairy algorithm stuff but more precise approximation # (less precise would be sqrt( avg * modw / modh * $n )) my $b = ($avg_aspect_ratio * $opt->getopts('xcs') - $opt->getopts('ycs')) / 2; my $d = ($b * $b) + $avg_aspect_ratio * $modw * $modh / $n; $x = (sqrt( $d ) - $b) * $n / $modh; # trouble ahead: must round to integer $x = $self->ceil( $x ); $x = 1 if $x <= 0; my $y = $self->ceil( $n / $x ); if( $x > 1 && $x * $y > ($x-1) * $self->ceil($n/($x-1)) ) { --$x; $y = $self->ceil( $n / $x ); } my ($neww, $newh) = ($self->floor( $modw / $x ) - $opt->getopts('xcs'), $self->floor( $modh / $y ) - $opt->getopts('ycs')); # finally, the testing: should be ok but better safe than sorry while( $x * $y < $n ) { ++$y; } while( ($neww + $opt->getopts('xcs')) * $x > $modw ) { --$neww; } while( ($newh + $opt->getopts('ycs')) * $y > $modh ) { --$newh; } return ($x, $y, $neww, $newh); } package SimpleFormatter; use vars ('@ISA'); @ISA = ('Formatter'); sub format { my $self = shift; my $opt = Options->new(); my $ccell = shift; my $cells = $ccell->cells(); my $x; # format in x direction? (stack horizontally) my @scale; no integer; my @dims = map { scalar $_->dim() } @$cells; # use dim hack to get ref rather than list if( $opt->getopts('x') > 1 ) { $opt->getopts('-io-')->message('Formatter: Warning: ignoring -y', 'W') if $opt->getopts('y') > 1; $x = 1; @scale = _format( \@dims, 1 ); } elsif( $opt->getopts('y') ) { # also x == 1 == y $x = 0; @scale = _format( \@dims, 0 ); } else { # x == 0 == y my @xscale = _format( \@dims, 1 ); my $xprod = 1; foreach ( @xscale ) { $xprod *= $_ } my @yscale = _format( \@dims, 0 ); my $yprod = 1; foreach ( @yscale ) { $yprod *= $_ } if( $xprod <= $yprod ) { # both are > 1, pick closest to 1 $x = 1; @scale = @xscale; } else { $x = 0; @scale = @yscale; } } # x == 0 == y my $cs = $opt->getopts('cs'); # spacing # ($xb,$yb) is LL corner of current image; ($mx,$my) is maximal (scaled) cell size my ($mx, $my, $xb, $yb) = (0) x 4; # Note that vertically, cells are stacked from bottom to top, i.e., in reverse order foreach ( $x ? (0 .. $#$cells) : reverse (0 .. $#$cells) ) { my $dx = int($dims[$_]->[0] * $scale[$_] + 0.5); my $dy = int($dims[$_]->[1] * $scale[$_] + 0.5); $cells->[$_]->write( $xb, $yb, $xb+$dx, $yb+$dy ); $mx = $dx if $mx < $dx; $my = $dy if $my < $dy; if( $x ) { $xb += $dx + $cs; } else { $yb += $dy + $cs; } } # return actual BB return $x ? [ 0, 0, $xb, $my ] : [ 0, 0, $mx, $yb ]; } # Input: # Ref to list of refs of lists of dimensions of cells # Output: # List of scaling factors, all of which will be >= 1 (at least one will be 1) sub _format { my ($dims, $x) = @_; my @scale = (); no integer; my $m = 0; # maximal my $r; foreach $r ( @$dims ) { my $d = $x ? $r->[0] : $r->[1]; push @scale, $d; $m = $d if $d > $m; } return map $m/$_, @scale; } 1;