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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +222 -0
- data/lib/jekyll/converters/markdown/pandoc.rb +47 -0
- data/lib/jekyll/pandoc/configuration.rb +120 -0
- data/lib/jekyll/pandoc/document.rb +202 -0
- data/lib/jekyll/pandoc/documents/bound.rb +58 -0
- data/lib/jekyll/pandoc/documents/imposed.rb +61 -0
- data/lib/jekyll/pandoc/documents/multiple.rb +76 -0
- data/lib/jekyll/pandoc/generator.rb +94 -0
- data/lib/jekyll/pandoc/generators/binder.rb +45 -0
- data/lib/jekyll/pandoc/generators/category.rb +32 -0
- data/lib/jekyll/pandoc/generators/imposition.rb +45 -0
- data/lib/jekyll/pandoc/generators/multiple.rb +62 -0
- data/lib/jekyll/pandoc/generators/posts.rb +54 -0
- data/lib/jekyll/pandoc/generators/site.rb +32 -0
- data/lib/jekyll/pandoc/paru_helper.rb +24 -0
- data/lib/jekyll/pandoc/printing.latex.liquid +10 -0
- data/lib/jekyll/pandoc/printing.rb +167 -0
- data/lib/jekyll/pandoc/renderer.rb +62 -0
- data/lib/jekyll/pandoc/renderers/binder.rb +32 -0
- data/lib/jekyll/pandoc/renderers/imposition.rb +82 -0
- data/lib/jekyll/pandoc/utils.rb +95 -0
- data/lib/jekyll-pandoc-multiple-formats.rb +16 -0
- metadata +311 -0
@@ -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
|