From 800a4b15b2497b96321fe53657851aa69ff411c5 Mon Sep 17 00:00:00 2001 From: Dianne Skoll Date: Tue, 24 Dec 2024 13:07:45 -0500 Subject: [PATCH] Add support for weekly PDF calendars with "-p+n" Remind option. --- man/rem2ps.1.in | 86 +++++++++++----- man/remind.1.in | 15 ++- rem2html/rem2html.in | 4 + rem2pdf/lib/Remind/PDF.pm | 209 ++++++++++++++++++++++++++++++++++++++ src/calendar.c | 61 ++++++++--- src/init.c | 16 ++- tests/test-rem | 11 ++ tests/test.cmp | 18 +++- 8 files changed, 370 insertions(+), 50 deletions(-) diff --git a/man/rem2ps.1.in b/man/rem2ps.1.in index 26fccc7c..29bf1875 100644 --- a/man/rem2ps.1.in +++ b/man/rem2ps.1.in @@ -637,70 +637,100 @@ However, back-ends should keep reading until EOF in case more data for subsequent months is forthcoming. .PP -.SH REM2PS PURE JSON INPUT FORMAT (-PPP OPTION) -\fBRemind \-ppp\fR emits \fIpure JSON\fR output. The format is -as follows: +.SH REM2PS PURE JSON INPUT FORMAT (-PPP OR -P+ OPTION) +\fBRemind \-ppp\fR and \fBremind \-p+\fR emit \fIpure JSON\fR output. +The format is as follows: .PP \fBRemind\fR outputs a JSON array. Each element of the array is a -\fImonth descriptor\fR. +\fImonth descriptor\fR or a \fIweek descriptor\fR in the case of +\fBremind \-p+\fR. .PP -Each month descriptor is a JSON object with the following elements: +Each descriptor is a JSON object with the following elements: +.TP +.B caltype \fItype\fR +The calendar type, either \fBmonthly\fR or \fBweekly\fR. Older versions +of \fBRemind\fR did not include a \fBcaltype\fR element, so a missing +\fBcaltype\fR should be treated as \fBmonthly\fR. .TP .B monthname \fIname\fR -The name of the month. +The name of the month. Present in monthly calendar types only. .TP .B year \fIyyyy\fR -The year. +The year. Present in monthly calendar types only. .TP .B daysinmonnth \fIn\fR -The number of days in the current month. +The number of days in the current month. Present in monthly calendar types only. .TP .B firstwkday \fIn\fR -The weekday of the first day of the month (0 = Sunday, 1 = Monday, 6 = Saturday). +The weekday of the first day of the month (0 = Sunday, 1 = Monday, 6 = Saturday). Present in monthly calendar types only. .TP .B mondayfirst \fIn\fR An indicator of whether or not the calendar week should start with -Sunday (n=0) or Monday (n=1). +Sunday (n=0) or Monday (n=1). Present in monthly calendar types only. .TP .B daynames \fR[\fIdays\fR] A seven-element array of day names; each element is a string representing -the names of the days from Sunday through Saturday. +the names of the days from Sunday through Saturday. Present in monthly calendar types only. .TP .B prevmonthname \fIname\fR -The name of the previous month. +The name of the previous month. Present in monthly calendar types only. .TP .B daysinprevmonth \fIn\fR -The number of days in the previous month. +The number of days in the previous month. Present in monthly calendar types only. .TP .B prevmonthyear \fIyyyy\fR The year of the previous month. (The same as \fByear\fR unless the current -month is January.) +month is January.) Present in monthly calendar types only. .TP .B nextmonthname \fIname\fR -The name of the following month. +The name of the following month. Present in monthly calendar types only. .TP .B daysinnextmonth \fIn\fR -The number of days in the following month. +The number of days in the following month. Present in monthly calendar types only. .TP .B nextmonthyear \fIyyyy\fR The year of the following month. (The same as \fByear\fR unless the -current month is December.) +current month is December.) Present in monthly calendar types only. .TP .B translations \fR{\fIobject\fR} A complete dump of the Remind translation table. In output for multiple -months, the translation table is included only with the first month. +months or weeks, the translation table is included only with the first month +or week. Present in both weekly and monthly calendar types. .TP .B entries \fR[\fIarray\fR] -The \fBentries\fR key consists of an array of calendar entries; each -entry is a JSON object that has the same format as described in the -\fBCALENDAR ENTRIES\fR section in the \fB\-PP FORMAT\fR section, -\fIwith the following difference\fR: In \fB\-PP\fR mode, if a reminder -has \fB%"\fR markers, only the text between the markers -is included in the \fBbody\fR element. In \fB\-PPP\fR mode, the -entire text \fIincluding\fR the \fB%"\fR markers is included and it's up to -the back-end to extract the portion between the markers if that -is desired. - +The \fBentries\fR key, present in both weekly and monthly calendar +types, consists of an array of calendar entries; each entry is a JSON +object that has the same format as described in the \fBCALENDAR +ENTRIES\fR section in the \fB\-PP FORMAT\fR section, \fIwith the +following difference\fR: In \fB\-PP\fR mode, if a reminder has +\fB%"\fR markers, only the text between the markers is included in the +\fBbody\fR element. In \fB\-PPP\fR mode, the entire text +\fIincluding\fR the \fB%"\fR markers is included and it's up to the +back-end to extract the portion between the markers if that is +desired. +.TP +.B dates \fR[\fIarray\fR] +The \fBdates\fR key, present in weekly calendar types only, +contains seven entries; one for each column in the weekly +calendar. Each entry is a JSON object containing the following +key/value pairs: +.RS +.TP +.B date \fR\fIYYYY-MM-DD\fR +The date of the column. +.TP +.B day \fR\fIDD\fR +The day number of the column. +.TP +.B dayname \fR\fIweekday_name\fR +The name of the weekday (possibly localized). +.TP +.B month \fR\fImonth_name\fR +The name of the month (possibly localized). +.TP +.B year \fR\fIYYYY\fR +The year. +.RE .SH AUTHOR rem2ps was written by Dianne Skoll diff --git a/man/remind.1.in b/man/remind.1.in index 88209c5a..079fefc9 100644 --- a/man/remind.1.in +++ b/man/remind.1.in @@ -182,12 +182,18 @@ If you immediately follow the \fBs\fR with the letter day they actually occur \fIas well as\fR on any preceding days specified by the reminder's \fIdelta\fR. .TP -.B \-p\fR[\fBa\fR][\fBp\fR][\fBp\fR][\fBq\fR]\fIn\fR +.B \-p\fR[\fBa\fR][\fBp\fR][\fBp\fR][\fBq\fR][+]\fIn\fR The \fB\-p\fR option is very similar to the \fB\-s\fR option, except -that the output contains additional information for use by the +that the output contains additional information for use by a back-end such as the \fBRem2PS\fR program, which creates a PostScript calendar, and various -other back-end programs. For this -option, \fIn\fR cannot start with "+"; it must specify a number of months. +other back-end programs. If \fIn\fR starts with "+", then it specifies +a number of weeks rather than a number of months, and back-ends are expected +to produce weekly calendars. Note that not all back-ends support +weekly calendars; currently, only \fBrem2pdf\fR does. Specifying a weekly +calendar implicitly enables the pure JSON interchange format, similar +to \fB\-ppp\fR. +.RS +.PP The format of the \fB\-p\fR output is described in the \fBrem2ps(1)\fR man page. If you immediately follow the \fBp\fR with the letter \fBa\fR, then \fBRemind\fR displays reminders on the calendar on the @@ -200,7 +206,6 @@ three p's, as in \fB\-ppp\fR, then \fBRemind\fR uses a pure JSON format, again documented in \fBrem2ps(1)\fR. If you include a \fBq\fR letter with this option, then the normal calendar-mode substitution filter is disabled and the %"...%" sequences are preserved in the output. -.RS .PP The \fB\-p\fR, \fB\-pp\fR and \fB\-ppp\fR options implicitly enable the \fB\-o\fR option. diff --git a/rem2html/rem2html.in b/rem2html/rem2html.in index dcc783a3..12a70d5d 100644 --- a/rem2html/rem2html.in +++ b/rem2html/rem2html.in @@ -303,6 +303,10 @@ sub parse_input my $found_data = 0; while() { chomp; + if ($_ eq '[') { + print STDERR "rem2html: It appears that you have invoked Remind with the -ppp option.\n Please use either -p or -pp, but not -ppp.\n"; + return 0; + } if (/# translations/) { slurp_translations(); next; diff --git a/rem2pdf/lib/Remind/PDF.pm b/rem2pdf/lib/Remind/PDF.pm index 9c39f18c..c9d8ed79 100644 --- a/rem2pdf/lib/Remind/PDF.pm +++ b/rem2pdf/lib/Remind/PDF.pm @@ -62,6 +62,9 @@ sub create_from_hash { my ($class, $hash, $specials_accepted) = @_; + if (exists($hash->{caltype}) && ($hash->{caltype} eq 'weekly')) { + return Remind::PDF::Weekly->create_from_hash($hash, $specials_accepted); + } bless $hash, $class; my $filtered_entries = []; @@ -1023,5 +1026,211 @@ sub render } } +package Remind::PDF::Weekly; +use base qw(Remind::PDF); + +=head1 NAME + +Remind::PDF::Weekly - render a weekly calendar + +=cut + +sub render +{ + my ($self, $cr, $settings) = @_; + $settings->{numbers_on_left} = 1; + $self->draw_headings($cr, $settings); + for (my $i=0; $i<7; $i++) { + $self->draw_entries($cr, $settings, $i); + } + $self->draw_lines($cr, $settings); + $cr->show_page(); +} + +sub draw_headings +{ + my ($self, $cr, $settings) = @_; + my $ymax = 0; + my $cell = ($settings->{width} - $settings->{margin_left} - $settings->{margin_right})/7; + + for (my $i=0; $i<7; $i++) { + my $date = $self->{dates}[$i]; + my $month = $date->{month}; + my $year = $date->{year}; + my $day = $date->{day}; + my $dayname = $date->{dayname}; + + my $layout = Pango::Cairo::create_layout($cr); + $layout->set_text(Encode::decode('UTF-8', $dayname)); + + my $desc = Pango::FontDescription->from_string($settings->{header_font} . ' ' . $settings->{header_size} . 'px'); + $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, $settings->{margin_top}); + Pango::Cairo::show_layout($cr, $layout); + $cr->restore(); + + $layout = Pango::Cairo::create_layout($cr); + $layout->set_text(Encode::decode('UTF-8', $day . " " . $month . " " . $year)); + my $es = $settings->{entry_size}; + if ($es > 8) { + $es = 8; + } + $desc = Pango::FontDescription->from_string($settings->{entry_font} . ' ' . $es . 'px'); + $layout->set_font_description($desc); + + my ($wid2, $h2) = $layout->get_pixel_size(); + $cr->save; + $cr->move_to($settings->{margin_left} + $i * $cell + $cell/2 - $wid2/2, $settings->{margin_top} + $h); + Pango::Cairo::show_layout($cr, $layout); + $cr->restore(); + + if ($h + $h2 > $ymax) { + $ymax = $h + $h2; + } + } + $self->{heading_bottom_y} = $ymax+ $settings->{border_size}; +} + +sub draw_entries +{ + my ($self, $cr, $settings, $i) = @_; + + my $cell = ($settings->{width} - $settings->{margin_left} - $settings->{margin_right})/7; + + # Coordinates of box from line-to-line + my $l2l_box = [$i * $cell + $settings->{margin_left}, + $self->{heading_bottom_y} + $settings->{margin_top}, + ($i+1) * $cell + $settings->{margin_left}, + $settings->{height} - $settings->{margin_bottom}]; + + # Coordinates of drawing-space box + my $box = [$l2l_box->[0] + $settings->{border_size}, + $l2l_box->[1] + $settings->{border_size}, + $l2l_box->[2] - $settings->{border_size}, + $l2l_box->[3] - $settings->{border_size}]; + + $self->{l2l_box} = $l2l_box; + $self->{box} = $box; + + # Do shading, if any + my $shade = $self->find_last_special('shade', $self->{entries}->[$i]); + if ($shade) { + $cr->save; + $cr->set_source_rgb($shade->{r} / 255, + $shade->{g} / 255, + $shade->{b} / 255); + $cr->rectangle($l2l_box->[0], $l2l_box->[1], + $l2l_box->[2] - $l2l_box->[0], + $l2l_box->[3] - $l2l_box->[1]); + $cr->fill(); + $cr->restore; + } + + # Get the "day number" size to leave room for moon and week specials + my $layout = Pango::Cairo::create_layout($cr); + $layout->set_text("31"); + my $desc = Pango::FontDescription->from_string($settings->{daynum_font} . ' ' . $settings->{daynum_size} . 'px'); + + $layout->set_font_description($desc); + my ($wid, $h) = $layout->get_pixel_size(); + + my $so_far = $box->[1] + $h + $settings->{border_size}; + + my $box_height = $box->[3] - $box->[1]; + my $done = 0; + foreach my $entry (@{$self->{entries}->[$i]}) { + # Moon and week should not adjust height + if ($entry->isa('Remind::PDF::Entry::moon') || + $entry->isa('Remind::PDF::Entry::week')) { + $entry->render($self, $cr, $settings, $box->[1], $i, $i, $box_height); + next; + } + + # An absolutely-positioned Pango markup should not adjust height + # either + if ($entry->isa('Remind::PDF::Entry::pango') && + defined($entry->{atx}) && defined($entry->{aty})) { + $entry->render($self, $cr, $settings, $box->[1], $i, $i, $box_height); + next; + } + + # Shade is done already + if ($entry->isa('Remind::PDF::Entry::shade')) { + next; + } + if ($done) { + $so_far += $settings->{border_size}; + } + $done = 1; + my $h2 = $entry->render($self, $cr, $settings, $so_far, $i, $i, $box_height); + $so_far += $h2; + } +} + +sub col_box_coordinates +{ + + my ($self, $so_far, $col, $height, $settings) = @_; + return (@{$self->{l2l_box}}); +} + + + +sub draw_lines +{ + my ($self, $cr, $settings) = @_; + + # Top horizonal line + $cr->move_to($settings->{margin_left}, $settings->{margin_top}); + $cr->line_to($settings->{width} - $settings->{margin_right}, $settings->{margin_top}); + $cr->stroke(); + + # Horizontal line below headings + $cr->move_to($settings->{margin_left}, $self->{heading_bottom_y} + $settings->{margin_top}); + $cr->line_to($settings->{width} - $settings->{margin_right}, $self->{heading_bottom_y} + $settings->{margin_top}); + $cr->stroke(); + + # Bottom horizontal line + $cr->move_to($settings->{margin_left}, $settings->{height} - $settings->{margin_bottom}); + $cr->line_to($settings->{width} - $settings->{margin_right}, $settings->{height} - $settings->{margin_bottom}); + $cr->stroke(); + + # Vertical lines + my $w = ($settings->{width} - $settings->{margin_left} - $settings->{margin_right})/7; + for (my $i=0; $i<=7; $i++) { + my $x = $settings->{margin_left} + ($i * $w); + $cr->move_to($x, $settings->{margin_top}); + $cr->line_to($x, $settings->{height} - $settings->{margin_bottom}); + $cr->stroke(); + } +} + +sub create_from_hash +{ + my ($class, $hash, $specials_accepted) = @_; + bless $hash, $class; + + my $filtered_entries = []; + my $date_to_index; + for (my $i=0; $i<7; $i++) { + $date_to_index->{$hash->{dates}[$i]->{date}} = $i; + } + + for (my $i=0; $i<7; $i++) { + $filtered_entries->[$i] = []; + } + foreach my $e (@{$hash->{entries}}) { + if ($hash->accept_special($e, $specials_accepted)) { + my $index = $date_to_index->{$e->{date}}; + push(@{$filtered_entries->[$index]}, Remind::PDF::Entry->new_from_hash($e)); + } + } + $hash->{entries} = $filtered_entries; + return $hash; +} + 1; diff --git a/src/calendar.c b/src/calendar.c index bdffa803..40248d69 100644 --- a/src/calendar.c +++ b/src/calendar.c @@ -274,6 +274,7 @@ static int ColToDay[7]; static int ColSpaces; static int DidAMonth; +static int DidAWeek; static int DidADay; static void ColorizeEntry(CalEntry const *e, int clamp); @@ -834,12 +835,33 @@ void ProduceCalendar(void) WriteIntermediateCalLine(); } - while (CalWeeks--) + DidAWeek = 0; + if (PsCal == PSCAL_LEVEL3) { + printf("[\n"); + } + while (CalWeeks--) { DoCalendarOneWeek(CalWeeks); + DidAWeek = 1; + } + if (PsCal == PSCAL_LEVEL3) { + printf("\n]\n"); + } return; } } +static void +SendTranslationTable(int pslevel) +{ + if (pslevel < PSCAL_LEVEL3) { + printf("# translations\n"); + } + DumpTranslationTable(stdout, 1); + if (pslevel < PSCAL_LEVEL3) { + printf("\n"); + } +} + /***************************************************************/ /* */ /* DoCalendarOneWeek */ @@ -870,9 +892,33 @@ static void DoCalendarOneWeek(int nleft) /* Output the entries */ /* If it's "Simple Calendar" format, do it simply... */ if (DoSimpleCalendar) { + if (PsCal == PSCAL_LEVEL3) { + if (DidAWeek) { + printf(",\n"); + } + printf("{\n\"caltype\":\"weekly\","); + if (!DidAWeek) { + printf("\"translations\":"); + SendTranslationTable(PsCal); + printf(","); + } + printf("\"dates\":["); + for (i=0; i<7; i++) { + if (i != 0) { + printf(","); + } + FromDSE(OrigDse+i-wd, &y, &m, &d); + printf("{\"dayname\":\"%s\",\"date\":\"%04d-%02d-%02d\",\"year\":%d,\"month\":\"%s\",\"day\":%d}", get_day_name((OrigDse+i-wd)%7),y, m+1, d, y, get_month_name(m), d); + } + printf("],\"entries\":["); + } + DidADay = 0; for (i=0; i<7; i++) { WriteSimpleEntries(i, OrigDse+i-wd); } + if (PsCal == PSCAL_LEVEL3) { + printf("\n]\n}"); + } return; } @@ -965,18 +1011,6 @@ static void DoCalendarOneWeek(int nleft) } } -static void -SendTranslationTable(int pslevel) -{ - if (pslevel < PSCAL_LEVEL3) { - printf("# translations\n"); - } - DumpTranslationTable(stdout, 1); - if (pslevel < PSCAL_LEVEL3) { - printf("\n"); - } -} - /***************************************************************/ /* */ /* DoSimpleCalendarOneMonth */ @@ -1030,6 +1064,7 @@ static void DoSimpleCalendarOneMonth(void) } printf("\n"); } else { + PrintJSONKeyPairString("caltype", "monthly"); PrintJSONKeyPairString("monthname", get_month_name(m)); PrintJSONKeyPairInt("year", y); PrintJSONKeyPairInt("daysinmonth", DaysInMonth(m, y)); diff --git a/src/init.c b/src/init.c index 3de72461..a41230ba 100644 --- a/src/init.c +++ b/src/init.c @@ -541,10 +541,14 @@ void InitRemind(int argc, char const *argv[]) DoSimpleCalendar = 1; IgnoreOnce = 1; PsCal = PSCAL_LEVEL1; + weeks = 0; while (*arg == 'a' || *arg == 'A' || *arg == 'q' || *arg == 'Q' || + *arg == '+' || *arg == 'p' || *arg == 'P') { - if (*arg == 'a' || *arg == 'A') { + if (*arg == '+') { + weeks = 1; + } else if (*arg == 'a' || *arg == 'A') { DoSimpleCalDelta = 1; } else if (*arg == 'p' || *arg == 'P') { /* JSON interchange formats always include @@ -560,8 +564,14 @@ void InitRemind(int argc, char const *argv[]) } arg++; } - PARSENUM(CalMonths, arg); - if (!CalMonths) CalMonths = 1; + if (weeks) { + PARSENUM(CalWeeks, arg); + if (!CalWeeks) CalWeeks = 1; + PsCal = PSCAL_LEVEL3; + } else { + PARSENUM(CalMonths, arg); + if (!CalMonths) CalMonths = 1; + } break; case 'l': diff --git a/tests/test-rem b/tests/test-rem index 703f95cb..bf8fe1b8 100644 --- a/tests/test-rem +++ b/tests/test-rem @@ -198,6 +198,17 @@ REM 4 MSG Normal SET $DefaultColor "256 0 0" EOF +# Test default color with weekly calendar +../src/remind -pq+ - 1 Jan 2012 9:00 <<'EOF' >> ../tests/test.out 2>&1 +REM 2 MSG Normal +SET $DefaultColor "255 0 0" +REM 3 MSG %"Red%" on the calendar! +SET $DefaultColor "-1 -1 -1" +REM 4 MSG Normal +# Should give an error +SET $DefaultColor "256 0 0" +EOF + # Test stdout ../src/remind - 1 jan 2012 <<'EOF' >> ../tests/test.out 2>&1 BANNER % diff --git a/tests/test.cmp b/tests/test.cmp index fcd56546..fe158c2b 100644 --- a/tests/test.cmp +++ b/tests/test.cmp @@ -22406,13 +22406,29 @@ February 29 -stdin-(7): Number too high [ { -"translations":{"LANGID":"en"},"monthname":"January","year":2012,"daysinmonth":31,"firstwkday":0,"mondayfirst":0,"daynames":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"prevmonthname":"December","daysinprevmonth":31,"prevmonthyear":2011,"nextmonthname":"February","daysinnextmonth":29,"nextmonthyear":2012,"entries":[ +"translations":{"LANGID":"en"},"caltype":"monthly","monthname":"January","year":2012,"daysinmonth":31,"firstwkday":0,"mondayfirst":0,"daynames":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"prevmonthname":"December","daysinprevmonth":31,"prevmonthyear":2011,"nextmonthname":"February","daysinnextmonth":29,"nextmonthyear":2012,"entries":[ {"date":"2012-01-02","filename":"-","lineno":1,"d":2,"priority":5000,"body":"Normal"}, {"date":"2012-01-03","filename":"-","lineno":3,"passthru":"COLOR","d":3,"priority":5000,"r":255,"g":0,"b":0,"rawbody":"%\"Red%\" on the calendar!","calendar_body":"Red","plain_body":"Red on the calendar!","body":"255 0 0 %\"Red%\" on the calendar!"}, {"date":"2012-01-04","filename":"-","lineno":5,"d":4,"priority":5000,"body":"Normal"} ] } ] +-stdin-(7): Number too high +-stdin-(7): Number too high +-stdin-(7): Number too high +-stdin-(7): Number too high +-stdin-(7): Number too high +-stdin-(7): Number too high +-stdin-(7): Number too high +-stdin-(7): Number too high +[ +{ +"caltype":"weekly","translations":{"LANGID":"en"},"dates":[{"dayname":"Sunday","date":"2012-01-01","year":2012,"month":"January","day":1},{"dayname":"Monday","date":"2012-01-02","year":2012,"month":"January","day":2},{"dayname":"Tuesday","date":"2012-01-03","year":2012,"month":"January","day":3},{"dayname":"Wednesday","date":"2012-01-04","year":2012,"month":"January","day":4},{"dayname":"Thursday","date":"2012-01-05","year":2012,"month":"January","day":5},{"dayname":"Friday","date":"2012-01-06","year":2012,"month":"January","day":6},{"dayname":"Saturday","date":"2012-01-07","year":2012,"month":"January","day":7}],"entries":[{"date":"2012-01-02","d":2,"priority":5000,"body":"Normal"}, +{"date":"2012-01-03","passthru":"COLOR","d":3,"priority":5000,"r":255,"g":0,"b":0,"rawbody":"%\"Red%\" on the calendar!","calendar_body":"Red","plain_body":"Red on the calendar!","body":"255 0 0 %\"Red%\" on the calendar!"}, +{"date":"2012-01-04","d":4,"priority":5000,"body":"Normal"} +] +} +] STDOUT is a: FILE STDOUT is a: PIPE +----------------------------------------------------------------------------+