#!/usr/bin/perl
#
# Technical Report Management Tool
#
# Markus Kuhn <https://www.cl.cam.ac.uk/~mgk25/>

use 5.016;
use warnings;
no warnings 'uninitialized';
use English;
use open ':utf8';
use utf8;                     # this file is UTF-8 encoded
use Encode;
use charnames ':full';
use POSIX qw(locale_h);
use Fcntl ':mode';            # for S_IROTH, etc.
use FindBin qw($RealBin);     # find directory where this file is located ...
use lib $RealBin;             # ... and add it to @INC
use TechReports;              # this also includes package/class TechReport
BEGIN { TechReport->import( qw( expand_date expand_date_us ) ); }
use UniConv qw(utf8_to_LaTeX utf8_to_ascii);
use CLWeb qw(utf8_to_sgml utf8_to_sgmlatt nobreak);
use CLMail qw(send_email);

binmode STDOUT, ":utf8";
binmode STDERR, ":utf8";

my $dpi         = 250;
my $keepsource  = 0;
my $pdfconcatenator = 'pdftk';

my $usage = <<End;
TR-Tool --  Technical Report Management Tool -- Markus.Kuhn\@cl.cam.ac.uk

Usage: tr-tool {commands}

Commands:   (square brackets indicate an optional part)

  txtindex[:<file>]     Output plain text list of all reports

  htmlindex[:<file>]    Output simple web list of all reports

  htmltable[:<file>]    Output HTML table of all reports

  bibindex[:<file>]     Output BibTeX entries for all reports

  rfcindex[:<file>]     Output RFC 1807 records for all reports

  dcindex[:<file>]      Output Dublin Core XML records for all reports

  oaiindex[:<file>]     Output OAI Static Repository for all reports

  phdindex[:<file>]     Output plain text list of all Cambridge PhDs

  marc<nr>[:<file>]     Output a MARC21 record for the report

  dblp[:<file>]         Output a DBLP XML records for all reports

  clep<nrs>[:<file>]    Output XML records for bulk import into the
                        Computer Lab\'s GNU EPrints publications archive

  cdtr<nrs>             Send announcement for techreport(s) to the
                        comp.doc.techreports newsgroup

  rss[:<file>]          Produce an RSS file of recent reports

  abstracts             Produce HTML files with abstracts for each report

  abstracts+            Update HTML files with abstracts for changed reports

  timestamps            Create or update tr-database-times.txt file with
                        record modification timestamps

  checktex              Output check.tex database proof-reading list

  pdfindex              Output LaTeX formatted tr-index.pdf

  pstitle<nr>[:<file>]  Produce tite page for report number <nr>
                        (default output filename: tr-title-<nr>.ps)

  pdftitle<nr>[:<file>] Produce tite page for report number <nr>
                        (default output filename: tr-title-<nr>.pdf)

  pdftitle[:<file>]     Produce tite pages for all reports
                        (default output filename: tr-titles.pdf)

  prefix<nr> <file>     Prefix the provided PDF/PostScript/DVI file with the
                        title page for report number <nr> and write
                        the result into <code>-<nr>.pdf

                        When prefixing a title page to a PDF file, there
                        is a choice of methods available to
                        concatenate the pages. Select one by placing
                        one of the following options before
                        prefix<nr>. (default: $pdfconcatenator)

                        pdftk     Use the pdftk 'cat' operation, which
                                  should cause least change to the PDF.

                        gs        Use Ghostscript to distill a PDF title
                                  page together with the PDF submission.
                                  This will partially interpret and
                                  recompress the PDF submission and can
                                  significantly affect the file size
                                  (either way) and may fix and/or
                                  add problems.

                        gsps      Like gs, but prefix the PDF with a
                                  PostScript titlepage, which may
                                  include setdistillerparams. This is
                                  closest to what happens with a
                                  PostScript submission, e.g. it will
                                  resample photos of excessive
                                  resolution.

                        acrobat   Prepare a PDF title page and print
                                  instructions on how to manually concatenate
                                  the submitted PDF to it using Adobe Acrobat.

                        pdflatex  Using pdflatex (pdfpages package) to
                                  concatenate is not recommended, as
                                  this technique strips hyperlinks.

                        pdfunite  Using pdfunite (poppler-utils package)
                                  is not recommended, as it breaks
                                  hyperlinks and strips pdfinfo metadata.

  stats[:filename]      Print out some simple statistics on the collection

  savedb[:filename]     Print out the database

  nagmail               Send reminder emails to authors of late reports
                        with to-appear deadline

  dpi<nr>               Set the ps2pdf downsampling resolution (default: $dpi)

  keepsource            preserve intermediate source files (*.tex, *-b.html)

Parameter <nrs> can be a range of tech-report numbers, as in "-5,8-10,12,20-".
An empty string for <nrs> selects all reports in the database.

End

# reformat text to 72 cols with indentation
sub reformat {
    my ($text, $indent, $item) = @_;
    my $out;
    my $line = '';
    my $maxlen = 72;
    my ($paragraph, $word);
    my $prefix = $item;
    $prefix = $indent if (!defined $item);
    foreach $paragraph (split(/\n/, $text)) {
	foreach $word (split(/[ \t]+/, $paragraph)) {
	    if (length($prefix) + length($line) + 1 + length($word)
		> $maxlen) {
		if (defined $out) { $out .= "\n"; } else { $out = ''; }
		$out .= $prefix . $line;
		$line = '';
		$prefix = $indent;
	    }
	    $line .= ($line ? ' ' : '') . $word;
	}
	if (defined $out) { $out .= "\n"; } else { $out = ''; }
	$out .= $prefix . $line;
	$line = '';
	$prefix = $indent;
    }
    return $out;
}

# Substitute all line separators in a string with " : " or
# -- if preceeded by punctuation or followed by parenthesis with " "
sub nolinesep {
    my ($s) = @_;

    $s =~ s/([\p{Pd}\.:,;?!])\N{LINE SEPARATOR}/$1 /g;
    $s =~ s/\N{LINE SEPARATOR}([\(])/ $1/g;
    $s =~ s/\N{LINE SEPARATOR}/ : /g;

    return $s;
}


# Substitute all line separators in a string with "<br>"
sub linesep_html {
    my ($s) = @_;

    $s =~ s/\N{LINE SEPARATOR}/<br>/g;

    return $s;
}

# output HTML meta elements
sub html_meta {
    my @h;
    while (@_) {
	my $name = shift;
	my $content = shift;
	push @h, ('<meta name="' , utf8_to_sgmlatt($name),
		  '" content="', utf8_to_sgmlatt($content), "\">\n")
	    if defined $content;
    }
    return @h;
}

# Quote a string for the shell (surround with "..." and
# escape $`"\ with backslash)
sub shellquote {
    my ($s) = @_;

    $s =~ s/([\$\`\"\\])/\\$1/g;
    $s = '"' . $s . '"';

    return $s;
}

# Call a shell command.
# This is just a wrapper around system(), with logging and failure checking.
# If there is only one scalar argument: if it contains shell metacharacters,
# this will invoke “/bin/sh -c” (without escaping metacharacters!), otherwise
# it is split into words and passed to execvp.
sub command {
    my $cmd = $_[0];
    $cmd =~ s/\s+.*$//;
    my $line = join(' ', @_);
    print STDERR "Calling “$line” ...\n";
    my $r = system @_;
    return unless $r;
    die("Failed to execute “$cmd”: $!\n") if $r == -1;
    die("Command “$cmd” died with signal ", $? & 127 ,"\n") if $? & 127;
    die("Command “$cmd” failed, return value ", $? >> 8, "\n");
}

# Convert fallback notations allowed in the input database file into UTF-8
sub db_to_utf8 {
    my ($s) = @_;

    $s =~ s/\\\\/\N{LINE SEPARATOR}/g;
    $s =~ s/\\_/\N{PARAGRAPH SEPARATOR}/g;

    return $s;
}

sub utf8_to_html_paragraphs {
    my ($s) = @_;

    if (defined $s) {
	$s =~ s/\N{PARAGRAPH SEPARATOR}/\n\n/g;
	$s = '<p>' . join("\n\n<p>", map { reformat($_) }
			  utf8_to_sgml(split(/\n\s*\n/, $s)));
	$s =~ s/\N{LINE SEPARATOR}/<br>\n/g;
	return $s;
    }
}

# Inserts into a LaTeX title string the breakpoint marker at positions
# that are better suited for a line break than others.
sub break_title {
    my ($title) = @_;

    my $break1 = '{\penalty-12}'; # weakest breakpoint
    my $break2 = '{\penalty-25}'; # middlestrong breakpoint
    my $break3 = '{\penalty-40}'; # strongest breakpoint

    # break preferably after punctuation
    $title =~ s/(\.)(\s)/$1$break3$2/g;
    $title =~ s/(\?)(\s)/$1$break3$2/g;
    $title =~  s/(!)(\s)/$1$break3$2/g;
    $title =~  s/(:)(\s)/$1$break3$2/g;
    $title =~ s/(\s)(\()/$1$break3$2/g;
    $title =~ s/(\))(\s)/$1$break3$2/g;
    $title =~  s/(;)(\s)/$1$break2$2/g;
    $title =~  s/(,)(\s)/$1$break2$2/g;
    $title =~ s/(--)(\s)/$1$break2$2/g;
    # or near common closed-class words
    for my $w (qw(of in into to with by for on through
	       a an the some each any
	       this that
	       I you he she it they
               me my your his her their
	       and or not none
               has have had be are is was as
	       but from at if then else about would more)) {
	$title =~ s/(\s)($w)(\s)/$1$break1$2$break1$3/g
    }

    return $title;
}

# encode pdfmark key/value pairs
sub pdfinfo {
    my @l;

    while (@_) {
	my $keyword = shift;
	my $value = shift;

	next unless defined $value;
	# pdfmark expects unspecified character set, therefore use ASCII only
	$value = utf8_to_ascii($value);
	# escape all chars not allowed in PS strings, namely backslash and
	# unmatched parentheses
	if (!($value =~ /^[^()\\]*(\([^()\\]*\))*[^()\\]*$/)) {
	    $value =~ s/([()\\])/\\$1/g;
	}
	push @l, "/$keyword (" . $value . ")";
    }
    return join(' ', @l);
}

# Join list elements into a single string separated by commas, but
# replace the last comma with " and ".
sub join_comma_and {
    my @l = @_;

    return shift @l unless @l > 1;
    my $last = pop @l;
    return join(', ', @l) . ' and ' . $last;
}

# encode the Author and Title field of pdfmark for a report
sub pdfauthortitle {
    my ($tr) = @_;

    return pdfinfo('Author' => join(', ', map {$_->fullname} $tr->authors),
		   'Title'  => $tr->titleline);
}

# Convert information from $notes{$nr} into the note to be printed
# on the first cover page
sub frontnote {
    my ($tr)  = @_;
    my @frontnotes;

    my @contributions = $tr->contributions;
    my @c;
    while (@contributions) {
        my $contribution = shift @contributions;
        my @contributors = @{shift @contributions};
	push @c, $contribution . ' by ' .
            join_comma_and(map { nobreak $_->fullname } @contributors);
    }
    push @frontnotes, ucfirst(join_comma_and(@c)) . '.' if @c;

    push @frontnotes, $tr->{front} if $tr->{front};

    return join("\n\n", @frontnotes);
}

# Convert information from $notes{$nr} into the note to be printed
# on the back of the cover page
sub backnote {
    my ($tr)  = @_;
    my @backnotes;

    if (my ($degree, $college, $submitted) = $tr->thesis) {
	my $backnote = "This technical report is based on a " .
	    "dissertation submitted ";
	$backnote .= expand_date($submitted) . ' ' if $submitted;
        if (@{$tr->{authors}} == 1) {
            $backnote .= "by the author";
        } elsif (@{$tr->{authors}} > 1) {
            # if a dissertation has more than one author,
            # assume the others were supervisors
            $backnote .= "by the first author";
        } else {
            die("no author");
        }
        $backnote .= " for the degree of ";
	if ($degree =~ /^o?phd$/) {
	    $backnote .= "Doctor of Philosophy";
	} elsif ($degree eq 'mphil-cstit') {
	    $backnote .= "Master of Philosophy " .
		"(Computer Speech, Text and Internet Technology)";
	} elsif ($degree eq 'mphil-acs') {
	    $backnote .= "Master of Philosophy (Advanced Computer Science)";
	} elsif ($degree eq 'cst-part2') {
	    $backnote .= "Bachelor of Arts (Computer Science Tripos)";
	} elsif ($degree eq 'cst-part3') {
	    $backnote .= "Master of Engineering (Computer Science Tripos)";
	} else {
	    die("TR-$tr->{nr}:unknown degree '$degree'!\n");
	}
	if ($degree eq 'ophd') {
	    if ($college) {
		$backnote .= " to ";
		$backnote .= "the " if $college =~ / of /;
		$backnote .= $college;
	    }
	} else {
	    $backnote .= " to the University of Cambridge";
	    $backnote .= ", $college" if $college;
	}
	$backnote .= '.';
	push @backnotes, $backnote;
    }
    if (exists $tr->{'colour'}) {
	push @backnotes, "Some figures in this document are best " .
	    "viewed in colour. If you received a black-and-white " .
	    "copy, please consult the online version if necessary.";
    }
    if (exists $tr->{'updated'}) {
	my @updates = split(/,/, $tr->{'updated'});
	my $backnote .=
	    "This version of the report incorporates minor changes " .
	    "to the ";
	$backnote .= expand_date($tr->{date}) . ' ' if $tr->{date};
	$backnote .= "original, which were released ";
        $backnote .= join_comma_and(map expand_date($_), @updates) . '.';
	push @backnotes, $backnote;
    }
    push @backnotes, $tr->{back} if $tr->{back};

    return join("\n\n", @backnotes);
}

# Convert information from $notes{$nr} into the notes to be printed
# on any of the cover pages
sub anynotes {
    my ($tr) = @_;

    return join("\n\n", grep {$_} (frontnote($tr), backnote($tr)));
}

# Read publication date from $date{$nr} and if that is empty, try to use
# information from $notes{$nr} instead. This is to handle old PhDs where
# we have a submission date but no publication date.
sub anydate {
    my ($tr)  = @_;

    return $tr->{date} if defined $tr->{date};
    my (undef,undef,$submission) = $tr->thesis;
    return $submission if $submission;
    return $tr->{'copyright-year'} if $tr->{'copyright-year'};
    return undef;
}

# Produce a LaTeX file containing the standard title page and its
# backside for the specified list of reports (@trs). The
# @trs list can include not only TechReport objects, but also strings
# with special instructions for pdflatex:
#
#   - "*.pdf" includes all pages of the named PDF file at that place
#   - "*.pdf/3-" etc. includes only some pages
#   - "pdfinfo ..." includes pdflatex instructions for setting pdfinfo
#     dictionary attributes.
sub latextitle {
    my ($filename, @trs) = @_;

    open(LATEXSRC, ">$filename.tex")
	|| die("Can't write to LaTeX file '$filename.tex':\n$!\n");
    print LATEXSRC "% AUTOMATICALLY GENERATED TEMPORARY FILE\n";
    print LATEXSRC "\\documentclass[12pt,twoside,a4paper]{report}\n";
    print LATEXSRC "\\usepackage{tr-title}\n";
    print LATEXSRC "\\usepackage{pdfpages}\n"
	if grep(/^(.*)\.pdf(\/(\d*-\d*))?$/, @trs);
    print LATEXSRC "\\begin{document}\n";
    foreach my $tr (@trs) {
	if ($tr =~ /^(.*)\.pdf(\/(\d*-\d*))?$/) {
	    my $filename = $1;
	    my $pages = $3;
	    $pages = '-' unless length($pages);
	    # special instruction: include the named PDF file at this place
	    # (Warning: this will only preserve the /Contents of the
	    # transferred pages, not the /Annots, hence hyperlinks
	    # will disappear.
	    # http://www.tug.org/pipermail/pdftex/2003-April/003945.html)
	    print LATEXSRC "\\includepdf[pages=$pages,noautoscale,fitpaper]{$filename.pdf}\n";
	    next;
	} elsif ($tr =~ /^pdfinfo\s+(.*)$/s) {
	    # special instruction: include pdfinfo data
	    print LATEXSRC "\\pdfinfo{\n$1}\n";
	    next;
	} elsif (!ref $tr) {
	    die("Unexpected special string '$tr'");
	}
	my $db = $tr->{db};
	print LATEXSRC '% ', $tr->code, "\n";
	print LATEXSRC "\\begin{trtitlepage}\n";
	print LATEXSRC ("\\newcommand{\\trtitle}{" .
			break_title(utf8_to_LaTeX($tr->{title}, '1.5ex')) .
			"}\n");
	print LATEXSRC "\\newcommand{\\trauthors}{";
        if ($tr->authors > 20) {
            print LATEXSRC "\\large "
        } elsif ($tr->authors > 15) {
            print LATEXSRC "\\Large "
        };
	print LATEXSRC "Edited by " if $tr->editors;
	print LATEXSRC (
	    utf8_to_LaTeX(join(', ', map {nobreak $_->fullname} $tr->authors))
	    . "}\n");
	print LATEXSRC ("\\newcommand{\\trnumber}{$tr->{nr}}\n");
	print LATEXSRC ("\\newcommand{\\trdate}{" .
			expand_date(anydate($tr)) . "}\n");
	print LATEXSRC ("\\newcommand{\\tsdepartment}{" .
			utf8_to_LaTeX($db->{department}) . "}\n");
	print LATEXSRC ("\\newcommand{\\tscode}{$db->{code}}\n");
	print LATEXSRC ("\\newcommand{\\tsissn}{$db->{issn}}\n")
	    if exists $db->{issn};
	print LATEXSRC ("\\newcommand{\\tsaddress}{" .
			utf8_to_LaTeX($db->{address}) . "}\n");
	print LATEXSRC ("\\newcommand{\\tsdepturl}{$db->{depturl}}\n");
	print LATEXSRC ("\\newcommand{\\trfrontnote}{" .
			utf8_to_LaTeX(frontnote($tr)) . "}\n");
	print LATEXSRC "\\maketrtitle\n";

        my @backpage;
        unless ($tr->editors) {
            my $cyear = $tr->{'copyright-year'} // substr(anydate($tr), 0, 4);
            my @owners = map {$_->fullname} $tr->authors;
            @owners = split(/, /, $tr->{'copyright-owner'})
                if $tr->{'copyright-owner'};
            push @owners, split(/, /, $tr->{'add-copyright-owner'})
                if $tr->{'add-copyright-owner'};
	    push @backpage, "\\copyright~$cyear\\ ",
                utf8_to_LaTeX(join(', ', @owners)), "\n";
	}

        my ($licence, $lurl, $lcode) = $tr->licence;
        if ($licence) {
            push @backpage, "\nThe following report is licensed under a ",
                $licence, " licence:\n\n",
                "\\quad\\href{$lurl}{\\textit{", utf8_to_LaTeX($lurl), "}}\n";
            if ($lcode eq 'by' || $lcode eq 'by-sa') {
                push @backpage, "\nUNIVERSITY OF CAMBRIDGE and the Coat of Arms are registered trade marks of The Chancellor, Masters, and Scholars of the University of Cambridge (“University Marks”). For the avoidance of doubt, the University Marks are not included in this Creative Commons licence.\n";
            }
        }

	my $backnote = backnote($tr);
	push @backpage, ("\n\\begin{backnote}",
			utf8_to_LaTeX($backnote), "\\end{backnote}\n")
	    if $backnote;

        push @backpage, series_backnote_latex($db, $tr);

        my $backpagelength = length(join('', @backpage));
        #say $backpagelength;
        # adjust column width based on text length
        my $width;
        if ($backpagelength > 3000) {
            $width = "170mm";
        } elsif ($backpagelength > 2000) {
            $width = "150mm";
        } else {
            $width = "112mm";
        }
        print LATEXSRC "\\setlength{\\trbackpagewidth}{$width}\n";
	print LATEXSRC "\\begin{trbackpage}\n";
        print LATEXSRC @backpage;
	print LATEXSRC "\\end{trbackpage}\n";
	print LATEXSRC "\\end{trtitlepage}\n";
    }
    print LATEXSRC "\\end{document}\n";
    close LATEXSRC;
}

# additional notes about the series for the LaTeX backpage
sub series_backnote_latex {
    my ($db, $tr) = @_;
    my @latex;

    if (exists $db->{reporturl}) {
        push @latex,
            ("\nTechnical reports published by the University of Cambridge\n",
             "\\tsdepartment\\ are freely available via the Internet:\n\n",
             "\\quad\\href{$db->{reporturl}}{\\textit{", $db->{reporturl}, "}}\n");
    }
    push @latex, ("\nSeries editor: ", utf8_to_LaTeX($db->{editor}), "\n")
        if exists $db->{editor};

    my @ids;
    push @ids, "ISSN&" . utf8_to_LaTeX($db->{issn})
        if exists $db->{issn};
    push @ids, "ISBN&" . utf8_to_LaTeX($tr->{isbn})
        if $tr && exists $tr->{isbn};
    push @ids, "DOI&\\href{https://doi.org/" . utf8_to_LaTeX($tr->doi) .
        "}{\\textit{https://doi.org/" . utf8_to_LaTeX($tr->doi) . "}}"
        if $tr && $tr->doi;
    push @latex, "\n\\begin{tabular}{\@{}l\@{ }l}\n", join("\\\\\n", @ids), "\n\\end{tabular}\n" if @ids;

    return @latex;
}

# Produce a PostScript file containing the standard title page and
# its backside for the specified list of reports
sub pstitle {
    my ($filename, @trs) = @_;

    unlink "$filename.ps"; # because dvips will append to this file

    latextitle($filename, @trs);

    if (@trs == 1) {
	# set PDF document information fields
	# when we print only a single title page
	latex_to_ps($filename, pdfauthortitle($trs[0]));
    } else {
	latex_to_ps($filename);
    }
}

# Produce a PDF file containing the standard title page and its
# backside for the specified list of reports (@trs). The
# @trs also can contain special instructions, see comment of
# latextitle() for details.
sub pdftitle {
    my ($filename, @trs) = @_;

    if (@trs == 1) {
	# set PDF document information fields
	# when we print only a single title page
	push @trs, ('pdfinfo ' . pdfauthortitle($trs[0]));
    }
    latextitle($filename, @trs);
    latex_to_pdf($filename);
}

sub latex_to_ps {
    my ($filename, $pdfinfo) = @_;

    print STDERR "Calling “latex $filename” ...\n";
    `latex -interaction=nonstopmode $filename`;
    $? && die("LaTeX failed, check '$filename.log' for details!\n");
    unlink "$filename.tex" unless $keepsource;
    unlink "$filename.log", "$filename.aux";
    print STDERR "Preparing PostScript header ...\n";
    open(HDR, ">$filename-hdr.ps")
	|| die("Can't open temporary file '$filename-hdr.ps':\n$!\n");
    if ($pdfinfo) {
	print HDR ("/pdfmark where " .
		   "{pop} {userdict /pdfmark /cleartomark load put} ifelse\n");
	print HDR ("[\n$pdfinfo/DOCINFO pdfmark\n");
    }
    # set a number of PDF distiller parameters according to
    # https://web.archive.org/web/20160509154901/http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/distillerparameters.pdf
    # http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/pdfmark_reference.pdf
    # file:///usr/share/doc/ghostscript/VectorDevices.htm
    print HDR <<END;
<< /PageSize [595 842] >> setpagedevice
<< /ASCII85EncodePages false
   /AutoRotatePages /None
   /CompressPages true
   /DownsampleColorImages true
   /ColorImageDownsampleType /Bicubic
   /ColorImageResolution $dpi
   /ColorACSImageDict << /QFactor 0.55
                         /HSamples [2 1 1 2]
                         /VSamples [2 1 1 2]
                         /Blend 1 >>
   /GrayACSImageDict  << /QFactor 0.55
                         /HSamples [2 1 1 2]
                         /VSamples [2 1 1 2]
                         /Blend 1 >>
   /DownsampleGrayImages true
   /GrayImageDownsampleType /Bicubic
   /GrayImageResolution $dpi
   /DownsampleMonoImages false
   /MonoImageFilter /CCITTFaxEncode
   /SubsetFonts true
   /MaxSubsetPct 100
>> setdistillerparams
END
    close HDR;
    command('dvips', '-q', '-Ppdf', '-G0', '-h',
	     "$filename-hdr.ps", "$filename.dvi");
    unlink "$filename.dvi", "$filename-hdr.ps" unless $keepsource;
}

sub latex_to_pdf {
    my ($filename) = @_;

    print STDERR "Calling “pdflatex $filename” ...\n";
    `pdflatex -interaction=nonstopmode $filename`;
    if ($?) {
	unlink "$filename.pdf";
	die("PDFLaTeX failed, check '$filename.log' for details!\n");
    }
    unlink "$filename.tex" unless $keepsource;
    unlink "$filename.log", "$filename.aux";
}

sub ps2pdf {
    my ($psfiles, $pdffile) = @_;

    if (!$pdffile) {
	$pdffile = $psfiles;
	$pdffile =~ s/\.ps$//i;
	$pdffile .= '.pdf';
    }
    my $cmd = "gs -q -dSAFER -o $pdffile -sDEVICE=pdfwrite -f $psfiles";
    command($cmd);
}

# Format a BibTeX entry for a report
sub bibtex_entry {
    my ($tr, %opt) = @_;
    my $db = $tr->{db};
    my @ss = ();
    my $type = 'TechReport';
    my $author = 'author';
    if ($tr->editors) {
	$type = 'Proceedings';
	$author = 'editor';
    }
    push @ss, "\@${type}{" . $tr->code . ",\n";
    if ($tr->{authors}) {
	my $a = join(' and ', map { $_->surnamefirst } $tr->authors);
	push @ss, reformat(utf8_to_LaTeX($a),
			   "          	  ",
			   "  $author =	 {"), "},\n";
    }
    if ($tr->{title}) {
	push @ss, reformat(utf8_to_LaTeX(nolinesep($tr->{title})),
			   "         	   ",
			   "  title = 	 {{"), "}},\n";
    }
    push @ss, "  year = 	 $1,\n"
	if ($tr->{date} =~ /^(\d{4})/);
    push @ss, "  month = 	 " .
	('???', 'jan', 'feb', 'mar', 'apr', 'may', 'jun',
	 'jul', 'aug', 'sep', 'oct', 'nov', 'dec')[$1] . ",\n"
	 if ($tr->{date} =~ /^\d{4}-(\d{2})/);
    my $online = $tr->url_file;
    push @ss, "  url = 	 {$online},\n" if $online;
    push @ss, "  institution =  {University of Cambridge, " .
	"$db->{department}},\n";
    if ($db->{address} && !$online) {
	$a = $db->{address};
	$a =~ s/\N{LINE SEPARATOR}/, /g;
	push @ss, reformat(utf8_to_LaTeX($a),
			   "          	  ",
			   "  address =	 {"), "},\n";
    }

    my $doi = $tr->doi;
    if ($doi && $opt{doi}) {
        $doi =~ s/_/\\{_\}/g;   # https://www.bibtex.com/f/doi-field/
        push @ss, "  doi = 	 {$doi},\n";
    }
    push @ss, "  issn = 	 {$db->{issn}},\n"
	if $db->{issn} && $opt{issn};
    push @ss, reformat(utf8_to_LaTeX($tr->{abstract}),
		       "            	  ",
		       "  abstract = 	 {"), "},\n"
	if $tr->{abstract} && $opt{abstract};
    push @ss, "  number = 	 {".$tr->code."}\n";
    push @ss, "}\n";

    return join('', @ss);
}


sub htmltable {
    my ($trs) = @_;

    my @t = (
	"<table class=tr>\n",
	"<tr>\n",
	"  <th class=nr>No</th>\n",
	"  <th class=title>Title</th>",
	"  <th class=authors>Authors</th>",
	"  <th class=date>Date</th>",
	"  <th class=notes>Notes</th>",
	"  <th class=download>Download</th>",
	"</tr>\n");
    my %repeat;
    for my $tr (@{$trs}) {
	if ($tr->isa('TRAuthor')) {
	    # insert an author heading instead
	    push @t, "<tr class=author";
	    push @t, " id='$tr->{crsid}'" if $tr->{crsid};
	    push @t, "><td colspan=6>",$tr->html({surnamefirst=>1}),"</tr>\n";
	    next;
	}
	push @t, $repeat{$tr->{nr}} ? "<tr>\n" : "<tr id=$tr->{nr}>\n";
	$repeat{$tr->{nr}}++;
        my $date = $tr->{date};
        unless ($date) {
            my $year = $tr->year;
            $date = "($year)" if $year;
        }
	push @t, (
	    "  <td class=nr>$tr->{nr}</td>\n",
	    "  <td class=title>", $tr->html_abs, "</td>\n",
	    "  <td class=authors>",
	    $tr->html_authors({ maxauthors => 3 }), "</td>\n",
	    "  <td class=date>", $date, "</td>\n",
	    "  <td class=notes>");
	my @notes;
	push @notes, 'PhD'   if exists $tr->{phd} || exists $tr->{ophd};
	push @notes, 'MPhil' if grep(/^mphil/, @{$tr->{_notes}});
	push @notes, 'BA'    if exists $tr->{'cst-part2'};
	push @notes, 'MEng'  if exists $tr->{'cst-part3'};
	push @notes, "$tr->{pages} p" if $tr->{pages};
	push @notes, "colour" if exists $tr->{colour};
	push @t, join('<br />', @notes), "</td>\n";
	push @t, '  <td class=download>';
	if (exists $tr->{'to-appear'}) {
	    push @t, 'to appear';
	} else {
	    push @t, $tr->downloads;
	}
	push @t, ("</td>\n",
		  "</tr>\n");
    }
    push @t, "</table>\n";

    return @t;
}

my $dbfile = 'tr-database.txt';
my $dbtimesfile = 'tr-database-times.txt';
my $absfile = 'tr-abstracts.txt';
my $tmpdir = '/var/tmp';

setlocale(LC_COLLATE, "en_GB.UTF-8");  # for sorting authors’ names

die $usage unless @ARGV;

# Read-in database
map { $_ = "$RealBin/$_" } $dbfile, $dbtimesfile, $absfile
    unless -e $dbfile;
my $db = TechReports->load($dbfile, $absfile);
$db->load_timestamps($dbtimesfile) if -r $dbtimesfile;

# prepare various auxiliary data elements
# today's date (YYYY-MM-DD)
my $today = POSIX::strftime('%Y-%m-%d', localtime(time));

# check whether we have an abstract for each report
my @missing_abstracts = grep { $_->url_file && !$_->{abstract} } $db->trs;
if (@missing_abstracts) {
    warn("Missing abstract for ",
	 join(', ', map { "TR-$_->{nr}" } @missing_abstracts), "\n");
}
# check whether we have a CRSID for at least one author
my @missing_crsid = ( grep { !grep { exists $_->{'crsid'} } $_->authors }
		      grep { $_->{'date'} ge 2016 } $db->trs );
if (@missing_crsid) {
    warn("At least one author of each recent TR should have a CRSID.\n",
         "No CRSID for ",
	 join(', ', map { "TR-$_->{nr}" } @missing_crsid), "\n");
}

# process command line arguments
while ($_ = shift(@ARGV)) {
    if (/^timestamps$/) {
	$db->save_timestamps($dbtimesfile);
    } elsif (/^savedb(:(.+))?$/) {
	my $fn = $2 ? $2 : '-';
	$db->save($fn);
    } elsif (/^abstracts(\+?)([\d,-]*)$/) {
	my $update = $1 eq '+';
	my $range = $2;
	my @generated_html;
	for my $tr ($db->range($range)) {
	    my $trno = $tr->code;
	    if ($update) {
		# skip processing for this report if existing HTML
		# file is younger than the last record change
		if (-r "$trno.html" && ((stat(_))[2] & S_IROTH)) {
		    my $mtime = (stat(_))[9];
		    next if $tr->{_lastmod} < $mtime;
		}
	    }
	    my @t;
            push @t, '<h1>',linesep_html(utf8_to_sgml($tr->{title})),"</h1>\n";

	    push @t, ('<p><b>', ($tr->editors ? 'Edited by ' : ''),
		      $tr->html_authors, "</b>\n");

	    my @ss = ();
            #push @ss, "Technical report $trno";
	    push @ss, expand_date($tr->{date}) if $tr->{date};
	    push @ss, "$tr->{pages} pages" if $tr->{pages};
	    push @t, '<p>' . reformat(join(', ', @ss)) . "\n\n" if @ss;
	    push @t, utf8_to_html_paragraphs(frontnote($tr));
	    push @t, utf8_to_html_paragraphs(backnote($tr));
            my @ids;
            my $doi = $tr->doi;
            if ($tr->{isbn}) {
                push @ids, qq{<tr><td>ISBN<td>$tr->{isbn}\n};
            }
            if ($doi) {
                push @ids, qq{<tr><td>DOI<td><a href="https://doi.org/$doi">https://doi.org/$doi</a>\n};
            }
            push @t, "<table class=plain>\n", @ids, "\n</table>\n" if @ids;
	    if ($tr->{abstract}) {
		push @t, "\n<h2 id=abstract>Abstract</h2>\n\n";
		push @t, utf8_to_html_paragraphs($tr->{abstract});
	    }

	    push @t, "\n<h2 id=fulltext>Full text</h2>\n\n<p>";
	    if (exists $tr->{'to-appear'}) {
		push @t, "This report is still under preparation";
		push @t, " and expected to become available by " .
		    expand_date($tr->{'to-appear'})
		    if ($tr->{'to-appear'});
		push @t, ".\n";
	    } else {
		my $online = $tr->downloads;
		if ($online) {
		    push @t, $online;
		} else {
		    push @t, "Only available on paper (could be scanned " .
			"on request).\n";
		}
	    }

            my ($licence, $lurl) = $tr->licence;
            if ($licence) {
                push @t, "\n<p>This report is licensed under a ",
                    qq{<a href="$lurl">}, utf8_to_sgml($licence),
                    " licence</a>.\n\n",
            }

            push @t, ("\n<h2 id=bibtex>BibTeX record</h2>\n\n<pre>\n",
		      bibtex_entry($tr, doi=>1) , "</pre>\n");

	    # Highwire Press tags for Google Scholar
	    # https://scholar.google.com/intl/en/scholar/inclusion.html#indexing
	    my @metas;
	    push @metas, html_meta('citation_title' => $tr->titleline);
	    push @metas, map { html_meta('citation_author' => $_->surnamefirst)}
	                     $tr->authors;
	    push @metas, html_meta('citation_publication_date'
				   => $1) if $tr->{date} =~ /^(\d{4})/;
	    push @metas, html_meta('citation_technical_report_institution'
				   => "University of Cambridge, " .
				   $db->{department});
	    push @metas, html_meta('citation_technical_report_number'
				   => $tr->code);
	    push @metas, html_meta('citation_doi' => $tr->doi);
	    push @metas, html_meta('citation_isbn' => $tr->{isbn});
	    push @metas, html_meta('citation_pdf_url' => $tr->url_file)
		if $tr->url_file =~ /\.pdf$/;
	    # update file mtime
	    push @metas, html_meta('ucampas-config' => 'change_check=0')
		if $update;

	    push @generated_html, CLWeb::write_htmlpage($trno, \@t,
							headers => \@metas,
							title => $trno,
							h1 => undef);
	}
	CLWeb::call_ucampas(@generated_html);
	unlink @generated_html unless $keepsource;
    } elsif (/^stats(:(.+))?$/) {
	my $fn = $2 ? $2 : '-';
	open(F, ">$fn") || die("Can't write into '$fn': $!\n");
	my $nrreports = 0;
	my $nrpages = 0;
	my $nronline = 0;
	my $nronlinepages = 0;
	my $nrphds = 0;
	my $nronlinephds = 0;
	my $nrmphils = 0;
	my $nronlinemphils = 0;
	my %authorindex;
	my %authoronlineindex;
	for my $tr ($db->trs) {
	    my $online = $tr->files;
	    my $phd = grep /^phd/, @{$tr->{_notes}};
	    my $mphil = grep /^mphil/, @{$tr->{_notes}};
	    $nrreports++;
	    $nronline++ if $online;
	    if ($tr->{pages}) {
		$nrpages += $tr->{pages};
		$nronlinepages += $tr->{pages} if $online;
	    }
	    $nrphds++ if $phd;
	    $nronlinephds++ if $phd && $online;
	    $nrmphils++ if $mphil;
	    $nronlinemphils++ if $mphil && $online;
	    foreach my $author ($tr->authors) {
		my $idname = $author->{crsid} // $author->fullname;
		push @{$authorindex{$idname}}, $tr;
		push @{$authoronlineindex{$idname}}, $tr if $online;
	    }
	}
	printf F "                             total online\n";
	printf F "Reports:                    %6d %6d\n",
                 $nrreports, $nronline;
	printf F "  PhD theses:               %6d %6d\n",
                 $nrphds, $nronlinephds;
	printf F "  MPhil theses:             %6d %6d\n",
                 $nrmphils, $nronlinemphils;
	printf F "Number of pages:            %6d %6d\n",
                 $nrpages, $nronlinepages;
	printf F "Authors:                    %6d %6d\n",
	         scalar(keys %authorindex),
	         scalar(keys %authoronlineindex);

	printf F "\nGaps in sequence: ";
	for (my $nr = 1; $nr <= ($db->trs)[-1]->{nr}; $nr++) {
	    print "$nr " unless defined $db->{$nr};
	}
	print "\n";
	#printf F "\nAuthors:\n" .
	#    join("\n", sort({lc($a) cmp lc($b)} keys %authorindex));
	#print "\n";
	close F;
    } elsif (/^txtindex(:(.+))?$/) {
	my $fn = $2 ? $2 : '-';
	open(F, ">$fn") || die("Can't write into '$fn': $!\n");
	print F "\n";
	print (F '-' x 72 . "\n");
	print F
	    "Technical Reports -- $db->{department}, University of Cambridge\n";
	print (F '-' x 72 . "\n\n");
	print F "ISSN $db->{issn}\n\n" if $db->{issn};
	print F "\n";
	for my $tr ($db->trs) {
	    my $s = $tr->titleline;
	    $s  = join(', ', map { $_->fullname } $tr->authors) . ": $s";
	    $s .= ", " . expand_date($tr->{date}) if $tr->{date};
	    $s .= ".";
 	    my @ss = ();
	    push @ss, "$tr->{pages} pages" if $tr->{pages};
	    $s .= ' (' . join(', ', @ss) . ')' if @ss;
	    $s = $tr->code . "\n" . reformat($s, "  ");
	    print F "$s\n\n";
	}
    } elsif (/^phdindex(:(.+))?$/) {
	my $fn = $2 ? $2 : '-';
	open(F, ">$fn") || die("Can't write into '$fn': $!\n");
	print F "\n";
	print F
	    "PhD dissertations published as Technical Reports\n" .
            "------------------------------------------------\n" .
            "\n$db->{department}, University of Cambridge\n\n";
	print F "\n";
	for my $tr (reverse $db->trs) {
	    next unless grep(/^phd/, @{$tr->{_notes}});
	    my $s = utf8_to_ascii($tr->titleline);
	    $s .= ", " . $tr->code;
	    $s .= ", " . expand_date($tr->{date}) if $tr->{date};
	    $s .= ".";
	    $s = utf8_to_ascii(join(', ', map { $_->fullname }
				    $tr->authors))
		. "\n" .reformat($s, "  ");
	    print F "$s\n\n";
	}
    } elsif (/^htmlindex(:(.+))?$/) {
	my $fn = $2 ? $2 : '-';
	open(F, ">$fn") || die("Can't write into '$fn': $!\n");
	print F <<END;
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<!-- THIS FILE IS AUTOMATICALLY REGENERATED - DO NOT EDIT -->
<html lang=en>
<head>
<title>University of Cambridge $db->{department} Technical Reports -- Index
</title>
</head>
<body>
<h1>Technical Reports Index</h1>
END
        print F "<p>University of Cambridge $db->{department}</p>\n"
	  if $db->{department};
        print F "<p>ISSN $db->{issn}</p>\n" if $db->{issn};
        print F "<dl>\n";
	for my $tr (reverse $db->trs) {
	    print F "<dt>",$tr->code,"</dt>\n";
	    my $s  = join('', $tr->html);
            print (F "<dd>" . reformat($s, "") . "</dd>\n");
	}
	print F <<END;
</dl>
<p>Last update: $today</p></body></html>
END
        close(F);
    } elsif (/^htmltable(:(.+))?$/) {
	my $fn = $2 ? $2 : '-';
	open(F, ">$fn") || die("Can't write into '$fn': $!\n");
	print F (
	    '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">', "\n",
	    "<!-- Automatically generated file, do not edit! -->\n",
	    "<title>Catalogue and downloads</title>\n");
	print F htmltable([ reverse $db->trs ]);
        close F;
    } elsif (/^authorstable(:(.+))?$/) {
	my $fn = $2 ? $2 : '-';
	open(F, ">$fn") || die("Can't write into '$fn': $!\n");
	print F (
	    '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">', "\n",
	    "<!-- Automatically generated file, do not edit! -->\n",
	    "<title>Authors index</title>\n");
	my %id2trs;
	my %id2author;
	for my $tr ($db->trs) {
	    for my $author ($tr->authors) {
		my $idname = $author->fullname;
		warn("Author’s name lacks whitespace: '$idname'\n")
		    unless $idname =~ / /; # so we can distinguish it from crsid
		# find a unique identifier for each author, preferably
		# crsid, otherwise full name
		my $author_id = $author->{crsid} // $idname;
		# map that identifier to list of TRs by that author,
		# as well as to the author's record with the longest full name
		push @{$id2trs{$author_id}}, $tr;
		unless ($id2author{$author_id} &&
			length($id2author{$author_id}->fullname) >
			length($idname)) {
		    $id2author{$author_id} = $author;
		}
	    }
	}
	my @ids = sort { use locale;
			 $id2author{$a}->sortname cmp
			 $id2author{$b}->sortname } keys %id2trs;
	my @rows;
	for my $author_id (@ids) {
	    my $author = $id2author{$author_id};
	    my @trs = @{$id2trs{$author_id}};
	    push @rows, $author;
	    push @rows, @trs;
	}
	print F htmltable(\@rows);
        close F;
    } elsif (/^marc([\d,-]*)(:(.+))?$/) {
	# http://www.loc.gov/marc
	my $range = $1;
	my $fn = $3 ? $3 : '-';
	open(F, ">$fn") || die("Can't write into '$fn': $!\n");
	my $enterdate=POSIX::strftime('%y%m%d', gmtime time);
	for my $tr ($db->range($range)) {
	    my ($degree, $college, $submitted) = $tr->thesis;
	    my $thesis = $degree ? 'm' : ' ';
	    my ($pubdate) = $tr->{date} =~ /^(\d{4})/;
	    # $pubdate=$1;
	    print F "008 ${enterdate}s$pubdate    enk      $thesis    000 0 eng d\n";
	    # 024 other standard identifier
	    #     8 unspecified type of standard number or code
	    print F "024 8  |a".$tr->code."\n";
	    # 084
	    print F "084    |aTechnical report ".$tr->{nr}."\n";
	    # Main entry heading and title
	    my @authors = $tr->authors;
	    my $nfc = 0;  # non-filing count (determiner skipped)
            if ($tr->{title} =~ /^(A|An|The) /) {
		$nfc = length($1)+1;
	    }
	    my $dotendauthor = $authors[0]->surnamefirst;
	    $dotendauthor .= '.' unless $dotendauthor =~ /\.$/;
	    if (@authors <= 3) {
		print F "100 1  |a$dotendauthor\n";
		print F "245 1$nfc |a$tr->{title} /|c" .
		    join(', ', map {$_->fullname} @authors) . ".\n";
	    } else {
		print F "245 0$nfc |a$tr->{title} /|c" .
		    $authors[0]->fullname . " ... [et al.].\n";
	    }
	    # 260
	    print F "260    |aCambridge :|bUniversity of Cambridge, $db->{department},|c$pubdate.\n";
	    print F "300    |a$tr->{pages} p. ;|c30 cm.\n";
	    print F "490 1  |aTechnical report,|xISSN $db->{issn};|vno. $tr->{nr}\n";
	    # 502 dissertation note
	    if ($degree) {
		my $degr;
		if ($degree eq 'phd' || $degree eq 'ophd') {
		    $degr = 'Ph.D';
		} elsif ($degree =~ /^mphil/) {
		    $degr = 'M.Phil';
		} elsif ($degree eq 'cst-part2') {
		    $degr = 'B.A.';
		} elsif ($degree eq 'cst-part3') {
		    $degr = 'M.Eng';
		} else {
		    die("Unknown degree '$degree'");
		}
		print F "502    |aBased on the author's dissertation ($degr)";
		print F " -- University of Cambridge" unless $degree eq 'ophd';
		print F ", " . substr($submitted, 0, 4) if $submitted;
		print F ".\n";
	    }
	    print F "504    |aIncludes bibliographical references.\n";
	    # 700 name added entries
	    if (@authors <= 3) {
		shift @authors;  # drop first author
		for my $author (@authors) {
		    my $dotendauthor = $author->surnamefirst;
		    $dotendauthor .= '.' unless $dotendauthor =~ /\.$/;
		    print F "700 1  |a$dotendauthor\n";
		}
	    } else {
		print F "700 1  |a$dotendauthor\n";
	    }
	    print F "810 2  |aUniversity of Cambridge.|b$db->{department}.|tTechnical report;|vno. $tr->{nr}.\n";
	    print F "\n";
	}
	close(F);
    } elsif (/^dblp(:(.+))?$/) {
	# This is still a construction site !!!
	# http://www.informatik.uni-trier.de/~ley/db/about/dblp.dtd
        # https://dl.acm.org/doi/10.14778/1687553.1687577
	my $fn = $2 ? $2 : '-';
	open(F, ">$fn") || die("Can't write into '$fn': $!\n");
	print F "<?xml version=\"1.0\"?>\n";
	print F "<!DOCTYPE dblp SYSTEM \"dblp.dtd\">\n";
	print F "<dblp>\n";
	for my $tr ($db->trs) {
	    print F "<article key=\"" . utf8_to_sgmlatt($tr->code) . "\">\n";
	    for my $author ($tr->authors) {
		print F "  <author>" . utf8_to_sgml($author->fullname) .
		    "</author>\n";
	    }
	    print F "  <title>" . utf8_to_sgml($tr->titleline) . "</title>\n";
	    print F "  <number>" . utf8_to_sgml($tr->code) . "</number>\n";
	    print F "  <year>$1</year>\n" if ($tr->{date} =~ /^(\d{4})/);
	    print F "  <month>" . $TechReport::month[$1] . "</month>\n"
		if ($tr->{date} =~ /^\d{4}-(\d{2})/);
            print F "  <isbn>" . utf8_to_sgml($tr->{isbn}) . "</isbn>\n" if $tr->{isbn};
	    print F "  <journal>" . utf8_to_sgml("University of Cambridge, $db->{department}, Technical Report") . "</journal>\n";
	    print F "  <url>" .
		utf8_to_sgml($tr->url_abs) .
		"</url>\n";
	    print F "</article>\n";
	}
	print F "</dblp>\n";
	close(F);
    } elsif (/^bibindex(?::(.+))?$/) {
	my $fn = $1;
	my @f = ( "\% University of Cambridge $db->{department} " .
		  "Technical Reports\n" .
		  "\% Automatically generated file -- do not edit\n");
        for my $tr ($db->trs) {
	    next if exists $tr->{'to-appear'};
            push @f, "\n" . bibtex_entry($tr, abstract=>1, issn=>1, doi=>1);
	}
	CLWeb::write_file($fn, @f);
    } elsif (/^rfcindex(:(.+))?$/) {
	# http://www.ietf.org/rfc/rfc1807.txt
	my $fn = $2 ? $2 : '-';
	open(F, ">$fn") || die("Can't write into '$fn': $!\n");
	for my $tr ($db->trs) {
	    next if exists $tr->{'to-appear'};
	    print F " BIB-VERSION:: CS-TR-v2.1\n";
	    my $id = 'cam.ac.uk//' . $tr->code; # not yet registered!
	    print F "          ID:: $id\n";
	    print F "       ENTRY:: " .
		expand_date_us(POSIX::strftime('%Y%m%d',
					       gmtime $tr->{_lastmod})) . "\n";
	    print F "ORGANIZATION:: University of Cambridge, $db->{department}\n";
	    print F "        TYPE:: Technical Report\n";
	    if ($tr->{title}) {
		print F reformat(utf8_to_ascii($tr->titleline),
				 "               ",
				 "       TITLE:: ") . "\n";
	    }
	    for my $author ($tr->authors) {
		print F ('      AUTHOR:: ',
			 utf8_to_ascii($author->surnamefirst), "\n");
	    }
	    print F "        DATE:: " . expand_date_us($tr->{date}) . "\n"
		if $tr->{date};
	    print F "       PAGES:: " . $tr->{pages} . "\n"
		if $tr->{pages};
	    print F "    LANGUAGE:: English\n";
	    foreach my $fn ($tr->files) {
		print F "OTHER_ACCESS:: URL:$db->{reporturl}$fn\n";
	    }
	    if (my $notes = anynotes($tr)) {
		print F reformat(utf8_to_ascii($notes), "               ",
				 "       NOTES:: ") . "\n";
	    }
	    print F reformat(utf8_to_ascii($tr->{abstract}), "               ",
			     "    ABSTRACT:: ") . "\n"
		if $tr->{abstract};
	    print F "         END:: $id\n";
	}
	close(F);
    } elsif (/^clep([^:]*)(:(.+))?$/) {
	# http://www.cl.cam.ac.uk/localsys/publications/eprintsxml.html
	my $range = $1;
	my $fn = $3;
	$fn = '-' unless defined $fn;
	open(F, ">$fn") || die("Can't write into '$fn': $!\n");
	print F "<eprintsdata>\n";
	for my $tr ($db->range($range)) {
	    print F "  <record>\n";
	    print F "    <field name=\"type\">scholarly_edition</field>\n";
	    for my $author ($tr->authors) {
		print F "    <field name=\"creators\" id=\"\">";
		print F "<part name=\"given\">" .
		    utf8_to_sgml($author->{forenames}) . "</part>"
		    if $author->{forenames};
		print F "<part name=\"family\">" .
		    utf8_to_sgml($author->{surname}) . "</part>"
		    if $author->{surname};
		print F "</field>\n";
	    }
	    print F "    <field name=\"title\">" .
		utf8_to_sgml($tr->titleline) . "</field>\n";
	    print F "    <field name=\"ispublished\">pub</field>\n";
	    print F "    <field name=\"monograph_type\">technical_report</field>\n";
	    if (my $notes = anynotes($tr)) {
		print F "    <field name=\"notes\">" .
		    utf8_to_sgml(nolinesep($notes)) .
		    "</field>\n";
	    }
	    print(F "    <field name=\"abstract\">" .
		  utf8_to_sgml($tr->{abstract}) .
		  "</field>\n")
		if $tr->{abstract};
	    if (my $date = $tr->{date}) {
		$date .= '-00' if $date =~ /^\d{4}-\d{2}$/;
		$date .= '-00-00' if $date =~ /^\d{4}$/;
		print F "    <field name=\"date_issue\">$date</field>\n";
	    }
	    print F "    <field name=\"publisher\">" .
		utf8_to_sgml("University of Cambridge $db->{department}") .
		"</field>\n";
	    print F "    <field name=\"place_of_pub\">" .
		utf8_to_sgml("Cambridge, UK") .
		"</field>\n";
	    print F "    <field name=\"pages\">$tr->{pages}</field>\n"
		if $tr->{pages};
	    print F "    <field name=\"id_number\">" .
		utf8_to_sgml($tr->code) .
		"</field>\n";
	    print F "    <field name=\"institution\">" .
		utf8_to_sgml("University of Cambridge") .
		"</field>\n";
	    print F "    <field name=\"department\">" .
		utf8_to_sgml("$db->{department}") .
		"</field>\n" if $db->{department};
	    print F "    <field name=\"issn\">" .
		utf8_to_sgml("$db->{issn}") .
		"</field>\n" if $db->{issn};
	    print F "    <field name=\"official_url\">" .
		utf8_to_sgml($tr->url_abs) .
		"</field>\n";
	    print F "  </record>\n";
	}
	print F "</eprintsdata>\n";
	close(F);
	# TODO
    } elsif (/^cdtr([^:]*)$/) {
	# See http://www.gweep.ca/~cdtr/guidelines.html for posting guidelines
	for my $tr ($db->range($1)) {
	    my $p;
	    my $pp;
	    next if exists $tr->{'to-appear'};
	    $p .= "Publication announcement:\n\n";
	    $p .= reformat(utf8_to_ascii($tr->{title}), "    ", "    ") . "\n\n"
		if $tr->{title};
	    $p .= reformat(utf8_to_ascii(join(', ', map {$_->fullname}
					      $tr->authors)),
			   "    ", "    ") . "\n\n"
		if $tr->authors;
	    $pp = (nobreak("Technical report ". $tr->code) . ", " .
		   nobreak("University of Cambridge") . ",\n" .
		   nobreak($db->{department}));
	    $pp .= ", PhD thesis"
		if grep(/^phd/, @{$tr->{_notes}});
	    $pp .= ", MPhil thesis"
		if grep(/^mphil/, @{$tr->{_notes}});
	    $pp .= ", MEng thesis"
		if $tr->{'cst-part3'};
	    $pp .= ", BA dissertation"
		if $tr->{'cst-part2'};
	    $pp .= ", " . nobreak(expand_date($tr->{date})) if $tr->{date};
	    $pp .= ", " . nobreak("$tr->{pages} pages") if $tr->{pages};
	    $p .= reformat(utf8_to_ascii($pp), "    ", "    ") . ".\n";
	    if (my $frontnote = frontnote($tr)) {
		$p .= "\n" .
		    reformat(utf8_to_ascii($frontnote), "    ", "    ") . "\n";
	    }
	    $p .= "\nThis document is now available at\n\n" .
		"    ".$tr->url_abs."\n\n";
	    $p .= "Abstract:\n\n" .
		reformat(utf8_to_ascii($tr->{abstract}), "", "") . "\n\n"
		    if $tr->{abstract};
	    # add signature
	    $p .= "-- \nUniversity of Cambridge, $db->{department},\n" .
		"Technical Reports";
	    $p .= " (ISSN $db->{issn})" if $db->{issn};
	    $p .= "\n";
	    $p .= $db->{reporturl} . "\n" if $db->{reporturl};
	    # post after manual confirmation
	    my $from = "tech-reports\@cl.cam.ac.uk";
	    my $subject = $tr->code.": " .
		utf8_to_ascii($tr->titleline);
	    my $newsgroups = "comp.doc.techreports,ucam.cl.library-list";
	    #my $newsgroups = "ucam.cl.test";
	    print "From: $from\n";
	    print "Subject: $subject\n";
	    print "Newsgroups: $newsgroups\n\n$p";
	    print "\nPress return to post the above, or Ctrl-C to abort\n";
	    <STDIN>;
	    my $inews = ("inews -S " .
			 " -f $from" .
			 " -n $newsgroups" .
			 " -t " . shellquote($subject));
	    open(POSTLOG, '>>tr-postlog.txt'); # record posting in logfile
	    if (open(INEWS, "|$inews")) {
		print "$inews\n\n";
		print INEWS $p;
		close(INEWS) || die("Couldn't close pipe to '$inews'!\n$!");
		print POSTLOG "$tr->{nr}|$today|$newsgroups\n";
	    } else {
		die("Couldn't open '|$inews'!");
	    }
	    close(POSTLOG);
	}
    } elsif (/^rss(:(.+))?$/) {
        # http://web.resource.org/rss/1.0/spec
	# http://blogs.law.harvard.edu/tech/rss
        # http://feedvalidator.org/check.cgi?url=http%3A%2F%2Fwww.cl.cam.ac.uk%2Ftechreports%2FUCAM-CL-TR-RSS.xml
	my $fn = $2 ? $2 : '-';
	open(F, ">$fn") || die("Can't write into '$fn': $!\n");
	print F "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
	print F "<rdf:RDF\n" .
	    "  xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns\#\"\n" .
	    "  xmlns=\"http://purl.org/rss/1.0/\"\n" .
	    "  xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n" .
	    ">\n";
	print F "  <channel rdf:about=\"" .
	    utf8_to_sgml($db->{reporturl}) . "\">\n";
	print F ("    <title>" . # "University of Cambridge " .
		 utf8_to_sgml($db->{department}) .
		 " Technical Reports</title>\n");
	print F ("    <link>" . utf8_to_sgml($db->{reporturl}) . "</link>\n");
	print F ("    <description>Recent research reports published by the "
		 . utf8_to_sgml($db->{department}) .
		 " at the University of Cambridge.</description>\n");
	#print F ("    <language>en</language>\n");
	#print F ("    <generator>tr-tool</generator>\n");
	my @recent = grep { !exists $_->{'to-appear'} } reverse $db->trs;
	splice(@recent, 15); # remove all but the latest 15
	print F ("    <items>\n");
	print F ("      <rdf:Seq>\n");
	for my $tr (@recent) {
	    print F ('        <rdf:li resource="' .
		     utf8_to_sgmlatt($tr->url_abs) .
		     "\" />\n");
	}
	print F ("      </rdf:Seq>\n");
	print F ("    </items>\n");
	print F ("  </channel>\n");
	for my $tr (@recent) {
	    print F ('  <item rdf:about="' .
		     utf8_to_sgmlatt($tr->url_abs) .
		     "\">\n");
	    print F ('    <title>' .
		     utf8_to_sgml($tr->titleline) . "</title>\n");
	    print F ('    <link>' .
		     utf8_to_sgml($tr->url_file) . "</link>\n");
	    #print F ('    <guid>' .
	    #         utf8_to_sgml($tr->url_abs) .
	    #         "</guid>\n");
	    # add some DC elements
	    for my $author ($tr->authors) {
		print(F '    <dc:creator>',
		      utf8_to_sgml($author->surnamefirst),
		      "</dc:creator>\n");
	    }
	    print(F "    <dc:publisher>University of Cambridge, " .
		  utf8_to_sgml($db->{department}) . "</dc:publisher>\n");
	    for my $contributor ($tr->contributors) {
		print(F '    <dc:contributor>' .
		      utf8_to_sgml($contributor->surnamefirst) .
		      "</dc:contributor>\n");
	    }
	    print F "    <dc:date>$tr->{date}</dc:date>\n"
		if $tr->{date};
	    print(F "    <description>\n" .
		  reformat(utf8_to_sgml($tr->{abstract}), "        ") .
		  "\n    </description>\n")
		if $tr->{abstract};
	    print F ("  </item>\n");
 	}
	print F "</rdf:RDF>\n";
    } elsif (/^dcindex(:(.+))?$/) {
	# http://dublincore.org/
	my $fn = $2 ? $2 : '-';
	open(F, ">$fn") || die("Can't write into '$fn': $!\n");
	print F "<?xml version=\"1.0\"?>\n";
	print(F '<!DOCTYPE rdf:RDF ' .
	      "PUBLIC \"-//DUBLIN CORE//DCMES DTD 2001 11 28//EN\"\n" .
	      '  "http://dublincore.org/documents/2001/11/28/dcmes-xml/' .
	      'dcmes-xml-dtd.dtd">' . "\n");
	print(F "<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n" .
	      "         xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n");
	for my $tr ($db->trs) {
	    next if $tr->{'to-appear'};
	    print F '<rdf:Description';
	    print(F ' rdf:about="' . utf8_to_sgmlatt($tr->url_file) . '"')
		if $tr->url_file;
	    print F ">\n";
	    if ($tr->{title}) {
		print(F reformat(utf8_to_sgml($tr->titleline) . "</dc:title>",
				 '    ', '  <dc:title>') . "\n");
	    }
	    for my $author ($tr->authors) {
		print(F '  <dc:creator>' . utf8_to_sgml($author->surnamefirst) .
		      "</dc:creator>\n");
	    }
	    print(F "  <dc:publisher>University of Cambridge, " .
		  utf8_to_sgml($db->{department}) . "</dc:publisher>\n");
	    for my $contributor ($tr->contributors) {
		print(F '  <dc:contributor>' .
		      utf8_to_sgml($contributor->surnamefirst) .
		      "</dc:contributor>\n");
	    }
	    print F "  <dc:date>$tr->{date}</dc:date>\n"
		if $tr->{date};
	    print F "  <dc:language>en</dc:language>\n";
	    print F "  <dc:type>Text</dc:type>\n";
	    print(F "  <dc:identifier>" . utf8_to_sgml($tr->code) .
		  "</dc:identifier>\n");
	    print(F "  <dc:identifier>ISSN " . utf8_to_sgml($db->{issn}) .
		  "</dc:identifier>\n")
		if $db->{issn};
	    print(F "  <dc:description>\n" .
		  reformat(utf8_to_sgml($tr->{abstract}), "    ") .
		  "\n  </dc:description>\n")
		if $tr->{abstract};
	    print F "</rdf:Description>\n";
	}
	print F "</rdf:RDF>\n";
	close(F);
    } elsif (/^oaiindex(:(.+))?$/) {
	# http://www.openarchives.org/OAI/2.0/guidelines-static-repository.htm
	# The datestamp specified in $mindate is the earliest one that
	# will show up in the produced file. Update it each time this
	# conversion routine changes, such that not only changes in
	# the database, but also changes in the conversion routine
        # cause datestamps to be renewed.
	my $mindate = '2003-01-01';
	#
	my $fn = $2 ? $2 : '-';
	my $gateway = "http://oaigateway.library.ucla.edu/gatewaynet/oai.aspx/";
	my $srurl = "$db->{reporturl}$db->{code}-OAI-SR.xml";
	$srurl =~ s|^https?://||;
	my $baseurl = $gateway . $srurl;
	open(F, ">$fn") || die("Can't write into '$fn': $!\n");
	print F <<EOT;
<?xml version="1.0" encoding="UTF-8"?>
<Repository xmlns="http://www.openarchives.org/OAI/2.0/static-repository"
            xmlns:oai="http://www.openarchives.org/OAI/2.0/"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/static-repository
                                http://www.openarchives.org/OAI/2.0/static-repository.xsd">
<Identify>
  <oai:repositoryName>University of Cambridge $db->{department} Technical Reports</oai:repositoryName>
  <oai:baseURL>$baseurl</oai:baseURL>
  <oai:protocolVersion>2.0</oai:protocolVersion>
  <oai:adminEmail>tech-reports\@cl.cam.ac.uk</oai:adminEmail>
  <oai:earliestDatestamp>$mindate</oai:earliestDatestamp>
  <oai:deletedRecord>no</oai:deletedRecord>
  <oai:granularity>YYYY-MM-DD</oai:granularity>
</Identify>
<ListMetadataFormats>
  <oai:metadataFormat>
    <oai:metadataPrefix>oai_dc</oai:metadataPrefix>
    <oai:schema>http://www.openarchives.org/OAI/2.0/oai_dc.xsd</oai:schema>
    <oai:metadataNamespace>http://www.openarchives.org/OAI/2.0/oai_dc/
        </oai:metadataNamespace>
  </oai:metadataFormat>
</ListMetadataFormats>
<ListRecords metadataPrefix="oai_dc">
EOT
        for my $tr ($db->trs) {
            next if exists $tr->{'to-appear'};
            my $id = 'cam.ac.uk//' . $tr->code; # not yet registered!
	    my $datestamp = POSIX::strftime('%Y-%m-%d',
					    gmtime $tr->{_lastmod});
	    $datestamp = $mindate if $datestamp lt $mindate;
	    print F "<oai:record>\n";
	    print F "  <oai:header>\n";
	    print F "    <oai:identifier>$id</oai:identifier>\n";
	    print F "    <oai:datestamp>$datestamp</oai:datestamp>\n";
	    print F <<EOT;
  </oai:header>
  <oai:metadata>
    <oai_dc:dc
       xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/"
       xmlns:dc="http://purl.org/dc/elements/1.1/"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/
       http://www.openarchives.org/OAI/2.0/oai_dc.xsd">
EOT
	    if ($tr->{title}) {
		print(F reformat(utf8_to_sgml($tr->titleline) . "</dc:title>",
				 '        ', '      <dc:title>') . "\n");
	    }
            for my $author ($tr->authors) {
                print(F '      <dc:creator>',
                      utf8_to_sgml($author->surnamefirst),
                      "</dc:creator>\n");
            }
	    print(F "      <dc:publisher>University of Cambridge, " .
		  utf8_to_sgml($db->{department}) . "</dc:publisher>\n");
            for my $contributor ($tr->contributors) {
                print(F '      <dc:contributor>' .
                      utf8_to_sgml($contributor->surnamefirst) .
                      "</dc:contributor>\n");
            }
            print F "      <dc:date>$tr->{date}</dc:date>\n"
                if $tr->{date};
	    print F "      <dc:language>en</dc:language>\n";
	    print F "      <dc:type>Text</dc:type>\n";
	    print(F "      <dc:identifier>" . utf8_to_sgml($tr->code) .
		  "</dc:identifier>\n");
	    print(F "      <dc:identifier>ISSN " . utf8_to_sgml($db->{issn}) .
		  "</dc:identifier>\n")
		if $db->{issn};
	    print(F '      <dc:identifier>' .
		  utf8_to_sgmlatt($tr->url_file) . "</dc:identifier>\n")
		if $tr->url_file;
	    print(F "      <dc:description>\n" .
		  reformat(utf8_to_sgml($tr->{abstract}), "        ") .
		  "\n      </dc:description>\n")
		if $tr->{abstract};
	    print F "    </oai_dc:dc>\n  </oai:metadata>\n</oai:record>\n";
	}
	print F "</ListRecords>\n</Repository>\n";
	close(F);
    } elsif (/^checktex$/) {
	my $fn = 'check.tex';
	open(F, ">$fn") || die("Can't write into '$fn': $!\n");
	print F <<END;
\\documentclass[10pt,a4paper,twocolumn,twoside]{article}
\\oddsidemargin  -5.4truemm
\\evensidemargin -5.4truemm
\\topmargin -5truemm
\\textwidth 170truemm
\\textheight 240truemm
\\headheight 0mm
\\parindent=0pt
\\parskip=\\medskipamount
\\renewcommand\\baselinestretch{1}
\\raggedbottom
\\begin{document}
END
        for my $tr  ($db->trs) {
	    print F "\\pagebreak[3]\\smallskip\\hrule\\par" .
                    "\\textbf{TR-$tr->{nr}:}\\par\n";
	    print F "{\\Large \\begin{center}\n";
	    print F break_title(utf8_to_LaTeX($tr->{title}, '1.5ex')) . "\n";
	    print F "\\end{center}}\n";
	    print F "\\begin{center}\n";
	    for my $author ($tr->authors) {
		print F utf8_to_LaTeX($author->{forenames}) . ' '
		    if $author->{forenames};
		print F '\underline{' .
		    utf8_to_LaTeX(nobreak $author->{surname}) . "}\\\\\n";
	    }
	    print F "\\end{center}\n";
	    print F "\\begin{center}\n";
	    if ($tr->{date}) {
		print F expand_date($tr->{date}). "\n";
	    } else {
		print F "Date: \\rule{30mm}{0.2mm}\n";
	    }
	    print F "\\end{center}\n";
	    if ($tr->{pages}) {
		print F "$tr->{pages}~p. \\hfill\n";
	    } else {
		print F "\\rule{10mm}{0.2mm}~p. \\hfill\n";
	    }
	    if (my ($degree, $college, $submission) = $tr->thesis) {
		if ($college && $submission) {
		    print F "$degree: " . utf8_to_LaTeX($college) . ", " .
			expand_date($submission) . "\n";
		} else {
		    print F
			"\\\\$degree:\\hspace{\\fill}college = " .
			($college ? utf8_to_LaTeX($college)
			 : "\\rule{50mm}{0.2mm}") .
			 "\\break" .
			 "\\hspace*{\\fill}submission date = " .
			 ($submission ? expand_date($submission)
			  : "\\rule{50mm}{0.2mm}\n");
		}
	    } else {
		print F "not a thesis";
	    }
	    print F "\\par\\medskip\n";
	    print F "\\hrule\n";
	}
	print F "\\end{document}\n";
	close(F);
    } elsif (/^pdfindex$/) {
	my $pdflatex = 1;
	my $title = "List of technical reports";
	my $pdfinfo = pdfinfo('Author' => $db->{editor},
			      'Title'  => $title);
	my $fn = 'tr-index';
	open(F, ">$fn.tex") || die("Can't write into '$fn.tex': $!\n");
	print F ("\\documentclass[12pt,a4paper,twocolumn,twoside]{article}\n",
                 "\\raggedbottom\n",
                 "\\usepackage[margin=20mm]{geometry}\n",
                 "\\usepackage{tr-title}\n",
                 "\\usepackage{color}\n",
                 "\\begin{document}\n",
                 "\\begin{trtitlepage}\n");
        print F ("\\newcommand{\\trtitle}{" . utf8_to_LaTeX($title) . "}\n");
        print F ("\\newcommand{\\trnumber}{}\n");
        print F ("\\newcommand{\\trauthors}{}\n");
	print F ("\\newcommand{\\trdate}{" . expand_date(substr($today, 0, 7))
		 . "}\n");
        print F ("\\newcommand{\\trfrontnote}{}\n");
	print F ("\\newcommand{\\tsdepartment}{" .
		 utf8_to_LaTeX($db->{department}) . "}\n");
	print F ("\\newcommand{\\tsaddress}{" .
		 utf8_to_LaTeX($db->{address}) . "}\n");
	print F ("\\newcommand{\\tsdepturl}{$db->{depturl}}\n");
	print F ("\\newcommand{\\tsissn}{$db->{issn}}\n");
        print F ("\\newcommand{\\tscode}{$db->{code}}\n");
	print F ("\\maketrtitle\n");
	print F ("\\begin{trbackpage}\n");
        print F (series_backnote_latex($db));
	print F ("\\end{trbackpage}\n");
	print F ("\\end{trtitlepage}\n");
	print F ("\\setlength{\\parindent}{0mm}\n",
                 "\\renewcommand{\\rmdefault}{psb}\\rmfamily % Adobe Sabon\n",
                 "\\setlength{\\parskip}{0mm plus 1mm}\n",
                 "\\setlength{\\fboxsep}{1pt}\n",
                 "\\definecolor{trback}{gray}{0.85}\n",
                 "\\RaggedRightRightskip 0pt plus 60mm\n");
        for my $tr ($db->trs) {
            next if exists $tr->{'to-appear'};
	    print F ("\\colorbox{trback}{\\makebox[\\linewidth]",
		     "[l]{\\hfill\\vphantom{\\fbox{U}}\\small ", $tr->code,
		     "\\hfill}}\n\n");
	    print F "\\begin{FlushLeft}\n";
	    print F (utf8_to_LaTeX(join(', ', map { nobreak $_->fullname }
						  $tr->authors)),
		     ":\n\n");
	    print F ("\\smallskip{\\large\n",
		     break_title(utf8_to_LaTeX($tr->{title})),
		     "\n\n}\\medskip\n");
	    my @notes;
	    push @notes, expand_date($tr->{date}) if $tr->{date};
	    push @notes, "$tr->{pages}~pages" if $tr->{pages};
            my @files = $tr->files;
            for my $fn (@files) {
                if ($fn =~ /\.pdf$/) {
       		    push @notes, "PDF";
	        } elsif ($fn =~ /\.ps\.gz$/) {
		    push @notes, "PostScript";
	        } elsif ($fn =~ /\.dvi\.gz$/) {
		    push @notes, "DVI";
	        }
            }
	    push @notes, "paper copy" unless @files;
	    print F ("{\\small " . join(", ", @notes) . "}\n\n");
	    if (my ($degree, $college, $submission) = $tr->thesis) {
		print F "{\\footnotesize ";
		if ($degree =~ /^(o?)phd$/) {
		    print F "PhD thesis";
		} elsif ($degree =~ /^mphil/) {
		    print F "MPhil thesis";
		} elsif ($degree =~ /^cst-part3/) {
		    print F "MEng dissertation";
		} elsif ($degree =~ /^cst-part2/) {
		    print F "BA dissertation";
		} else {
		    die;
		}
		if ($college || $submission) {
		    my @notes;
		    push @notes, utf8_to_LaTeX($college) if $college;
		    push @notes, expand_date($submission)
			if $submission;
		    print F ' (' . join(", ", @notes) . ')';
		}
		print F "\\par}\n";
	    }
	    if (my $frontnote = frontnote($tr)) {
		print F "{\\footnotesize\\frenchspacing " .
		    reformat(utf8_to_LaTeX($frontnote));
		print F "\\par}\n";
	    }
	    print F "\\end{FlushLeft}\n";
	    if ($tr->{abstract}) {
		print F ("\n\n{\\footnotesize\\setlength{\\parindent}{5mm}\n" .
			 "\\frenchspacing\\noindent \\textbf{Abstract:} " .
			 reformat(utf8_to_LaTeX($tr->{abstract})) .
			 "\\par}\\bigskip\n");
	    }
	    print F "\\pagebreak[2]\n\n";
	}
	if ($pdflatex && $pdfinfo) {
	    print F "\\pdfinfo{\n$pdfinfo}\n";
	}
	print F "\\end{document}\n";
	close(F);
	if ($pdflatex) {
	    latex_to_pdf($fn);
	} else {
	    latex_to_ps($fn, $pdfinfo);
	    ps2pdf("$fn.ps");
	    unlink "$fn.ps";
	}
    } elsif (/^(ps|pdf)title(.*?)(?::(.*))?$/) {
	my $pdflatex = 1;
	my $type = $1;
	my $filename = $3;
	my @trs = $db->range($2);
	if ($filename) {
	    $filename =~ s/\.$type$//
		or die("filename '$filename' must have extension $type\n");
	} else {
	    $filename = (@trs == 1) ? 'tr-title-'.$trs[0]->{nr} : 'tr-titles';
	}
	if ($type eq 'pdf') {
	    if ($pdflatex) {
		pdftitle($filename, @trs);
	    } else {
		pstitle($filename, @trs);
		ps2pdf("$filename.ps");
		unlink "$filename.ps";
	    }
	} else {
	    pstitle($filename, @trs);
	}
    } elsif (/^prefix(\d+)(?:,(\d*-\d*))?$/) {
	my $nr = $1;
	my $pages = $2;
	my $tr = $db->{$nr};
	die("TR-$nr does not exist\n") unless $tr;
	my $titlefile = "title-$nr";
	my $trfile = $tr->code . '.pdf';
	my @srcfile = ( shift(@ARGV) );
	if ($srcfile[0] =~ /^(.*\/)?([^\/]*)\.gz$/) {
	    # if source file is compressed, then uncompress it
	    unshift @srcfile, "$tmpdir/$2";
	    command("zcat $srcfile[1] >$srcfile[0]");
	}
	if ($srcfile[0] =~ /^(.*\/)?([^\/]*)\.dvi$/) {
	    # if source file is DVI, then dvips it
	    unshift @srcfile, "$tmpdir/$2.ps";
	    command("dvips -q -Ppdf -G0 -o $srcfile[0] $srcfile[1]");
	}
	if ($srcfile[0] =~ /^(.*\/)?([^\/]*)\.ps$/) {
	    if ($pages) {
		# extract desired page range
		unshift @srcfile, "$tmpdir/$2-cut.ps";
		command("psselect -p$pages $srcfile[1] >>$srcfile[0]");
		undef $pages;
	    }
	    pstitle("$titlefile", $tr);
	    my $pdfinfo = pdfauthortitle($tr);
	    ps2pdf(join(' ', "$titlefile.ps", $srcfile[0],
			'-c', shellquote("[ $pdfinfo /DOCINFO pdfmark")),
		   $trfile);
	    unlink "$titlefile.ps";
	} elsif ($srcfile[0] =~ /^(.*\/)?([^\/]*)\.pdf$/) {
	    if ($pdfconcatenator eq 'pdftk') {
		# use pdftk to merge the titlepage with the report body
		$pages .= 'end' if $pages =~ /\d+-$/;
		pdftitle($titlefile, $tr);
		my $tmpfile = "$trfile~";
		command('pdftk', "A=$titlefile.pdf", "B=$srcfile[0]",
			'cat', 'A', 'B'.$pages, 'output', $tmpfile);
		unlink $titlefile unless $keepsource;
		# and now set pdfinfo fields, which the previous cat
		# command does not preserve
		my $cmd = 'pdftk';
		my $line = "pdftk $tmpfile update_info_utf8 - output $trfile";
		print STDERR "Calling “$line” ...\n";
		open(my $p, '|-', $line)
		    or die("Failed to execute “$cmd”: $!\n");
		printf($p "InfoBegin\nInfoKey: %s\nInfoValue: %s\n",
		       'Author' => join(', ', map {$_->fullname} $tr->authors));
		printf($p "InfoBegin\nInfoKey: %s\nInfoValue: %s\n",
		       'Title' => $tr->titleline);
		close $p;
		die("Command “$cmd” died with signal ", $? & 127 ,"\n")
		    if $? & 127;
		die("Command “$cmd” failed, return value ", $? >> 8, "\n")
		    if $?;
		unlink $tmpfile;
	    } elsif ($pdfconcatenator eq 'pdfunite') {
		# use pdfunite to merge the titlepage with the report body
		die("Page range selection is not yet supported with pdfunite\n")
                    if $pages;
		pdftitle($titlefile, $tr);
		command('pdfunite', "$titlefile.pdf", $srcfile[0], $trfile);
		unlink $titlefile unless $keepsource;
	    } elsif ($pdfconcatenator =~ /^gs/) {
		# use ghostscript to merge the titlepage with the report body
		my ($firstpage, $lastpage) = ($pages =~ /^(\d*)-(\d*)$/);
		my @gs_opts;
		push @gs_opts, "-dFirstPage=$firstpage" if $firstpage;
		push @gs_opts, "-dLastPage=$lastpage" if $lastpage;
		my $fmt = 'pdf';
		$fmt = 'ps' if $pdfconcatenator eq 'gsps';
		if ($fmt eq 'ps') {
		    pstitle($titlefile, $tr);
		} else {
		    pdftitle($titlefile, $tr);
		}
		my $pdfinfo = pdfauthortitle($tr);
		ps2pdf(join(' ', "$titlefile.$fmt", @gs_opts, $srcfile[0],
			    '-c', shellquote("[ $pdfinfo /DOCINFO pdfmark")),
		       $trfile);
		unlink "$titlefile.$fmt";
	    } elsif ($pdfconcatenator eq 'acrobat') {
		# prepare title-page to be manually merged with Acrobat
		pdftitle($tr->code, $tr);
		print("\nNow use Adobe Acrobat (e.g. on Windows TS) to open\n",
		      `/anfs/www/tools/bin/filerpath "$trfile"`,
		      "and then insert ",
		      `/anfs/www/tools/bin/filerpath "$srcfile[0]"`,
		      "after the last page. Finally: chmod a-x $trfile\n\n");
	    } elsif ($pdfconcatenator eq 'pdflatex') {
		# use pdflatex with \includepdf to merge the titlepage with
		# the report body; not recommended: this drops hyperlinks
		$pages = '/' . $pages if length($pages);
		pdftitle($tr->code,
			 ($tr, 'pdfinfo ' . pdfauthortitle($tr),
			  $srcfile[0] . $pages));
	    } else {
		die("Unknown PDF concatenation method '$pdfconcatenator'!\n");
	    }
	} else {
	    die("Don't know how to proceed with source file '$srcfile[0]'!\n");
	}
	my $orig = pop @srcfile;    # do not erase original source file
	unlink @srcfile; # erase all the temporary derived source files
	system("ls -ld $orig $trfile");
    } elsif (/^nagmail$/) {
	for my $tr ($db->trs) {
	    my $deadline = $tr->{'to-appear'};
	    next unless defined $deadline;
            $deadline .= '-12' if $deadline =~ /^\d{4}-\d{2}\z/;
	    next unless $deadline lt $today;
	    my $authors = ($tr->authors)[0]->fullname;
	    $authors .= ' <'.($tr->authors)[0]->{'crsid'}.'>'
		if ($tr->authors)[0]->{'crsid'};
	    $authors .= ', et al.' if $tr->authors > 1;
            my @to = grep { $_ } map { $_->{'crsid'} } $tr->authors;
            my @here = grep { getpwnam($_) } @to;
            @to = ($here[0]) if @here;
	    if ($tr->files) {
		print STDERR "Filed Technical Report " . $tr->code .
		    " by $authors still has “to-appear=$deadline”.\n";
		next;
	    }
            my $body = reformat(<<"EOT" =~ s/[\s\n]+/ /rg);
The allocated $db->{'department'} Technical Report number
${\($tr->code)} by $authors was due for publication by $deadline.
Please complete your submission soon, or reply with an update on when you
expect your manuscript for ${\($tr->code)} to be ready, so we can
update our database accordingly.
EOT
            $body .= "\n\n" . $tr->url_abs . "\n\n";
            my $from = 'tech-reports';
            my @cc = ($from);
            #@to = ('mgk25'); @cc = ();
	    send_email(from => $from, to => \@to, cc => \@cc,
                       subject => $tr->code.' overdue', body => $body);
            sleep 2;
	}
    } elsif (/^dpi(\d+)$/) {
	$dpi = $1;
    } elsif (/^pdftk|gs|gsps|pdflatex|pdfunite|acrobat$/) {
	$pdfconcatenator = $_;
    } elsif (/^keepsource$/) {
	$keepsource = 1;
    } else {
        die("Unknown command line command '$_'\n");
    }
}
