kramdown-converter-pdf 1.0.1
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.
- checksums.yaml +7 -0
- data/CONTRIBUTERS +3 -0
- data/COPYING +30 -0
- data/VERSION +1 -0
- data/lib/kramdown/converter/pdf.rb +614 -0
- metadata +89 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5b24dbe64c979804933619756ff3c956eae922cb53aa00a84d974a2f1e51fe4c
|
4
|
+
data.tar.gz: 1f73a92236f30de69bab315ddc02e193b3ea76f494adbfff4e6cb76341f53de0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e18ca1234a2c8aeb1362808bbb51734a259831ebecf932217f97250628d29294849c4ed8d4e72bee6b70d6cac67b5976d8f896b8fec25714f7392998fe5a0452
|
7
|
+
data.tar.gz: 10008adc53eb0e1e99c76e6215a30726659433612761bda35e359ddf94061cd2349fbd8133b9aa44f6874002cc337abbef3cf0792b9d13956fe72fb2f7dc6fbf
|
data/CONTRIBUTERS
ADDED
data/COPYING
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
kramdown - fast, pure-Ruby Markdown-superset converter
|
2
|
+
Copyright (C) 2009-2013 Thomas Leitner <t_leitner@gmx.at>
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
5
|
+
copy of this software and associated documentation files (the
|
6
|
+
"Software"), to deal in the Software without restriction, including
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included
|
13
|
+
in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
16
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
18
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
|
23
|
+
|
24
|
+
Some test cases and the benchmark files are based on test cases from
|
25
|
+
the MDTest test suite:
|
26
|
+
|
27
|
+
MDTest
|
28
|
+
Copyright (c) 2007 Michel Fortin
|
29
|
+
<http://www.michelf.com/>
|
30
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.1
|
@@ -0,0 +1,614 @@
|
|
1
|
+
# -*- coding: utf-8; frozen_string_literal: true -*-
|
2
|
+
#
|
3
|
+
#--
|
4
|
+
# Copyright (C) 2009-2019 Thomas Leitner <t_leitner@gmx.at>
|
5
|
+
#
|
6
|
+
# This file is part of kramdown which is licensed under the MIT.
|
7
|
+
#++
|
8
|
+
#
|
9
|
+
|
10
|
+
require 'prawn'
|
11
|
+
require 'prawn/table'
|
12
|
+
require 'kramdown/converter/base'
|
13
|
+
require 'kramdown/utils'
|
14
|
+
require 'open-uri'
|
15
|
+
|
16
|
+
module Kramdown
|
17
|
+
|
18
|
+
module Converter
|
19
|
+
|
20
|
+
# Converts an element tree to a PDF using the prawn PDF library.
|
21
|
+
#
|
22
|
+
# This basic version provides a nice starting point for customizations but can also be used
|
23
|
+
# directly.
|
24
|
+
#
|
25
|
+
# There can be the following two methods for each element type: render_TYPE(el, opts) and
|
26
|
+
# TYPE_options(el, opts) where +el+ is a kramdown element and +opts+ an hash with rendering
|
27
|
+
# options.
|
28
|
+
#
|
29
|
+
# The render_TYPE(el, opts) is used for rendering the specific element. If the element is a span
|
30
|
+
# element, it should return a hash or an array of hashes that can be used by the #formatted_text
|
31
|
+
# method of Prawn::Document. This method can then be used in block elements to actually render
|
32
|
+
# the span elements.
|
33
|
+
#
|
34
|
+
# The rendering options are passed from the parent to its child elements. This allows one to
|
35
|
+
# define general options at the top of the tree (the root element) that can later be changed or
|
36
|
+
# amended.
|
37
|
+
#
|
38
|
+
#
|
39
|
+
# Currently supports the conversion of all elements except those of the following types:
|
40
|
+
#
|
41
|
+
# :html_element, :img, :footnote
|
42
|
+
#
|
43
|
+
#
|
44
|
+
class Pdf < Base
|
45
|
+
|
46
|
+
VERSION = '1.0.1'
|
47
|
+
|
48
|
+
include Prawn::Measurements
|
49
|
+
|
50
|
+
def initialize(root, options)
|
51
|
+
super
|
52
|
+
@stack = []
|
53
|
+
@dests = {}
|
54
|
+
end
|
55
|
+
|
56
|
+
# PDF templates are applied before conversion. They should contain code to augment the
|
57
|
+
# converter object (i.e. to override the methods).
|
58
|
+
def apply_template_before?
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns +false+.
|
63
|
+
def apply_template_after?
|
64
|
+
false
|
65
|
+
end
|
66
|
+
|
67
|
+
DISPATCHER_RENDER = Hash.new {|h, k| h[k] = "render_#{k}" } #:nodoc:
|
68
|
+
DISPATCHER_OPTIONS = Hash.new {|h, k| h[k] = "#{k}_options" } #:nodoc:
|
69
|
+
|
70
|
+
# Invoke the special rendering method for the given element +el+.
|
71
|
+
#
|
72
|
+
# A PDF destination is also added at the current location if th element has an ID or if the
|
73
|
+
# element is of type :header and the :auto_ids option is set.
|
74
|
+
def convert(el, opts = {})
|
75
|
+
id = el.attr['id']
|
76
|
+
id = generate_id(el.options[:raw_text]) if !id && @options[:auto_ids] && el.type == :header
|
77
|
+
if !id.to_s.empty? && !@dests.key?(id)
|
78
|
+
@pdf.add_dest(id, @pdf.dest_xyz(0, @pdf.y))
|
79
|
+
@dests[id] = @pdf.dest_xyz(0, @pdf.y)
|
80
|
+
end
|
81
|
+
send(DISPATCHER_RENDER[el.type], el, opts)
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
# Render the children of this element with the given options and return the results as array.
|
87
|
+
#
|
88
|
+
# Each time a child is rendered, the +TYPE_options+ method is invoked (if it exists) to get
|
89
|
+
# the specific options for the element with which the given options are updated.
|
90
|
+
def inner(el, opts)
|
91
|
+
@stack.push([el, opts])
|
92
|
+
result = el.children.map do |inner_el|
|
93
|
+
options = opts.dup
|
94
|
+
options.update(send(DISPATCHER_OPTIONS[inner_el.type], inner_el, options))
|
95
|
+
convert(inner_el, options)
|
96
|
+
end.flatten.compact
|
97
|
+
@stack.pop
|
98
|
+
result
|
99
|
+
end
|
100
|
+
|
101
|
+
# ----------------------------
|
102
|
+
# :section: Element rendering methods
|
103
|
+
# ----------------------------
|
104
|
+
|
105
|
+
def root_options(_root, _opts)
|
106
|
+
{font: 'Times-Roman', size: 12, leading: 2}
|
107
|
+
end
|
108
|
+
|
109
|
+
def render_root(root, opts)
|
110
|
+
@pdf = setup_document(root)
|
111
|
+
inner(root, root_options(root, opts))
|
112
|
+
create_outline(root)
|
113
|
+
finish_document(root)
|
114
|
+
@pdf.render
|
115
|
+
end
|
116
|
+
|
117
|
+
def header_options(el, opts)
|
118
|
+
size = opts[:size] * 1.15**(6 - el.options[:level])
|
119
|
+
{
|
120
|
+
font: "Helvetica", styles: (opts[:styles] || []) + [:bold],
|
121
|
+
size: size, bottom_padding: opts[:size], top_padding: opts[:size]
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
def render_header(el, opts)
|
126
|
+
render_padded_and_formatted_text(el, opts)
|
127
|
+
end
|
128
|
+
|
129
|
+
def p_options(el, opts)
|
130
|
+
bpad = (el.options[:transparent] ? opts[:leading] : opts[:size])
|
131
|
+
{align: :justify, bottom_padding: bpad}
|
132
|
+
end
|
133
|
+
|
134
|
+
def render_p(el, opts)
|
135
|
+
if el.children.size == 1 && el.children.first.type == :img
|
136
|
+
render_standalone_image(el, opts)
|
137
|
+
else
|
138
|
+
render_padded_and_formatted_text(el, opts)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def render_standalone_image(el, opts)
|
143
|
+
img = el.children.first
|
144
|
+
line = img.options[:location]
|
145
|
+
|
146
|
+
if img.attr['src'].empty?
|
147
|
+
warning("Rendering an image without a source is not possible#{line ? " (line #{line})" : ''}")
|
148
|
+
return nil
|
149
|
+
elsif img.attr['src'] !~ /\.jpe?g$|\.png$/
|
150
|
+
warning("Cannot render images other than JPEG or PNG, " \
|
151
|
+
"got #{img.attr['src']}#{line ? " on line #{line}" : ''}")
|
152
|
+
return nil
|
153
|
+
end
|
154
|
+
|
155
|
+
img_dirs = (@options[:image_directories] || ['.']).dup
|
156
|
+
begin
|
157
|
+
img_path = File.join(img_dirs.shift, img.attr['src'])
|
158
|
+
image_obj, image_info = @pdf.build_image_object(open(img_path))
|
159
|
+
rescue StandardError
|
160
|
+
img_dirs.empty? ? raise : retry
|
161
|
+
end
|
162
|
+
|
163
|
+
options = {position: :center}
|
164
|
+
if img.attr['height'] && img.attr['height'] =~ /px$/
|
165
|
+
options[:height] = img.attr['height'].to_i / (@options[:image_dpi] || 150.0) * 72
|
166
|
+
elsif img.attr['width'] && img.attr['width'] =~ /px$/
|
167
|
+
options[:width] = img.attr['width'].to_i / (@options[:image_dpi] || 150.0) * 72
|
168
|
+
else
|
169
|
+
options[:scale] = [(@pdf.bounds.width - mm2pt(20)) / image_info.width.to_f, 1].min
|
170
|
+
end
|
171
|
+
|
172
|
+
if img.attr['class'] =~ /\bright\b/
|
173
|
+
options[:position] = :right
|
174
|
+
@pdf.float { @pdf.embed_image(image_obj, image_info, options) }
|
175
|
+
else
|
176
|
+
with_block_padding(el, opts) do
|
177
|
+
@pdf.embed_image(image_obj, image_info, options)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def blockquote_options(_el, _opts)
|
183
|
+
{styles: [:italic]}
|
184
|
+
end
|
185
|
+
|
186
|
+
def render_blockquote(el, opts)
|
187
|
+
@pdf.indent(mm2pt(10), mm2pt(10)) { inner(el, opts) }
|
188
|
+
end
|
189
|
+
|
190
|
+
def ul_options(_el, opts)
|
191
|
+
{bottom_padding: opts[:size]}
|
192
|
+
end
|
193
|
+
|
194
|
+
def render_ul(el, opts)
|
195
|
+
with_block_padding(el, opts) do
|
196
|
+
el.children.each do |li|
|
197
|
+
@pdf.float { @pdf.formatted_text([text_hash("•", opts)]) }
|
198
|
+
@pdf.indent(mm2pt(6)) { convert(li, opts) }
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def ol_options(_el, opts)
|
204
|
+
{bottom_padding: opts[:size]}
|
205
|
+
end
|
206
|
+
|
207
|
+
def render_ol(el, opts)
|
208
|
+
with_block_padding(el, opts) do
|
209
|
+
el.children.each_with_index do |li, index|
|
210
|
+
@pdf.float { @pdf.formatted_text([text_hash("#{index + 1}.", opts)]) }
|
211
|
+
@pdf.indent(mm2pt(6)) { convert(li, opts) }
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def li_options(_el, _opts)
|
217
|
+
{}
|
218
|
+
end
|
219
|
+
|
220
|
+
def render_li(el, opts)
|
221
|
+
inner(el, opts)
|
222
|
+
end
|
223
|
+
|
224
|
+
def dl_options(_el, _opts)
|
225
|
+
{}
|
226
|
+
end
|
227
|
+
|
228
|
+
def render_dl(el, opts)
|
229
|
+
inner(el, opts)
|
230
|
+
end
|
231
|
+
|
232
|
+
def dt_options(_el, opts)
|
233
|
+
{styles: (opts[:styles] || []) + [:bold], bottom_padding: 0}
|
234
|
+
end
|
235
|
+
|
236
|
+
def render_dt(el, opts)
|
237
|
+
render_padded_and_formatted_text(el, opts)
|
238
|
+
end
|
239
|
+
|
240
|
+
def dd_options(_el, _opts)
|
241
|
+
{}
|
242
|
+
end
|
243
|
+
|
244
|
+
def render_dd(el, opts)
|
245
|
+
@pdf.indent(mm2pt(10)) { inner(el, opts) }
|
246
|
+
end
|
247
|
+
|
248
|
+
def math_options(_el, _opts)
|
249
|
+
{}
|
250
|
+
end
|
251
|
+
|
252
|
+
def render_math(el, opts)
|
253
|
+
if el.options[:category] == :block
|
254
|
+
@pdf.formatted_text([{text: el.value}], block_hash(opts))
|
255
|
+
else
|
256
|
+
{text: el.value}
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def hr_options(_el, opts)
|
261
|
+
{top_padding: opts[:size], bottom_padding: opts[:size]}
|
262
|
+
end
|
263
|
+
|
264
|
+
def render_hr(el, opts)
|
265
|
+
with_block_padding(el, opts) do
|
266
|
+
@pdf.stroke_horizontal_line(@pdf.bounds.left + mm2pt(5), @pdf.bounds.right - mm2pt(5))
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def codeblock_options(_el, opts)
|
271
|
+
{font: 'Courier', color: '880000', bottom_padding: opts[:size]}
|
272
|
+
end
|
273
|
+
|
274
|
+
def render_codeblock(el, opts)
|
275
|
+
with_block_padding(el, opts) do
|
276
|
+
@pdf.formatted_text([text_hash(el.value, opts, false)], block_hash(opts))
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def table_options(_el, opts)
|
281
|
+
{bottom_padding: opts[:size]}
|
282
|
+
end
|
283
|
+
|
284
|
+
def render_table(el, opts)
|
285
|
+
data = []
|
286
|
+
el.children.each do |container|
|
287
|
+
container.children.each do |row|
|
288
|
+
data << []
|
289
|
+
row.children.each do |cell|
|
290
|
+
if cell.children.any? {|child| child.options[:category] == :block }
|
291
|
+
line = el.options[:location]
|
292
|
+
warning("Can't render tables with cells containing block " \
|
293
|
+
"elements#{line ? " (line #{line})" : ''}")
|
294
|
+
return
|
295
|
+
end
|
296
|
+
cell_data = inner(cell, opts)
|
297
|
+
data.last << cell_data.map {|c| c[:text] }.join('')
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
with_block_padding(el, opts) do
|
302
|
+
@pdf.table(data, width: @pdf.bounds.right) do
|
303
|
+
el.options[:alignment].each_with_index do |alignment, index|
|
304
|
+
columns(index).align = alignment unless alignment == :default
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def text_options(_el, _opts)
|
311
|
+
{}
|
312
|
+
end
|
313
|
+
|
314
|
+
def render_text(el, opts)
|
315
|
+
text_hash(el.value.to_s, opts)
|
316
|
+
end
|
317
|
+
|
318
|
+
def em_options(_el, opts)
|
319
|
+
if opts[:styles]&.include?(:italic)
|
320
|
+
{styles: opts[:styles].reject {|i| i == :italic }}
|
321
|
+
else
|
322
|
+
{styles: (opts[:styles] || []) << :italic}
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
def strong_options(_el, opts)
|
327
|
+
{styles: (opts[:styles] || []) + [:bold]}
|
328
|
+
end
|
329
|
+
|
330
|
+
def a_options(el, _opts)
|
331
|
+
hash = {color: '000088'}
|
332
|
+
if el.attr['href'].start_with?('#')
|
333
|
+
hash[:anchor] = el.attr['href'].sub(/\A#/, '')
|
334
|
+
else
|
335
|
+
hash[:link] = el.attr['href']
|
336
|
+
end
|
337
|
+
hash
|
338
|
+
end
|
339
|
+
|
340
|
+
def render_em(el, opts)
|
341
|
+
inner(el, opts)
|
342
|
+
end
|
343
|
+
alias render_strong render_em
|
344
|
+
alias render_a render_em
|
345
|
+
|
346
|
+
def codespan_options(_el, _opts)
|
347
|
+
{font: 'Courier', color: '880000'}
|
348
|
+
end
|
349
|
+
|
350
|
+
def render_codespan(el, opts)
|
351
|
+
text_hash(el.value, opts)
|
352
|
+
end
|
353
|
+
|
354
|
+
def br_options(_el, _opts)
|
355
|
+
{}
|
356
|
+
end
|
357
|
+
|
358
|
+
def render_br(_el, opts)
|
359
|
+
text_hash("\n", opts, false)
|
360
|
+
end
|
361
|
+
|
362
|
+
def smart_quote_options(_el, _opts)
|
363
|
+
{}
|
364
|
+
end
|
365
|
+
|
366
|
+
def render_smart_quote(el, opts)
|
367
|
+
text_hash(smart_quote_entity(el).char, opts)
|
368
|
+
end
|
369
|
+
|
370
|
+
def typographic_sym_options(_el, _opts)
|
371
|
+
{}
|
372
|
+
end
|
373
|
+
|
374
|
+
def render_typographic_sym(el, opts)
|
375
|
+
str = if el.value == :laquo_space
|
376
|
+
::Kramdown::Utils::Entities.entity('laquo').char +
|
377
|
+
::Kramdown::Utils::Entities.entity('nbsp').char
|
378
|
+
elsif el.value == :raquo_space
|
379
|
+
::Kramdown::Utils::Entities.entity('raquo').char +
|
380
|
+
::Kramdown::Utils::Entities.entity('nbsp').char
|
381
|
+
else
|
382
|
+
::Kramdown::Utils::Entities.entity(el.value.to_s).char
|
383
|
+
end
|
384
|
+
text_hash(str, opts)
|
385
|
+
end
|
386
|
+
|
387
|
+
def entity_options(_el, _opts)
|
388
|
+
{}
|
389
|
+
end
|
390
|
+
|
391
|
+
def render_entity(el, opts)
|
392
|
+
text_hash(el.value.char, opts)
|
393
|
+
end
|
394
|
+
|
395
|
+
def abbreviation_options(_el, _opts)
|
396
|
+
{}
|
397
|
+
end
|
398
|
+
|
399
|
+
def render_abbreviation(el, opts)
|
400
|
+
text_hash(el.value, opts)
|
401
|
+
end
|
402
|
+
|
403
|
+
def img_options(_el, _opts)
|
404
|
+
{}
|
405
|
+
end
|
406
|
+
|
407
|
+
def render_img(el, *) #:nodoc:
|
408
|
+
line = el.options[:location]
|
409
|
+
warning("Rendering span images is not supported for PDF converter#{line ? " (line #{line})" : ''}")
|
410
|
+
nil
|
411
|
+
end
|
412
|
+
|
413
|
+
def xml_comment_options(_el, _opts) #:nodoc:
|
414
|
+
{}
|
415
|
+
end
|
416
|
+
alias xml_pi_options xml_comment_options
|
417
|
+
alias comment_options xml_comment_options
|
418
|
+
alias blank_options xml_comment_options
|
419
|
+
alias footnote_options xml_comment_options
|
420
|
+
alias raw_options xml_comment_options
|
421
|
+
alias html_element_options xml_comment_options
|
422
|
+
|
423
|
+
def render_xml_comment(el, opts) #:nodoc:
|
424
|
+
# noop
|
425
|
+
end
|
426
|
+
alias render_xml_pi render_xml_comment
|
427
|
+
alias render_comment render_xml_comment
|
428
|
+
alias render_blank render_xml_comment
|
429
|
+
|
430
|
+
def render_footnote(el, *) #:nodoc:
|
431
|
+
line = el.options[:location]
|
432
|
+
warning("Rendering #{el.type} not supported for PDF converter#{line ? " (line #{line})" : ''}")
|
433
|
+
nil
|
434
|
+
end
|
435
|
+
alias render_raw render_footnote
|
436
|
+
alias render_html_element render_footnote
|
437
|
+
|
438
|
+
# ----------------------------
|
439
|
+
# :section: Organizational methods
|
440
|
+
#
|
441
|
+
# These methods are used, for example, to up the needed Prawn::Document instance or to create
|
442
|
+
# a PDF outline.
|
443
|
+
# ----------------------------
|
444
|
+
|
445
|
+
# This module gets mixed into the Prawn::Document instance.
|
446
|
+
module PrawnDocumentExtension
|
447
|
+
|
448
|
+
# Extension for the formatted box class to recognize images and move text around them.
|
449
|
+
module CustomBox
|
450
|
+
|
451
|
+
def available_width
|
452
|
+
return super unless @document.respond_to?(:converter) && @document.converter
|
453
|
+
|
454
|
+
@document.image_floats.each do |pn, _x, y, w, h|
|
455
|
+
next if @document.page_number != pn
|
456
|
+
if @at[1] + @baseline_y <= y - @document.bounds.absolute_bottom &&
|
457
|
+
(@at[1] + @baseline_y + @arranger.max_line_height +
|
458
|
+
@leading >= y - h - @document.bounds.absolute_bottom)
|
459
|
+
return @width - w
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
super
|
464
|
+
end
|
465
|
+
|
466
|
+
end
|
467
|
+
|
468
|
+
Prawn::Text::Formatted::Box.extensions << CustomBox
|
469
|
+
|
470
|
+
# Access the converter instance from within Prawn
|
471
|
+
attr_accessor :converter
|
472
|
+
|
473
|
+
def image_floats
|
474
|
+
@image_floats ||= []
|
475
|
+
end
|
476
|
+
|
477
|
+
# Override image embedding method for adding image positions to #image_floats.
|
478
|
+
def embed_image(pdf_obj, info, options)
|
479
|
+
# find where the image will be placed and how big it will be
|
480
|
+
w, h = info.calc_image_dimensions(options)
|
481
|
+
|
482
|
+
if options[:at]
|
483
|
+
x, y = map_to_absolute(options[:at])
|
484
|
+
else
|
485
|
+
x, y = image_position(w, h, options)
|
486
|
+
move_text_position h
|
487
|
+
end
|
488
|
+
|
489
|
+
#--> This part is new
|
490
|
+
if options[:position] == :right
|
491
|
+
image_floats << [page_number, x - 15, y, w + 15, h + 15]
|
492
|
+
end
|
493
|
+
|
494
|
+
# add a reference to the image object to the current page
|
495
|
+
# resource list and give it a label
|
496
|
+
label = "I#{next_image_id}"
|
497
|
+
state.page.xobjects[label] = pdf_obj
|
498
|
+
|
499
|
+
# add the image to the current page
|
500
|
+
instruct = "\nq\n%.3f 0 0 %.3f %.3f %.3f cm\n/%s Do\nQ"
|
501
|
+
add_content(sprintf(instruct, w, h, x, y - h, label))
|
502
|
+
end
|
503
|
+
|
504
|
+
end
|
505
|
+
|
506
|
+
# Return a hash with options that are suitable for Prawn::Document.new.
|
507
|
+
#
|
508
|
+
# Used in #setup_document.
|
509
|
+
def document_options(_root)
|
510
|
+
{
|
511
|
+
page_size: 'A4', page_layout: :portrait, margin: mm2pt(20),
|
512
|
+
info: {
|
513
|
+
Creator: 'kramdown PDF converter',
|
514
|
+
CreationDate: Time.now,
|
515
|
+
},
|
516
|
+
compress: true, optimize_objects: true
|
517
|
+
}
|
518
|
+
end
|
519
|
+
|
520
|
+
# Create a Prawn::Document object and return it.
|
521
|
+
#
|
522
|
+
# Can be used to define repeatable content or register fonts.
|
523
|
+
#
|
524
|
+
# Used in #render_root.
|
525
|
+
def setup_document(root)
|
526
|
+
doc = Prawn::Document.new(document_options(root))
|
527
|
+
doc.extend(PrawnDocumentExtension)
|
528
|
+
doc.converter = self
|
529
|
+
doc
|
530
|
+
end
|
531
|
+
|
532
|
+
# Used in #render_root.
|
533
|
+
def finish_document(root)
|
534
|
+
# no op
|
535
|
+
end
|
536
|
+
|
537
|
+
# Create the PDF outline from the header elements in the TOC.
|
538
|
+
def create_outline(root)
|
539
|
+
toc = ::Kramdown::Converter::Toc.convert(root).first
|
540
|
+
|
541
|
+
text_of_header = lambda do |el|
|
542
|
+
if el.type == :text
|
543
|
+
el.value
|
544
|
+
else
|
545
|
+
el.children.map {|c| text_of_header.call(c) }.join('')
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
add_section = lambda do |item, parent|
|
550
|
+
text = text_of_header.call(item.value)
|
551
|
+
destination = @dests[item.attr[:id]]
|
552
|
+
if !parent
|
553
|
+
@pdf.outline.page(title: text, destination: destination)
|
554
|
+
else
|
555
|
+
@pdf.outline.add_subsection_to(parent) do
|
556
|
+
@pdf.outline.page(title: text, destination: destination)
|
557
|
+
end
|
558
|
+
end
|
559
|
+
item.children.each {|c| add_section.call(c, text) }
|
560
|
+
end
|
561
|
+
|
562
|
+
toc.children.each do |item|
|
563
|
+
add_section.call(item, nil)
|
564
|
+
end
|
565
|
+
end
|
566
|
+
|
567
|
+
# ----------------------------
|
568
|
+
# :section: Helper methods
|
569
|
+
# ----------------------------
|
570
|
+
|
571
|
+
# Move the prawn document cursor down before and/or after yielding the given block.
|
572
|
+
#
|
573
|
+
# The :top_padding and :bottom_padding options are used for determinig the padding amount.
|
574
|
+
def with_block_padding(_el, opts)
|
575
|
+
@pdf.move_down(opts[:top_padding]) if opts.key?(:top_padding)
|
576
|
+
yield
|
577
|
+
@pdf.move_down(opts[:bottom_padding]) if opts.key?(:bottom_padding)
|
578
|
+
end
|
579
|
+
|
580
|
+
# Render the children of the given element as formatted text and respect the top/bottom
|
581
|
+
# padding (see #with_block_padding).
|
582
|
+
def render_padded_and_formatted_text(el, opts)
|
583
|
+
with_block_padding(el, opts) { @pdf.formatted_text(inner(el, opts), block_hash(opts)) }
|
584
|
+
end
|
585
|
+
|
586
|
+
# Helper function that returns a hash with valid "formatted text" options.
|
587
|
+
#
|
588
|
+
# The +text+ parameter is used as value for the :text key and if +squeeze_whitespace+ is
|
589
|
+
# +true+, all whitespace is converted into spaces.
|
590
|
+
def text_hash(text, opts, squeeze_whitespace = true)
|
591
|
+
text = text.gsub(/\s+/, ' ') if squeeze_whitespace
|
592
|
+
hash = {text: text}
|
593
|
+
[:styles, :size, :character_spacing, :font, :color, :link,
|
594
|
+
:anchor, :draw_text_callback, :callback].each do |key|
|
595
|
+
hash[key] = opts[key] if opts.key?(key)
|
596
|
+
end
|
597
|
+
hash
|
598
|
+
end
|
599
|
+
|
600
|
+
# Helper function that returns a hash with valid options for the prawn #text_box extracted
|
601
|
+
# from the given options.
|
602
|
+
def block_hash(opts)
|
603
|
+
hash = {}
|
604
|
+
[:align, :valign, :mode, :final_gap, :leading, :fallback_fonts,
|
605
|
+
:direction, :indent_paragraphs].each do |key|
|
606
|
+
hash[key] = opts[key] if opts.key?(key)
|
607
|
+
end
|
608
|
+
hash
|
609
|
+
end
|
610
|
+
|
611
|
+
end
|
612
|
+
|
613
|
+
end
|
614
|
+
end
|
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kramdown-converter-pdf
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Thomas Leitner
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-01-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: kramdown
|
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
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: prawn-table
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.2.2
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.2.2
|
55
|
+
description:
|
56
|
+
email: t_leitner@gmx.at
|
57
|
+
executables: []
|
58
|
+
extensions: []
|
59
|
+
extra_rdoc_files: []
|
60
|
+
files:
|
61
|
+
- CONTRIBUTERS
|
62
|
+
- COPYING
|
63
|
+
- VERSION
|
64
|
+
- lib/kramdown/converter/pdf.rb
|
65
|
+
homepage: https://github.com/kramdown/converter-pdf
|
66
|
+
licenses:
|
67
|
+
- MIT
|
68
|
+
metadata: {}
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options: []
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '2.3'
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
requirements: []
|
84
|
+
rubyforge_project:
|
85
|
+
rubygems_version: 2.7.3
|
86
|
+
signing_key:
|
87
|
+
specification_version: 4
|
88
|
+
summary: kramdown-converter-pdf uses Prawn to convert a kramdown document to PDF
|
89
|
+
test_files: []
|