mirror of
https://salsa.debian.org/dskoll/remind.git
synced 2026-04-16 06:18:47 +02:00
Start on rem2pdf via Cairo/Pango
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
40
rem2pdf/lib/Remind/PDF/Entry.pm
Normal file
40
rem2pdf/lib/Remind/PDF/Entry.pm
Normal file
@@ -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;
|
||||
|
||||
321
rem2pdf/lib/Remind/PDF/Month.pm
Normal file
321
rem2pdf/lib/Remind/PDF/Month.pm
Normal file
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user