udrs 0.1.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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c2c17a09654a55b9b2e30ee71b9f6f5d940f32e8
4
+ data.tar.gz: 7ece63f00f97b75c1e955679c0f55a681a5802b3
5
+ SHA512:
6
+ metadata.gz: da98fe1ffe17900362377dc6bc3baeab6807578b8746ec96f21fefde97af0f72db7a65a7354a92591879e57a1432c0e53ff89d41c0a1a210f34c9b2875dcf8c3
7
+ data.tar.gz: 8a944d33c23d1ce309e78c65523ba15951d25fca0fbf8c2eab62c2b5b9df8304c6e0672525c400a1e9ab1ae44b50b676b8d9913b01524a2de575cb3c09a95fcb
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Michon van Dooren
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,57 @@
1
+ # UDRS
2
+
3
+ UDRS stands for the Universal Document Rendering System. This is an ambition name, and the gem does not in any way live up to the expectations that the name creates.
4
+
5
+ What it does do, in short, is allow you to write a single template for a file, and then render that template to a number of formats.
6
+
7
+ Currently, the following formats are supported:
8
+ - PDF
9
+ - ESC/P (Epson receipt printers)
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'udrs'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install udrs
26
+
27
+ ## Usage
28
+
29
+ Create a view in the normal location, with the `.udrs` extension. The use the methods provided in doc to create your document.
30
+
31
+ Example template:
32
+
33
+ ```ruby
34
+ udrs_document do |doc|
35
+ doc.section('Hello world!')
36
+ doc.subsection('Introduction to the world')
37
+ doc.text('Lorem ipsum dolor sit amet')
38
+
39
+ doc.table([:fit, :expand, :fit]) do |table|
40
+ table.row(10, 'Lorem ipsum', 12.34)
41
+ table.row do |row|
42
+ row.cell(10)
43
+ row.cell('Lorem ipsum dolor sit amet', style: :bold)
44
+ row.cell(-12)
45
+ end
46
+ end
47
+
48
+ doc.footer do
49
+ doc.text('Copyright 1970 Anonymous', size: :small)
50
+ end
51
+ end
52
+ ```
53
+
54
+ ## License
55
+
56
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
57
+
@@ -0,0 +1,3 @@
1
+ require 'udrs/document'
2
+ require 'udrs/engine'
3
+ require 'udrs/version'
@@ -0,0 +1,11 @@
1
+ require 'udrs/components/code'
2
+ require 'udrs/components/container'
3
+ require 'udrs/components/footer'
4
+ require 'udrs/components/header'
5
+ require 'udrs/components/logo'
6
+ require 'udrs/components/page_end'
7
+ require 'udrs/components/raw'
8
+ require 'udrs/components/spacer'
9
+ require 'udrs/components/table'
10
+ require 'udrs/components/text'
11
+ require 'udrs/components/t_text'
@@ -0,0 +1,11 @@
1
+ module UDRS
2
+ module Components
3
+ class Code
4
+ attr_reader :code
5
+
6
+ def initialize(code)
7
+ @code = code
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module UDRS
2
+ module Components
3
+ class Container
4
+ attr_reader :items
5
+
6
+ def initialize
7
+ @items = []
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module UDRS
2
+ module Components
3
+ class Footer < Container
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,15 @@
1
+ module UDRS
2
+ module Components
3
+ class Header
4
+ attr_reader :text, :level
5
+
6
+ LEVELS = 1..3
7
+
8
+ def initialize(text, level = 1)
9
+ @text = text.to_s
10
+ fail ArgumentError, "Invalid level: #{level}" unless LEVELS.include?(level)
11
+ @level = level
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module UDRS
2
+ module Components
3
+ class Logo
4
+ attr_reader :alignment, :valignment
5
+
6
+ def initialize(options = {})
7
+ @alignment = options[:alignment] || :center
8
+ @valignment = options[:valignment] || :top
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,6 @@
1
+ module UDRS
2
+ module Components
3
+ class PageEnd
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ module UDRS
2
+ module Components
3
+ class Raw
4
+ attr_reader :block
5
+
6
+ def initialize(&block)
7
+ @block = block
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module UDRS
2
+ module Components
3
+ class Spacer
4
+ attr_reader :amount
5
+
6
+ def initialize(amount)
7
+ @amount = amount
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ module UDRS
2
+ module Components
3
+ class TText
4
+ attr_reader :title, :body
5
+
6
+ def initialize(title, body)
7
+ @title = title.to_s
8
+ @body = body.to_s
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,70 @@
1
+ module UDRS
2
+ module Components
3
+ class Table
4
+ attr_reader :columns, :rows
5
+
6
+ COLUMN_SIZES = %i(fit expand hide)
7
+
8
+ def initialize(*columns, &block)
9
+ @columns = columns
10
+ invalid = (columns.reject { |c| c.is_a?(Fixnum) } - COLUMN_SIZES).uniq
11
+ fail ArgumentError, "Invalid column size(s): #{invalid.join(', ')}" unless invalid.empty?
12
+
13
+ @rows = []
14
+ block.call(self)
15
+ end
16
+
17
+ def row(*args, &block)
18
+ @rows << Row.new(self, *args, &block)
19
+ end
20
+
21
+ def get_column_widths
22
+ return columns.each_with_index.map do |column, i|
23
+ case column
24
+ # Fixed width
25
+ when Fixnum
26
+ next [column, column, column]
27
+
28
+ # Fit
29
+ when :fit
30
+ size = rows.map { |r| r.cells[i] }.map(&:text).map(&:size).max
31
+ next [size, size, size]
32
+
33
+ # Expand
34
+ when :expand
35
+ size = rows.map { |r| r.cells[i] }.map(&:text).map(&:size).max
36
+ next [0, size, 9999]
37
+
38
+ # Hide
39
+ when :hide
40
+ next [0, 0, 0]
41
+
42
+ else
43
+ fail ArgumentError, "Cannot calculate column width for #{column}"
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ class Row
50
+ attr_reader :cells
51
+
52
+ def initialize(table, *args, &block)
53
+ options = {}
54
+ options = args.pop if args.last.is_a?(Hash)
55
+
56
+ @cells = []
57
+ args.each { |c| cell(c, options) }
58
+ block.call(self) if block
59
+
60
+ if @cells.size != table.columns.size
61
+ fail ArgumentError, "Row contains #{cells.size} cells, but #{table.columns.size} are required"
62
+ end
63
+ end
64
+
65
+ def cell(text, options = {})
66
+ @cells << Text.new(text, options)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,33 @@
1
+ require 'attribute_predicates'
2
+
3
+ module UDRS
4
+ module Components
5
+ class Text
6
+ attr_reader :text, :is_bold, :is_italic, :is_underline, :size, :alignment
7
+
8
+ SIZES = %i(tiny small normal medium large huge)
9
+ ALIGNENTS = %i(left center right)
10
+
11
+ def initialize(text, options = {})
12
+ @text = text.to_s
13
+
14
+ # Text size
15
+ @size = :normal
16
+ @size = options[:size] if options[:size].present?
17
+ fail ArgumentError, "Invalid size: #{@size}" unless SIZES.include?(@size)
18
+
19
+ # Style(s)
20
+ style = [*options[:style]]
21
+ @is_bold = style.delete(:bold)
22
+ @is_italic = style.delete(:italic)
23
+ @is_underline = style.delete(:underline)
24
+ fail ArgumentError, "Invalid style(s): #{style.join(', ')}" unless style.empty?
25
+
26
+ # Alignment
27
+ @alignment = :left
28
+ @alignment = options[:align] if options[:align].present?
29
+ fail ArgumentError, "Invalid alignment: #{@alignment}" unless ALIGNENTS.include?(@alignment)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,109 @@
1
+ require 'udrs/components'
2
+ require 'udrs/renderers'
3
+
4
+ require 'attribute_predicates'
5
+
6
+ module UDRS
7
+ class Document
8
+ attr_reader :is_rendering
9
+
10
+ TYPES = %i(pdf escp)
11
+
12
+ def initialize(type)
13
+ fail ArgumentError, "Unknown udrs type #{type}" unless TYPES.include?(type)
14
+ @type = type
15
+ case type
16
+ when :pdf
17
+ @renderer = Renderers::PDFRenderer.new
18
+ when :escp
19
+ @renderer = Renderers::ESCPRenderer.new
20
+ end
21
+
22
+ @container = Components::Container.new
23
+ @rendering = false
24
+ end
25
+
26
+ # Check the document type
27
+ def pdf?
28
+ return @type == :pdf
29
+ end
30
+ def escp?
31
+ return @type == :escp
32
+ end
33
+
34
+ # Add components
35
+ def end_page(*args)
36
+ add_component(Components::PageEnd.new(*args))
37
+ end
38
+ def spacer(*args)
39
+ add_component(Components::Spacer.new(*args))
40
+ end
41
+ def section(txt)
42
+ add_component(Components::Header.new(txt, 1))
43
+ end
44
+ def subsection(txt)
45
+ add_component(Components::Header.new(txt, 2))
46
+ end
47
+ def subsubsection(txt)
48
+ add_component(Components::Header.new(txt, 3))
49
+ end
50
+ def text(*args)
51
+ add_component(Components::Text.new(*args))
52
+ end
53
+ def ttext(*args)
54
+ add_component(Components::TText.new(*args))
55
+ end
56
+ def logo(*args)
57
+ add_component(Components::Logo.new(*args))
58
+ end
59
+ def code(*args)
60
+ add_component(Components::Code.new(*args))
61
+ end
62
+ def table(*args, &block)
63
+ add_component(Components::Table.new(*args, &block))
64
+ end
65
+ def raw(*args, &block)
66
+ add_component(Components::Raw.new(*args, &block))
67
+ end
68
+ def footer(*args, &block)
69
+ footer = Components::Footer.new(*args)
70
+ using_container(footer, &block)
71
+ add_component(footer)
72
+ end
73
+
74
+ # Render the components using the renderer
75
+ def render
76
+ unless @rendered
77
+ @is_rendering = true
78
+ @rendered = @renderer.render(@container)
79
+ @is_rendering = false
80
+ end
81
+ return @rendered
82
+ end
83
+ def render!
84
+ @rendered = nil
85
+ return render
86
+ end
87
+
88
+ private
89
+
90
+ # If the rendering has already started, immediately render instead of caching
91
+ #
92
+ # This _should_ only happen in blocks passed to raw
93
+ def add_component(component)
94
+ if is_rendering?
95
+ @renderer.render_item(component)
96
+ else
97
+ @container.items << component
98
+ end
99
+ end
100
+
101
+ # Perform a block with the current container set to something else
102
+ def using_container(container, &block)
103
+ old_container = @container
104
+ @container = container
105
+ block.call
106
+ @container = old_container
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,14 @@
1
+ require 'udrs/engine/rails_helper'
2
+ require 'udrs/engine/template_renderer'
3
+
4
+ module UDRS
5
+ module Engine
6
+ class RailsEngine < Rails::Engine
7
+ ActionView::Base.send(:include, UDRS::Engine::RailsHelper)
8
+ ActionView::Template.register_template_handler(:udrs, UDRS::Engine::TemplateRenderer)
9
+
10
+ Mime::Type.register_alias('application/pdf', :pdf) if !Mime::Type.lookup_by_extension(:pdf)
11
+ Mime::Type.register_alias('application/octet-stream', :escp) if !Mime::Type.lookup_by_extension(:escp)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ module UDRS
2
+ module Engine
3
+ module RailsHelper
4
+ def udrs_document(&block)
5
+ doc = UDRS::Document.new(params[:format].to_sym)
6
+ block.call(doc)
7
+ return doc.render
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ module UDRS
2
+ module Engine
3
+ class TemplateRenderer
4
+ def self.call(template)
5
+ return <<-END
6
+ output = #{template.source.strip}
7
+ @filename = "\#{controller.action_name}.\#{params[:format]}"
8
+ controller.response.headers['Content-Disposition'] = 'inline; filename="\#{@filename}"'
9
+ return output
10
+ END
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,2 @@
1
+ require 'udrs/renderers/escp_renderer'
2
+ require 'udrs/renderers/pdf_renderer'
@@ -0,0 +1,17 @@
1
+ module UDRS
2
+ module Renderers
3
+ class Base
4
+ def render_item(item)
5
+ method_name = "render_#{item.class.name.demodulize.underscore}"
6
+ fail NotImplementedError, "Cannot render #{item.class.name}" unless respond_to?(method_name, true)
7
+ method(method_name).call(item)
8
+ end
9
+
10
+ protected
11
+
12
+ def render_items(items)
13
+ items.each(&method(:render_item))
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,156 @@
1
+ require 'udrs/renderers/base'
2
+ require 'udrs/renderers/escp_renderer/font.rb'
3
+ require 'udrs/renderers/escp_renderer/line.rb'
4
+ require 'udrs/renderers/escp_renderer/table.rb'
5
+
6
+ module UDRS
7
+ module Renderers
8
+ # The renderer for ESC/P, short for Epson Standard Code for Printers
9
+ class ESCPRenderer < Base
10
+ NUL = 0.chr
11
+ EOT = 4.chr
12
+ ENQ = 5.chr
13
+ HT = 9.chr
14
+ LF = 10.chr
15
+ FF = 12.chr
16
+ CR = 13.chr
17
+ DLE = 16.chr
18
+ DC4 = 20.chr
19
+ CAN = 24.chr
20
+ ESC = 27.chr
21
+ FS = 28.chr
22
+ GS = 29.chr
23
+
24
+ CHARACTER_MAP = {
25
+ '€' => "\xD5",
26
+ '£' => "\x9C",
27
+ '¢' => "\xBD",
28
+ '«' => "\xAE",
29
+ '»' => "\xAF",
30
+ '→' => "\xAF",
31
+ '©' => "\xB8",
32
+ '®' => "\xA9",
33
+ }
34
+
35
+ CODE_NUM = 48 # The code num for PDF417
36
+ CODE_SIZE = 3
37
+
38
+ def render(container)
39
+ @buffer = ''
40
+ @buffer << "#{ESC}@" # Re-init printer
41
+ @buffer << "#{ESC}t#{19.chr}" # Reset character code table to PC858: Euro
42
+ @buffer << "#{ESC}M#{1.chr}" # Set the font
43
+ @buffer << "#{ESC}G#{0.chr}" # Set double-strike to off
44
+ apply_font_size(:normal)
45
+ apply_bold(false)
46
+ apply_underline(:off)
47
+
48
+ # Render the items
49
+ render_item(container)
50
+ render_page_end(nil)
51
+
52
+ # Open the cash drawer
53
+ @buffer << "#{ESC}p#{0.chr}#{100.chr}#{100.chr}"
54
+
55
+ # Replace special characters
56
+ buffer = @buffer.dup
57
+ CHARACTER_MAP.each do |char_orig, char_new|
58
+ buffer.gsub!(char_orig, char_new)
59
+ end
60
+
61
+ return buffer
62
+ end
63
+
64
+ private
65
+
66
+ ##############################
67
+ # Render methods
68
+ ##############################
69
+
70
+ def render_page_end(page_end)
71
+ # Cut the paper at page end
72
+ @buffer << "\n\n\n\n\n\x1DV1\n"
73
+ end
74
+
75
+ def render_spacer(spacer)
76
+ add_spacer
77
+ end
78
+
79
+ def render_header(header)
80
+ case header.level
81
+ when 1
82
+ add_spacer
83
+ add_line(header.text, size: :huge, bold: true, align: :center)
84
+ add_spacer
85
+
86
+ when 2
87
+ add_spacer
88
+ add_line(header.text, size: :large, bold: true)
89
+
90
+ when 3
91
+ add_line(header.text, size: :medium, bold: true)
92
+
93
+ else
94
+ fail ArgumentError, "Invalid header level: #{header.level}"
95
+ end
96
+ end
97
+
98
+ def render_text(item)
99
+ add_line(item.text, text_to_options(item).merge(reflow: true))
100
+ end
101
+
102
+ def render_t_text(item)
103
+ add_line("#{item.title}: #{item.body}", size: :normal, bold: false, underline: false, reflow: true)
104
+ end
105
+
106
+ def render_logo(logo)
107
+ @buffer << "#{GS}(L#{6.chr}#{0.chr}#{48.chr}#{69.chr}#{32.chr}#{32.chr}#{1.chr}#{1.chr}"
108
+ @last_added = :logo
109
+ add_spacer
110
+ end
111
+
112
+ def render_code(code)
113
+ get_pl_ph = proc do |*params|
114
+ # From the docs:
115
+ # pL, pH specify the number of parameters after pH as (pL + pH*256)
116
+ return [params.size % 256, params.size / 256]
117
+ end
118
+
119
+ # The way to build a single line/setting of the code
120
+ part = proc do |function, *params|
121
+ fail ArgumentError unless params.all? { |p| p.is_a?(String) && p.length == 1 }
122
+ pl, ph = get_pl_ph.call(CODE_NUM, function, *params)
123
+ next "#{GS}(k#{pl.chr}#{ph.chr}#{CODE_NUM.chr}#{function.chr}#{params.join}"
124
+ end
125
+
126
+ @buffer << "\n"
127
+ @buffer << "#{ESC}a#{1.chr}" # Center
128
+ @buffer << part.call(67, CODE_SIZE.chr) # Set size
129
+ @buffer << part.call(69, 51.chr) # Set error correction
130
+ @buffer << part.call(80, 48.chr, *code.code.chars) # Set the code data
131
+ @buffer << part.call(81, 48.chr) # Start the print
132
+ @buffer << "#{ESC}a#{0.chr}" # End centering
133
+
134
+ @last_added = :code
135
+ end
136
+
137
+ def render_container(container)
138
+ render_items(container.items)
139
+ end
140
+
141
+ def render_footer(footer)
142
+ add_spacer
143
+ render_items(footer.items)
144
+ end
145
+
146
+ def render_raw(raw)
147
+ raw.block.call(@buffer)
148
+ end
149
+
150
+ def add_spacer
151
+ @buffer << ' ' * @line_width unless @last_added == :spacer
152
+ @last_added = :spacer
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,84 @@
1
+ module UDRS
2
+ module Renderers
3
+ # Font styling
4
+ class ESCPRenderer < Base
5
+ private
6
+
7
+ FONT_SIZE = {
8
+ tiny: [0b0100_0001, 56],
9
+ small: [0b0100_0001, 56],
10
+ normal: [0b0100_0001, 56],
11
+ medium: [0b0100_0000, 42],
12
+ large: [0b0111_0111, 28],
13
+ huge: [0b0111_0110, 21],
14
+ }
15
+
16
+ UNDERLINE = {
17
+ off: 0,
18
+ light: 1,
19
+ medium: 2,
20
+ }
21
+
22
+ def text_to_options(item)
23
+ return {
24
+ size: item.size,
25
+ bold: item.is_bold?,
26
+ underline: item.is_underline?,
27
+ align: item.alignment,
28
+ }
29
+ end
30
+
31
+ def font(options = {}, &block)
32
+ font_size(options[:size] || @font_size) do
33
+ bold(options[:bold] || @font_bold) do
34
+ underline(options[:underline] || @font_underline) do
35
+ block.call
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ def font_size(size, &block)
42
+ old_size = @font_size
43
+ apply_font_size(size)
44
+ block.call
45
+ apply_font_size(old_size)
46
+ end
47
+
48
+ def apply_font_size(size)
49
+ fail ArgumentError, "Invalid font size: #{size}" unless FONT_SIZE.key?(size)
50
+ @font_size = size
51
+ size = FONT_SIZE[size]
52
+ @buffer << "#{ESC}!#{size[0].chr}"
53
+ @line_width = size[1]
54
+ end
55
+
56
+ def bold(bold, &block)
57
+ old_bold = @font_bold
58
+ apply_bold(bold)
59
+ block.call
60
+ apply_bold(old_bold)
61
+ end
62
+
63
+ def apply_bold(status)
64
+ fail ArgumentError, 'Status must be a boolean' unless status == !!status
65
+ @font_bold = status
66
+ @buffer << "#{ESC}E#{status ? 1.chr : 0.chr}"
67
+ end
68
+
69
+ def underline(underline, &block)
70
+ old_underline = @font_underline
71
+ apply_underline(underline)
72
+ block.call
73
+ apply_underline(old_underline)
74
+ end
75
+
76
+ def apply_underline(underline)
77
+ underline = :medium if underline == !!underline
78
+ fail ArgumentError, 'Invalid underline value' unless UNDERLINE.key?(underline)
79
+ @font_underline = underline
80
+ @buffer << "#{ESC}_#{UNDERLINE[underline].chr}"
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,77 @@
1
+ require 'facets/string/word_wrap'
2
+
3
+ module UDRS
4
+ module Renderers
5
+ # Line methods
6
+ class ESCPRenderer < Base
7
+ private
8
+
9
+ def add_lines(lines, options = {})
10
+ lines.each { |l| add_line(l, options) }
11
+ end
12
+
13
+ def add_line(line, options = {})
14
+ # Reflow if needed
15
+ line = line.gsub(/\s+/, ' ') if options[:reflow]
16
+
17
+ # If multiple lines, process each separately
18
+ if line.include?("\n")
19
+ line.split("\n").each { |line| add_line(line, options) }
20
+ return
21
+ end
22
+
23
+ # Apply the font
24
+ font(options) do
25
+ # Get width from options
26
+ width = options[:width] || @line_width
27
+ width = (width * @line_width).ceil if width.is_a?(Float)
28
+ fail ArgumentError, "Width cannot exceed max line width" if width > @line_width
29
+
30
+ # Get indent from options
31
+ indent = options[:indent] || 0
32
+ fail ArgumentError, "Indent cannot be larger than width" if indent >= width
33
+ width -= indent
34
+
35
+ # Process the line into lines
36
+ lines = line_to_block(line, width, options)
37
+
38
+ # Apply indent
39
+ lines.map! { |line| (' ' * indent) + line }
40
+
41
+ # Store lines
42
+ @buffer << lines.join
43
+ @last_added = :line
44
+ end
45
+ end
46
+
47
+ def line_to_block(line, width, options = {})
48
+ line = line.to_s
49
+ if line.include?("\n")
50
+ lines = line.split("\n")
51
+ return lines.map { |line| line_to_block(line) }.flatten
52
+ end
53
+
54
+ # Wrap lines
55
+ lines = line.word_wrap(width).split("\n")
56
+
57
+ # Pad all lines
58
+ lines.map! do |line|
59
+ diff = width - line.size
60
+ case options[:align] || :left
61
+ when :left
62
+ next line + (' ' * diff)
63
+ when :right
64
+ next (' ' * diff) + line
65
+ when :center
66
+ half = diff / 2.0
67
+ next (' ' * half.ceil) + line + (' ' * half.floor)
68
+ else
69
+ fail ArgumentError, "Invalid alignment #{options[:align]}"
70
+ end
71
+ end
72
+
73
+ return lines
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,70 @@
1
+ module UDRS
2
+ module Renderers
3
+ # Tables
4
+ class ESCPRenderer < Base
5
+ private
6
+
7
+ def render_table(table)
8
+ add_spacer
9
+
10
+ # Get the column widths
11
+ column_widths = _calculate_table_column_widths(table)
12
+
13
+ # For each of the rows, render each cell according to the column with + cell properties
14
+ table.rows.each do |row|
15
+ # Get the non-hidden cells + their blocks
16
+ cells = []
17
+ parts = []
18
+ column_widths.zip(row.cells).each do |width, cell|
19
+ next if width == 0
20
+ cells << cell
21
+ parts << line_to_block(cell.text, width, text_to_options(cell))
22
+ end
23
+
24
+ # Make all the blocks have the same amount of lines
25
+ max_length = parts.map(&:size).max
26
+ column_widths.zip(parts).each do |width, lines|
27
+ lines << (' ' * width) while lines.size < max_length
28
+ end
29
+
30
+ # Add lines
31
+ parts.transpose.each do |line_parts|
32
+ cells.zip(line_parts).each_with_index do |(cell, part), i|
33
+ @buffer << ' ' if i > 0
34
+ font(text_to_options(cell)) do
35
+ @buffer << part
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ @last_added = :table
42
+ add_spacer
43
+ end
44
+
45
+ def _calculate_table_column_widths(table)
46
+ # Calculate the column widths
47
+ widths = table.get_column_widths
48
+ totals = [widths.map(&:first).sum, widths.map(&:second).sum, widths.map(&:third).sum]
49
+ columns = (table.columns - [:hide]).size
50
+ spacers = columns - 1
51
+
52
+ # Calculate the flex space
53
+ flex = @line_width - totals[0] - spacers
54
+ flex_columns = table.columns.count(:expand)
55
+ flex_indexes = table.columns.each_with_index.select { |c, i| c == :expand }.map(&:second)
56
+ flex_total_weight = widths.values_at(*flex_indexes).map(&:second).sum
57
+ fail ArgumentError, 'Table cannot fit' if flex < flex_columns
58
+
59
+ # Determine the final column width
60
+ final_widths = widths.map do |min, desired, max|
61
+ next min if min == max
62
+ next (flex * (desired / flex_total_weight - 0.001)).round
63
+ end
64
+ fail ArgumentError, 'Unable to calculate column widths' if final_widths.sum + spacers > @line_width
65
+
66
+ return final_widths
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,180 @@
1
+ require 'udrs/renderers/base'
2
+
3
+ require 'prawn'
4
+ require 'prawn/table'
5
+ require 'barby/barcode/qr_code'
6
+ require 'barby/outputter/prawn_outputter'
7
+
8
+ module UDRS
9
+ module Renderers
10
+ class PDFRenderer < Base
11
+ FORMAT_TIME = '%A %-d %B %Y %H:%M (%P)'
12
+
13
+ def render(container)
14
+ # Create the pdf
15
+ @pdf = Prawn::Document.new
16
+
17
+ # Allow bold fonts
18
+ @pdf.font_families.update(
19
+ 'DejaVuSans' => {
20
+ normal: 'app/assets/DejaVuSans.ttf',
21
+ bold: 'app/assets/DejaVuSans-Bold.ttf',
22
+ },
23
+ )
24
+ @pdf.font('DejaVuSans', size: @pdf.font_size)
25
+
26
+ # Render the items
27
+ render_item(container)
28
+
29
+ # Render the pdf
30
+ return @pdf.render
31
+ end
32
+
33
+ private
34
+
35
+ SIZE_MAP = {
36
+ tiny: 9,
37
+ small: 11,
38
+ normal: 12,
39
+ medium: 14,
40
+ large: 16,
41
+ huge: 18,
42
+ }
43
+
44
+ def render_page_end(page_end)
45
+ @pdf.start_new_page
46
+ end
47
+
48
+ def render_spacer(spacer)
49
+ if spacer.amount > 0
50
+ @pdf.move_down(spacer.amount)
51
+ else
52
+ @pdf.move_up(-1 * spacer.amount)
53
+ end
54
+ end
55
+
56
+ def render_header(header)
57
+ case header.level
58
+ when 1
59
+ @pdf.move_down(15)
60
+ @pdf.font(@pdf.font.name, size: SIZE_MAP[:huge], style: :bold) do
61
+ @pdf.text(header.text)
62
+ end
63
+
64
+ when 2
65
+ @pdf.move_down(10)
66
+ @pdf.font(@pdf.font.name, size: SIZE_MAP[:large], style: :bold) do
67
+ @pdf.text(header.text)
68
+ end
69
+
70
+ when 3
71
+ @pdf.move_down(5)
72
+ @pdf.font(@pdf.font.name, size: SIZE_MAP[:medium], style: :bold) do
73
+ @pdf.text(header.text)
74
+ end
75
+
76
+ else
77
+ fail ArgumentError, "Invalid header level: #{header.level}"
78
+ end
79
+ end
80
+
81
+ def render_text(item)
82
+ @pdf.font('DejaVuSans', style: _text_to_style(item), size: SIZE_MAP[item.size]) do
83
+ @pdf.text(item.text, align: item.alignment)
84
+ end
85
+ end
86
+
87
+ def render_t_text(item)
88
+ c = @pdf.cursor
89
+ title_width = @pdf.bounds.width / 4
90
+ @pdf.bounding_box([0, c], width: @pdf.bounds.width) do
91
+ title_box = @pdf.bounding_box([0, 0], width: title_width) do
92
+ @pdf.font('DejaVuSans', style: :bold) do
93
+ @pdf.text(item.title)
94
+ end
95
+ end
96
+ content_box = @pdf.bounding_box(
97
+ [title_width, title_box.height],
98
+ width: @pdf.bounds.width - title_width,
99
+ ) do
100
+ @pdf.text(item.body)
101
+ end
102
+ @pdf.move_cursor_to([title_box.bottom, content_box.bottom].max)
103
+ end
104
+ end
105
+
106
+ def render_logo(logo)
107
+ @pdf.image('app/assets/images/logo.png', position: logo.alignment, vposition: logo.valignment)
108
+ end
109
+
110
+ def render_code(code)
111
+ qrcode = Barby::QrCode.new(code.code)
112
+ outputter = Barby::PrawnOutputter.new(qrcode)
113
+ dim = 4
114
+ @pdf.move_down(outputter.full_height * dim)
115
+ outputter.annotate_pdf(@pdf, xdim: dim)
116
+ @pdf.move_up(outputter.full_height)
117
+ end
118
+
119
+ def render_table(table)
120
+ table_data = table.rows.map { |r| r.cells.map(&:text) }
121
+ @pdf.table(table_data) do |tbl|
122
+ # Lines between all cells
123
+ tbl.cells.borders = %i(bottom right)
124
+ tbl.rows(-1).borders = %i(right)
125
+ tbl.columns(-1).borders = %i(bottom)
126
+ tbl.rows(-1).columns(-1).borders = []
127
+
128
+ # Column sizes
129
+ table.columns.each_with_index do |width, ci|
130
+ next if width == :expand # Default, so do nothing
131
+
132
+ # For fit, calculate the needed width
133
+ if width == :fit
134
+ widths = table.rows.map do |row|
135
+ cell = row.cells[ci]
136
+ next @pdf.width_of(cell.text, style: _text_to_style(cell), size: SIZE_MAP[cell.size])
137
+ end
138
+ width = widths.max
139
+ end
140
+
141
+ pdf_cell = tbl.column(ci).first
142
+ pdf_cell.width = width + pdf_cell.padding_left + pdf_cell.padding_right
143
+ end
144
+
145
+ # Cell styles
146
+ table.rows.each_with_index do |row, ri|
147
+ row.cells.each_with_index do |cell, ci|
148
+ pdf_cell = tbl.rows(ri).columns(ci)
149
+ pdf_cell.align = cell.alignment
150
+ pdf_cell.font_style = _text_to_style(cell)
151
+ pdf_cell.font_size = SIZE_MAP[cell.size]
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ def render_container(container)
158
+ render_items(container.items)
159
+ end
160
+
161
+ def render_footer(footer)
162
+ @pdf.bounding_box([0, 35], width: @pdf.bounds.width, height: 35) do
163
+ render_items(footer.items)
164
+ end
165
+ end
166
+
167
+ def render_raw(raw)
168
+ raw.block.call(@pdf)
169
+ end
170
+
171
+ def _text_to_style(text)
172
+ style = :normal
173
+ style = :bold if text.is_bold?
174
+ # style = :italic if text.is_italic?
175
+ # style = :underline if text.is_underline?
176
+ return style
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,3 @@
1
+ module Udrs
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'udrs/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'udrs'
8
+ spec.version = Udrs::VERSION
9
+ spec.authors = ['Michon van Dooren']
10
+ spec.email = ['michon1992@gmail.com']
11
+
12
+ spec.summary = %q{Simple templates to generate documents in various formats.}
13
+ spec.homepage = 'https://github.com/MaienM/UDRS'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = Dir.glob('lib/**/**/*') + %w(udrs.gemspec LICENSE.txt README.md)
17
+ spec.bindir = 'exe'
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'attribute_predicates', '~> 0.2'
22
+ spec.add_dependency 'barby', '~> 0.6'
23
+ spec.add_dependency 'facets', '~> 3'
24
+ spec.add_dependency 'prawn', '~> 2'
25
+ spec.add_dependency 'prawn-table', '~> 0.2'
26
+ spec.add_dependency 'rqrcode', '~> 0.9'
27
+ spec.add_runtime_dependency 'rails', '~> 4'
28
+ spec.add_development_dependency 'bundler', '~> 1.10'
29
+ spec.add_development_dependency 'rake', '~> 10.0'
30
+ end
metadata ADDED
@@ -0,0 +1,199 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: udrs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Michon van Dooren
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-05-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: attribute_predicates
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: barby
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: facets
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: prawn
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: prawn-table
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.2'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rqrcode
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.9'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.9'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rails
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '4'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '4'
111
+ - !ruby/object:Gem::Dependency
112
+ name: bundler
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.10'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.10'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rake
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '10.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '10.0'
139
+ description:
140
+ email:
141
+ - michon1992@gmail.com
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - LICENSE.txt
147
+ - README.md
148
+ - lib/udrs.rb
149
+ - lib/udrs/components.rb
150
+ - lib/udrs/components/code.rb
151
+ - lib/udrs/components/container.rb
152
+ - lib/udrs/components/footer.rb
153
+ - lib/udrs/components/header.rb
154
+ - lib/udrs/components/logo.rb
155
+ - lib/udrs/components/page_end.rb
156
+ - lib/udrs/components/raw.rb
157
+ - lib/udrs/components/spacer.rb
158
+ - lib/udrs/components/t_text.rb
159
+ - lib/udrs/components/table.rb
160
+ - lib/udrs/components/text.rb
161
+ - lib/udrs/document.rb
162
+ - lib/udrs/engine.rb
163
+ - lib/udrs/engine/rails_helper.rb
164
+ - lib/udrs/engine/template_renderer.rb
165
+ - lib/udrs/renderers.rb
166
+ - lib/udrs/renderers/base.rb
167
+ - lib/udrs/renderers/escp_renderer.rb
168
+ - lib/udrs/renderers/escp_renderer/font.rb
169
+ - lib/udrs/renderers/escp_renderer/line.rb
170
+ - lib/udrs/renderers/escp_renderer/table.rb
171
+ - lib/udrs/renderers/pdf_renderer.rb
172
+ - lib/udrs/version.rb
173
+ - udrs.gemspec
174
+ homepage: https://github.com/MaienM/UDRS
175
+ licenses:
176
+ - MIT
177
+ metadata: {}
178
+ post_install_message:
179
+ rdoc_options: []
180
+ require_paths:
181
+ - lib
182
+ required_ruby_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ required_rubygems_version: !ruby/object:Gem::Requirement
188
+ requirements:
189
+ - - ">="
190
+ - !ruby/object:Gem::Version
191
+ version: '0'
192
+ requirements: []
193
+ rubyforge_project:
194
+ rubygems_version: 2.4.5.1
195
+ signing_key:
196
+ specification_version: 4
197
+ summary: Simple templates to generate documents in various formats.
198
+ test_files: []
199
+ has_rdoc: