I've been doing a lot of work with Sudoku this week because the next issue of The Perl Review it almost entirely devoted to it. We'll have things
to make the puzzle and things to solve it.
Eric Maki's puzzle generator output text, so I wanted to turn that into something a bit nicer. I figured it would be a snap for PDF::API2, and it mostly was when I figured out what the methods actually did.
I was surprised that I couldn't find more PDF::API2 examples, so I offer this one, with some notes at the end.
#!/usr/bin/perl
=head1 NAME
sudoku_maker - create Sudoku puzzles with PDF::API2
=head1 SYNOPSIS
% perl sudoku_maker > sudoku.pdf
- - 6 8 - 4 - - -
- - - - 9 - 7 - 8
- - - 5 - - - 9 -
1 - - - 4 - - - 9
- - - - - - 5 - -
4 6 - - - 1 - 3 -
8 7 - - - - 4 - -
- - - - 5 - 2 - -
- - - - - 2 - 1 -
=head1 DESCRIPTION
This is a proof-of-concept script. Eric Maki created a Sudoku puzzle
generator, but he output the text you see in the SYNOPSIS. I wanted
to turn that into a nice puzzle so I started tinkering with PDF::API2.
Eric's source will be part of the Spring 2006 issue of The Perl Review
+.
If you want to change the input, change C<get_puzzle> to parse it
correctly.
=head1 TO DO
=over 4
=item * most things can be configurable, but I hardcoded them
=item * i'd like to generate several puzzles per page
=item * the C<place_digit> routine is a bit of guess work for font cen
+tering.
=back
=head1 AUTHOR
brian d foy, C<< <bdfoy@cpan.org> >
=head1 COPYRIGHT and LICENSE
Copyright 2006, brian d foy, All rights reserved.
This software is available under the same terms as perl.
=cut
use strict;
use warnings;
use PDF::API2;
use constant PAGE_WIDTH => 595;
use constant PAGE_HEIGHT => 842;
use constant MARGIN => 25;
use constant WIDE_LINE_WIDTH => 3;
use constant LINE_WIDTH => 2;
use constant THIN_LINE_WIDTH => 1;
use constant SQUARE_SIDE => 270;
use constant FONT_SIZE => int( 0.70 * SQUARE_SIDE / 9 );
my $pdf = PDF::API2->new;
my $font = $pdf->corefont( 'Helvetica-Bold' );
run() unless caller;
sub run
{
$pdf->mediabox( PAGE_WIDTH, PAGE_HEIGHT );
my $gfx = $pdf->page->gfx;
$gfx->strokecolor( '#000' );
$gfx->linewidth( WIDE_LINE_WIDTH );
make_grid(
$gfx,
( PAGE_WIDTH - SQUARE_SIDE ) / 2 , # x
PAGE_HEIGHT - SQUARE_SIDE - MARGIN, # y
);
populate_puzzle( $gfx, get_puzzle() );
print $pdf->stringify;
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# #
sub populate_puzzle
{
my( $gfx, $array ) = @_;
foreach my $row ( 0 .. $#$array )
{
my $row_array = $array->[$row];
foreach my $column ( 0 .. $#$row_array )
{
next unless defined $row_array->[$column];
place_digit( $gfx, $row, $column, $row_array->[$column] )
}
}
}
sub place_digit
{
my( $gfx, $row, $column, $digit ) = @_;
my $x_start = ( PAGE_WIDTH - SQUARE_SIDE ) / 2;
my $y_start = PAGE_HEIGHT - SQUARE_SIDE - MARGIN;
my $x_offset = 0.30 * SQUARE_SIDE / 9; # empirically derived
my $y_offset = 0.25 * SQUARE_SIDE / 9;
my $x = $x_start + $column * SQUARE_SIDE / 9 + $x_offset;
my $y = $y_start + $row * SQUARE_SIDE / 9 + $y_offset;
$gfx->textlabel( $x, $y, $font, FONT_SIZE, $digit );
}
sub get_puzzle
{
my @array;
print STDERR "Waiting for puzzle input!\n";
while( <STDIN> )
{
chomp;
s/^\s|\s$//g;
next unless length $_;
push @array, [ map { $_ eq '-' ? undef : $_ } split ];
}
return \@array;
}
sub make_grid
{
my( $gfx, $lower_left_x, $lower_left_y ) = @_;
make_outline( $gfx, $lower_left_x, $lower_left_y );
$gfx->linewidth( THIN_LINE_WIDTH );
make_blocks( $gfx, $lower_left_x, $lower_left_y, 9 );
$gfx->linewidth( LINE_WIDTH );
make_blocks( $gfx, $lower_left_x, $lower_left_y, 3 );
}
sub make_blocks
{
my( $gfx, $lower_left_x, $lower_left_y, $cells ) = @_;
my( $xs, $ys ) =
map {
my $point = $_;
[ map { $point + $_ * SQUARE_SIDE / $cells } 1 .. $cells -
+ 1 ];
} ( $lower_left_x, $lower_left_y );
foreach my $x ( @$xs )
{
make_line( $gfx,
$x, $lower_left_y,
$x, $lower_left_y + SQUARE_SIDE,
);
}
foreach my $y ( @$ys )
{
make_line( $gfx,
$lower_left_x, $y,
$lower_left_x + SQUARE_SIDE, $y,
);
}
}
sub make_outline
{
my( $gfx, $lower_left_x, $lower_left_y ) = @_;
my( $upper_right_x, $upper_right_y ) =
map { $_ + SQUARE_SIDE } ( $lower_left_x, $lower_left_y );
my @points = (
[ $lower_left_x, $lower_left_y - WIDE_LINE_WIDTH / 2,
$lower_left_x, $upper_right_y ],
[ $lower_left_x, $upper_right_y - WIDE_LINE_WIDTH / 2,
$upper_right_x, $upper_right_y ],
[ $upper_right_x - WIDE_LINE_WIDTH / 2, $upper_right_y,
$upper_right_x, $lower_left_y ],
[ $upper_right_x, $lower_left_y + WIDE_LINE_WIDTH / 2,
$lower_left_x, $lower_left_y ],
);
foreach my $tuple ( @points ) { make_line( $gfx, @$tuple ) }
}
sub make_line
{
my( $gfx, $x, $y, $x2, $y2 ) = @_;
$gfx->move( $x, $y );
$gfx->line( $x2, $y2 );
$gfx->fillstroke;
}
__END__
- - 6 8 - 4 - - -
- - - - 9 - 7 - 8
- - - 5 - - - 9 -
1 - - - 4 - - - 9
- - - - - - 5 - -
4 6 - - - 1 - 3 -
8 7 - - - - 4 - -
- - - - 5 - 2 - -
- - - - - 2 - 1 -
Some notes
Some of this is just my newness to the inner workings of PDF, and some
to the interface issues. PDF::API2 is really a low level module,
so I can't really complain. We're supposed to build stuff on top of it.
Some of you may be able to elaborate on or correct these simple observations:
- You need a graphics object to make graphics. That seems simple, but there
aren't many examples in the distro, and most people seem concerned with
placing text. To get a graphics object, you need a page on which to put the graphics, so
you need a page object first.
- To make the lines and other graphics elements show up, you have to
call fillstroke.
- Drawing a line doesn't move your cursor. The line starts at the
current "pen" position and draws the line to the point you specify. Your pen
stays put. This isn't LOGO. :)
- The coordinates start at the lower lefthand corner. Y increases as you go up the page, and
X increases as you go to the right.
- To make two lines join nicely, you have to take into account their line
widths.
- When placing the pen, you don't have to use integers.
- For text, the starting point is the lower left corner of the first character (although
it looks like there is stuff to affect this).
That's enough for 6 AM, I think. :)
Re: Making Sudoku Puzzles Using PDF::API2
by talexb (Chancellor) on Feb 21, 2006 at 16:35 UTC
|
These are all good observations, but they just follow on from PostScript, which is what the PDF is based on.
In 1987 I started working at a desktop publishing company and was assigned the job of rationalizing the printer drivers. I quickly discovered that while most laser printers had their origin at the top left corner, the Apple LaserWriter (a PostScript printer) had its origin at the bottom left corner. So for most printers, going down the page meant increasing y values, but for the LaserWriter it meant a decreasing y value.
As you describe, joining lines could be done by filling out a triangle (meaning lines meeting at a small angle left a big elbow), by chopping off the triangle (making lines meeting at 90 degrees have a 45 degree corner) or by having a rounded corner. PostScript is very flexible that way, but can also be quite CPU-intensive.
Adobe's Red Book and Blue Book were my PostScript bibles back then.
Alex / talexb / Toronto
"Groklaw is the open-source mentality applied to legal research" ~ Linus Torvalds
| [reply] |
Re: Making Sudoku Puzzles Using PDF::API2
by mercutio_viz (Scribe) on Feb 22, 2006 at 07:39 UTC
|
Brian,
Your observations are right on the money! Yes, the distro has very few examples. The older .3r77 has more samples available, but many of them are not compatible with the .4x release, as the author has done a major redesign of the object structure. (I'm still trying to grasp it...)
One minor point: fillstroke is just one option for "painting" glyphs or objects on the page. There are also separate "fill" and "stroke" functions. The fill method fills the "inside" of the object or glyph, whereas the stroke method draws the border. This permits objects to have an outline color different than the fill color.
When I get a few minutes I'll play around with the sudoku thing and see what I can come up with.
-MC
UPDATE: I did my first sudoku puzzle - very cool! I can't believe I'd never tried one before. Anyway, that helped me to visualize what to do with the PDF stuff. I am working on a few things with the PDF files - multiple puzzles per page and multiple pages per PDF. I'll keep you posted on my progress.
| [reply] |
Re: Making Sudoku Puzzles Using PDF::API2
by danderson (Beadle) on Feb 24, 2006 at 21:16 UTC
|
As has been pointed out, PDFs are based on postscript (PS). Postscript's line drawing functions (lineto, curveto etc) do "act like LOGO" in that the cursor moves; it surprises me that PDF line drawing functions have changed this.
Some excellent PS references are:
The PS tutorial and cookbook: http://www-cdf.fnal.gov/offline/PostScript/BLUEBOOK.PDF
The PS language reference manual: http://partners.adobe.com/asn/developer/pdfs/tn/PLRM.pdf
And perhaps best of all, Bill Casselman's "Mathematical Illustrations," which isn't amazingly cheap but is available free online at http://www.math.ubc.ca/~cass/graphics/manual/ | [reply] |
NEW CODE: Making Sudoku Puzzles Using PDF::API2
by mercutio_viz (Scribe) on Mar 23, 2006 at 20:29 UTC
|
FYI, now that the latest Perl Review has been released I've had a chance to play around with the Sudoku generator. I added some spice to allow one to create multiple Sudoku puzzles in a PDF document. Attached are a few scripts:
A slightly modified sudoku_generator.pl file
A modestly modified sudoku_maker.pl file, renamed to sudoku2pdf.pl for de-obfuscation purposes
A sample shell script, create_sudoku_puzzles.sc that accepts two command line arguments: number of puzzles to create and PDF filename; it will then loop through and create x number of puzzles in the given PDF file
Here they are:
sudoku_generator.pl
sudoku2pdf.pl
create_sudoku_puzzles.sc
I've tested this shell script values up to 100 and it seems to work okay. YRMV, but have fun with it!
-MC
P.S. - To brian, Eric, and the rest of you TPR guys: Thanks! I just subscribed to The Perl Review and I love it!
| [reply] [d/l] [select] |
NEW CODE - Re: Making Sudoku Puzzles Using PDF::API2
by mercutio_viz (Scribe) on Feb 24, 2006 at 08:58 UTC
|
Brian,
I was able to add some stuff to your proof-of-concept script. I confess that I'm relatively new to Perl so please forgive any obvious coding no-no's. I've managed to create an algorithm that will place up to six puzzles on one page, and then add a page if an attempt to place a seventh puzzle is made. The cycle can be repeated indefinitely. (See the UPDATE section under the description.)
There are some rough edges, of course, but the basic premise has been proven. I've used the built in PDF document properties for control: the 'Author' property will be set to 'sudoku_maker.pl' when the script creates a new PDF, and will only append to PDFs with this 'Author' value. I put "puzzlecount=##" in the 'Keywords' property and the script will pull from that string and parse out the puzzle count so that it knows where to place the next puzzle.
Please feel free to use this code as you see fit, if at all. I'm definitely open to suggestions on how to make it better, so any input is gladly accepted.
-MC
#!/usr/bin/perl
=head1 NAME
sudoku_maker.pl - create Sudoku puzzles with PDF::API2
=head1 SYNOPSIS
% <sudoku text> | perl sudoku_maker.pl sudoku.pdf
- - 6 8 - 4 - - -
- - - - 9 - 7 - 8
- - - 5 - - - 9 -
1 - - - 4 - - - 9
- - - - - - 5 - -
4 6 - - - 1 - 3 -
8 7 - - - - 4 - -
- - - - 5 - 2 - -
- - - - - 2 - 1 -
=head1 DESCRIPTION
This is a proof-of-concept script. Eric Maki created a Sudoku puzzle
generator, but he output the text you see in the SYNOPSIS. I wanted
to turn that into a nice puzzle so I started tinkering with PDF::API2.
Eric's source will be part of the Spring 2006 issue of The Perl Review
+.
If you want to change the input, change C<get_puzzle> to parse it
correctly.
UPDATE: Some additions by Michael S. Collins to make the PDF stuff mor
+e usable:
Added cmd line arg: input/output PDF file name
Changed program output to write directly to PDF file instead of ST
+DOUT
Added PDF-specific information:
'Author' = 'sudoku_maker.pl'
'Keywords' = 'puzzlecount=##' (where ## = number of puzzles in
+ PDF doc)
(To view this information in Adobe Reader, click File > Document P
+roperties)
Added quick & dirty validation routine:
If supplied PDF file does not exist it is created
('.pdf' appended if nec)
If supplied PDF file does exist, checks for 'Author' to be equ
+al to
'sudoku_maker.pl'; dies if not
This prevents the script from putting sudoku puzzles on 'norma
+l' PDFs!
Looks for 'puzzlecount=##' in 'Keywords'; if not found, start
+at 0
Added logic to put allow six puzzles per page
After six puzzles, a new page is added and newest puzzle placed
Repeated calls to sudoku_maker.pl with the same PDF file will resu
+lt in
many puzzles appended
=head1 TO DO
=over 4
=item * most things can be configurable, but I hardcoded them
=item * i'd like to generate several puzzles per page (see MC UPDATE a
+bove)
=item * the C<place_digit> routine is a bit of guess work for font cen
+tering.
=back
=head1 AUTHOR
brian d foy, C<< <bdfoy@cpan.org> >
=head1 COPYRIGHT and LICENSE
Copyright 2006, brian d foy, All rights reserved.
This software is available under the same terms as perl.
=cut
use strict;
use warnings;
use PDF::API2;
use constant PAGE_WIDTH => 595;
use constant PAGE_HEIGHT => 842;
use constant MARGIN => 25;
use constant GUTTER => 32;
use constant WIDE_LINE_WIDTH => 3;
use constant LINE_WIDTH => 2;
use constant THIN_LINE_WIDTH => 1;
use constant SQUARE_SIDE => 243; # changed from brian's 270
use constant FONT_SIZE => int( 0.70 * SQUARE_SIDE / 9 );
use constant MAX_PUZZLES => 6; # max number puzzles per page
# Now we cheat a bit: there are only 6 possible starting x,y positions
# So we put them in a pair of arrays, one for x positions, the other f
+or y
# Then when we call make_grid and place_digit we use these as our x,y
# coordinates: puzzle 1 = $xpos[1],$ypos[1], puzzle 2 = $xpos[2],$ypos
+[2]...
# NOTE: puzzle 6 = $xpos[0],$ypos[0] because we use modulus
# GUTTER = horizontal space between puzzles
my @xpos = (
( PAGE_WIDTH + GUTTER) / 2, # xpos, puzz
+le 6
( PAGE_WIDTH - (SQUARE_SIDE * 2 + GUTTER) ) / 2, # xpos, puzz
+le 1
( PAGE_WIDTH + GUTTER) / 2, # xpos, puzz
+le 2
( PAGE_WIDTH - (SQUARE_SIDE * 2 + GUTTER) ) / 2, # xpos, puzz
+le 3
( PAGE_WIDTH + GUTTER) / 2, # xpos, puzz
+le 4
( PAGE_WIDTH - (SQUARE_SIDE * 2 + GUTTER) ) / 2, # xpos, puzz
+le 5
);
my @ypos = (
PAGE_HEIGHT - SQUARE_SIDE * 3 - GUTTER * 2 - MARGIN, # ypos, puzz
+le 6
PAGE_HEIGHT - SQUARE_SIDE - MARGIN, # ypos, puzz
+le 1
PAGE_HEIGHT - SQUARE_SIDE - MARGIN, # ypos, puzz
+le 2
PAGE_HEIGHT - SQUARE_SIDE * 2 - GUTTER - MARGIN, # ypos, puzz
+le 3
PAGE_HEIGHT - SQUARE_SIDE * 2 - GUTTER - MARGIN, # ypos, puzz
+le 4
PAGE_HEIGHT - SQUARE_SIDE * 3 - GUTTER * 2 - MARGIN, # ypos, puzz
+le 6
);
my $pdf;
my $puzzle_count; # how many puzzles in this PDF
my $page_count; # how many pages in this PDF
my $puzzlenum; # which puzzle on this page (1, 2, 3...)
# if have cmd line arg, assume it's a file name + path
# if file exists, assume it's a pdf, open it for appending puzzles
# if file does not exist, create it
my $infile = $ARGV[0];
print "Using $infile as pdf file...\n";
if ( -f $infile ) {
$pdf = PDF::API2->open( $infile ) or
die "Unable to open PDF file $infile \n";
my %pdfinfo = $pdf->info;
my $keywords = $pdfinfo{'Author'};
if ( $keywords !~ m/sudoku_maker\.pl/ ) {
die "This is not a PDF created by sudoku_maker.pl \n";
}
(undef,$puzzle_count) = split "=",$pdfinfo{'Keywords'};
if ( ! $puzzle_count ) {
# no puzzle count, setting it to 0
$puzzle_count = 0;
}
$page_count = $pdf->pages;
} else {
$pdf = PDF::API2->new;
$pdf->info( 'Author' => 'sudoku_maker.pl' );
$page_count = 1;
$puzzle_count = 0;
$pdf->mediabox( PAGE_WIDTH, PAGE_HEIGHT );
if ( substr( $infile, -4 ) ne '.pdf' ) {
$infile .= '.pdf';
}
}
my $font = $pdf->corefont( 'Helvetica-Bold' );
run() unless caller;
sub run
{
my $page; # it's generally easier to have a page object av
+ailable
$page = $pdf->openpage( $page_count );
if ( ! $page ) {
$page = $pdf->page; # first page of brand new PDF doc
}
# check to see if we need to add a page
if ( ++$puzzle_count / MAX_PUZZLES > $page_count ) {
$page = $pdf->page; # adds a new page, sets $page obj to new
+ page
}
# determine which puzzle on page to use:
# 1st puzzle = upper left, 2nd = upper right
# 3rd puzzle = mid left, 4th = mid right
# 5th puzzle = lower left, 6th = lower right
$puzzlenum = $puzzle_count % MAX_PUZZLES;
my $gfx = $page->gfx;
$gfx->strokecolor( '#000' );
$gfx->linewidth( WIDE_LINE_WIDTH );
make_grid(
$gfx,
$xpos[ $puzzlenum ], # x
$ypos[ $puzzlenum ], # y
);
populate_puzzle( $gfx, get_puzzle() );
$pdf->info( 'Keywords' => "puzzlecount=$puzzle_count" );
$pdf->saveas($infile);
print "$infile now has $puzzle_count Sudoku puzzle";
if ( $puzzle_count > 1 ) {
print "s";
} # if
}
print ".\n\n";
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# #
sub populate_puzzle
{
my( $gfx, $array ) = @_;
foreach my $row ( 0 .. $#$array )
{
my $row_array = $array->[$row];
foreach my $column ( 0 .. $#$row_array )
{
next unless defined $row_array->[$column];
place_digit( $gfx, $row, $column, $row_array->[$column] )
}
}
}
sub place_digit
{
my( $gfx, $row, $column, $digit ) = @_;
my $x_start = $xpos[$puzzlenum];
my $y_start = $ypos[$puzzlenum];
my $x_offset = 0.30 * SQUARE_SIDE / 9; # empirically derived
my $y_offset = 0.25 * SQUARE_SIDE / 9;
my $x = $x_start + $column * SQUARE_SIDE / 9 + $x_offset;
my $y = $y_start + $row * SQUARE_SIDE / 9 + $y_offset;
$gfx->textlabel( $x, $y, $font, FONT_SIZE, $digit );
}
sub get_puzzle
{
my @array;
print STDERR "Waiting for puzzle input!\n";
while( <STDIN> )
{
chomp;
s/^\s|\s$//g;
next unless length $_;
push @array, [ map { $_ eq '-' ? undef : $_ } split ];
}
return \@array;
}
sub make_grid
{
my( $gfx, $lower_left_x, $lower_left_y ) = @_;
make_outline( $gfx, $lower_left_x, $lower_left_y );
$gfx->linewidth( THIN_LINE_WIDTH );
make_blocks( $gfx, $lower_left_x, $lower_left_y, 9 );
$gfx->linewidth( LINE_WIDTH );
make_blocks( $gfx, $lower_left_x, $lower_left_y, 3 );
}
sub make_blocks
{
my( $gfx, $lower_left_x, $lower_left_y, $cells ) = @_;
my( $xs, $ys ) =
map {
my $point = $_;
[ map { $point + $_ * SQUARE_SIDE / $cells } 1 .. $cells -
+ 1 ];
} ( $lower_left_x, $lower_left_y );
foreach my $x ( @$xs )
{
make_line( $gfx,
$x, $lower_left_y,
$x, $lower_left_y + SQUARE_SIDE,
);
}
foreach my $y ( @$ys )
{
make_line( $gfx,
$lower_left_x, $y,
$lower_left_x + SQUARE_SIDE, $y,
);
}
}
sub make_outline
{
my( $gfx, $lower_left_x, $lower_left_y ) = @_;
my( $upper_right_x, $upper_right_y ) =
map { $_ + SQUARE_SIDE } ( $lower_left_x, $lower_left_y );
my @points = (
[ $lower_left_x, $lower_left_y - WIDE_LINE_WIDTH / 2,
$lower_left_x, $upper_right_y ],
[ $lower_left_x, $upper_right_y - WIDE_LINE_WIDTH / 2,
$upper_right_x, $upper_right_y ],
[ $upper_right_x - WIDE_LINE_WIDTH / 2, $upper_right_y,
$upper_right_x, $lower_left_y ],
[ $upper_right_x, $lower_left_y + WIDE_LINE_WIDTH / 2,
$lower_left_x, $lower_left_y ],
);
foreach my $tuple ( @points ) { make_line( $gfx, @$tuple ) }
}
sub make_line
{
my( $gfx, $x, $y, $x2, $y2 ) = @_;
$gfx->move( $x, $y );
$gfx->line( $x2, $y2 );
$gfx->fillstroke;
}
__END__
- - 6 8 - 4 - - -
- - - - 9 - 7 - 8
- - - 5 - - - 9 -
1 - - - 4 - - - 9
- - - - - - 5 - -
4 6 - - - 1 - 3 -
8 7 - - - - 4 - -
- - - - 5 - 2 - -
- - - - - 2 - 1 -
| [reply] [d/l] |
|
|