n page A4 ->(2n+1) page A5 pdfs: success without finesse
Reece Arnott <[email protected]>
| Newsgroups | gmane.org.user-groups.linux.dunedin.general |
|---|---|
| Message-ID | <[email protected]> |
Due to no initial knowledge of Perl or PDF internals this is a bit of a
call for help and also a bit of documentation on how I scratched my own
itch.
I have a smallish e-book reader and reading pdfs on it that are
formatted for A4 size screens is a pain. I got the idea that I could
take pdfs with A4 size pages and turn them into pdfs with twice as many
pages that were A5 size (note that the A standard is done so that this
is true, A0 is 2xA1, A1 is 2xA2 etc. but what I've written is done in
such a way that it will just chop the page in half without worrying
about the actual dimensions). I've done that using a perl script and
additional package that started from an answer in stackoverflow:
https://stackoverflow.com/questions/5133725/split-a4-page-pdf-to-two-a5-pdf-page
The CAM::PDF perl library used has some documentation etc. here:
http://search.cpan.org/dist/CAM-PDF/
The problem is that my solution (that works) is O(n^2) instead of O(n)
in the number of pages: for each page of the pdf it has to go to disk,
load the entire pdf into memory, then cut out all but the page we're
interested in. There are a number of issues I've solved along the way
and I thought I'd solved this issue as well. Below is the working code
and below that is a code snippet that I thought should work and a short
description of it. I am hampered possibly by my lack of knowledge of
perl and definitely by my lack of understanding of the pdf internal data
structures.
The below code does the following:
- it assumes that there are a number of pdfs in the current folder you
want to split and have a sub-folder called "low" in which these will be
stored (called that for historical reasons as I put all my scripts in
the same folder structure and historically used the scripts to take high
quality audio and video and make it lower quality).
- The pdfs may be from many different sources with lots of the cool
things in pdfs that break my simple script e.g bookmarks and hyperlinking.
- For this reason the first thing it does in the "for each pdf in this
folder" loop is to run the pdf through a pdf->ps->pdf conversion scheme.
In some cases this takes a very long time, as mentioned in the comments
this may be due to converting text into images in weird circumstances.
It is also worth mentioning that the output of the script breaks
Ghostscript if you try and run it through the same process so its not
the best script in the world. I'd prefer to have something that gave
output that played well with others but I've reached the 'good enough'
point given my ignorance of pdf internals.
- Also worth mentioning is that there may be a magical combination of
switches for the a2ps and ps2pdf commands (that are currently doing the
pdf->ps->pdf conversion) that would give the same result as my script
but, again, I haven't needed to know much about them before now so my
ignorance gets in the way.
- The output pdf starts as simply the first page of the initial pdf (the
output of the pdf->ps->pdf conversion process) and ends as 2n+1 pages
where n is the number of pages in the initial pdf. When Calibre does
conversion it also duplicates the first page to use as a title page, in
my case its just because I don't know how to create a blank pdf with the
CAM::PDF toolkit.
- For each page of the initial pdf, the page in question is isolated by
loading the entire pdf into memory and then chopping out all but the
relevant page.
- this one page pdf is then appended to the end of the output pdf twice.
- The next to last page is rotated and cropped so that it is just the
top half of the page (displayed sideways).
- The last page is then rotated and cropped so that it is just the
bottom half of the page(displayed sideways).
- Optionally you can save a copy of the pdf we are building in memory at
the end of each page process by uncommenting one or two of three lines
which save it as a separate file each time based on the page number, or
over the top of itself each time. On the 606 A4 page I tried as my main
test this increased the average time of processing per page from 2 to 15
seconds (but the time increased as you got nearer the end of the process
so this 15 second average doesn't really say much aside from the
increase in total time was approximately (15-2)*606 seconds).
- Finally after all pages have been processed the memory structure is saved.
(Note that on Ubuntu 14.04 I had to install packages for a2ps and
libcam-pdf-perl for the below script to work)
------
#!/usr/bin/perl
use strict;
use warnings;
use CAM::PDF;
my @files = glob("*.pdf");
foreach my $file (@files) {
print "Working on file: $file\n";
print "Stripping any bookmarks etc. by creating new file run through
pdf->ps->pdf conversion\n";
#Note the -1 is needed as by default a2ps prints 2 sheets per page
my $result=`a2ps "$file" -1 -o - | ps2pdf -dPDFSETTINGS=/screen -
"low/$file"`;
# This may result in a blow out in pdf size without the /screen option
(and takes an incredibly long time to process both parts of the above
command even with it).
#Presumably this is due to an issue where it will "sometimes convert
text to high-resolution bitmapped fonts rather than to embedded outline
fonts. Currently this will always occur when the Postscript file uses
CID-keyed or double-byte fonts, when the input file uses kshow, or in
some cases if it uses fonts with non-standard encodings; it may occur in
some other cases as well."
print $result . "\n";
my $pdffile = "low/".$file;
my $pdfout = "low/split-".$file;
#Didn't know how to create an empty structure
#So started by creating a new one based on the first page of the old one
(so can use as title page)
#and appending the new pages to the end
my $pdf = CAM::PDF->new($pdffile) or die $CAM::PDF::errstr;
my $numpgs=$pdf->numPages();
$pdf->extractPages(1);
for (my $pagenum=1; $pagenum <= $numpgs;$pagenum++){
print "Splitting page $pagenum of $numpgs for file \"$file\"\n";
#extract a single page from the old pdf
my $duplicate = CAM::PDF->new($pdffile) or die $CAM::PDF::errstr;
$duplicate->extractPages($pagenum);
#add it twice to the end of the new pdf
$pdf->appendPDF($duplicate);
$pdf->appendPDF($duplicate);
#Should be able to do something with this except it does not seem to
work as I would expect
#It seems to add a blank page to the end of the pdf and overwrite
$currentpage+1 with a duplicate of $currentpage
#$pdf->duplicatePage($currentpage);
#Crop one to the top half of the (rotated) page
my $pagedict = $pdf->getPage($pdf->numPages()-1);
$pagedict->{Rotate} = CAM::PDF::Node->new('number', 90);
my $oldbox = $pdf->getValue($pagedict->{CropBox} || $pagedict->{MediaBox});
my @box = map {$pdf->getValue($_)} @{$oldbox};
$pagedict->{CropBox} = CAM::PDF::Node->new('array', [
map {CAM::PDF::Node->new('number', $_)} $box[0],
($box[3]+$box[1])/2, $box[2], $box[3]
]);
#Crop the other to Bottom half of the (rotated) page
$pagedict = $pdf->getPage($pdf->numPages());
$pagedict->{Rotate} = CAM::PDF::Node->new('number', 90);
$pagedict->{CropBox} = CAM::PDF::Node->new('array', [
map {CAM::PDF::Node->new('number', $_)} $box[0], $box[1], $box[2],
($box[3]+$box[1])/2
]);
#Save each time in case of an issue with the next page
#my $fname = "low/split-".$pagenum;
#$pdf->cleanoutput($fname);
#$pdf->cleanoutput($pdfout);
}
$pdf->cleanoutput($pdfout);
}
------
I thought the below would get rid of the O(n^2) issue as it uses a new
data structure outside of the for loop that has the entire A4 pdf in it
and creates a new one page pdf data structure where the content of the
page is replaced by the content of the relevant page each time through
the loop. In the case of the 606 page pdf this took the time per page
down 100 fold (to the point where the 7 minutes to do the initial
pdf->ps->pdf conversion dominated) but some of the pages it created were
just wrong. It worked fine on the 10 page academic paper I had as a
quick test but in the case of this 606 page maths textbook it had issues
with some pages. I think it may have been where there were graphs and
text on the page; only the graphs were shown.
#This one is to be a single page pdf with the contents of the page being
replaced by the current page
my $singlepagepdf = CAM::PDF->new($pdffile) or die $CAM::PDF::errstr;
$singlepagepdf->extractPages(1);
#This is the data structure we choose each page from
my $dup = CAM::PDF->new($pdffile) or die $CAM::PDF::errstr;
for (my $pagenum=1; $pagenum <= $numpgs;$pagenum++){
#print "Splitting page $pagenum of $numpgs for file \"$file\"\n";
#extract a single page from the old pdf and put it in the single page
pdf data structure
my $content=$dup->getPageContent($pagenum);
$singlepagepdf->setPageContent(1, $content);
#add it twice to the end of the new pdf
$pdf->appendPDF($singlepagepdf);
$pdf->appendPDF($singlepagepdf);
Maybe someone who has knowledge of the internals of pdfs and/or has used
this perl library before can work out the command(s) I should be using
but its just a little frustrating thing that I've lost enough sleep over
that I won't be spending any more time on it myself (unless someone can
throw me a bone): it works, I don't have a lot of large pdfs I want to
split, and as I can just leave it going in the background all day if
need be, its just an affront to my sense of rightness and 'computational
aesthetics' :-)
--
"Believing men would act in their own interest was not cynicism, it turned out, but sheerest optimism; in reality men do not meet so high a standard."
-- Harry Potter and the Methods of Rationality (Chapter 84)
http://hpmor.com/
Reece Arnott
Dunedin
New Zealand
_______________________________________________
DunLUG mailing list
[email protected]
http://lists.ethernal.org/listinfo/dunlug
DunLUG Wiki - http://dunlug.kallisti.net.nz/