minicomic 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/README +106 -0
  2. data/Rakefile +39 -0
  3. data/lib/minicomic.rb +447 -0
  4. data/test/test_all.rb +0 -0
  5. metadata +49 -0
data/README ADDED
@@ -0,0 +1,106 @@
1
+ = minicomic
2
+
3
+ +minicomic+ is a library providing a set of rake rules for building
4
+ print and web-ready files for minicomics from a set of SVG files.
5
+
6
+ Currently, it is hard-coded for generating 5.5x8.5in format black-and-white
7
+ comics of the sort most suitable for inexpensive photocopying.
8
+
9
+ == requirements
10
+
11
+ In addition to the obvious requirements for Ruby and Rake, +minicomic+
12
+ requires the following software to be installed and available on your
13
+ shell's path:
14
+
15
+ * Inkscape
16
+ * psutils
17
+ * Ghostscript
18
+ * ImageMagick
19
+ * pngcrush
20
+
21
+ If you're on Ubuntu, these correspond to the +inkscape+, +psutils+,
22
+ +gs-gpl+, +imagemagick+ and +pngcrush+ packages, respectively.
23
+
24
+ == usage
25
+
26
+ The simplest way to use +minicomic+ is to create a +Rakefile+ as follows:
27
+
28
+ require 'minicomic'
29
+
30
+ minicomic '.'
31
+
32
+ +minicomic+ will look for pages in a +pages/+ folder in the given directory
33
+ (in this particular case, '.' meaning the directory where the +Rakefile+ lives).
34
+
35
+ +minicomic+ looks for SVG files in +pages/+ named according to the following
36
+ conventions:
37
+
38
+ +page-NN.svg+:: page NN
39
+ +front-cover.svg+:: the front cover of the comic (optional)
40
+ +back-cover.svg+:: the back cover of the comic (optional)
41
+ +inside-front.svg+:: the inside front cover of the comic (optional)
42
+ +inside-back.svg+:: the inside back cover of the comic (optional)
43
+
44
+ It is also possible to have two-page spreads in single files:
45
+
46
+ +pages-NN-MM.svg+:: the spread spanning pages NN-MM
47
+ +cover.svg+:: the cover (back and front together in one file; optional)
48
+
49
+ Page numbers start at 1 (page 1 is a right-handed page, and the first
50
+ interior page). Page documents should be 5.5x8.5in or smaller for single
51
+ pages, and 11x8.5in for two-page spreads.
52
+
53
+ == print output
54
+
55
+ When generating output for print, +minicomic+ will round the number of
56
+ interior pages up to the next multiple of four, padding with blank pages
57
+ as needed. The page graphics will be scaled down slightly from their full
58
+ size, and smaller graphics will be centered.
59
+
60
+ To generate a "proof" PDF that you can examine to see what spreads will
61
+ look like in the assembled comic, use:
62
+
63
+ +rake proof+
64
+
65
+ The PDF will be created as +print/proof.pdf+. Since I rarely use the
66
+ inside covers for anything, +minicomic+ places the front and back covers
67
+ opposite the first and last interior pages respectively.
68
+
69
+ To generate a set of PDFs suitable for printing and assembly, use:
70
+
71
+ +rake print+
72
+
73
+ This will generate a set of three PDFs:
74
+
75
+ +print/duplex.pdf+:: suitable for duplex printing
76
+ +print/front.pdf+ and +print/back.pdf+:: front and back sides for single-sided printing
77
+
78
+ If you are using a duplex printer, if you're lucky it will deposit its
79
+ output pages face-up and they will be ready for assembly (this is the norm).
80
+ Otherwise if it deposits its pages face-down, you will have reverse them before
81
+ you can assemble the comic.
82
+
83
+ When using the single-sided PDFs, _you will need to experiment_ to find the
84
+ correct order to use them in, and the correct way to flip the paper. For my
85
+ printer, I print +back.pdf+ first, then flip the stack the long way before
86
+ printing +front.pdf+ on it. Other printers will differ depending on how the
87
+ paper is loaded in the tray, and how it is stacked on output.
88
+
89
+ == web output
90
+
91
+ You can generate files for web upload via:
92
+
93
+ +rake web+
94
+
95
+ When generating output for the web, +minicomic+ will generate a set of 4-bit
96
+ greyscale PNGs, and a corresponding set of JPEG thumbnails:
97
+
98
+ +web/page-NN.png+:: page NN
99
+ +web/thumbnail-NN.jpeg:: thumbnail of page NN
100
+
101
+ == cleanup
102
+
103
+ You can easily get rid of the temporary and output files with:
104
+
105
+ +rake clean+
106
+
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
5
+
6
+ Rake::TestTask.new do |task|
7
+ task.libs << 'lib'
8
+ task.libs << 'test'
9
+ task.test_files = [ "test/test_all.rb" ]
10
+ task.verbose = true
11
+ end
12
+
13
+ Rake::RDocTask.new do |task|
14
+ task.rdoc_files.include "README"
15
+ task.main = "README"
16
+ end
17
+
18
+ gemspec = Gem::Specification.new do |gemspec|
19
+ gemspec.platform = Gem::Platform::RUBY
20
+ gemspec.name = "minicomic"
21
+ gemspec.version = "0.0.1"
22
+ gemspec.author = "MenTaLguY <mental@rydia.net>"
23
+ gemspec.summary = "Rake rules for minicomic impressions"
24
+ gemspec.test_file = 'test/test_all.rb'
25
+ gemspec.files = FileList[ 'Rakefile', 'test/*.rb', 'lib/**/*.rb' ]
26
+ gemspec.require_paths = [ 'lib' ]
27
+ gemspec.has_rdoc = true
28
+ gemspec.extra_rdoc_files = %w(README)
29
+ gemspec.rdoc_options = %w(--main README)
30
+ end
31
+
32
+ task :package => [ :test ]
33
+ Rake::GemPackageTask.new( gemspec ) do |task|
34
+ task.gem_spec = gemspec
35
+ task.need_tar = true
36
+ end
37
+
38
+ task :default => [ :package ]
39
+
data/lib/minicomic.rb ADDED
@@ -0,0 +1,447 @@
1
+ #
2
+ # minicomic.rb: Rake tasks for minicomics using Inkscape and other tools
3
+ #
4
+ # Copyright 2007 MenTaLguY <mental@rydia.net>
5
+ #
6
+ # This library is made available under the same terms as Ruby.
7
+ #
8
+
9
+ require 'set'
10
+ require 'rake'
11
+
12
+ module Minicomic
13
+ extend self
14
+
15
+ private
16
+
17
+ # in PostScript points
18
+ FORMAT_WIDTH = ( 5.5 * 72 ).to_i
19
+ FORMAT_HEIGHT = ( 8.5 * 72 ).to_i
20
+ PAGE_SCALE = 0.932
21
+
22
+ # other constants
23
+ LEFT = 0
24
+ RIGHT = 1
25
+ PHI = ( 1 + Math.sqrt( 5 ) ) / 2
26
+
27
+ def eps_from_svg( eps_file, svg_file )
28
+ file eps_file => [ svg_file ] do
29
+ sh 'inkscape', '-T', '-B', '-E', eps_file, svg_file
30
+ end
31
+ end
32
+
33
+ def back_eps( out_file, in_file )
34
+ file out_file => [ in_file ] do
35
+ sh 'psselect', '-e', in_file, out_file
36
+ end
37
+ end
38
+
39
+ def front_eps( out_file, in_file )
40
+ file out_file => [ in_file ] do
41
+ sh 'psselect', '-o', in_file, out_file
42
+ end
43
+ end
44
+
45
+ def make2up( in_file, out_file )
46
+ sh 'psnup', "-W#{ FORMAT_WIDTH }", "-H#{ FORMAT_HEIGHT }", "-h#{ FORMAT_WIDTH * 2 }", "-w#{ FORMAT_HEIGHT }", '-2', in_file, out_file
47
+ end
48
+
49
+ def duplex_eps( out_file, in_file )
50
+ temp_file = "#{ out_file }.temp"
51
+ file out_file => [ in_file ] do
52
+ sh 'psbook', in_file, temp_file
53
+ begin
54
+ make2up temp_file, out_file
55
+ ensure
56
+ rm temp_file
57
+ end
58
+ end
59
+ end
60
+
61
+ def proof_eps( out_file, in_file )
62
+ temp_file = "#{ out_file }.temp"
63
+ file out_file => [ in_file ] do
64
+ sh 'psselect', '-p1,3-_3,_1', in_file, temp_file
65
+ begin
66
+ make2up temp_file, out_file
67
+ ensure
68
+ rm temp_file
69
+ end
70
+ end
71
+ end
72
+
73
+ def pdf_from_eps( pdf_file, eps_file )
74
+ file pdf_file => [ eps_file ] do
75
+ sh 'ps2pdf', '-sPAPERSIZE=letter', eps_file, pdf_file
76
+ end
77
+ end
78
+
79
+ def png_from_svg( png_file, svg_file )
80
+ temp_png_file = "#{ png_file }.temp"
81
+ file png_file => [ svg_file ] do
82
+ sh 'inkscape', '-d', '80', '-C', '-y', '1.0', '-e', temp_png_file, svg_file
83
+ begin
84
+ sh 'pngcrush', '-force', '-bit_depth', '4', '-c', '0', '-q', temp_png_file, png_file
85
+ ensure
86
+ rm temp_png_file
87
+ end
88
+ end
89
+ end
90
+
91
+ def thumbnail_jpeg_from_image( jpeg_file, image_file )
92
+ file jpeg_file => [ image_file ] do
93
+ sh 'convert', '-filter', 'sinc', '-resize', "x96", image_file, jpeg_file
94
+ end
95
+ end
96
+
97
+ class BookletLayout
98
+ def initialize( stream, *pages )
99
+ @bboxes = {}
100
+ @stream = stream
101
+
102
+ pages.push nil if pages.size % 2 != 0 # pad to even number of pages
103
+
104
+ # move the back cover to the beginning, to be paired with the front cover
105
+ pages.unshift pages.pop
106
+
107
+ # pair up adjacent pages into spreads
108
+ spreads = (0...(pages.size/2)).map { |i| [ pages[i*2], pages[i*2+1] ] }
109
+
110
+ # format each spread, breaking spreads back into invdividual pages after
111
+ pages = spreads.inject( [] ) do |acc, spread|
112
+ is_cover = acc.empty?
113
+ acc.push *format_pages( is_cover, *spread )
114
+ end
115
+
116
+ # return the back cover to the end
117
+ pages.push pages.shift
118
+
119
+ emit_document pages
120
+ end
121
+
122
+ def format_pages( is_cover, *pages )
123
+ spread = pages.map do |page|
124
+ if page
125
+ [ bbox(*page), page[2] ]
126
+ else
127
+ nil
128
+ end
129
+ end.map do |page|
130
+ if page
131
+ bbox, document = page
132
+ dims = (0..1).map { |d| ( bbox[d].end - bbox[d].begin ) * PAGE_SCALE }
133
+ [ bbox, dims, document ]
134
+ else
135
+ nil
136
+ end
137
+ end
138
+
139
+ if spread.all? # i.e. both?
140
+ spread_dims = spread.map { |page| page[1] }
141
+ left_document = spread[LEFT][2]
142
+ right_document = spread[RIGHT][2]
143
+ if left_document == right_document
144
+ tx = translate_spread( *spread_dims )
145
+ elsif is_cover
146
+ tx = spread_dims.map { |dims| translate_single( dims ) }
147
+ else
148
+ tx = translate_facing( *spread_dims )
149
+ end
150
+ [ [ spread[LEFT][0], tx[LEFT], left_document ],
151
+ [ spread[RIGHT][0], tx[RIGHT], right_document ] ]
152
+ else
153
+ spread.map do |page|
154
+ if page
155
+ [ page[0], translate_single( page[1] ), page[2] ]
156
+ else
157
+ nil
158
+ end
159
+ end
160
+ end
161
+ end
162
+
163
+ def bbox( i, n, document )
164
+ rx, ry = @bboxes[document] ||= (
165
+ rx, ry = 0.0..(FORMAT_WIDTH.to_f * n), 0.0..(FORMAT_HEIGHT.to_f)
166
+ File.open( document, "r" ) do |stream|
167
+ stream.each_line do |line|
168
+ case line
169
+ when /^%%BoundingBox: (\d+) (\d+) (\d+) (\d+)/
170
+ rx, ry = ($1.to_f)..($3.to_f), ($2.to_f)..($4.to_f)
171
+ break
172
+ when /^%%EndComments/
173
+ break
174
+ end
175
+ end
176
+ end
177
+ [ rx, ry ]
178
+ )
179
+ width = rx.end - rx.begin
180
+ [ ( rx.begin + width * i / n )..( rx.begin + width * ( i + 1 ) / n ), ry ]
181
+ end
182
+
183
+ def bottom_margin( height )
184
+ ( FORMAT_HEIGHT.to_f - height ) / PHI
185
+ end
186
+
187
+ def horizontal_margin( width )
188
+ ( FORMAT_WIDTH.to_f - width ) / 2
189
+ end
190
+
191
+ def translate_single( dims )
192
+ [ horizontal_margin( dims[0] ), bottom_margin( dims[1] ) ]
193
+ end
194
+
195
+ def translate_facing( left, right )
196
+ left_margin = horizontal_margin( left[0] )
197
+ right_margin = horizontal_margin( right[0] )
198
+ gutter = [ [ left_margin, right_margin ].min, 0 ].max
199
+ [ [ left_margin + ( gutter / 3 ), bottom_margin( left[1] ) ],
200
+ [ right_margin - ( gutter / 3 ), bottom_margin( right[1] ) ] ]
201
+ end
202
+
203
+ def translate_spread( left, right )
204
+ [ [ FORMAT_WIDTH.to_f - left[0], bottom_margin( left[1] ) ],
205
+ [ 0, bottom_margin( right[1] ) ] ]
206
+ end
207
+
208
+ def emit_dsc( name, value=nil )
209
+ if value
210
+ @stream.puts "%%#{ name }: #{ value }"
211
+ else
212
+ @stream.puts "%%#{ name }"
213
+ end
214
+ end
215
+
216
+ def emit_document( pages )
217
+ @stream.puts "%!PS-Adobe-3.0"
218
+ emit_dsc 'Creator', 'minicomic'
219
+ emit_dsc 'Pages', pages.size
220
+ #emit_dsc 'Orientation', 'Portrait'
221
+ #bbox = "0 0 #{ FORMAT_WIDTH } #{ FORMAT_HEIGHT }"
222
+ #emit_dsc 'BoundingBox', bbox
223
+ #emit_dsc 'HiResBoundingBox', bbox
224
+ emit_dsc 'EndComments'
225
+ pages.each_with_index do |page, n|
226
+ if page
227
+ emit_page( n, *page )
228
+ else
229
+ emit_empty_page( n )
230
+ end
231
+ end
232
+ emit_dsc 'EOF'
233
+ end
234
+
235
+ def emit_clip_rect( x0, y0, x1, y1 )
236
+ @stream.puts "newpath"
237
+ @stream.puts "#{ x0 } #{ y0 } moveto"
238
+ @stream.puts "#{ x0 } #{ y1 } lineto"
239
+ @stream.puts "#{ x1 } #{ y1 } lineto"
240
+ @stream.puts "#{ x1 } #{ y0 } lineto"
241
+ @stream.puts "closepath eoclip newpath"
242
+ end
243
+
244
+ def emit_page( n, bbox, translate, document )
245
+ emit_dsc 'Page', "#{ n } #{ n }"
246
+
247
+ @stream.puts "save"
248
+
249
+ emit_clip_rect( 0, 0, FORMAT_WIDTH, FORMAT_HEIGHT )
250
+
251
+ @stream.puts <<EOS
252
+ #{ bbox.map { |r| -r.begin }.join( ' ' ) } translate
253
+ #{ PAGE_SCALE } #{ PAGE_SCALE } scale
254
+ #{ translate.join( ' ' ) } translate
255
+ EOS
256
+
257
+ emit_clip_rect( bbox[0].begin, bbox[1].begin, bbox[0].end, bbox[1].end )
258
+
259
+ emit_dsc 'BeginDocument', File.basename( document )
260
+ File.open( document, 'r' ) do |input|
261
+ input.each_line do |line|
262
+ @stream.puts line
263
+ end
264
+ end
265
+ emit_dsc 'EndDocument'
266
+ emit_dsc 'PageTrailer'
267
+ @stream.puts "restore"
268
+ end
269
+
270
+ def emit_empty_page( n )
271
+ emit_dsc 'Page', "#{ n } #{ n }"
272
+ @stream.puts "showpage"
273
+ end
274
+ end
275
+
276
+ def layout_booklet( layout_ps, *pages )
277
+ file layout_ps => pages.compact.map { |i, n, file| file } do
278
+ begin
279
+ File.open( layout_ps, 'w' ) do |stream|
280
+ BookletLayout.new( stream, *pages )
281
+ end
282
+ rescue
283
+ rm layout_ps
284
+ raise
285
+ end
286
+ end
287
+ end
288
+
289
+ def special_name_re( name )
290
+ Regexp.new( Regexp.quote( name ).gsub( /-/, "\\W*" ), "i" )
291
+ end
292
+
293
+ def normalize_special_name( name )
294
+ name = name.gsub( /[a-z](?=[A-Z])/, '\&-' )
295
+ name.gsub!( /\W+/, '-' )
296
+ name.downcase!
297
+ name
298
+ end
299
+
300
+ SPECIAL_NAMES = %w(front-cover back-cover cover inside-front inside-back)
301
+ SPECIAL_NAME_RES = SPECIAL_NAMES.map { |n| special_name_re( n ) }
302
+
303
+ public
304
+
305
+ def minicomic( dir )
306
+ pages_dir = File.join( dir, 'pages' )
307
+
308
+ scratch_dir = File.join( dir, 'scratch' )
309
+ directory scratch_dir
310
+
311
+ web_dir = File.join( dir, 'web' )
312
+ directory web_dir
313
+
314
+ print_dir = File.join( dir, 'print' )
315
+ directory print_dir
316
+
317
+ layout_ps = File.join( scratch_dir, 'layout.ps' )
318
+
319
+ specials = Set.new
320
+ pages = []
321
+
322
+ FileList[File.join( pages_dir, '*.svg' )].each do |page_svg|
323
+ name = File.basename( page_svg )
324
+ name.sub!( /\.svg$/, '' )
325
+
326
+ min = max = nil
327
+ print_name = name
328
+ case name
329
+ when *SPECIAL_NAME_RES
330
+ name = normalize_special_name( name )
331
+ next unless SPECIAL_NAMES.include? name # warning?
332
+ next if specials.include? name # warning?
333
+ specials.add name
334
+ if name == 'front-cover'
335
+ web_name = "page-00"
336
+ else
337
+ web_name = nil
338
+ end
339
+ when /^page\W*(\d+)$/i
340
+ min = max = $1.to_i
341
+ suffix = "%02d" % [ min ]
342
+ print_name = "page-#{ suffix }"
343
+ web_name = print_name
344
+ when /^pages?\W*(\d+)\D+(\d+)$/i
345
+ min, max = $1.to_i, $2.to_i
346
+ min, max = max, min if max < min
347
+ suffix = "%02d-%02d" % [ min, max ]
348
+ print_name = "pages-#{ suffix }"
349
+ web_name = print_name
350
+ else
351
+ # warning?
352
+ next
353
+ end
354
+
355
+ page_eps = File.join( scratch_dir, "#{ print_name }.eps" )
356
+
357
+ if min and min > 0
358
+ size = max - min + 1
359
+ (0...size).each do |n|
360
+ pages[n+min-1] = [ n, size, page_eps ]
361
+ end
362
+ end
363
+
364
+ eps_from_svg( page_eps, page_svg )
365
+ task page_eps => [ scratch_dir ]
366
+
367
+ if web_name
368
+ page_png = File.join( web_dir, "#{ web_name }.png" )
369
+ png_from_svg( page_png, page_svg )
370
+ task page_png => [ web_dir ]
371
+
372
+ thumbnail_name = web_name.sub( /^page/, 'thumbnail' )
373
+ thumbnail_jpeg = File.join( web_dir, "#{ thumbnail_name }.jpeg" )
374
+ thumbnail_jpeg_from_image( thumbnail_jpeg, page_png )
375
+ task thumbnail_jpeg => [ web_dir ]
376
+
377
+ task :web => [ page_png, thumbnail_jpeg ]
378
+ end
379
+ end
380
+
381
+ pages = [ nil, nil ] + pages + [ nil ] * ( pages.size % 4 ) + [ nil, nil ]
382
+
383
+ specials.each do |name|
384
+ filename = File.join( scratch_dir, "#{ name }.eps" )
385
+ case name
386
+ when 'front-cover'
387
+ pages[0] ||= [ 0, 1, filename ] # 'cover' takes precedence
388
+ when 'back-cover'
389
+ pages[-1] ||= [ 0, 1, filename ] # 'cover' takes precedence
390
+ when 'cover'
391
+ pages[-1] = [ 0, 2, filename ]
392
+ pages[0] = [ 1, 2, filename ]
393
+ when 'inside-front'
394
+ pages[1] = [ 0, 1, filename ]
395
+ when 'inside-back'
396
+ pages[-2] = [ 0, 1, filename ]
397
+ end
398
+ end
399
+
400
+ layout_booklet( layout_ps, *pages )
401
+
402
+ duplex_eps = File.join( scratch_dir, "duplex.eps" )
403
+
404
+ %w(proof back front duplex).each do |kind|
405
+ case kind
406
+ when 'duplex', 'proof'
407
+ source = layout_ps
408
+ else
409
+ source = duplex_eps
410
+ end
411
+ kind_eps = File.join( scratch_dir, "#{ kind }.eps" )
412
+ send( "#{ kind }_eps", kind_eps, source )
413
+ task kind_eps => [ scratch_dir ]
414
+ kind_pdf = File.join( print_dir, "#{ kind }.pdf" )
415
+ pdf_from_eps( kind_pdf, kind_eps )
416
+ task kind_pdf => [ print_dir ]
417
+ task kind => [ kind_pdf ]
418
+ end
419
+
420
+ desc "Generate PDF files for printing"
421
+ task :print => [ :duplex, :front, :back ]
422
+
423
+ desc "Generate a proof PDF for review"
424
+ task :proof
425
+
426
+ desc "Remove all output and intermediate files"
427
+ task :clean do
428
+ rm_rf print_dir
429
+ rm_rf web_dir
430
+ rm_rf scratch_dir
431
+ end
432
+
433
+ desc "Generate graphics for the web"
434
+ task :web
435
+
436
+ desc "Generate web and print output"
437
+ task :both => [ :print, :web ]
438
+
439
+ task :default => [ :both ]
440
+ end
441
+
442
+ end
443
+
444
+ def minicomic( dir )
445
+ Minicomic.minicomic( dir )
446
+ end
447
+
data/test/test_all.rb ADDED
File without changes
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: minicomic
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.0.1
7
+ date: 2007-03-29 00:00:00 -04:00
8
+ summary: Rake rules for minicomic impressions
9
+ require_paths:
10
+ - lib
11
+ email:
12
+ homepage:
13
+ rubyforge_project:
14
+ description:
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ authors:
29
+ - MenTaLguY <mental@rydia.net>
30
+ files:
31
+ - Rakefile
32
+ - test/test_all.rb
33
+ - lib/minicomic.rb
34
+ - README
35
+ test_files:
36
+ - test/test_all.rb
37
+ rdoc_options:
38
+ - --main
39
+ - README
40
+ extra_rdoc_files:
41
+ - README
42
+ executables: []
43
+
44
+ extensions: []
45
+
46
+ requirements: []
47
+
48
+ dependencies: []
49
+