diff --git a/rem2pdf/bin/rem2pdf b/rem2pdf/bin/rem2pdf index 402145a2..414acf6f 100755 --- a/rem2pdf/bin/rem2pdf +++ b/rem2pdf/bin/rem2pdf @@ -5,9 +5,10 @@ use warnings; use Encode; use Cairo; use Pango; - use Getopt::Long; +use Remind::PDF::Month; + my $media_to_size = { "Letter" => [ 612, 792], "Tabloid" => [ 792, 1224], @@ -25,38 +26,67 @@ my $media_to_size = { "10x14" => [ 720, 1008], }; -my $landscape = 0; -my $numbers_on_left = 0; -my $small_calendars = 0; -my $fill_entire_page = 0; +my $settings = { + landscape => 0, + numbers_on_left => 0, + small_calendars => 0, + fill_entire_page => 0, -my $media = 'Letter'; + media => 'Letter', + width => 612, + height => 792, -my $title_font = 'Sans 14'; -my $small_cal_font = 'Sans'; -my $header_font = 'Sans 14'; -my $entry_font = 'Sans 8'; -my $daynum_font = 'Sans Bold Oblique 14'; + title_font => 'Helvetica', + header_font => 'Helvetica', + daynum_font => 'Helvetica Bold Oblique', + entry_font => 'Helvetica', + small_cal_font => 'Helvetica', -my $border_size = 6; -my $line_thickness = 1; + title_size => '14px', + header_size => '14px', + daynum_size => '14px', + entry_size => '8px', -my $margin_top = 36; -my $margin_bottom = 36; -my $margin_left = 36; -my $margin_right = 36; + border_size => 6, + line_thickness => 1, -sub read_one_month -{ + margin_top => 36, + margin_bottom => 36, + margin_left => 36, + margin_right => 36, +}; + +my $done_one = 0; + +my $surface = Cairo::PdfSurface->create_for_stream(sub { print $_[1]; }, undef, + $settings->{width}, $settings->{height}); +my $cr = Cairo::Context->create($surface); +$cr->set_line_width($settings->{line_thickness}); + +while(1) { + my ($obj, $err) = Remind::PDF::Month->create_from_stream(*STDIN, + {color => 1, + shade => 1, + moon => 1, + week => 1,}); + + if (!$obj) { + if (!$done_one) { + print STDERR "$err\n"; + exit(1); + } + last; + } + $done_one = 1; + $obj->render($cr, $settings); } +$surface->finish(); + + __END__ -my $surface = Cairo::PdfSurface->create('/tmp/x.pdf', 612, 792); -my $cr = Cairo::Context->create($surface); - -$cr->set_line_width($line_thickness); for (my $x=36; $x < 612-36; $x += 72) { $cr->move_to($x, 0); $cr->line_to($x, 792); @@ -72,5 +102,3 @@ $cr->set_source_rgb(0,0,0); $cr->set_font_size(36); $cr->show_text(Encode::decode('UTF-8', "מֵם סוֹפִית‎çédo")); -$surface->finish(); - diff --git a/rem2pdf/lib/Remind/PDF/Entry.pm b/rem2pdf/lib/Remind/PDF/Entry.pm new file mode 100644 index 00000000..11098287 --- /dev/null +++ b/rem2pdf/lib/Remind/PDF/Entry.pm @@ -0,0 +1,40 @@ +package Remind::PDF::Entry; +use strict; +use warnings; + +sub new_from_hash +{ + my ($class, $hash) = @_; + if (exists($hash->{passthru})) { + my $special = lc($hash->{passthru}); + if ($special =~ /^(html|htmlclass|week|moon|shade|color|colour|postscript|psfile)$/) { + $special = 'color' if $special eq 'colour'; + $class = 'Remind::PDF::Entry::' . $special; + } else { + $class = 'Remind::PDF::Entry::UNKNOWN'; + } + } + return bless($hash, $class); +} + +package Remind::PDF::Entry::html; +use base 'Remind::PDF::Entry'; +package Remind::PDF::Entry::htmlclass; +use base 'Remind::PDF::Entry'; +package Remind::PDF::Entry::week; +use base 'Remind::PDF::Entry'; +package Remind::PDF::Entry::moon; +use base 'Remind::PDF::Entry'; +package Remind::PDF::Entry::shade; +use base 'Remind::PDF::Entry'; +package Remind::PDF::Entry::color; +use base 'Remind::PDF::Entry'; +package Remind::PDF::Entry::postscript; +use base 'Remind::PDF::Entry'; +package Remind::PDF::Entry::psfile; +use base 'Remind::PDF::Entry'; +package Remind::PDF::Entry::UNKNOWN; +use base 'Remind::PDF::Entry'; + +1; + diff --git a/rem2pdf/lib/Remind/PDF/Month.pm b/rem2pdf/lib/Remind/PDF/Month.pm new file mode 100644 index 00000000..42ddc829 --- /dev/null +++ b/rem2pdf/lib/Remind/PDF/Month.pm @@ -0,0 +1,321 @@ +package Remind::PDF::Month; +use strict; +use warnings; + +use Cairo; +use Pango; + +use Remind::PDF::Entry; +use Encode; + +use JSON::MaybeXS; + +=head1 NAME + +Remind::Month - hold one month's worth of calendar data + +=cut + +sub create_from_stream +{ + my ($class, $in, $specials_accepted) = @_; + while (<$in>) { + chomp; + if ($_ eq '# rem2ps begin' || + $_ eq '# rem2ps2 begin') { + my $self = bless {}, $class; + return $self->read_one_month($in, $_, $specials_accepted); + } elsif ($_ eq '[') { + return (undef, "Unsupported format: Use remind -pp, not remind -ppp"); + } + } + return (undef, "Could not find any remind -p output anywhere"); +} + +sub read_one_month +{ + my ($self, $in, $first_line, $specials_accepted) = @_; + $self->{entries} = []; + $self->{daynames} = []; + $self->{monthname} = ''; + $self->{year} = ''; + $self->{daysinmonth} = 0; + $self->{firstwkday} = 0; + $self->{mondayfirst} = 0; + $self->{prevmonthname} = ''; + $self->{nextmonthname} = ''; + $self->{daysinprevmonth} = 0; + $self->{daysinnextmonth} = 0; + $self->{prevmonthyear} = 0; + $self->{nextmonthyear} = 0; + + for (my $i=0; $i<=31; $i++) { + $self->{entries}->[$i] = []; + } + + my $line = $in->getline(); + chomp($line); + + # Month Year Days FirstWkday MondayFirst + if ($line =~ /^(\S+) (\d+) (\d+) (\d+) (\d+)/) { + $self->{monthname} = $1; + $self->{year} = $2; + $self->{daysinmonth} = $3; + $self->{firstwkday} = $4; + $self->{mondayfirst} = $5; + } else { + return (undef, "Cannot interpret line: $line"); + } + + # Day names + $line = $in->getline(); + chomp($line); + if ($line =~ /^\S+ \S+ \S+ \S+ \S+ \S+ \S+$/) { + @{$self->{daynames}} = split(/ /, $line); + } else { + return (undef, "Cannot interpret line: $line"); + } + + # Prev month, num days + $line = $in->getline(); + chomp($line); + if ($line =~ /^\S+ \d+$/) { + ($self->{prevmonthname}, $self->{daysinprevmonth}) = split(/ /, $line); + } else { + return (undef, "Cannot interpret line: $line"); + } + # Next month, num days + $line = $in->getline(); + chomp($line); + if ($line =~ /^\S+ \d+$/) { + ($self->{nextmonthname}, $self->{daysinnextmonth}) = split(/ /, $line); + } else { + return (undef, "Cannot interpret line: $line"); + } + + if ($first_line eq '# rem2ps2 begin') { + # remind -pp format + return $self->read_one_month_pp($in, $specials_accepted); + } + + # Old-style "remind -p" + # TODO: Eventually support this? + return (undef, "Format not supported: Use -pp or -ppp, not plain -p"); +} + +sub read_one_month_pp +{ + my ($self, $in, $specials_accepted) = @_; + + my $json = JSON::MaybeXS->new(utf8 => 1); + my $line; + while ($line = $in->getline()) { + chomp($line); + if ($line eq '# rem2ps2 end') { + return ($self, undef); + } + my $hash; + eval { + $hash = $json->decode($line); + }; + if (!$hash) { + return (undef, "Unable to decode JSON: $@"); + } + + my $day = $hash->{date}; + $day =~ s/^\d\d\d\d-\d\d-//; + $day =~ s/^0//; + if ($self->accept_special($hash, $specials_accepted)) { + push(@{$self->{entries}->[$day]}, Remind::PDF::Entry->new_from_hash($hash)); + } + } + return (undef, "Missing # rem2ps2 end marker"); +} + +sub accept_special +{ + my ($self, $hash, $specials_accepted) = @_; + return 1 unless exists($hash->{passthru}); + return 1 if $specials_accepted->{lc($hash->{passthru})}; + return 0; +} + +sub find_last_special +{ + my ($self, $special, $entries) = @_; + my $class = "Remind::PDF::Entry::$special"; + my $found = undef; + foreach my $e (@$entries) { + $found = $e if ($e->isa($class)); + } + return $found; +} + +sub render +{ + my ($self, $cr, $settings) = @_; + + $cr->set_line_cap('square'); + my $so_far = $self->draw_title($cr, $settings); + + # Top line + $cr->move_to($settings->{margin_left}, $so_far); + $cr->line_to($settings->{width} - $settings->{margin_right}, $so_far); + $cr->stroke(); + + my $top_line = $so_far; + + $so_far = $self->draw_daynames($cr, $settings, $so_far); + + # Line under the days + $cr->move_to($settings->{margin_left}, $so_far); + $cr->line_to($settings->{width} - $settings->{margin_right}, $so_far); + $cr->stroke(); + + # First column + my $first_col = $self->{firstwkday}; + if ($self->{mondayfirst}) { + $first_col--; + if ($first_col < 0) { + $first_col = 6; + } + } + + # Last column + my $last_col = ($first_col + $self->{daysinmonth} - 1) % 7; + + # Number of rows + my $rows = 1; + my $last_day_on_row = 7 - $first_col; + while ($last_day_on_row < $self->{daysinmonth}) { + print STDERR "$rows $last_day_on_row\n"; + $last_day_on_row += 7; + $rows++; + } + + # Add a row for small calendars if necessary + if (($settings->{small_calendars} != 0) && ($first_col == 0) && ($last_col == 6)) { + $rows++; + } + + my ($start_col, $start_day); + for (my $row = 0; $row < $rows; $row++) { + if ($row == 0) { + $start_day = 1; + $start_col = $first_col; + } else { + $start_col = 0; + } + print STDERR "Drawing row $row $start_day $start_col\n"; + $so_far = $self->draw_row($cr, $settings, $so_far, $row, $start_day, $start_col); + $start_day += 7 - $start_col; + $cr->move_to($settings->{margin_left}, $so_far); + $cr->line_to($settings->{width} - $settings->{margin_right}, $so_far); + $cr->stroke(); + } + + # The vertical lines + my $cell = ($settings->{width} - $settings->{margin_left} - $settings->{margin_right}) / 7; + for (my $i=0; $i<=7; $i++) { + $cr->move_to($settings->{margin_left} + $i * $cell, $top_line); + $cr->line_to($settings->{margin_left} + $i * $cell, $so_far); + $cr->stroke(); + } +} + +sub draw_row +{ + my ($self, $cr, $settings, $so_far, $row, $start_day, $start_col) = @_; + + my $col = $start_col; + my $day = $start_day; + my $height = 0; + while ($col < 7) { + my $h = $self->draw_day($cr, $settings, $so_far, $day, $col); + $height = $h if ($h > $height); + $day++; + $col++; + last if ($day > $self->{daysinmonth}); + } + return $so_far + $height + $settings->{border_size}; +} + +sub draw_day +{ + my ($self, $cr, $settings, $so_far, $day, $col) = @_; + + # Draw the day number + my $layout = Pango::Cairo::create_layout($cr); + $layout->set_text($day); + my $desc = Pango::FontDescription->from_string($settings->{daynum_font} . ' ' . $settings->{daynum_size}); + + $layout->set_font_description($desc); + my ($wid, $h) = $layout->get_pixel_size(); + + my $cell = ($settings->{width} - $settings->{margin_left} - $settings->{margin_right}) / 7; + $cr->save; + if ($settings->{numbers_on_left}) { + $cr->move_to($settings->{margin_left} + ($col * $cell) + $settings->{border_size}, $so_far + $settings->{border_size}); + } else { + $cr->move_to($settings->{margin_left} + ($col * $cell) + $cell - $settings->{border_size} - $wid, $so_far + $settings->{border_size}); + } + Pango::Cairo::show_layout($cr, $layout); + $cr->restore(); + return $h + $settings->{border_size}; +} + +sub draw_daynames +{ + my ($self, $cr, $settings, $so_far) = @_; + + my $w = $settings->{width} - $settings->{margin_left} - $settings->{margin_right}; + my $cell = $w/7; + + $so_far += $settings->{border_size}; + my $height = 0; + for (my $i=0; $i<7; $i++) { + my $j; + if ($self->{mondayfirst}) { + $j = ($i + 1) % 7; + } else { + $j = $i; + } + my $layout = Pango::Cairo::create_layout($cr); + $layout->set_text(Encode::decode('UTF-8', $self->{daynames}->[$j])); + my $desc = Pango::FontDescription->from_string($settings->{header_font} . ' ' . $settings->{header_size}); + + $layout->set_font_description($desc); + + my ($wid, $h) = $layout->get_pixel_size(); + $cr->save; + $cr->move_to($settings->{margin_left} + $i * $cell + $cell/2 - $wid/2, $so_far); + Pango::Cairo::show_layout($cr, $layout); + $cr->restore(); + if ($h > $height) { + $height = $h; + } + } + return $so_far + $height + $settings->{border_size}; +} + +sub draw_title +{ + my ($self, $cr, $settings) = @_; + my $title = $self->{monthname} . ' ' . $self->{year}; + + my $layout = Pango::Cairo::create_layout($cr); + $layout->set_text(Encode::decode('UTF-8', $title)); + my $desc = Pango::FontDescription->from_string($settings->{title_font} . ' ' . $settings->{title_size}); + + $layout->set_font_description($desc); + + my ($w, $h) = $layout->get_pixel_size(); + $cr->save(); + $cr->move_to($settings->{width}/2 - $w/2, $settings->{margin_top}); + Pango::Cairo::show_layout($cr, $layout); + $cr->restore(); + return $h + $settings->{margin_top} + $settings->{border_size}; +} + +1; +