#!/usr/bin/perl
use strict;
use warnings;
use lib '/usr/local/lib/perl5';

use Encode;
use Cairo;
use Pango;
use Getopt::Long;

my $VERSION = '04.00.03';

use Remind::PDF;

my $media_to_size = {
    "Letter"    => [ 612,  792],
    "Tabloid"   => [ 792, 1224],
    "Ledger"    => [1224,  792],
    "Legal"     => [ 612, 1008],
    "Statement" => [ 396,  612],
    "Executive" => [ 540,  720],
    "A3"        => [ 842, 1190],
    "A4"        => [ 595,  842],
    "A5"        => [ 420,  595],
    "B4"        => [ 729, 1032],
    "B5"        => [ 519,  729],
    "Folio"     => [ 612,  936],
    "Quarto"    => [ 612,  780],
    "10x14"     => [ 720, 1008],
};

my $help = 0;

my $settings = {
        landscape => 0,
        numbers_on_left => 0,
        small_calendars => 0,
        fill_entire_page => 0,

        media => 'Letter',
        width => 0,
        height => 0,

        title_font => 'Sans',
        header_font => 'Sans',
        daynum_font => 'Sans Bold Oblique',
        entry_font => 'Sans',
        small_cal_font => 'Sans',

        title_size => 14,
        header_size => 12,
        daynum_size => 14,
        entry_size => 8,

        border_size => 4,
        line_thickness => 1,

        margin_top => 36,
        margin_bottom => 36,
        margin_left => 36,
        margin_right => 36,

        verbose => 0,
};

my $me = $0;
$me =~ s/^.*\///;

set_default_media();

sub usage
{
        print <<"EOF";
$me (version $VERSION): Convert Remind -pp output to a PDF calendar.

Usage: remind -pp [options] filename | $me [options] > out.pdf

Options:

--landscape, -l         Print in landscape orientation
--small-calendars=N     Choose location for small calendars
-cN                     Synonym for --small-calendars=N
--left-numbers, -x      Print day numbers on the left
--fill-page, -e         Fill the entire page
--media=MEDIA, -mMEDIA  Size for specified media
--width=W, -wW          Specify media width in 1/72nds of an inch
--height=H, -hH         Specify media height in 1/72nds of an inch
--title-font=FONT       Specify font for calendar title
--header-font=FONT      Specify font for weekday names
--daynum-font=FONT      Specify font for day numbers
--entry-font=FONT       Specify font for calendar entries
--small-cal-font=FONT   Specify font for small calendars
--title-size=S          Specify size of font for calendar title in points
--header-size=S         Specify size of font for weekday names
--daynum-size=S         Specify size of font for day numbers
--entry-size=S          Specify size of font for calendar entries
--border-size=S         Specify size of gaps between items in 1/72nds of an inch
--line-thickness=S      Specify line thickness in 1/72nds of an inch
--margin-top=S          Specify top margin size in 1/72nds of an inch
--margin-bottom=S       Specify bottom margin size in 1/72nds of an inch
--margin-left=S         Specify left margin size in 1/72nds of an inch
--margin-right=S        Specify right margin size in 1/72nds of an inch
--verbose, -v           Print progress messages
--help                  Display this help
EOF
}

Getopt::Long::Configure('bundling_values');

my $ret = GetOptions('landscape|l' =>        \$settings->{landscape},
                     'small-calendars|c=i' =>  \$settings->{small_calendars},
                     'left-numbers|x' =>     \$settings->{numbers_on_left},
                     'fill-page|e' =>        \$settings->{fill_entire_page},
                     'media|m=s' =>          \$settings->{media},
                     'width|w=i' =>          \$settings->{width},
                     'height|h=i' =>         \$settings->{height},
                     'title-font=s' =>       \$settings->{title_font},
                     'header-font=s' =>      \$settings->{header_font},
                     'daynum-font=s' =>      \$settings->{daynum_font},
                     'entry-font=s' =>       \$settings->{entry_font},
                     'small-cal-font=s' =>   \$settings->{small_cal_font},
                     'title-size=f' =>       \$settings->{title_size},
                     'header-size=f' =>      \$settings->{header_size},
                     'daynum-size=f' =>      \$settings->{daynum_size},
                     'entry-size=f' =>       \$settings->{entry_size},
                     'border-size=f' =>      \$settings->{border_size},
                     'line-thickness=f' =>   \$settings->{line_thickness},
                     'margin-top=f' =>       \$settings->{margin_top},
                     'margin-bottom=f' =>    \$settings->{margin_bottom},
                     'margin-left=f' =>      \$settings->{margin_left},
                     'margin-right=f' =>     \$settings->{margin_right},
                     'verbose|v' =>          \$settings->{verbose},
                     'help' =>               \$help
    );
if (!$ret) {
        usage();
        exit(1);
}

if ($help) {
        usage();
        exit(0);
}

if ($settings->{width} <= 0 ||
    $settings->{height} <= 0) {
        my $size = $media_to_size->{ucfirst($settings->{media})};
        if (!$size) {
                if (lc($settings->{media}) ne 'help') {
                        print STDERR "Unknown media " . $settings->{media} . "\n";
                }
                set_default_media();
                printf("%-12s   Size in 1/72 in\n", "Valid media:");
                foreach my $m (sort { $a cmp $b } (keys(%$media_to_size))) {
                        if ($m eq $settings->{media}) {
                                print "* ";
                        } else {
                                print "  ";
                        }
                        printf("%-12s   %4d x %4d\n", $m,
                               $media_to_size->{$m}->[0],
                               $media_to_size->{$m}->[1]);
                }
                exit(1);
        }
        $settings->{width} = $size->[0];
        $settings->{height} = $size->[1];
}

if ($settings->{landscape}) {
        my $tmp = $settings->{width};
        $settings->{width} = $settings->{height};
        $settings->{height} = $tmp;
}

# Don't read from a terminal
if (-t STDIN) {
        print STDERR "I can't read data from a terminal.  Please run like this:\n";
        print STDERR "  remind -pp [options] filename | $me [options] > out.pdf\n";
        exit(1);
}

my $done_one = 0;

my $errored_out = 0;

my $surface = Cairo::PdfSurface->create_for_stream(sub { print $_[1] unless $errored_out; }, undef,
                                                   $settings->{width}, $settings->{height});

# set_metadata not available in older versions of Cairo
eval { $surface->set_metadata('title', 'Calendar'); };
eval { $surface->set_metadata('author', 'Remind (https://dianne.skoll.ca/projects/remind/)'); };
eval { $surface->set_metadata('creator', 'rem2pdf (https://dianne.skoll.ca/projects/remind/)'); };
eval { $surface->set_metadata('subject', 'Calendar'); };

my $cr = Cairo::Context->create($surface);
$cr->set_line_width($settings->{line_thickness});

while(1) {
        my ($obj, $err) = Remind::PDF->create_from_stream(*STDIN,
                                                          {color => 1,
                                                           shade => 1,
                                                           moon => 1,
                                                           pango => 1,
                                                           week => 1,});

        if (!$obj) {
                if (!$done_one) {
                        $errored_out = 1;
                        print STDERR "$me: $err\n";
                        exit(1);
                }
                last;
        }
        $done_one = 1;
        $obj->render($cr, $settings);
}

$surface->finish();

sub set_default_media
{
        my $paper;
        $paper = $ENV{PAPERSIZE};
        if ($paper && set_media(ucfirst($paper))) {
                return 1;
        }
        if ($ENV{PAPERCONF}) {
                if (set_media_from_file($ENV{PAPERCONF})) {
                        return 1;
                }
        }
        if (set_media_from_file('/etc/papersize')) {
                return 1;
        }
        return set_media('Letter');
}

sub set_media
{
        my ($m) = @_;

        return 0 unless $media_to_size->{$m};
        $settings->{media} = $m;
        return 1;
}

sub set_media_from_file
{
        my ($fn) = @_;
        if (!open(IN, '<', $fn)) {
                return 0;
        }
        while(<IN>) {
                chomp;
                s/^\s+//;
                s/\s+$//;
                next if ($_ eq '');
                next if ($_ =~ /^#/);
                my $m = $_;
                close(IN);
                return set_media($m);
        }
        close(IN);
        return 0;
}

__END__

=head1 NAME

rem2pdf - draw a PDF calendar from Remind output

=head1 SYNOPSIS

    remind -pp [options] file | rem2pdf [options] > output.pdf

=head1 DESCRIPTION

B<rem2pdf> reads the standard input, which should be the results of
running B<remind> with the B<-p>, B<-pp> or B<-ppp> options.  It emits
PDF code that draws a calendar to standard output.

B<rem2pdf> uses the Pango text formatting library (L<https://pango.gnome.org/>)
and the Cairo graphics library (L<https://www.cairographics.org/>) to produce
its output.  The CPAN modules Pango (L<https://metacpan.org/pod/Pango>)
and Cairo (L<https://metacpan.org/pod/Cairo>) are prerequisites.

B<rem2pdf> assumes that its input stream is valid UTF-8.  If this is not
the case, it may render output incorrectly or even fail to render
output at all.

=head1 OPTIONS

=over

=item --landscape, -l

Print the calendar in landscape orientation.  Essentially, this swaps
the width and height of the output media.

=item --small-calendars=I<n>, -cI<n>

Control the inclusion of small calendars for the previous and next
month.  Possible values for I<n> are:

=over

=item Z<>0

Do not draw any small calendares

=item Z<>1

Place the small calendars at the bottom-right if there is room;
otherwise, place them at the top-left.

=item Z<>2

Place the small calendars at the top-left if there is room; otherwise,
place them at the bottom-right.

=item Z<>3

Place the previous month's small calendar at the top-left and the next
month's at the bottom-right if there is room; otherwise, follow
I<n>=1.  A moment's thought reveals that an option which splits the
calendars if there is room and otherwise follows I<n>=2 yields the
same results.

=back

=item --left-numbers, -x

Draw the day numbers in the top-left corner of each day's box rather than
the default top-right.

=item --fill-page, -e

Make the calendar fill the available space on the page.

=item --media=I<media>, -mI<media>

Specify the paper size (Letter, A4, etc.)  For a list of valid media sizes,
run:

    rem2pdf --media=help

The default media size will be marked with an asterisk.

=item --width=I<n>, -wI<n>, --height=I<m>, -hI<m>

Rather than specifying a named media size, directly specify the width
and height of the output in 1/72ths of an inch.  You must specify both
width and height for the options to be respected.

=item --title-font=I<font>

Specify the font used for the calendar title.  It can be any font that
the Pango library on your system can use.  The default is Sans.  If
you choose a font with spaces in its name, you may need to quote this
argument.

=item --header-font=I<font>

Specify the font used for the weekday names.  The default is Sans.

=item --daynum-font=I<font>

Specify the font used for the day numbers.  The default is
Sans Bold Oblique.

=item --entry-font=I<font>

Specify the font used for calendar entries.  The default is Sans.

=item --small-cal-font=I<font>

Specify the font used for the small next- and previous-month
calendars.  The default is Sans.

=item --title-size=I<n>

Specify the size of the title font in 1/72ths of an inch.  The default
is 14.  This size, and indeed all following sizes, may be specified as
floating-point numbers.

=item --header-size=I<n>

Specify the size of the header font in 1/72ths of an inch.  The default is 14.

=item --daynum-size=I<n>

Specify the size of the day number font in 1/72ths of an inch.  The
default is 14.

=item --entry-size=I<n>

Specify the size of the calendar entry font in 1/72ths of an inch.
The default is 8.

=item --border-size=I<n>

Specify the size of the blank border between the contents of a calendar
box and the centre of the lines surrounding it, in 1/72ths of an inch.
The default is 4.

=item --line-thickness=I<n>

Specify the thickness of the lines drawn on the calendar.  The default is 1.

=item --margin-top=I<n>

The size of the margin at the top of the page in 1/72ths of an inch.
The default is 36.

=item --margin-bottom=I<n>

The size of the margin at the bottom of the page in 1/72ths of an inch.
The default is 36.

=item --margin-left=I<n>

The size of the margin at the left of the page in 1/72ths of an inch.
The default is 36.

=item --margin-right=I<n>

The size of the margin at the right of the page in 1/72ths of an inch.
The default is 36.

=item --verbose, -v

Print (on STDERR) the name of the month and year for each month that
is rendered.

=back

=head1 USAGE

To use B<rem2df>, pipe the output of B<remind> with one of the
B<-p>, B<-pp> or B<-ppp> options into B<rem2pdf>.  The PDF output
will be sent to standard output.  So for example, to print a 12-month
calendar for the year 2030, use:

  remind -pp12 /dev/null Jan 2030 | rem2pdf -e -l -c=3 | lpr

You can concatenate multiple B<remind> runs.  For example, the following
will produce a PDF calendar for January through March of 2023, and
June of 2023 (for a total of four pages);

  (remind -pp3 Jan 2023 /dev/null ; \
   remind -p June 2023 /dev/null) | rem2pdf -e -l -c=3 > cal.pdf

=head1 FORMATTED TEXT

B<rem2pdf> supports a B<SPECIAL> reminder type called B<PANGO>.  This
lets you format text using the Pango markup language, described at
L<https://docs.gtk.org/Pango/pango_markup.html>.  Here are some
examples:

  REM Mon SPECIAL PANGO <b>Bold</b> and <i>italic</i>
  REM Tue SPECIAL PANGO <span face="zapf chancery">Fancy</span>
  REM Wed SPECIAL PANGO <span foreground="red"><b>Bold red</b></span>

Other back-ends such as B<rem2ps> and B<rem2html> will ignore PANGO
special reminders.

Neither B<remind> nor B<rem2pdf> will check the markup to ensure
it is syntactically correct.  If you use invalid Pango markup, the
Pango library will print a warning and B<rem2pdf> will not render any
output for the invalid reminder.

=head1 ABSOLUTELY-POSITIONED TEXT

If your B<PANGO> special reminder starts with C<@I<x>,I<y>> where I<x>
and I<y> are floating-point numbers, then the Pango marked-up test is
positioned absolutely with respect to the day's box (and is not
counted when calculating the box's height.)

A positive I<x> value positions the left edge of the text I<x> points
to the right of the left side of the calendar box, while a negative
I<x> value positions the right edge of the text I<x> points to the left
of the right side of the calendar box.

A positive I<y> value positions the top edge of the text I<y> points
below the top of the calendar box, while a negative I<y> value
positions the bottom edge of the text I<y> points above the bottom of
the calendar box.

If you use absolutely-positioned text, it's up to you to make sure it
doesn't overlap other text; B<rem2pdf> takes no special precautions to
prevent this.

As an example, this places Sunrise and Sunset times at the bottom left
of each calendar box:

  REM SPECIAL PANGO @1,-1 <span size="4800"><i>Rise [sunrise($U)] Set [sunset($U)]</i></span>

(Note that Pango expresses font sizes in 1024's of a point, so a size of
4800 works out to about 4.6 points.)

=head1 AUTHOR

B<Rem2PDF> was written by Dianne Skoll <dianne@skoll.ca>

=head1 SEE ALSO

B<remind>, B<rem2ps>, B<rem2html>, B<tkremind>

