├── MIT-LICENSE ├── README.md ├── bin └── impose ├── examples └── minibook.rb ├── layouts ├── card-fold4.pdf ├── card-fold4.png ├── card-fold8.pdf ├── card-fold8.png ├── duodecimo - recto.png ├── duodecimo - verso.png ├── duodecimo-2c - recto.png ├── duodecimo-2c - verso.png ├── duodecimo-2c.pdf ├── duodecimo-i - recto.png ├── duodecimo-i - verso.png ├── duodecimo-i.pdf ├── duodecimo.pdf ├── folio - recto.png ├── folio - verso.png ├── folio.pdf ├── octavo - recto.png ├── octavo - verso.png ├── octavo.pdf ├── quarto - recto.png ├── quarto - verso.png ├── quarto.pdf ├── sexto - recto.png ├── sexto - verso.png ├── sexto.pdf ├── sextodecimo - recto.png ├── sextodecimo - verso.png └── sextodecimo.pdf ├── lib └── pdf │ └── impose │ ├── builder.rb │ ├── ext.rb │ ├── form.rb │ ├── forms │ ├── card_fold.rb │ ├── duodecimo.rb │ ├── folio.rb │ ├── octavo.rb │ ├── quarto.rb │ ├── sexto.rb │ └── sextodecimo.rb │ ├── page.rb │ ├── signature.rb │ └── version.rb └── pdf-impose.gemspec /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Jamis Buck 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PDF::Impose 2 | 3 | `PDF::Impose` is a utility and library for reformatting PDF files, in order to lay out multiple pages of the original document on a single page. The original pages are arranged in such a way that the new page may be folded and cut to produce a _signature_--a small booklet in which the pages are in the expected order. In this way, an existing PDF can be printed, folded, cut, and bound into a handmade book or booklet. 4 | 5 | This process of laying out pages in this way is called [_imposition_](https://en.wikipedia.org/wiki/Imposition). 6 | 7 | 8 | ## Installation 9 | 10 | `PDF::Impose` and its dependencies may be installed via RubyGems: 11 | 12 | $ gem install pdf-impose 13 | 14 | ## Caveats 15 | 16 | This has been tested on a variety of PDFs, and while it works great for most, there are some that it does not work correctly on. Pull requests would be welcome, to increase the number of PDFs that can correctly be imposed. 17 | 18 | ## Usage 19 | 20 | The easiest way to use `PDF::Impose` is via the command-line tool: 21 | 22 | $ impose -h 23 | Usage: impose [options] 24 | -l, --layout LAYOUT The form to use when laying out pages for imposition. 25 | Default is "quarto". 26 | (Specify "list" to see all available forms.) 27 | -o, --orient ORIENT How each sheet should be oriented. 28 | Possible options are "portrait" or "landscape". 29 | Default is "portrait". 30 | -f, --forms COUNT The number of forms to use for each signature. 31 | Default is dependent on the form used. 32 | -d, --dimensions DIM Either the name of a paper size, or a WxH (width/height) 33 | specification. Measurements must be in points. 34 | Default is "LETTER". 35 | (Specify "list" to see all named paper sizes.) 36 | -m, --margin SIZE The minimum margin (in points) for the chosen form. 37 | Default is 36 points. 38 | -s, --start PAGE The page at which to start imposing. 39 | Default is 1. 40 | -e, --end PAGE The page at which to stop imposing. 41 | Default is the last page of the source document. 42 | -M, --[no-]marks Whether or not to include registration marks. 43 | Default is to include registration marks. 44 | -O, --output FILENAME The name of the file to which to write the resulting PDF. 45 | Default is the original filename with "imposed" appended. 46 | -h, --help This help screen. 47 | 48 | To impose an existing PDF in quarto on A4 sheets, the following would suffice: 49 | 50 | $ impose -l quarto -d A4 my-document.pdf 51 | 52 | This would produce a new PDF called `my-document-imposed.pdf`. 53 | 54 | 55 | ## Default Layouts and Forms 56 | 57 | The process of imposition takes a source document and lays out its pages in a particular form. The form used depends on how many pages you want to fit on a single sheet, and how many times you want to fold the paper to produce a signature. 58 | 59 | `PDF::Impose` supports several common imposition forms, which should satisfy most needs. If you need a specific layout, though, it is not hard to define a custom imposition form. (See the "minibook" example in this repository.) 60 | 61 | The following forms are supported by default. 62 | 63 | 64 | ### Four-page card-fold (`card-fold4`) 65 | 66 | The four-page card fold takes a four-page document and lays out the pages on a single sheet in the following order: 67 | 68 | 69 | 70 | When printed, the page can be folded in half twice to make a simple pamphlet of the original four pages. This form works best with the sheet in portrait orientation. 71 | 72 | $ impose -o portrait -l card-fold4 document.pdf 73 | # produces document-imposed.pdf 74 | 75 | 76 | ### Eight-page card-fold (`card-fold8`) 77 | 78 | The eight-page card fold takes an eight-page document and lays out the pages on a single sheet in the following order: 79 | 80 | 81 | 82 | When printed, the page can be folded and cut to make a simple pamphlet of the original eight pages. (Instructions for this type of construction may be found here: [http://www.wikihow.com/Make-a-Booklet-from-Paper](http://www.wikihow.com/Make-a-Booklet-from-Paper)). This form works best with the sheet in landscape orientation. 83 | 84 | $ impose -o landscape -l card-fold8 document.pdf 85 | # produces document-imposed.pdf 86 | 87 | 88 | ### Folio (`folio`) 89 | 90 | This form applies two pages to each side of a sheet of paper, in the following order: 91 | 92 | Recto (front): 93 | 94 | 95 | 96 | Verso (back): 97 | 98 | 99 | 100 | When printed, the page can be folded in half to produce a simple pamphlet of four pages. For documents of more than four pages, multiple sheets can be folded and nested inside each other to form a signature. By default, up to eight forms (four sheets, one form for each side of the sheet) will be treated as a single signature. For documents of more than eight pages, multiple signatures will be produced, though you can control how many forms to include in a signature with the `--forms` switch. 101 | 102 | This form works best with landscape orientation. 103 | 104 | $ impose -o landscape -l folio document.pdf 105 | # produces document-imposed.pdf 106 | 107 | 108 | ### Quarto (`quarto`) 109 | 110 | This form applies four pages to each side of a sheet of paper, in the following order: 111 | 112 | Recto (front): 113 | 114 | 115 | 116 | Verso (back): 117 | 118 | 119 | 120 | When printed, the page can be folded in half twice to produce a simple pamphlet of eight pages. For documents of more than eight pages, multiple sheets can be folded and nested inside each other to form a signature. By default, up to four forms (two sheets, one form for each side of the sheet) will be treated as a single signature. For documents of more than sixteen pages, multiple signatures will be produced, though you can control how many forms to include in a signature with the `--forms` switch. 121 | 122 | This form works best with portrait orientation. 123 | 124 | $ impose -o portrait -l quarto document.pdf 125 | # produces document-imposed.pdf 126 | 127 | 128 | ### Sexto (`sexto`) 129 | 130 | This form applies six pages to each side of a sheet of paper, in the following order: 131 | 132 | Recto (front): 133 | 134 | 135 | 136 | Verso (back): 137 | 138 | 139 | 140 | When printed, the page can be folded three times to produce a simple pamphlet of twelve pages. For documents of more than twelve pages, multiple sheets can be folded and nested inside each other to form a signature. By default, up to four forms (two sheets, one form for each side of the sheet) will be treated as a single signature. For documents of more than twenty-four pages, multiple signatures will be produced, though you can control how many forms to include in a signature with the `--forms` switch. 141 | 142 | This form is awkward to apply when dealing with standard letter-sized pages, but for non-standard page sizes it may work very well. 143 | 144 | $ impose -l sexto document.pdf 145 | # produces document-imposed.pdf 146 | 147 | 148 | ### Octavo (`octavo`) 149 | 150 | This form applies eight pages to each side of a sheet of paper, in the following order: 151 | 152 | Recto (front): 153 | 154 | 155 | 156 | Verso (back): 157 | 158 | 159 | 160 | When printed, the page can be folded three times to produce a simple pamphlet of sixteen pages. For documents of more than sixteen pages, multiple sheets can be folded and nested inside each other to form a signature. By default, two forms (one sheet, one form for each side of the sheet) will be treated as a single signature. For documents of more than sixteen pages, multiple signatures will be produced, though you can control how many forms to include in a signature with the `--forms` switch. 161 | 162 | This form works well in landscape orientation. 163 | 164 | $ impose -o landscape -l octavo document.pdf 165 | # produces document-imposed.pdf 166 | 167 | 168 | ### Duodecimo (`duodecimo`) 169 | 170 | This form applies twelve pages to each side of a sheet of paper, in the following order: 171 | 172 | Recto (front): 173 | 174 | 175 | 176 | Verso (back): 177 | 178 | 179 | 180 | When printed, the bottommost strip can be separated and folded in quarto, while the remaining octavo is folded per octavo. The octavo is then nested inside the quarto to form the signature. For documents of more than twenty-four pages, multiple signatures will be produced, though you can control how many forms to include in a signature with the `--forms` switch. 181 | 182 | This form is awkward to apply when dealing with standard letter-sized pages, but for non-standard page sizes it may work very well. 183 | 184 | $ impose -l duodecimo document.pdf 185 | # produces document-imposed.pdf 186 | 187 | 188 | ### Duodecimo (Quarto-inside) (`duodecimo-i`) 189 | 190 | This form applies twelve pages to each side of a sheet of paper, in the following order: 191 | 192 | Recto (front): 193 | 194 | 195 | 196 | Verso (back): 197 | 198 | 199 | 200 | When printed, the bottommost strip can be separated and folded in quarto, while the remaining octavo is folded per octavo. The quarto is then nested inside the octavo to form the signature. (Note that this is slightly different than how the `duodecimo` form works!) For documents of more than twenty-four pages, multiple signatures will be produced, though you can control how many forms to include in a signature with the `--forms` switch. 201 | 202 | This form is awkward to apply when dealing with standard letter-sized pages, but for non-standard page sizes it may work very well. 203 | 204 | $ impose -l duodecimo-i document.pdf 205 | # produces document-imposed.pdf 206 | 207 | 208 | ### Duodecimo (Two-Cut) (`duodecimo-2c`) 209 | 210 | This form applies twelve pages to each side of a sheet of paper, in the following order: 211 | 212 | Recto (front): 213 | 214 | 215 | 216 | Verso (back): 217 | 218 | 219 | 220 | When printed, each strip of four can be separated and folded in quarto, with the quartos nested to form the signature. (Note that this is different than how the `duodecimo` form works!) For documents of more than twenty-four pages, multiple signatures will be produced, though you can control how many forms to include in a signature with the `--forms` switch. 221 | 222 | This form is awkward to apply when dealing with standard letter-sized pages, but for non-standard page sizes it may work very well. 223 | 224 | $ impose -l duodecimo-2c document.pdf 225 | # produces document-imposed.pdf 226 | 227 | 228 | ### Sextodecimo (`sextodecimo`) 229 | 230 | This form applies sixteen pages to each side of a sheet of paper, in the following order: 231 | 232 | Recto (front): 233 | 234 | 235 | 236 | Verso (back): 237 | 238 | 239 | 240 | When printed, the sheet is cut in half to form two octavos which, when folded, are nested to form the signature. For documents of more than thirty-two pages, multiple signatures will be produced, though you can control how many forms to include in a signature with the `--forms` switch. 241 | 242 | This form works best in portrait mode when used with standard paper sizes. 243 | 244 | $ impose -o portrait -l sextodecimo document.pdf 245 | # produces document-imposed.pdf 246 | 247 | 248 | ## Author 249 | 250 | Jamis Buck 251 | 252 | 253 | ## License 254 | 255 | This code is released and distributed under the terms of the MIT license. See the associated `MIT-LICENSE` file for details. 256 | -------------------------------------------------------------------------------- /bin/impose: -------------------------------------------------------------------------------- 1 | #!/bin/sh ruby 2 | 3 | require 'optparse' 4 | require 'pdf/impose/builder' 5 | 6 | def parse_page_size(dim) 7 | match = dim.match(/^(\d+)x(\d+)$/) 8 | if match 9 | [match[1].to_i, match[2].to_i] 10 | else 11 | dim 12 | end 13 | end 14 | 15 | def show_available_forms! 16 | puts 'Available forms (with aliases, if any):' 17 | PDF::Impose::Builder::PRIMARY_LAYOUTS.keys.each do |key| 18 | aliases = if PDF::Impose::Builder::ALIASES[key].any? 19 | ' (' + PDF::Impose::Builder::ALIASES[key].join(', ') + ')' 20 | else 21 | '' 22 | end 23 | 24 | puts " * #{key}#{aliases}" 25 | end 26 | 27 | exit(-1) 28 | end 29 | 30 | def show_available_paper_sizes! 31 | puts 'Named paper sizes (with dimensions in points):' 32 | PDF::Core::PageGeometry::SIZES.each do |name, (width, height)| 33 | puts " * #{name} - #{width}x#{height}" 34 | end 35 | 36 | exit(-1) 37 | end 38 | 39 | options = { 40 | layout: 'quarto', 41 | orientation: :portrait, 42 | forms_per_signature: nil, 43 | page_size: 'LETTER', 44 | margin: 36, 45 | start_page: 1, 46 | end_page: nil, 47 | marks: true, 48 | repeat: 1 49 | } 50 | 51 | output_filename = nil 52 | 53 | OptionParser.new do |parser| 54 | parser.banner = "Usage: #{File.basename($PROGRAM_NAME)} [options] " 55 | parser.summary_indent = " " 56 | parser.summary_width = 22 57 | 58 | parser.on('-l', '--layout LAYOUT', 59 | 'The form to use when laying out pages for imposition.', 60 | "Default is \"#{options[:layout]}\".", 61 | '(Specify "list" to see all available forms.)' 62 | ) do |layout| 63 | show_available_forms! if layout == 'list' 64 | options[:layout] = layout 65 | end 66 | 67 | parser.on('-o', '--orient ORIENT', 68 | 'How each sheet should be oriented.', 69 | 'Possible options are "portrait" or "landscape".', 70 | "Default is \"#{options[:orientation]}\"." 71 | ) do |orientation| 72 | options[:orientation] = orientation.to_sym 73 | end 74 | 75 | parser.on('-f', '--forms COUNT', Integer, 76 | 'The number of forms to use for each signature.', 77 | 'Default is dependent on the form used.' 78 | ) do |count| 79 | options[:forms_per_signature] = count 80 | end 81 | 82 | parser.on('-d', '--dimensions DIM', 83 | 'Either the name of a paper size, or a WxH (width/height)', 84 | 'specification. Measurements must be in points.', 85 | "Default is \"#{options[:page_size]}\".", 86 | '(Specify "list" to see all named paper sizes.)' 87 | ) do |dim| 88 | show_available_paper_sizes! if dim == 'list' 89 | options[:page_size] = parse_page_size(dim) 90 | end 91 | 92 | parser.on('-m', '--margin SIZE', Integer, 93 | 'The minimum margin (in points) for the chosen form.', 94 | "Default is #{options[:margin]} points." 95 | ) do |size| 96 | options[:margin] = size 97 | end 98 | 99 | parser.on('-s', '--start PAGE', Integer, 100 | 'The page at which to start imposing.', 101 | "Default is #{options[:start_page]}." 102 | ) do |page| 103 | options[:start_page] = page 104 | end 105 | 106 | parser.on('-e', '--end PAGE', Integer, 107 | 'The page at which to stop imposing.', 108 | "Default is the last page of the source document." 109 | ) do |page| 110 | options[:end_page] = page 111 | end 112 | 113 | parser.on('-M', '--[no-]marks', 114 | 'Whether or not to include registration marks.', 115 | 'Default is to include registration marks.' 116 | ) do |marks| 117 | options[:marks] = marks 118 | end 119 | 120 | parser.on('-r', '--repeat COUNT', Integer, 121 | 'How many times each page should be repeated.', 122 | 'Default is once for each page.' 123 | ) do |count| 124 | options[:repeat] = count 125 | end 126 | 127 | parser.on('-O', '--output FILENAME', 128 | 'The name of the file to which to write the resulting PDF.', 129 | 'Default is the original filename with "imposed" appended.' 130 | ) do |filename| 131 | output_filename = filename 132 | end 133 | 134 | parser.on('-h', '--help', 135 | 'This help screen.' 136 | ) do 137 | puts parser.help 138 | exit(-1) 139 | end 140 | end.parse! 141 | 142 | filename = ARGV.first or abort 'please specify a PDF file to impose' 143 | imposer = PDF::Impose::Builder.new(filename, options) 144 | 145 | output_filename ||= File.basename(filename, '.pdf') + '-imposed.pdf' 146 | imposer.emit output_filename 147 | -------------------------------------------------------------------------------- /examples/minibook.rb: -------------------------------------------------------------------------------- 1 | # This takes a PDF and fits either 72 (MiniBook::Octavo) or 84 2 | # (MiniBook::TripleQuarto) leaves on a single sheet of paper. If 3 | # used with LETTER size paper, this can produce a text block that 4 | # is roughly 1.25 inches by 1 inch. 5 | # 6 | # See https://twitter.com/jamis/status/847497088950599685 7 | 8 | require 'pdf/impose/builder' 9 | 10 | # Notes on book structure: 11 | # 12 | # * blank sheet is glued to cover and first signature (endpapers) 13 | # 14 | # * half-title - page 1 15 | # * blank - 2 (verso of 1) 16 | # * title - 3 17 | # * colophon - 4 (verso of 3) 18 | # * dedication - 5 19 | # * blank - 6 (verso of 5) 20 | # * contents - 7 - end 21 | # 22 | # * blank sheet is glued to cover and last signature (endpapers) 23 | # 24 | # 72 double-sided leaves => 144 pages 25 | # 84 double-sided leaves => 168 pages 26 | # 27 | # Thus, if using a traditional book structure, you reserve the 28 | # first six pages and produce content for the rest (138 or 162). 29 | 30 | module MiniBook 31 | class TripleQuarto < PDF::Impose::Form 32 | per_signature 2 33 | 34 | cut_row 1, 2, 3, 4, 5, 6, 7 35 | cut_col 4, 8 36 | 37 | layout :page_numbers 38 | 39 | recto %w(.24 .1 .4 .21 .20 .5 .8 .17 .16 .9 .12 .13), 40 | %w(.48 .25 .28 .45 .44 .29 .32 .41 .40 .33 .36 .37), 41 | %w(.72 .49 .52 .69 .68 .53 .56 .65 .64 .57 .60 .61), 42 | %w(.96 .73 .76 .93 .92 .77 .80 .89 .88 .81 .84 .85), 43 | %w(.120 .97 .100 .117 .116 .101 .104 .113 .112 .105 .108 .109), 44 | %w(.144 .121 .124 .141 .140 .125 .128 .137 .136 .129 .132 .133), 45 | %w(.168 .145 .148 .165 .164 .149 .152 .161 .160 .153 .156 .157) 46 | 47 | verso %w(.14 .11 .10 .15 .18 .7 .6 .19 .22 .3 .2 .23), 48 | %w(.38 .35 .34 .39 .42 .31 .30 .43 .46 .27 .26 .47), 49 | %w(.62 .59 .58 .63 .66 .55 .54 .67 .70 .51 .50 .71), 50 | %w(.86 .83 .82 .87 .90 .79 .78 .91 .94 .75 .74 .95), 51 | %w(.110 .107 .106 .111 .114 .103 .102 .115 .118 .99 .98 .119), 52 | %w(.134 .131 .130 .135 .138 .127 .126 .139 .142 .123 .122 .143), 53 | %w(.158 .155 .154 .159 .162 .151 .150 .163 .166 .147 .146 .167) 54 | end 55 | 56 | class Octavo < PDF::Impose::Form 57 | per_signature 2 58 | 59 | layout :page_numbers 60 | 61 | cut_row 2, 4 62 | cut_col 4, 8 63 | 64 | recto %w(*1 *16 *9 *8 *17 *32 *25 *24 *33 *48 *41 *40), 65 | %w(.4 .13 .12 .5 .20 .29 .28 .21 .36 .45 .44 .37), 66 | %w(*49 *64 *57 *56 *65 *80 *73 *72 *81 *96 *89 *88), 67 | %w(.52 .61 .60 .53 .68 .77 .76 .69 .84 .93 .92 .85), 68 | %w(*97 *112 *105 *104 *113 *128 *121 *120 *129 *144 *137 *136), 69 | %w(.100 .109 .108 .101 .116 .125 .124 .117 .132 .141 .140 .133) 70 | 71 | verso %w(*39 *42 *47 *34 *23 *26 *31 *18 *7 *10 *15 *2), 72 | %w(.38 .43 .46 .35 .22 .27 .30 .19 .6 .11 .14 .3), 73 | %w(*87 *90 *95 *82 *71 *74 *79 *66 *55 *58 *63 *50), 74 | %w(.86 .91 .94 .83 .70 .75 .78 .67 .54 .59 .62 .51), 75 | %w(*135 *138 *143 *130 *119 *122 *127 *114 *103 *106 *111 *98), 76 | %w(.134 .139 .142 .131 .118 .123 .126 .115 .102 .107 .110 .99) 77 | end 78 | end 79 | 80 | 81 | filename = ARGV[0] or abort 'please specify a filename to read' 82 | 83 | imposer = PDF::Impose::Builder.new(filename, 84 | orientation: :landscape, 85 | layout: MiniBook::Octavo) 86 | 87 | imposer.emit 'minibook.pdf' 88 | -------------------------------------------------------------------------------- /layouts/card-fold4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/card-fold4.pdf -------------------------------------------------------------------------------- /layouts/card-fold4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/card-fold4.png -------------------------------------------------------------------------------- /layouts/card-fold8.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/card-fold8.pdf -------------------------------------------------------------------------------- /layouts/card-fold8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/card-fold8.png -------------------------------------------------------------------------------- /layouts/duodecimo - recto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/duodecimo - recto.png -------------------------------------------------------------------------------- /layouts/duodecimo - verso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/duodecimo - verso.png -------------------------------------------------------------------------------- /layouts/duodecimo-2c - recto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/duodecimo-2c - recto.png -------------------------------------------------------------------------------- /layouts/duodecimo-2c - verso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/duodecimo-2c - verso.png -------------------------------------------------------------------------------- /layouts/duodecimo-2c.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/duodecimo-2c.pdf -------------------------------------------------------------------------------- /layouts/duodecimo-i - recto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/duodecimo-i - recto.png -------------------------------------------------------------------------------- /layouts/duodecimo-i - verso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/duodecimo-i - verso.png -------------------------------------------------------------------------------- /layouts/duodecimo-i.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/duodecimo-i.pdf -------------------------------------------------------------------------------- /layouts/duodecimo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/duodecimo.pdf -------------------------------------------------------------------------------- /layouts/folio - recto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/folio - recto.png -------------------------------------------------------------------------------- /layouts/folio - verso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/folio - verso.png -------------------------------------------------------------------------------- /layouts/folio.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/folio.pdf -------------------------------------------------------------------------------- /layouts/octavo - recto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/octavo - recto.png -------------------------------------------------------------------------------- /layouts/octavo - verso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/octavo - verso.png -------------------------------------------------------------------------------- /layouts/octavo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/octavo.pdf -------------------------------------------------------------------------------- /layouts/quarto - recto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/quarto - recto.png -------------------------------------------------------------------------------- /layouts/quarto - verso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/quarto - verso.png -------------------------------------------------------------------------------- /layouts/quarto.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/quarto.pdf -------------------------------------------------------------------------------- /layouts/sexto - recto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/sexto - recto.png -------------------------------------------------------------------------------- /layouts/sexto - verso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/sexto - verso.png -------------------------------------------------------------------------------- /layouts/sexto.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/sexto.pdf -------------------------------------------------------------------------------- /layouts/sextodecimo - recto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/sextodecimo - recto.png -------------------------------------------------------------------------------- /layouts/sextodecimo - verso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/sextodecimo - verso.png -------------------------------------------------------------------------------- /layouts/sextodecimo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamis/impose/d861a5d503b48993974768ea3b453ff07632a419/layouts/sextodecimo.pdf -------------------------------------------------------------------------------- /lib/pdf/impose/builder.rb: -------------------------------------------------------------------------------- 1 | require 'pdf/reader' 2 | require 'prawn' 3 | require 'prawn/templates' 4 | 5 | require 'pdf/impose/ext' 6 | require 'pdf/impose/forms/card_fold' 7 | require 'pdf/impose/forms/duodecimo' 8 | require 'pdf/impose/forms/folio' 9 | require 'pdf/impose/forms/quarto' 10 | require 'pdf/impose/forms/octavo' 11 | require 'pdf/impose/forms/sexto' 12 | require 'pdf/impose/forms/sextodecimo' 13 | require 'pdf/impose/signature' 14 | 15 | module PDF 16 | module Impose 17 | class Builder 18 | PRIMARY_LAYOUTS = { 19 | 'card-fold4' => PDF::Impose::Forms::CardFold::Quarto, 20 | 'card-fold8' => PDF::Impose::Forms::CardFold::Octavo, 21 | 'folio' => PDF::Impose::Forms::Folio::Standard, 22 | 'folio-1' => PDF::Impose::Forms::Folio::Recto, 23 | 'quarto' => PDF::Impose::Forms::Quarto, 24 | 'sexto' => PDF::Impose::Forms::Sexto, 25 | 'octavo' => PDF::Impose::Forms::Octavo, 26 | 'duodecimo' => PDF::Impose::Forms::Duodecimo::OneCutOutside, 27 | 'duodecimo-i' => PDF::Impose::Forms::Duodecimo::OneCutInside, 28 | 'duodecimo-2c' => PDF::Impose::Forms::Duodecimo::TwoCut, 29 | 'sextodecimo' => PDF::Impose::Forms::Sextodecimo::Nested 30 | }.freeze 31 | 32 | ALIASES = { 33 | 'card-fold4' => %w( card-fold ), 34 | 'card-fold8' => %w( ), 35 | 'folio' => %w( f fo ), 36 | 'folio-1' => %w( f1 fo1 ), 37 | 'quarto' => %w( 4to ), 38 | 'sexto' => %w( 6to 6mo ), 39 | 'octavo' => %w( 8vo octavo ), 40 | 'duodecimo' => %w( twelvemo 12mo ), 41 | 'duodecimo-i' => %w( twelvemo-i 12mo-i ), 42 | 'duodecimo-2c' => %w( twelvemo-2c 12mo-2c ), 43 | 'sextodecimo' => %w( sixteenmo 16mo ) 44 | }.freeze 45 | 46 | LAYOUTS = ALIASES.keys.each_with_object({}) do |key, hash| 47 | hash[key] = PRIMARY_LAYOUTS[key] 48 | ALIASES[key].each { |name| hash[name] = hash[key] } 49 | end.freeze 50 | 51 | # source: the name of a PDF document to lay out 52 | # options: 53 | # layout: quarto, octavo, etc. 54 | # page_size: passed through to Prawn 55 | # orientation: passed through to Prawn as :page_layout 56 | # forms_per_signature: defaults to layout.per_signature 57 | # margin: point size of margin of page (default = 32 points) 58 | # start_page: defaults to 1 59 | # end_page: defaults to last page of source document 60 | # marks: true or false, whether to include registration marks (default true) 61 | # repeat: how many times to repeat each page 62 | def initialize(source, options={}) 63 | layout_arg = options[:layout] 64 | 65 | @layout = if layout_arg.respond_to?(:recto) 66 | layout_arg 67 | else 68 | LAYOUTS[options[:layout].to_s.downcase] 69 | end 70 | 71 | raise "`#{options[:layout]}' is not a supported layout" if @layout.nil? 72 | 73 | @margin = options[:margin] || 36 # half inch 74 | @marks = options.fetch(:marks, true) 75 | 76 | @repeat = options[:repeat] || 1 77 | 78 | @source = PDF::Reader.new(source) 79 | @destination = Prawn::Document.new( 80 | skip_page_creation: true, margin: 0, 81 | page_size: options[:page_size], page_layout: options[:orientation]) 82 | 83 | @start_page = options[:start_page] || 1 84 | @end_page = [options[:end_page] || 1e6, @source.page_count].min 85 | 86 | @page_count = (@end_page - @start_page + 1) * @repeat 87 | 88 | @forms_per_signature = options[:forms_per_signature] || 89 | @layout.per_signature 90 | @pages_per_signature = @forms_per_signature * @layout.pages_per_form 91 | 92 | @signature_count = (@page_count + @pages_per_signature - 1) / 93 | @pages_per_signature 94 | 95 | @signatures = (1..@signature_count).map do |s| 96 | first = (s - 1) * @pages_per_signature 97 | last = first + @pages_per_signature - 1 98 | Signature.new(first, last) 99 | end 100 | 101 | _apply 102 | end 103 | 104 | def emit(filename) 105 | @destination.render_file filename 106 | end 107 | 108 | def _apply 109 | source_width = @source.page(1).page_object[:MediaBox][2] 110 | source_height = @source.page(1).page_object[:MediaBox][3] 111 | 112 | height = @destination.margin_box.height 113 | 114 | sheet_width = @destination.margin_box.width - @margin * 2 115 | sheet_height = height - @margin * 2 116 | 117 | max_cell_width = sheet_width / @layout.columns_per_form 118 | max_cell_height = sheet_height / @layout.rows_per_form 119 | 120 | width_ratio = max_cell_width.to_f / source_width 121 | height_ratio = max_cell_height.to_f / source_height 122 | scale = [width_ratio, height_ratio].min 123 | 124 | cell_width = source_width * scale 125 | cell_height = source_height * scale 126 | 127 | form_width = cell_width * @layout.columns_per_form 128 | form_height = cell_height * @layout.rows_per_form 129 | 130 | pages = (@start_page..@end_page).flat_map { |n| [n - 1] * @repeat } 131 | 132 | recto = true 133 | @layout.layout_signatures(@signatures) do |form| 134 | left = if recto 135 | @destination.margin_box.width - @margin - form_width 136 | else 137 | @margin 138 | end 139 | 140 | @destination.start_new_page 141 | 142 | _draw_guides(recto, form_width, form_height) if @marks 143 | 144 | form.each_page do |page| 145 | x = page.column * cell_width + left 146 | y = height - (page.row + 1) * cell_height - @margin 147 | @destination.page.import_page @source, pages[page.number], 148 | x, y, cell_width, cell_height, 149 | page.mirror? 150 | end 151 | 152 | recto = !recto 153 | end 154 | 155 | self 156 | end 157 | 158 | def _draw_guides(recto, width, height) 159 | if recto 160 | x2 = @destination.margin_box.width - @margin 161 | x1 = x2 - width 162 | else 163 | x1 = @margin 164 | x2 = x1 + width 165 | end 166 | 167 | y2 = @destination.margin_box.height - @margin 168 | y1 = y2 - height 169 | 170 | l1 = x1 - @margin / 2.0 171 | l2 = l1 + @margin / 4.0 172 | r1 = x2 + @margin / 4.0 173 | r2 = x2 + @margin / 2.0 174 | 175 | b1 = y1 - @margin / 2.0 176 | b2 = y1 - @margin / 4.0 177 | t1 = y2 + @margin / 4.0 178 | t2 = y2 + @margin / 2.0 179 | 180 | cx = recto ? (r1 + r2) / 2 : (l1 + l2) / 2 181 | cy = (t1 + t2) / 2 182 | radius = (r2 - r1) / 2 183 | 184 | @destination.stroke do 185 | @destination.line([l1, y1], [l2, y1]) 186 | @destination.line([l1, y2], [l2, y2]) 187 | @destination.line([r1, y1], [r2, y1]) 188 | @destination.line([r1, y2], [r2, y2]) 189 | 190 | @destination.line([x1, t1], [x1, t2]) 191 | @destination.line([x2, t1], [x2, t2]) 192 | @destination.line([x1, b1], [x1, b2]) 193 | @destination.line([x2, b1], [x2, b2]) 194 | 195 | @destination.circle([cx, cy], radius) 196 | @destination.line([cx - 1.5 * radius, cy], [cx + 1.5 * radius, cy]) 197 | @destination.line([cx, cy - 1.5 * radius], [cx, cy + 1.5 * radius]) 198 | end 199 | 200 | cut_rows = @layout.cut_row || [] 201 | if cut_rows.any? 202 | row_height = height.to_f / @layout.rows_per_form 203 | 204 | @destination.stroke do 205 | cut_rows.each do |row| 206 | y = y2 - row_height * row 207 | @destination.line([l1, y], [l2, y]) 208 | @destination.line([r1, y], [r2, y]) 209 | end 210 | end 211 | end 212 | 213 | cut_cols = @layout.cut_col || [] 214 | if cut_cols.any? 215 | col_width = width.to_f / @layout.columns_per_form 216 | 217 | @destination.stroke do 218 | cut_cols.each do |col| 219 | x = x1 + col_width * col 220 | @destination.line([x, t1], [x, t2]) 221 | @destination.line([x, b1], [x, b2]) 222 | end 223 | end 224 | end 225 | end 226 | end 227 | end 228 | end 229 | -------------------------------------------------------------------------------- /lib/pdf/impose/ext.rb: -------------------------------------------------------------------------------- 1 | module PDF 2 | module Impose 3 | module Ext 4 | module Reference 5 | def extract_content_stream 6 | content = stream 7 | content = content.filtered_stream if content.respond_to?(:filtered_stream) 8 | 9 | if data[:Filter] 10 | options = [] 11 | 12 | if data[:DecodeParams].is_a?(Hash) 13 | options = [data[:DecodeParams]] 14 | elsif data[:DecodeParams] 15 | options = data[:DecodeParams] 16 | end 17 | 18 | Array(data[:Filter]).each_with_index do |filter, index| 19 | content = PDF::Reader::Filter. 20 | with(filter, options[index]).filter(content) 21 | end 22 | end 23 | 24 | content 25 | end 26 | end 27 | 28 | module Page 29 | def merge_object_resources(object) 30 | object_resources = document.deref(object.data[:Resources]) 31 | 32 | object_resources.keys.each do |resource| 33 | case object_resources[resource] 34 | when ::Hash then 35 | resources[resource] ||= {} 36 | resources[resource].update(object_resources[resource]) 37 | when ::Array then 38 | resources[resource] ||= [] 39 | resources[resource] |= object_resources[resource] 40 | when PDF::Core::Reference then 41 | resources[resource] = object_resources[resource] 42 | else 43 | klass = object_resources[resource].class 44 | abort "unknown resource type #{klass} for #{resource}" 45 | end 46 | end 47 | end 48 | 49 | def import_page(reader, page_number, dx, dy, width, height, mirror) 50 | return unless page_number 51 | 52 | ref = reader.objects.page_references[page_number] 53 | return unless ref 54 | 55 | object = document.state.store. 56 | send(:load_object_graph, reader.objects, ref) 57 | 58 | merge_object_resources object 59 | 60 | contents = object.data[:Contents] 61 | contents = [contents] unless contents.is_a?(Array) 62 | 63 | contents.each do |content| 64 | content = content.extract_content_stream 65 | box = object.data[:MediaBox] 66 | 67 | scale = 1.0 68 | width_ratio = width.to_f / box[2] 69 | height_ratio = height.to_f / box[3] 70 | 71 | scale = [width_ratio, height_ratio].min 72 | 73 | document.save_graphics_state do 74 | document.translate dx, dy 75 | document.scale scale 76 | 77 | if mirror 78 | document.translate box[2], box[3] 79 | document.rotate 180 80 | end 81 | 82 | document.add_content content 83 | end 84 | end 85 | end 86 | end 87 | end 88 | end 89 | end 90 | 91 | require 'pdf/core' 92 | 93 | PDF::Core::Reference.include PDF::Impose::Ext::Reference 94 | PDF::Core::Page.include PDF::Impose::Ext::Page 95 | -------------------------------------------------------------------------------- /lib/pdf/impose/form.rb: -------------------------------------------------------------------------------- 1 | require 'pdf/impose/page' 2 | 3 | module PDF 4 | module Impose 5 | # A Form represents a collection of pages that will be printed together on a 6 | # single sheet of paper, in a particular (grid) layout. 7 | # 8 | # Form subclasses define the layout of individual pages via the `recto` and 9 | # `verso` methods (`recto` == front, `verso` == back). 10 | # 11 | # class Quarto < Form 12 | # per_signature 4 13 | # 14 | # recto %w(3* *3), 15 | # %w(0. .0) 16 | # 17 | # verso %w(1* *1), 18 | # %w(2. .2) 19 | # end 20 | # 21 | # Each row of the recto/verso configuration is a series of cells, representing 22 | # individual pages. They take the following format: 23 | # 24 | # *n OR .n OR n* OR n. 25 | # 26 | # 'n' is which pair from the signature is to be set in this position. If the 27 | # dot or asterisk comes before the number, then the first element of the pair 28 | # is used (the lower page number). If the dot or asterisk comes after, then 29 | # the last element of the pair is used (the higher page number). 30 | # 31 | # A '.' means the page is set without rotation. A '*' means the page is 32 | # rotated 180 degrees to turn it upside down. 33 | 34 | class Form 35 | class < 2.0' 25 | s.add_dependency 'prawn-templates', '~> 0.0' 26 | s.add_dependency 'pdf-reader', '~> 1.4' 27 | end 28 | --------------------------------------------------------------------------------