jekyll-printing-press 1.0.0rc1

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,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pdf/info'
4
+ require_relative 'utils'
5
+
6
+ module Jekyll
7
+ module Pandoc
8
+ # Mixin to take a single page PDF and generate a ready for print
9
+ # PDF.
10
+ #
11
+ # The implementation uses a Liquid template written in TeX to
12
+ # import the original PDF and generate a new PDF with the printing
13
+ # layout.
14
+ module Printing
15
+ # How many pages fit per sheet. To obtain them divide the sheet
16
+ # size value by the page size value. An A0 sheet fits 128 A7 pages.
17
+ #
18
+ # @return [Hash]
19
+ SHEET_SIZES = {
20
+ a7: 2,
21
+ a6: 4,
22
+ a5: 8,
23
+ a4: 16,
24
+ a3: 32,
25
+ a2: 64,
26
+ a1: 128,
27
+ a0: 256
28
+ }.freeze
29
+
30
+ # Which sheet sizes are landscaped
31
+ #
32
+ # @return [Array]
33
+ LANDSCAPE = [2, 8, 32, 128].freeze
34
+
35
+ private
36
+
37
+ # Options for the template
38
+ #
39
+ # @return [Array]
40
+ def options
41
+ @options ||= [('landscape' if landscape?)].compact
42
+ end
43
+
44
+ # LaTeX template for processing a PDF and rearranging its pages
45
+ #
46
+ # @return [Liquid::Template]
47
+ def template
48
+ @template ||= Liquid::Template.parse(File.read(File.join(File.dirname(__FILE__), 'printing.latex.liquid')))
49
+ end
50
+
51
+ # Generates an array of pages in the order they need to be printed.
52
+ # This depends on the printer implementation.
53
+ #
54
+ # @return [Array]
55
+ def pages
56
+ raise NotImplementError
57
+ end
58
+
59
+ # Sheet size. The sheet is the paper where the pages are printed
60
+ # on.
61
+ #
62
+ # @return [Symbol]
63
+ def sheetsize
64
+ @sheetsize ||= site.config.dig('pandoc', 'printing', 'sheetsize').to_sym.tap do |x|
65
+ raise NoMethodError unless SHEET_SIZES.key? x
66
+ end
67
+ rescue NoMethodError
68
+ Jekyll.logger.warn "Sheetsize is missing or incorrect, please add one of #{SHEET_SIZES.keys.join(', ')} to _config.yml"
69
+ raise ArgumentError, 'Please configure the jekyll-pandoc-multiple-formats plugin'
70
+ end
71
+
72
+ # Page size is the same as papersize
73
+ #
74
+ # @return [Symbol]
75
+ def pagesize
76
+ @pagesize ||= site.config['pandoc'].papersize.tap do |x|
77
+ raise NoMethodError unless SHEET_SIZES.key? x
78
+ end
79
+ rescue NoMethodError
80
+ Jekyll.logger.warn "Papersize is missing or incorrect, please add one of #{SHEET_SIZES.keys.join(', ')} to PDF format in _config.yml"
81
+ raise ArgumentError, 'Please configure the jekyll-pandoc-multiple-formats plugin'
82
+ end
83
+
84
+ # Represents a blank page
85
+ #
86
+ # @return [String]
87
+ def blank_page
88
+ @blank_page ||= '{}'
89
+ end
90
+
91
+ # Page count in the source PDF
92
+ #
93
+ # PDFInfo requires `pdfinfo` from Poppler installed.
94
+ #
95
+ # XXX: It may be possible to extract the metadata just by
96
+ # `#binread`ing the file, but apparently PDF has many ways to
97
+ # inform the page count, so we're adding this dependency for
98
+ # simplicity.
99
+ #
100
+ # @return [Integer]
101
+ def page_count
102
+ @page_count ||= PDF::Info.new(document.source_document.tempfile.path).metadata[:page_count]
103
+ end
104
+
105
+ # Pages per sheet
106
+ #
107
+ # @return [Integer]
108
+ def nup
109
+ @nup ||= SHEET_SIZES[sheetsize] / SHEET_SIZES[pagesize]
110
+ end
111
+
112
+ # Information for rendering the template
113
+ #
114
+ # @return [Hash]
115
+ def template_payload
116
+ @template_payload ||=
117
+ {
118
+ 'nup' => nup,
119
+ 'sheetsize' => "#{sheetsize}paper",
120
+ 'options' => options,
121
+ 'path' => document.source_document.tempfile.path,
122
+ 'pages' => pages
123
+ }
124
+ end
125
+
126
+ # Renders the template. We're following Jekyll's process where
127
+ # templates are rendered and written in different steps.
128
+ #
129
+ # @return [String]
130
+ def render_template
131
+ template.render(**template_payload)
132
+ end
133
+
134
+ # Generates a new PDF and writes it into the site.
135
+ #
136
+ # @return [String] empty output
137
+ def convert(_)
138
+ Dir.mktmpdir do |dir|
139
+ Dir.chdir dir do
140
+ Open3.popen2e(Utils.tectonic, '-') do |stdin, stdout, thread|
141
+ stdin.puts render_template
142
+ stdin.close
143
+ stderr = stdout.read
144
+
145
+ # Wait for the process to finish and raise an error if it fails
146
+ unless thread.value.success?
147
+ raise StandardError, "I couldn't generate #{document.relative_path}, the process said: #{stderr}"
148
+ end
149
+
150
+ # Copy the contents to the tempfile
151
+ IO.copy_stream(File.open('texput.pdf'), document.tempfile.path)
152
+ end
153
+ end
154
+ end
155
+
156
+ ''
157
+ end
158
+
159
+ # Detect if sheet should be printed in landscape mode.
160
+ #
161
+ # @return [Boolean]
162
+ def landscape?
163
+ LANDSCAPE.include? nup
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'paru_helper'
4
+ require_relative 'utils'
5
+
6
+ module Jekyll
7
+ module Pandoc
8
+ # Renderer for Pandoc formats. Contrary to Jekyll's workflow, the
9
+ # renderer doesn't call a {Jekyll::Converter}, because they convert
10
+ # based on origin format and not destination format.
11
+ class Renderer < Jekyll::Renderer
12
+ # Converts document using pandoc. Pandoc needs the document
13
+ # content, maybe already rendered with Liquid, and the document
14
+ # metadata.
15
+ #
16
+ # For binary formats, the conversion string will be empty, and we
17
+ # write the output file on a temporary file that is later copied
18
+ # to the destination by {Jekyll::Pandoc::Document#write}.
19
+ #
20
+ # @param content [String]
21
+ # @return [String] empty string when binary file
22
+ def convert(content)
23
+ output = super
24
+ type = document.type
25
+ extra = {}
26
+ extra[:output] = document.tempfile.path if document.binary?
27
+
28
+ content = <<~EOD
29
+ #{Utils.sanitize_data(document.data).to_yaml}
30
+ ---
31
+
32
+ #{output}
33
+ EOD
34
+
35
+ File.open(File.join('/tmp', document.relative_path), 'w') do |f|
36
+ f.write content
37
+ end
38
+
39
+ ParuHelper.from(from: 'markdown', to: type, **site.config['pandoc'].options[type], **extra) << content
40
+ end
41
+
42
+ # Don't convert Markdown to HTML, but do convert everything else
43
+ #
44
+ # @return [array]
45
+ def converters
46
+ @converters ||= super.reject do |c|
47
+ c.instance_of? Jekyll::Converters::Markdown
48
+ end
49
+ end
50
+
51
+ # Output extension
52
+ #
53
+ # @return [String]
54
+ def output_ext
55
+ @output_ext ||= ".#{document.type}"
56
+ end
57
+
58
+ # Do nothing
59
+ def assign_pages!; end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../renderer'
4
+ require_relative '../printing'
5
+
6
+ module Jekyll
7
+ module Pandoc
8
+ module Renderers
9
+ # Applies binder imposition to a PDF document
10
+ class Binder < Jekyll::Pandoc::Renderer
11
+ include Jekyll::Pandoc::Printing
12
+
13
+ # @return [String]
14
+ def output_ext
15
+ @output_ext ||= '-bound.pdf'
16
+ end
17
+
18
+ private
19
+
20
+ # Generates page order for binding, taking into account how many
21
+ # pages fit per sheet.
22
+ #
23
+ # @return [Array]
24
+ def pages
25
+ @pages ||= (1..page_count).map do |page|
26
+ [page] * nup
27
+ end.flatten
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../renderer'
4
+ require_relative '../printing'
5
+
6
+ module Jekyll
7
+ module Pandoc
8
+ module Renderers
9
+ # Applies imposition to a PDF document
10
+ class Imposition < Jekyll::Pandoc::Renderer
11
+ include Jekyll::Pandoc::Printing
12
+
13
+ # @return [String]
14
+ def output_ext
15
+ @output_ext ||= '-imposed.pdf'
16
+ end
17
+
18
+ private
19
+
20
+ # Pages rounded up to the nearest modulo of 4
21
+ #
22
+ # @return [Integer]
23
+ def signed_pages
24
+ @signed_pages ||= (page_count + 3) / 4 * 4
25
+ end
26
+
27
+ # The signature is the amount of pages per fold, so it needs to
28
+ # be a modulo of 4.
29
+ #
30
+ # Default is {#page_count} rounded to the next modulo of 4.
31
+ #
32
+ # @return [Integer]
33
+ def signature
34
+ @signature ||= (site.config.dig('pandoc', 'printing', 'signature') || signed_pages).tap do |s|
35
+ raise ArgumentError, 'Signature needs to be modulo of 4' unless (s % 4).zero?
36
+ end
37
+ end
38
+
39
+ # Pages that are empty. At most you'll have 3 empty pages.
40
+ #
41
+ # @return [Integer]
42
+ def blank_pages
43
+ @blank_pages ||= signed_pages - page_count
44
+ end
45
+
46
+ # Generates page order for imposition, taking into account how
47
+ # many pages fit per sheet.
48
+ #
49
+ # @return [Array]
50
+ def pages
51
+ # Generate a list of page numbers and pad to signed pages with
52
+ # blank pages.
53
+ padded = (1..page_count).to_a + Array.new(blank_pages, blank_page)
54
+
55
+ # Each fold is the size of the signature
56
+ padded.each_slice(signature).map do |fold|
57
+ # And is split in half
58
+ first, last = fold.each_slice(fold.size / 2).to_a
59
+
60
+ # Add padding
61
+ last << nil
62
+ # Reverse and split in pairs
63
+ last = last.reverse.each_slice(2)
64
+ # Just split in pairs
65
+ first = first.each_slice(2)
66
+
67
+ # Apply page order, for instance a sheet would have pages
68
+ # 20, 1, 2, and 19, and then apply pages on sheet depending
69
+ # on its N-up number.
70
+ #
71
+ # If nup is greater than 2, instead of making this algorithm
72
+ # more complex, we just print nup/2 copies of the PDF. So
73
+ # you have more to share!
74
+ last.zip(first.to_a).flatten.compact.each_slice(2).map do |pair|
75
+ pair * (nup / 2)
76
+ end
77
+ end.flatten
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module Jekyll
6
+ module Pandoc
7
+ # Finds external commands
8
+ module Utils
9
+ extend self
10
+
11
+ # Remove these keys from {#data}
12
+ #
13
+ # @return [Array]
14
+ EXCLUDED_DATA = %w[excerpt permalink].freeze
15
+
16
+ # Remove and transform values for safe YAML dumping.
17
+ #
18
+ # Copied almost verbatim from jekyll-linked-posts
19
+ #
20
+ # @return [Hash]
21
+ def sanitize_data(data)
22
+ data.reject do |k, _|
23
+ EXCLUDED_DATA.include? k
24
+ end.transform_values do |value|
25
+ case value
26
+ when Jekyll::Document
27
+ value.data['uuid']
28
+ when Jekyll::Convertible
29
+ value.data['uuid']
30
+ when Set
31
+ value.map do |v|
32
+ v.respond_to?(:data) ? v.data['uuid'] : v
33
+ end
34
+ when Array
35
+ value.map do |v|
36
+ v.respond_to?(:data) ? v.data['uuid'] : v
37
+ end
38
+ when Hash
39
+ value.transform_values do |v|
40
+ v.respond_to?(:data) ? v.data['uuid'] : v
41
+ end
42
+ else
43
+ value
44
+ end
45
+ end
46
+ end
47
+
48
+ # Yes, we do respond to any method called.
49
+ def respond_to_missing?(_, _)
50
+ true
51
+ end
52
+
53
+ # Allow to call #program_name and #program_name? instead of calling
54
+ # `which` everytime.
55
+ def method_missing(name, *_args)
56
+ n = name.to_s.sub(/\?\z/, '').to_sym
57
+
58
+ define_method(n) do
59
+ which n.to_s
60
+ end
61
+
62
+ define_method(:"#{n}?") do
63
+ !which(n.to_s).empty?
64
+ rescue ArgumentError
65
+ false
66
+ end
67
+
68
+ public_send name
69
+ end
70
+
71
+ # Find a program in PATH by name or throw an error
72
+ #
73
+ # @param util [String] Program name
74
+ # @return [String] Program path
75
+ def which(util)
76
+ @which_cache ||= {}
77
+ @which_cache[util] ||=
78
+ begin
79
+ result = nil
80
+
81
+ Open3.popen2('which', util) do |stdin, stdout, thread|
82
+ stdin.close
83
+ result = stdout.read.strip
84
+
85
+ unless thread.value.success?
86
+ raise ArgumentError, "Couldn't find #{util} on your PATH, maybe you need to install it?"
87
+ end
88
+ end
89
+
90
+ result
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'jekyll/pandoc/configuration'
4
+ require_relative 'jekyll/converters/markdown/pandoc'
5
+ require_relative 'jekyll/pandoc/generators/posts'
6
+ require_relative 'jekyll/pandoc/generators/category'
7
+ require_relative 'jekyll/pandoc/generators/site'
8
+ require_relative 'jekyll/pandoc/generators/imposition'
9
+ require_relative 'jekyll/pandoc/generators/binder'
10
+
11
+ # We modify the configuration post read, and not after init, because
12
+ # Jekyll resets twice and any modification to config will invalidate the
13
+ # cache.
14
+ Jekyll::Hooks.register :site, :post_read, priority: :high do |site|
15
+ site.config['pandoc'] = Jekyll::Pandoc::Configuration.new(site).tap(&:process)
16
+ end