Beefy Boxes and Bandwidth Generously Provided by pair Networks
Come for the quick hacks, stay for the epiphanies.
 
PerlMonks  

Making Sudoku Puzzles Using PDF::API2

by brian_d_foy (Abbot)
on Feb 21, 2006 at 12:27 UTC ( [id://531667]=CUFP: print w/replies, xml ) Need Help??

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. :)

--
brian d foy <brian@stonehenge.com>
Subscribe to The Perl Review

Replies are listed 'Best First'.
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

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.

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/
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!

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 -

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: CUFP [id://531667]
Approved by GrandFather
Front-paged by GrandFather
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others examining the Monastery: (5)
As of 2024-04-19 23:44 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found