pdf-impose 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d1c61ffc5f76f744484e97641eb3a42934e3517d
4
+ data.tar.gz: 30e272a0ec02dd406fa6fabad2e1ca7c3539c6fe
5
+ SHA512:
6
+ metadata.gz: 98ec292d86c84b64742c5b76502cde1d67136118b108ddeb5a27d201cab34f0b9dbd9adecf216a5e5e5073fd418ad951c573ebf63e17c0830fb48e5c5450dbc1
7
+ data.tar.gz: 73f462fafcaab365289ed4f3d7439addaf44cc70322a20535607e33270a169a2c9b2cd9da6c91f5c3212617b6edb192607ea207d29f6d7103d1edd9bbb6c5600
@@ -0,0 +1,20 @@
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.
@@ -0,0 +1,255 @@
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] <input.pdf>
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
+ <img src="layouts/card-fold4.png" width="200" />
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
+ <img src="layouts/card-fold8.png" width="400" />
81
+
82
+ When printed, the page can be folded in half three times to make a simple pamphlet of the original eight pages. 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
+ <img src="layouts/folio - recto.png" width="200" />
95
+
96
+ Verso (back):
97
+
98
+ <img src="layouts/folio - verso.png" width="200" />
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
+ <img src="layouts/quarto - recto.png" width="200" />
115
+
116
+ Verso (back):
117
+
118
+ <img src="layouts/quarto - verso.png" width="200" />
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
+ <img src="layouts/sexto - recto.png" width="200" />
135
+
136
+ Verso (back):
137
+
138
+ <img src="layouts/sexto - verso.png" width="200" />
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
+ <img src="layouts/octavo - recto.png" width="400" />
155
+
156
+ Verso (back):
157
+
158
+ <img src="layouts/octavo - verso.png" width="400" />
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
+ <img src="layouts/duodecimo - recto.png" width="200" />
175
+
176
+ Verso (back):
177
+
178
+ <img src="layouts/duodecimo - verso.png" width="200" />
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
+ <img src="layouts/duodecimo-i - recto.png" width="200" />
195
+
196
+ Verso (back):
197
+
198
+ <img src="layouts/duodecimo-i - verso.png" width="200" />
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
+ <img src="layouts/duodecimo-2c - recto.png" width="200" />
215
+
216
+ Verso (back):
217
+
218
+ <img src="layouts/duodecimo-2c - verso.png" width="200" />
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
+ <img src="layouts/sextodecimo - recto.png" width="200" />
235
+
236
+ Verso (back):
237
+
238
+ <img src="layouts/sextodecimo - verso.png" width="200" />
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 <jamis@jamisbuck.org>
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.
@@ -0,0 +1,138 @@
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
+ }
49
+
50
+ output_filename = nil
51
+
52
+ OptionParser.new do |parser|
53
+ parser.banner = "Usage: #{File.basename($PROGRAM_NAME)} [options] <input.pdf>"
54
+ parser.summary_indent = " "
55
+ parser.summary_width = 22
56
+
57
+ parser.on('-l', '--layout LAYOUT',
58
+ 'The form to use when laying out pages for imposition.',
59
+ "Default is \"#{options[:layout]}\".",
60
+ '(Specify "list" to see all available forms.)'
61
+ ) do |layout|
62
+ show_available_forms! if layout == 'list'
63
+ options[:layout] = layout
64
+ end
65
+
66
+ parser.on('-o', '--orient ORIENT',
67
+ 'How each sheet should be oriented.',
68
+ 'Possible options are "portrait" or "landscape".',
69
+ "Default is \"#{options[:orientation]}\"."
70
+ ) do |orientation|
71
+ options[:orientation] = orientation.to_sym
72
+ end
73
+
74
+ parser.on('-f', '--forms COUNT', Integer,
75
+ 'The number of forms to use for each signature.',
76
+ 'Default is dependent on the form used.'
77
+ ) do |count|
78
+ options[:forms_per_signature] = count
79
+ end
80
+
81
+ parser.on('-d', '--dimensions DIM',
82
+ 'Either the name of a paper size, or a WxH (width/height)',
83
+ 'specification. Measurements must be in points.',
84
+ "Default is \"#{options[:page_size]}\".",
85
+ '(Specify "list" to see all named paper sizes.)'
86
+ ) do |dim|
87
+ show_available_paper_sizes! if dim == 'list'
88
+ options[:page_size] = parse_page_size(dim)
89
+ end
90
+
91
+ parser.on('-m', '--margin SIZE', Integer,
92
+ 'The minimum margin (in points) for the chosen form.',
93
+ "Default is #{options[:margin]} points."
94
+ ) do |size|
95
+ options[:margin] = size
96
+ end
97
+
98
+ parser.on('-s', '--start PAGE', Integer,
99
+ 'The page at which to start imposing.',
100
+ "Default is #{options[:start_page]}."
101
+ ) do |page|
102
+ options[:start_page] = page
103
+ end
104
+
105
+ parser.on('-e', '--end PAGE', Integer,
106
+ 'The page at which to stop imposing.',
107
+ "Default is the last page of the source document."
108
+ ) do |page|
109
+ options[:end_page] = page
110
+ end
111
+
112
+ parser.on('-M', '--[no-]marks',
113
+ 'Whether or not to include registration marks.',
114
+ 'Default is to include registration marks.'
115
+ ) do |marks|
116
+ options[:marks] = marks
117
+ end
118
+
119
+ parser.on('-O', '--output FILENAME',
120
+ 'The name of the file to which to write the resulting PDF.',
121
+ 'Default is the original filename with "imposed" appended.'
122
+ ) do |filename|
123
+ output_filename = filename
124
+ end
125
+
126
+ parser.on('-h', '--help',
127
+ 'This help screen.'
128
+ ) do
129
+ puts parser.help
130
+ exit(-1)
131
+ end
132
+ end.parse!
133
+
134
+ filename = ARGV.first or abort 'please specify a PDF file to impose'
135
+ imposer = PDF::Impose::Builder.new(filename, options)
136
+
137
+ output_filename ||= File.basename(filename, '.pdf') + '-imposed.pdf'
138
+ imposer.emit output_filename
@@ -0,0 +1,221 @@
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,
22
+ 'quarto' => PDF::Impose::Forms::Quarto,
23
+ 'sexto' => PDF::Impose::Forms::Sexto,
24
+ 'octavo' => PDF::Impose::Forms::Octavo,
25
+ 'duodecimo' => PDF::Impose::Forms::Duodecimo::OneCutOutside,
26
+ 'duodecimo-i' => PDF::Impose::Forms::Duodecimo::OneCutInside,
27
+ 'duodecimo-2c' => PDF::Impose::Forms::Duodecimo::TwoCut,
28
+ 'sextodecimo' => PDF::Impose::Forms::Sextodecimo::Nested
29
+ }.freeze
30
+
31
+ ALIASES = {
32
+ 'card-fold4' => %w( card-fold ),
33
+ 'card-fold8' => %w( ),
34
+ 'folio' => %w( f fo ),
35
+ 'quarto' => %w( 4to ),
36
+ 'sexto' => %w( 6to 6mo ),
37
+ 'octavo' => %w( 8vo octavo ),
38
+ 'duodecimo' => %w( twelvemo 12mo ),
39
+ 'duodecimo-i' => %w( twelvemo-i 12mo-i ),
40
+ 'duodecimo-2c' => %w( twelvemo-2c 12mo-2c ),
41
+ 'sextodecimo' => %w( sixteenmo 16mo )
42
+ }.freeze
43
+
44
+ LAYOUTS = ALIASES.keys.each_with_object({}) do |key, hash|
45
+ hash[key] = PRIMARY_LAYOUTS[key]
46
+ ALIASES[key].each { |name| hash[name] = hash[key] }
47
+ end.freeze
48
+
49
+ # source: the name of a PDF document to lay out
50
+ # options:
51
+ # layout: quarto, octavo, etc.
52
+ # page_size: passed through to Prawn
53
+ # orientation: passed through to Prawn as :page_layout
54
+ # forms_per_signature: defaults to layout.per_signature
55
+ # margin: point size of margin of page (default = 32 points)
56
+ # start_page: defaults to 1
57
+ # end_page: defaults to last page of source document
58
+ # marks: true or false, whether to include registration marks (default true)
59
+ def initialize(source, options={})
60
+ layout_arg = options[:layout]
61
+
62
+ @layout = if layout_arg.respond_to?(:recto)
63
+ layout_arg
64
+ else
65
+ LAYOUTS[options[:layout].to_s.downcase]
66
+ end
67
+
68
+ raise "`#{options[:layout]}' is not a supported layout" if @layout.nil?
69
+
70
+ @margin = options[:margin] || 36 # half inch
71
+ @marks = options.fetch(:marks, true)
72
+
73
+ @source = PDF::Reader.new(source)
74
+ @destination = Prawn::Document.new(
75
+ skip_page_creation: true, margin: 0,
76
+ page_size: options[:page_size], page_layout: options[:orientation])
77
+
78
+ @start_page = options[:start_page] || 1
79
+ @end_page = [options[:end_page] || 1e6, @source.page_count].min
80
+
81
+ @page_count = @end_page - @start_page + 1
82
+
83
+ @forms_per_signature = options[:forms_per_signature] ||
84
+ @layout.per_signature
85
+ @pages_per_signature = @forms_per_signature * @layout.pages_per_form
86
+
87
+ @signature_count = (@page_count + @pages_per_signature - 1) /
88
+ @pages_per_signature
89
+
90
+ @signatures = (1..@signature_count).map do |s|
91
+ first = (s - 1) * @pages_per_signature + @start_page
92
+ last = first + @pages_per_signature - 1
93
+ Signature.new(first, last)
94
+ end
95
+
96
+ _apply
97
+ end
98
+
99
+ def emit(filename)
100
+ @destination.render_file filename
101
+ end
102
+
103
+ def _apply
104
+ source_width = @source.page(1).page_object[:MediaBox][2]
105
+ source_height = @source.page(1).page_object[:MediaBox][3]
106
+
107
+ height = @destination.margin_box.height
108
+
109
+ sheet_width = @destination.margin_box.width - @margin * 2
110
+ sheet_height = height - @margin * 2
111
+
112
+ max_cell_width = sheet_width / @layout.columns_per_form
113
+ max_cell_height = sheet_height / @layout.rows_per_form
114
+
115
+ width_ratio = max_cell_width.to_f / source_width
116
+ height_ratio = max_cell_height.to_f / source_height
117
+ scale = [width_ratio, height_ratio].min
118
+
119
+ cell_width = source_width * scale
120
+ cell_height = source_height * scale
121
+
122
+ form_width = cell_width * @layout.columns_per_form
123
+ form_height = cell_height * @layout.rows_per_form
124
+
125
+ recto = true
126
+ @layout.layout_signatures(@signatures) do |form|
127
+ left = if recto
128
+ @destination.margin_box.width - @margin - form_width
129
+ else
130
+ @margin
131
+ end
132
+
133
+ @destination.start_new_page
134
+
135
+ _draw_guides(recto, form_width, form_height) if @marks
136
+
137
+ form.each_page do |page|
138
+ x = page.column * cell_width + left
139
+ y = height - (page.row + 1) * cell_height - @margin
140
+ @destination.page.import_page @source, page.number - 1,
141
+ x, y, cell_width, cell_height,
142
+ page.mirror?
143
+ end
144
+
145
+ recto = !recto
146
+ end
147
+
148
+ self
149
+ end
150
+
151
+ def _draw_guides(recto, width, height)
152
+ if recto
153
+ x2 = @destination.margin_box.width - @margin
154
+ x1 = x2 - width
155
+ else
156
+ x1 = @margin
157
+ x2 = x1 + width
158
+ end
159
+
160
+ y2 = @destination.margin_box.height - @margin
161
+ y1 = y2 - height
162
+
163
+ l1 = x1 - @margin / 2.0
164
+ l2 = l1 + @margin / 4.0
165
+ r1 = x2 + @margin / 4.0
166
+ r2 = x2 + @margin / 2.0
167
+
168
+ b1 = y1 - @margin / 2.0
169
+ b2 = y1 - @margin / 4.0
170
+ t1 = y2 + @margin / 4.0
171
+ t2 = y2 + @margin / 2.0
172
+
173
+ cx = recto ? (r1 + r2) / 2 : (l1 + l2) / 2
174
+ cy = (t1 + t2) / 2
175
+ radius = (r2 - r1) / 2
176
+
177
+ @destination.stroke do
178
+ @destination.line([l1, y1], [l2, y1])
179
+ @destination.line([l1, y2], [l2, y2])
180
+ @destination.line([r1, y1], [r2, y1])
181
+ @destination.line([r1, y2], [r2, y2])
182
+
183
+ @destination.line([x1, t1], [x1, t2])
184
+ @destination.line([x2, t1], [x2, t2])
185
+ @destination.line([x1, b1], [x1, b2])
186
+ @destination.line([x2, b1], [x2, b2])
187
+
188
+ @destination.circle([cx, cy], radius)
189
+ @destination.line([cx - 1.5 * radius, cy], [cx + 1.5 * radius, cy])
190
+ @destination.line([cx, cy - 1.5 * radius], [cx, cy + 1.5 * radius])
191
+ end
192
+
193
+ cut_rows = @layout.cut_row || []
194
+ if cut_rows.any?
195
+ row_height = height.to_f / @layout.rows_per_form
196
+
197
+ @destination.stroke do
198
+ cut_rows.each do |row|
199
+ y = y2 - row_height * row
200
+ @destination.line([l1, y], [l2, y])
201
+ @destination.line([r1, y], [r2, y])
202
+ end
203
+ end
204
+ end
205
+
206
+ cut_cols = @layout.cut_col || []
207
+ if cut_cols.any?
208
+ col_width = width.to_f / @layout.columns_per_form
209
+
210
+ @destination.stroke do
211
+ cut_cols.each do |col|
212
+ x = x1 + col_width * col
213
+ @destination.line([x, t1], [x, t2])
214
+ @destination.line([x, b1], [x, b2])
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,92 @@
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
+ ref = reader.objects.page_references[page_number]
51
+ return unless ref
52
+
53
+ object = document.state.store.
54
+ send(:load_object_graph, reader.objects, ref)
55
+
56
+ merge_object_resources object
57
+
58
+ contents = object.data[:Contents]
59
+ contents = [contents] unless contents.is_a?(Array)
60
+
61
+ contents.each do |content|
62
+ content = content.extract_content_stream
63
+ box = object.data[:MediaBox]
64
+
65
+ scale = 1.0
66
+ width_ratio = width.to_f / box[2]
67
+ height_ratio = height.to_f / box[3]
68
+
69
+ scale = [width_ratio, height_ratio].min
70
+
71
+ document.save_graphics_state do
72
+ document.translate dx, dy
73
+ document.scale scale
74
+
75
+ if mirror
76
+ document.translate box[2], box[3]
77
+ document.rotate 180
78
+ end
79
+
80
+ document.add_content content
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ require 'pdf/core'
90
+
91
+ PDF::Core::Reference.include PDF::Impose::Ext::Reference
92
+ PDF::Core::Page.include PDF::Impose::Ext::Page
@@ -0,0 +1,149 @@
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 <<self
36
+ attr_reader :rows_per_form, :columns_per_form
37
+
38
+ def per_signature(n = nil)
39
+ @per_signature = n || @per_signature
40
+ end
41
+
42
+ def pages_per_form
43
+ rows_per_form * columns_per_form
44
+ end
45
+
46
+ def cut_row(*rows)
47
+ @cut_row = rows.any? ? rows : @cut_row
48
+ end
49
+
50
+ def cut_col(*cols)
51
+ @cut_col = cols.any? ? cols : @cut_col
52
+ end
53
+
54
+ # method can be :pages or :signatures (the default), and determines what
55
+ # the numbers in the recto/verso layouts represent.
56
+ def layout(method = nil)
57
+ @layout = method || @layout
58
+ end
59
+
60
+ def recto(*rows)
61
+ if rows.any?
62
+ @recto = _parse_configuration(rows)
63
+ else
64
+ @recto
65
+ end
66
+ end
67
+
68
+ def verso(*rows)
69
+ if rows.any?
70
+ @verso = _parse_configuration(rows)
71
+ else
72
+ @verso
73
+ end
74
+ end
75
+
76
+ def layout_signatures(signatures)
77
+ l = layout || :signatures
78
+ signatures.each do |signature|
79
+ offset = 0
80
+ while offset < signature.pairs.length
81
+ [recto, verso].compact.each do |side|
82
+ pages = []
83
+
84
+ side.each do |element|
85
+ n = if l == :signatures
86
+ idx = offset + element.offset
87
+ signature.pairs[idx].send(element.which)
88
+ else
89
+ signature.pairs[offset].first + element.offset - 1
90
+ end
91
+
92
+ pages << PDF::Impose::Page.new(element.column, element.row,
93
+ n, element.flip)
94
+ end
95
+
96
+ yield self.new(pages)
97
+ end
98
+
99
+ offset += pages_per_form
100
+ end
101
+ end
102
+
103
+ self
104
+ end
105
+
106
+ class LayoutElement < Struct.new(:column, :row, :offset, :which, :flip)
107
+ end
108
+
109
+ def _parse_configuration(rows)
110
+ @rows_per_form = rows.length
111
+ @columns_per_form = rows[0].length
112
+
113
+ [].tap do |config|
114
+ rows.each.with_index do |columns, row|
115
+ columns.each.with_index do |cell, column|
116
+ if cell !~ /^\s*([*.])?(\d+)([*.])?\s*$/
117
+ raise "invalid form specification: #{cell.inspect}"
118
+ end
119
+
120
+ first = $1
121
+ ofs = $2.to_i
122
+ last = $3
123
+
124
+ if first.nil? && last.nil?
125
+ raise "invalid form specification: #{cell.inspect}"
126
+ end
127
+
128
+ flip = (first || last) == '*'
129
+ config << LayoutElement.new(column, row, ofs,
130
+ first ? :first : :last, flip)
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ attr_reader :pages
138
+
139
+ def initialize(pages)
140
+ @pages = pages
141
+ end
142
+
143
+ def each_page
144
+ @pages.each { |page| yield page }
145
+ self
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,25 @@
1
+ require 'pdf/impose/form'
2
+
3
+ module PDF
4
+ module Impose
5
+ module Forms
6
+ module CardFold
7
+ class Octavo < Form
8
+ per_signature 1
9
+
10
+ layout :page_numbers
11
+
12
+ recto %w(*3 *2 *1 *8),
13
+ %w(.4 .5 .6 .7)
14
+ end
15
+
16
+ class Quarto < Form
17
+ per_signature 1
18
+
19
+ recto %w(*0 0*),
20
+ %w(.1 1.)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,67 @@
1
+ require 'pdf/impose/form'
2
+
3
+ module PDF
4
+ module Impose
5
+ module Forms
6
+ module Duodecimo
7
+ # cut into three 4to strips
8
+ class TwoCut < Form
9
+ per_signature 2
10
+
11
+ cut_row 1, 2
12
+
13
+ recto %w(.3 3. 0. .0),
14
+ %w(.7 7. 4. .4),
15
+ %w(.11 11. 8. .8)
16
+
17
+ verso %w(.1 1. 2. .2),
18
+ %w(.5 5. 6. .6),
19
+ %w(.9 9. 10. .10)
20
+ end
21
+
22
+ # one-cut, 4to on outside
23
+ class OneCutOutside < Form
24
+ per_signature 2
25
+
26
+ cut_row 2
27
+
28
+ recto %w(*4 4* 11* *11),
29
+ %w(.7 7. 8. .8),
30
+ %w(.3 3. 0. .0)
31
+
32
+ verso %w(*10 10* 5* *5),
33
+ %w(.9 9. 6. .6),
34
+ %w(.1 1. 2. .2)
35
+ end
36
+
37
+ # one-cut, 4to on inside
38
+ class OneCutInside < Form
39
+ per_signature 2
40
+
41
+ cut_row 2
42
+
43
+ recto %w(*0 0* 7* *7),
44
+ %w(.3 3. 4. .4),
45
+ %w(.11 11. 8. .8)
46
+
47
+ verso %w(*6 6* 1* *1),
48
+ %w(.5 5. 2. .2),
49
+ %w(.9 9. 10. .10)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ # 0 | 1 . 24
57
+ # 1 | 2 . 23
58
+ # 2 | 3 . 22
59
+ # 3 | 4 . 21
60
+ # 4 | 5 . 20
61
+ # 5 | 6 . 19
62
+ # 6 | 7 . 18
63
+ # 7 | 8 . 17
64
+ # 8 | 9 . 16
65
+ # 9 | 10 . 15
66
+ # 10 | 11 . 14
67
+ # 11 | 12 . 13
@@ -0,0 +1,14 @@
1
+ require 'pdf/impose/form'
2
+
3
+ module PDF
4
+ module Impose
5
+ module Forms
6
+ class Folio < Form
7
+ per_signature 8
8
+
9
+ recto %w(0. .0)
10
+ verso %w(.1 1.)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,26 @@
1
+ require 'pdf/impose/form'
2
+
3
+ module PDF
4
+ module Impose
5
+ module Forms
6
+ class Octavo < Form
7
+ per_signature 2
8
+
9
+ recto %w(3* *3 *0 0*),
10
+ %w(4. .4 .7 7.)
11
+
12
+ verso %w(1* *1 *2 2*),
13
+ %w(6. .6 .5 5.)
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ # 0 | 1 . 16
20
+ # 1 | 2 . 15
21
+ # 2 | 3 . 14
22
+ # 3 | 4 . 13
23
+ # 4 | 5 . 12
24
+ # 5 | 6 . 11
25
+ # 6 | 7 . 10
26
+ # 7 | 8 . 9
@@ -0,0 +1,22 @@
1
+ require 'pdf/impose/form'
2
+
3
+ module PDF
4
+ module Impose
5
+ module Forms
6
+ class Quarto < Form
7
+ per_signature 4
8
+
9
+ recto %w(3* *3),
10
+ %w(0. .0)
11
+
12
+ verso %w(*2 2*),
13
+ %w(.1 1.)
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ # 0 | 1 . 8
20
+ # 1 | 2 . 7
21
+ # 2 | 3 . 6
22
+ # 3 | 4 . 5
@@ -0,0 +1,26 @@
1
+ require 'pdf/impose/form'
2
+
3
+ module PDF
4
+ module Impose
5
+ module Forms
6
+ class Sexto < Form
7
+ per_signature 4
8
+
9
+ recto %w(0. .0),
10
+ %w(3* *3),
11
+ %w(4. .4)
12
+
13
+ verso %w(.1 1.),
14
+ %w(*2 2*),
15
+ %w(.5 5.)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ # 0 | 1 . 12
22
+ # 1 | 2 . 11
23
+ # 2 | 3 . 10
24
+ # 3 | 4 . 9
25
+ # 4 | 5 . 8
26
+ # 5 | 6 . 7
@@ -0,0 +1,43 @@
1
+ require 'pdf/impose/form'
2
+
3
+ module PDF
4
+ module Impose
5
+ module Forms
6
+ module Sextodecimo
7
+ # two 8vo forms that nest one inside the other
8
+ class Nested < Form
9
+ per_signature 2
10
+
11
+ cut_row 2
12
+
13
+ recto %w(7* *7 *0 0*),
14
+ %w(4. .4 .3 3.),
15
+ %w(15* *15 *8 8*),
16
+ %w(12. .12 .11 11.)
17
+
18
+ verso %w(1* *1 *6 6*),
19
+ %w(2. .2 .5 5.),
20
+ %w(9* *9 *14 14*),
21
+ %w(10. .10 .13 13.)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ # 0 | 1 . 32
29
+ # 1 | 2 . 31
30
+ # 2 | 3 . 30
31
+ # 3 | 4 . 29
32
+ # 4 | 5 . 28
33
+ # 5 | 6 . 27
34
+ # 6 | 7 . 26
35
+ # 7 | 8 . 25
36
+ # 8 | 9 . 24
37
+ # 9 | 10 . 23
38
+ # 10 | 11 . 22
39
+ # 11 | 12 . 21
40
+ # 12 | 13 . 20
41
+ # 13 | 14 . 19
42
+ # 14 | 15 . 18
43
+ # 15 | 16 . 17
@@ -0,0 +1,22 @@
1
+ module PDF
2
+ module Impose
3
+ # A Page represents a single page from the source document. It
4
+ # indicates the page (by number), as well as where on the form (column/row)
5
+ # it should go, and whether it should be mirrored (inverted, by rotation)
6
+ # on the form.
7
+ class Page
8
+ attr_reader :column, :row
9
+ attr_reader :number
10
+ attr_reader :mirror
11
+
12
+ alias mirror? mirror
13
+
14
+ def initialize(column, row, number, mirror = false)
15
+ @column = column
16
+ @row = row
17
+ @number = number
18
+ @mirror = mirror
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ module PDF
2
+ module Impose
3
+ # A signature is composed of one or more forms.
4
+ class Signature
5
+ attr_reader :first, :last, :pairs
6
+
7
+ def initialize(first, last)
8
+ @first = first
9
+ @last = last
10
+
11
+ @pairs = []
12
+ f, l = first, last
13
+ while f < l
14
+ @pairs << [f, l]
15
+ f += 1
16
+ l -= 1
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ module PDF
2
+ module Impose
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pdf-impose
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jamis Buck
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-04-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: prawn
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: prawn-templates
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pdf-reader
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.4'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.4'
55
+ description: |2
56
+ Arrange pages of existing PDF documents so they fit on a single page, and
57
+ so they can be folded and cut to produce signatures that may then be assembled
58
+ to form a bound book.
59
+ email:
60
+ - jamis@jamisbuck.org
61
+ executables:
62
+ - impose
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - MIT-LICENSE
67
+ - README.md
68
+ - bin/impose
69
+ - lib/pdf/impose/builder.rb
70
+ - lib/pdf/impose/ext.rb
71
+ - lib/pdf/impose/form.rb
72
+ - lib/pdf/impose/forms/card_fold.rb
73
+ - lib/pdf/impose/forms/duodecimo.rb
74
+ - lib/pdf/impose/forms/folio.rb
75
+ - lib/pdf/impose/forms/octavo.rb
76
+ - lib/pdf/impose/forms/quarto.rb
77
+ - lib/pdf/impose/forms/sexto.rb
78
+ - lib/pdf/impose/forms/sextodecimo.rb
79
+ - lib/pdf/impose/page.rb
80
+ - lib/pdf/impose/signature.rb
81
+ - lib/pdf/impose/version.rb
82
+ homepage: http://github.com/jamis/impose
83
+ licenses:
84
+ - MIT
85
+ metadata: {}
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubyforge_project:
102
+ rubygems_version: 2.5.1
103
+ signing_key:
104
+ specification_version: 4
105
+ summary: A utility and library for imposition -- arranging pages on a sheet of paper
106
+ for optimal printing
107
+ test_files: []