udrs 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: