eleanor 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,328 @@
1
+ # Copyright (c) 2008 chiisaitsu <chiisaitsu@gmail.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+ #
21
+ # ---
22
+ #
23
+ # This is Eleanor's backend. Eleanor uses a paper metaphor to talk to the
24
+ # backend: after parsing and pagination, Eleanor "writes" the screenplay to
25
+ # "paper," which by default is PDF. But you could write a backend to target
26
+ # anything, like XML, XSL-FO, Postscript, RTF, the screen, a socket, whatever.
27
+ # Depending on the capabilities of the paper, a backend may choose to ignore
28
+ # some of the user's configuration options, like font, line height, and character
29
+ # spacing.
30
+ #
31
+ # Eleanor's interface with the backend is really simple, allowing wide latitude
32
+ # in how it's implemented. No special modules or classes are needed. Just add
33
+ # five instance methods to the Screenplay class, and Eleanor will call them when
34
+ # appropriate. (You'll probably end up adding other methods to other classes in
35
+ # your implementation, too.)
36
+ #
37
+ # [initialize_paper!]
38
+ # Called when the screenplay is initialized. Can be used to setup any
39
+ # instance variables needed by the backend.
40
+ # [line_height]
41
+ # Called when calculating the height of a paragraph. Returns a Length. If the
42
+ # paper supports custom line heights, this method should make use of
43
+ # Screenplay#line_height_points, which returns the value specified in the
44
+ # user's configuration YAML.
45
+ # [save_paper(out_filename, in_filename=nil)]
46
+ # Writes out the paper representation to +out_filename+. If +out_filename+ is
47
+ # nil, the method may use +in_filename+ to generate an output filename. For
48
+ # example, if +in_filename+ is "screenplay.txt", the method might return
49
+ # "screenplay.pdf".
50
+ # [text_width(str)]
51
+ # Returns the width (a Length) of +str+ in the current font size and character
52
+ # spacing. If the paper supports custom font sizes and character spacing,
53
+ # this method should make use of Screenplay#font_size and
54
+ # Screenplay#char_spacing, which return the values specified in the user's
55
+ # configuration YAML.
56
+ # [write_to_paper!]
57
+ # Called after parsing and pagination. Translates the screenplay to the paper
58
+ # representation.
59
+ #
60
+ # This implementation uses libHaru[http://libharu.org/], a free and open-source
61
+ # PDF library written in ANSI C that comes with Ruby bindings. It allows text
62
+ # underlining by surrounding bits of text in underscores:
63
+ #
64
+ # You can underline a single _word_, or _many words at once._
65
+
66
+ require 'hpdf'
67
+
68
+ # Some handy, high-level methods for libHaru's HPDFPage class.
69
+ class HPDFPage
70
+
71
+ alias :begin_text_ :begin_text
72
+ alias :end_text_ :end_text
73
+ alias :text_width_ :text_width
74
+
75
+ # Overridden to implement underlining.
76
+ def begin_text
77
+ self.begin_text_
78
+ @underline_coords= []
79
+ end
80
+
81
+ # Overridden to implement underlining.
82
+ def end_text
83
+ self.end_text_
84
+ @underline_coords.each do |pair|
85
+ self.move_to(pair[0][0], pair[0][1])
86
+ self.line_to(pair[1][0], pair[1][1])
87
+ self.stroke
88
+ end
89
+ end
90
+
91
+ # Writes a line, +str+, to the page and moves the text pointer down the page
92
+ # by +line_height_pts+ points. Any text surrounded by underscores is
93
+ # underlined.
94
+ def line str, line_height_pts
95
+ unless str.nil? || str.empty?
96
+ pos= self.get_current_text_pos
97
+ # implement underlining: split the line on "_" => every other segment is a
98
+ # string of text that should be underlined.
99
+ segs= str.split(/_/)
100
+ segs.each_with_index do |seg, si|
101
+ if si % 2 == 1
102
+ y= pos[1] - 1 # draw underline one point below line
103
+ coord1= [pos[0] + self.text_width(segs[0..(si - 1)].join), y]
104
+ coord2= [coord1[0] + self.text_width(seg), y]
105
+ @underline_coords << [coord1, coord2]
106
+ end
107
+ end
108
+ # finally, print the line
109
+ self.show_text(segs.join)
110
+ end
111
+ self.move_text_pos(0, -line_height_pts)
112
+ end
113
+
114
+ # Writes a centered line at the current vertical position. See line.
115
+ def line_center str, line_height_pts
116
+ self.move_text_pos(-self.get_current_text_pos[0] +
117
+ (self.get_width / 2.0) -
118
+ (self.text_width(str) / 2.0),
119
+ 0)
120
+ self.line(str, line_height_pts)
121
+ end
122
+
123
+ # Writes a line flushed to the right margin, which is specified by
124
+ # +margin_right+, a Length, at the current vertical position. See line.
125
+ def line_flush_right str, line_height_pts, margin_right
126
+ self.move_text_pos(-self.get_current_text_pos[0] +
127
+ self.get_width -
128
+ (margin_right.to_points.to_f) -
129
+ self.text_width(str),
130
+ 0)
131
+ self.line(str, line_height_pts)
132
+ end
133
+
134
+ # Moves the text pointer horizontally to the given margin +length+.
135
+ def margin_left= length
136
+ self.move_text_pos(-self.get_current_text_pos[0] + length.to_points.to_f, 0)
137
+ end
138
+
139
+ # Moves the text pointer down the page by +length+.
140
+ def move_down length
141
+ self.move_text_pos(0, -length.to_points.to_f)
142
+ end
143
+
144
+ # Moves the text pointer to the very first line on the page.
145
+ def move_to_top
146
+ # + 3 because without it, there's a little gap at the top
147
+ self.move_text_pos(0,
148
+ -self.get_current_text_pos[1] +
149
+ self.get_height -
150
+ self.get_current_font_size + 3)
151
+ end
152
+
153
+ # Overridden to implement underlining.
154
+ def text_width str
155
+ self.text_width_(str.gsub(/_/, ''))
156
+ end
157
+
158
+ end
159
+
160
+
161
+
162
+ module Eleanor
163
+
164
+ class Page
165
+
166
+ # An implementation detail in the backend. See lib/eleanor/hpdfpaper.rb.
167
+ def write_to_paper pdf_page, line_height_pts
168
+ pdf_page.begin_text
169
+ # header
170
+ if self.header
171
+ pdf_page.move_to_top
172
+ pdf_page.move_down(self.header_margin_top)
173
+ pdf_page.line_center(self.header, line_height_pts)
174
+ end
175
+ # page number
176
+ if self.page_number_display
177
+ pdf_page.move_to_top
178
+ pdf_page.move_down(self.page_number_margin_top)
179
+ pdf_page.line_flush_right(self.page_number_display,
180
+ line_height_pts,
181
+ self.page_number_margin_right)
182
+ end
183
+ # finally, paragraphs
184
+ pdf_page.move_to_top
185
+ pdf_page.move_down(self.margin_top_actual)
186
+ prev_para= nil
187
+ @paras.each do |para|
188
+ pdf_page.move_down(prev_para.margin_between(para)) if prev_para
189
+ para.write_to_paper(pdf_page, line_height_pts)
190
+ prev_para= para
191
+ end
192
+ pdf_page.end_text
193
+ end
194
+
195
+ end
196
+
197
+
198
+
199
+ class Paragraph
200
+
201
+ # An implementation detail in the backend. See lib/eleanor/hpdfpaper.rb.
202
+ def write_to_paper pdf_page, line_height_pts
203
+ underline_broken= false
204
+ @lines.each do |line|
205
+ if underline_broken
206
+ line= '_' + line
207
+ underline_broken= false
208
+ end
209
+ if line.count('_') % 2 == 1
210
+ line << '_'
211
+ underline_broken= true
212
+ end
213
+ case self.align.to_s.strip.downcase
214
+ when 'left'
215
+ pdf_page.margin_left= self.margin_left
216
+ pdf_page.line(line, line_height_pts)
217
+ when 'center'
218
+ pdf_page.line_center(line, line_height_pts)
219
+ when 'right'
220
+ pdf_page.line_flush_right(line, line_height_pts, self.margin_right)
221
+ else
222
+ raise "configuration error: invalid align value " \
223
+ "#{self.align.inspect} for #{self.class}"
224
+ end
225
+ end
226
+ if underline_broken
227
+ warn "warning: runaway underline at paragraph:\n #{self}"
228
+ end
229
+ end
230
+
231
+ end
232
+
233
+
234
+
235
+ class Screenplay
236
+
237
+ # A required method in the backend. See lib/eleanor/hpdfpaper.rb.
238
+ def initialize_paper!
239
+ @pdf= HPDFDoc.new
240
+ font_name= (%r{[\./\\]} =~ self.font ?
241
+ @pdf.load_ttfont_from_file(self.font, HPDFDoc::HPDF_TRUE) :
242
+ self.font)
243
+ @pdf_font= @pdf.get_font(font_name, nil)
244
+ @first_pdf_page= add_pdf_page(@pages.first)
245
+ end
246
+
247
+ # A required method in the backend. See lib/eleanor/hpdfpaper.rb.
248
+ def line_height
249
+ self.line_height_points.points
250
+ end
251
+
252
+ # A required method in the backend. See lib/eleanor/hpdfpaper.rb.
253
+ def save_paper out_filename, in_filename=nil
254
+ out_filename ||= (File.join(File.dirname(in_filename),
255
+ File.basename(in_filename,
256
+ File.extname(in_filename))) +
257
+ '.pdf')
258
+ @pdf.save_to_file(out_filename)
259
+ end
260
+
261
+ # A required method in the backend. See lib/eleanor/hpdfpaper.rb.
262
+ def text_width str
263
+ @first_pdf_page.text_width(str).points
264
+ end
265
+
266
+ # A required method in the backend. See lib/eleanor/hpdfpaper.rb.
267
+ def write_to_paper!
268
+ if self.title
269
+ @pdf.set_info_attr(HPDFDoc::HPDF_INFO_TITLE, self.title.gsub(/_/, ''))
270
+ end
271
+ @pdf.set_info_attr(HPDFDoc::HPDF_INFO_AUTHOR, self.author) if self.author
272
+ @pdf.set_info_attr(HPDFDoc::HPDF_INFO_CREATOR,
273
+ "#{Eleanor::NAME} #{Eleanor::VERSION}")
274
+ @title_pages.each do |page|
275
+ pdf_page= add_pdf_page(page, @first_pdf_page)
276
+ page.write_to_paper(pdf_page, self.line_height_points)
277
+ end
278
+ @pages.each_with_index do |page, i|
279
+ pdf_page= (i == 0 ? @first_pdf_page : add_pdf_page(page))
280
+ page.write_to_paper(pdf_page, self.line_height_points)
281
+ end
282
+ end
283
+
284
+ private
285
+
286
+ def add_pdf_page eleanor_page, before_pdf_page=nil
287
+ pdf_page= (before_pdf_page.nil??
288
+ @pdf.add_page :
289
+ @pdf.insert_page(before_pdf_page))
290
+ pdf_page.set_width(eleanor_page.width.to_points.to_f)
291
+ pdf_page.set_height(eleanor_page.height.to_points.to_f)
292
+ pdf_page.set_line_width(0.75) # underline stroke width
293
+ pdf_page.set_font_and_size(@pdf_font, self.font_size.to_points.to_f)
294
+ pdf_page.set_char_space(self.char_spacing.to_points.to_f)
295
+ pdf_page
296
+ end
297
+
298
+ end
299
+
300
+
301
+
302
+ class TitlePage
303
+
304
+ # An implementation detail in the backend. See lib/eleanor/hpdfpaper.rb.
305
+ def write_to_paper pdf_page, line_height_pts
306
+ pdf_page.begin_text
307
+ pdf_page.move_to_top
308
+ pdf_page.move_down(self.margin_top)
309
+ pdf_page.line_center(@title, line_height_pts) unless @title.nil?
310
+ pdf_page.move_down(1.lines)
311
+ unless @by.nil?
312
+ pdf_page.line_center(@by, line_height_pts)
313
+ pdf_page.move_down(1.lines)
314
+ end
315
+ pdf_page.line_center(@author, line_height_pts) unless @author.nil?
316
+ unless @contact.nil?
317
+ pdf_page.move_to_top
318
+ pdf_page.move_down(self.height -
319
+ (@contact.size * line_height_pts).points -
320
+ self.margin_bottom)
321
+ pdf_page.margin_left= self.margin_left
322
+ @contact.each { |line| pdf_page.line(line, line_height_pts) }
323
+ end
324
+ pdf_page.end_text
325
+ end
326
+ end
327
+
328
+ end
@@ -0,0 +1,172 @@
1
+ # Copyright (c) 2008 chiisaitsu <chiisaitsu@gmail.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+ #
21
+ # ---
22
+ #
23
+ # See Length class.
24
+
25
+ # A handy way to pass around lengths of various units and mix and match them
26
+ # regardless of unit. Could be a little more robust but works well enough.
27
+ #
28
+ # Units are defined by adding entries to the UNITS hash. That's it. By way of
29
+ # some metaprogramming, each entry in the hash creates a new subclass of
30
+ # Length that shares the name of the entry's key. So, currently there are
31
+ # three Length subclasses: Inches, Lines, and Points.
32
+ #
33
+ # Length.line_height must be set before anything is done with any Lengths. Set
34
+ # it to a value in points:
35
+ #
36
+ # Length.line_height= 12 # OK, now go for it
37
+ #
38
+ # New Length subclass instances can be created in three ways:
39
+ #
40
+ # * Constructors: Points.new(32)
41
+ # * Length conversion instance methods: Inches.new(0.5).to_points
42
+ # * Numeric instance methods: 32.points
43
+ #
44
+ # To get at the raw Numeric that Lengths encapsulate, use to_i or to_f:
45
+ #
46
+ # 1.5.inches.to_i # => 1
47
+ # 1.5.inches.to_f # => 1.5
48
+ #
49
+ # All the operations that apply to Numerics can be applied to Lengths, too:
50
+ #
51
+ # 1.inches + 12.lines # => #<Inches:0x7fed2694 @val=3.0>
52
+ # 12.lines + 1.inches # => #<Lines:0x7fece364 @val=18.0>
53
+ # 2.points * 100 # => #<Points:0x7feca278 @val=200.0>
54
+ # 72.points == 1.inches # => true
55
+ # 72.points == 1 # error!
56
+ #
57
+ # More examples:
58
+ #
59
+ # Length.line_height= 12 # => 12
60
+ # Inches.new(1) # => #<Inches:0x7ff37120 @val=1.0>
61
+ # Inches.new(1).to_points # => #<Points:0x7ff01ebc @val=72.0>
62
+ # 6.lines # => #<Lines:0x7fefde48 @val=6.0>
63
+ # 6.lines.to_points.to_f # => 72
64
+ # 6.lines + 0.5.inches # => #<Lines:0x7fef44ec @val=9.0>
65
+ # Inches.new(72.points) # => #<Inches:0x7ff95ae0 @val=1.0>
66
+
67
+ class Length
68
+
69
+ # All units are defined in terms of points.
70
+ UNITS= {
71
+ :Inches => { :points => 72.0, :abbrev => 'in' },
72
+ :Lines => { :points => nil, :abbrev => 'ln' },
73
+ :Points => { :points => 1.0, :abbrev => 'pt' }
74
+ }
75
+
76
+ include Comparable
77
+
78
+ # This is filled in once line_height is set. Maps the abbreviations in
79
+ # the UNITS hash to their corresponding Length subclasses, e.g.,
80
+ # 'in' => Inches.
81
+ ABBREVIATIONS= {}
82
+
83
+ # Sets the line height to +points+. Since calling this kicks off the
84
+ # metaprogramming that builds the Length subclasses, line height must be set
85
+ # before anything else is done.
86
+ def self.line_height= points
87
+ UNITS[:Lines][:points]= points
88
+ UNITS.each_pair do |this_unit, this_meta|
89
+ # create new class this_unit
90
+ klass= Class.new(self)
91
+ # class gets to_<length> methods for each class of length
92
+ klass.class_eval do
93
+ UNITS.each_pair do |unit, meta|
94
+ define_method("to_#{unit.to_s.downcase}") do
95
+ points= (@val * this_meta[:points]) / meta[:points]
96
+ Object.const_get(unit).new(points)
97
+ end
98
+ end
99
+ end
100
+ Object.const_set(this_unit, klass)
101
+ # add entry to abbreviations table
102
+ ABBREVIATIONS[this_meta[:abbrev]]= klass
103
+ # add <this_unit> method to Numeric
104
+ Numeric.class_eval do
105
+ define_method(this_unit.to_s.downcase) { klass.new(self) }
106
+ end
107
+ end
108
+ end
109
+
110
+ # Returns the line height.
111
+ def self.line_height
112
+ UNITS[:Lines][:points]
113
+ end
114
+
115
+ # If +str+ represents a Length, returns it. Returns nil otherwise. Strings
116
+ # that represent Lengths end in a Length abbreviation, e.g., "32in", "-1.72pt".
117
+ def self.parse str
118
+ match= /[a-zA-Z]+$/.match(str)
119
+ if match.nil? || !ABBREVIATIONS.has_key?(match[0])
120
+ nil
121
+ else
122
+ ABBREVIATIONS[match[0]].new(str[0..-(match[0].length + 1)].to_f)
123
+ end
124
+ end
125
+
126
+ # +obj+ must be a Length.
127
+ def <=> obj
128
+ if obj.is_a? Length
129
+ @val <=> obj.send("to_#{self.class.name.downcase}").to_f
130
+ else
131
+ raise TypeError, "#{self.class} is incomparable to #{obj.class}"
132
+ end
133
+ end
134
+
135
+ # +val+ may be a Numeric, a String that can be coerced into a Numeric, or
136
+ # a Length.
137
+ def initialize val
138
+ @val=
139
+ case val
140
+ when Numeric, String
141
+ val.to_f
142
+ when Length
143
+ val.send("to_#{self.class.name.downcase}").to_f
144
+ else
145
+ raise TypeError, "cannot initialize #{self.class} from #{val.class}"
146
+ end
147
+ end
148
+
149
+ def to_i
150
+ @val.to_i
151
+ end
152
+
153
+ def to_f
154
+ @val
155
+ end
156
+
157
+ def to_s
158
+ "#{@val.to_s} #{self.class.name.downcase}"
159
+ end
160
+
161
+ private
162
+
163
+ def method_missing meth, *args
164
+ if @val.respond_to? meth
165
+ args= args.map { |a| a.is_a?(Length) ? self.class.new(a).to_f : a }
166
+ self.class.new(@val.send(meth, *args))
167
+ else
168
+ super(meth, args)
169
+ end
170
+ end
171
+
172
+ end