clamsy 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,7 @@
1
+ === 0.0.3 (Apr 25, 2010)
2
+
3
+ * support to replace pictures (useful for inserting digital signatures) [#ngty]
4
+
1
5
  === 0.0.2 (Apr 22, 2010)
2
6
 
3
7
  = 2nd official (yet still as embarrassing) release
@@ -4,56 +4,204 @@ Ruby wrapper for generating a single pdf for multiple contexts from an odt templ
4
4
 
5
5
  clamsy = clumsy + shellish
6
6
  | |
7
- | |-- under the hood, we are making system calls like ooffice,
8
- | ps2pdf & pdf2ps, etc.
7
+ | |-- under the hood, we are making system calls to ooffice &
8
+ | cups server needs to be running
9
9
  |
10
- .-- setup isn't straight forward, need to install a couple of packages
10
+ |-- setup isn't straight forward, need to install a couple of packages
11
11
  (how bad it is depends on ur platform) & even manual setting up of
12
- cups printer is needed
12
+ cups-pdf printer is needed
13
13
 
14
- == Using It (sorry, still WIP for more decent examples)
14
+ == 1. Basics
15
15
 
16
- #1. Generating single context pdf:
16
+ A template odt is just an ordinary odt (http://en.wikipedia.org/wiki/OpenDocument).
17
+ In order for it to function as a template, we just need to decorate it (just use
18
+ openoffice, pls don't buy ms office for that) with placeholders & embedded ruby code.
19
+
20
+ Under the hood, clamsy is using a slightly hacked version of the awesome tenjin
21
+ template engine (http://www.kuwata-lab.com/tenjin) to do the rendering. The following
22
+ is all u need to know to write odt templates:
23
+
24
+ * '{? ... ?}' represents embedded Ruby statement (this differs from vanilla tenjin)
25
+ * '#{ ... }' represents embedded Ruby expression
26
+ * '${ ... }' represents embedded Ruby expression which is to be escaped
27
+ (eg. '& < > "' are escaped to '&amp; &lt; &gt; &quot;')
28
+
29
+ == 2. Examples
30
+
31
+ === 2.1. Text Replacement Example
32
+
33
+ Template Odt:
34
+
35
+ --------------------------------------------
36
+ #{@company_full_name}
37
+ #{@company_address_line_1}
38
+ #{@company_address_line_2}
39
+
40
+ S/N Item Description Amount (SGD)
41
+ {? @items.each_with_index do |item, i| ?}
42
+ #{i+1} #{item.description} #{amount}
43
+ {? end ?}
44
+ Total #{total}
45
+ ----------------------------------- Page 1 -
46
+
47
+ Code:
48
+
49
+ class Item
50
+ attr_reader :description, :amount
51
+ def initialize(description, amount)
52
+ @description, @amount = description, amount
53
+ end
54
+ end
55
+
56
+ items = [
57
+ Item.new('Shells (x2 Bags)', 10),
58
+ Item.new('Clams (x2 Bags)', 20)
59
+ ]
60
+
61
+ context = {
62
+ :company_full_name => 'Monster Inc',
63
+ :company_address_line_1 => 'Planet Mars, Street xyz,',
64
+ :company_address_line_2 => 'Postal Code 009900',
65
+ :items => items,
66
+ :total => items.inject(0) {|sum, item| sum + item.amount }
67
+ }
68
+
69
+ Clamsy.process(
70
+ context,
71
+ path_to_template_odt, # eg. '/tmp/invoice.odt'
72
+ path_to_final_pdf, # eg. '/tmp/invoice.pdf'
73
+ )
74
+
75
+ Generated Pdf:
76
+
77
+ --------------------------------------------
78
+ Monster Inc
79
+ Planet Mars, Street xyz,
80
+ Postal Code 009900
81
+
82
+ S/N Item Description Amount (SGD)
83
+
84
+ 1 Shells (x2 Bags) 10
85
+ 2 Clams (x2 Bags) 20
86
+
87
+ Total 30
88
+ ----------------------------------- Page 1 -
89
+
90
+ === 2.2: Multiple Contexts Example
91
+
92
+ Taking the above example one step further, if we have multiple contexts:
93
+
94
+ items << Item.new('Shrimps (x2 Bags)', 15)
95
+ items << Item.new('Prawns (x2 Bags)', 25)
96
+
97
+ contexts = [{
98
+ :company_full_name => 'Monster Inc',
99
+ :company_address_line_1 => 'Planet Mars, Street xyz,',
100
+ :company_address_line_2 => 'Postal Code 009900',
101
+ :items => items[0..1],
102
+ :total => items[0..1].inject(0) {|sum, item| sum + item.amount }
103
+ }, {
104
+ :company_full_name => 'Fisherman Inc',
105
+ :company_address_line_1 => 'Planet Jupiter, Street xyz,',
106
+ :company_address_line_2 => 'Postal Code 008800',
107
+ :items => items[2..3],
108
+ :total => items[2..3].inject(0) {|sum, item| sum + item.amount }
109
+ }]
17
110
 
18
111
  Clamsy.process(
19
- {:someone => 'Peter', :mood => 'Happy'},
20
- path_to_template_odt,
21
- path_to_final_pdf
112
+ contexts,
113
+ path_to_template_odt, # eg. '/tmp/invoice.odt'
114
+ path_to_final_pdf # eg. '/tmp/invoice.pdf'
22
115
  )
23
116
 
24
- #2. Generating multi-contexts pdf:
117
+ Generated Pdf:
118
+
119
+ --------------------------------------------
120
+ Monster Inc
121
+ Planet Mars, Street xyz,
122
+ Postal Code 009900
123
+
124
+ S/N Item Description Amount (SGD)
125
+
126
+ 1 Shells (x2 Bags) 10
127
+ 2 Clams (x2 Bags) 20
128
+
129
+ Total 30
130
+ ----------------------------------- Page 1 -
131
+ --------------------------------------------
132
+ Fisherman Inc
133
+ Planet Jupiter, Street xyz,
134
+ Postal Code 008800
135
+
136
+ S/N Item Description Amount (SGD)
137
+
138
+ 1 Shrimps (x2 Bags) 15
139
+ 2 Prawns (x2 Bags) 25
140
+
141
+ Total 40
142
+ ----------------------------------- Page 1 -
143
+
144
+ As you can see, (whether you like it or not) the pages are merely concatenated
145
+ together to form a single pdf !!
146
+
147
+ === 2.3: Image Replacement Example
148
+
149
+ It is possible to replace images in an odt file, a useful feature if u need to insert
150
+ digital signature into the final pdf. To acheive this, u have to:
151
+ * insert a placeholder image into the odt file,
152
+ * resize & move it to your heart content
153
+ * right click on the picture, under [Picture ...]/[Options], assign a unique value
154
+ for [Name]
155
+
156
+ Template Odt:
157
+
158
+ --------------------------------------------
159
+ ~~~~~~~~~~~~~~~~~~
160
+ Hey | nice_guy_image |
161
+ ~~~~~~~~~~~~~~~~~~
162
+
163
+ This is a gentle reminder that i still
164
+ owe you some money ...
165
+
166
+ ~~~~~~~~~~~~~~~~~~~~~~
167
+ | my_signature_image |
168
+ ~~~~~~~~~~~~~~~~~~~~~~
169
+ ----------------------------------- Page 1 -
170
+
171
+ Assuming 'nice_guy_image' & 'my_signature_image' as names of the images, the following
172
+ code does the appropriate replacing of images:
25
173
 
26
174
  Clamsy.process(
27
- [
28
- {:someone => 'Peter', :mood => 'Happy'},
29
- {:someone => 'Jane', :mood => 'Sad'}
30
- ],
31
- path_to_template_odt,
32
- path_to_final_pdf
175
+ context = {:_pictures => {
176
+ :nice_guy_image => path_to_avatar_image, # eg. '/tmp/handsome.png'
177
+ :my_signature_image => path_to_signature_image # eg. '/tmp/signature.png'
178
+ }},
179
+ path_to_template_odt # eg. '/tmp/confession.odt'
180
+ path_to_final_pdf # eg. '/tmp/confession.pdf'
33
181
  )
34
182
 
35
- == Pre-requisites
183
+ == 3. Pre-requisites
36
184
 
37
- #1. Archlinux
185
+ === 3.1. Archlinux
38
186
 
39
- * Installing packages:
40
- $ sudo pacman -S ghostscript cups cups-pdf go-openoffice
187
+ * Installing packages:
188
+ $ sudo pacman -S ghostscript cups cups-pdf go-openoffice
41
189
 
42
- * Setting up the cups-pdf virtual printer by following instructions @
43
- http://wiki.archlinux.org/index.php/CUPS#Configuring_CUPS-PDF_virtual_printer
190
+ * Setting up the cups-pdf virtual printer by following instructions @
191
+ http://wiki.archlinux.org/index.php/CUPS#Configuring_CUPS-PDF_virtual_printer
44
192
 
45
- * Making sure cups is running:
46
- $ sudo /etc/rc.d/cups start
193
+ * Making sure cups is running:
194
+ $ sudo /etc/rc.d/cups start
47
195
 
48
- #2. Ubuntu (to-be-updated)
196
+ === 3.2. Ubuntu (to-be-updated)
49
197
 
50
- #3. Mac (to-be-updated)
198
+ === 3.3. Mac (to-be-updated)
51
199
 
52
200
  == TODO
53
201
 
54
202
  * add support for configuration, eg. Clamsy.configure {|config| ... }
55
203
 
56
- * automate cups-pdf printer setup
204
+ * automate cups-pdf printer setup or have ooffice directly export odt to pdf
57
205
 
58
206
  * avoid making system call to 'ooffice'
59
207
 
data/Rakefile CHANGED
@@ -12,6 +12,7 @@ begin
12
12
  gem.authors = ["NgTzeYang"]
13
13
  gem.add_dependency "rubyzip", "= 0.9.4"
14
14
  gem.add_dependency "rghost", "= 0.8.7.2"
15
+ gem.add_dependency "nokogiri", "= 1.4.1"
15
16
  gem.add_development_dependency "bacon", ">= 1.1.0"
16
17
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
18
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.2
1
+ 0.0.3
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{clamsy}
8
- s.version = "0.0.2"
8
+ s.version = "0.0.3"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["NgTzeYang"]
12
- s.date = %q{2010-04-22}
12
+ s.date = %q{2010-04-25}
13
13
  s.description = %q{}
14
14
  s.email = %q{ngty77@gmail.com}
15
15
  s.extra_rdoc_files = [
@@ -28,12 +28,16 @@ Gem::Specification.new do |s|
28
28
  "lib/clamsy.rb",
29
29
  "lib/clamsy/tenjin.rb",
30
30
  "spec/clamsy_spec.rb",
31
+ "spec/data/clamsy.png",
32
+ "spec/data/clamsy2.png",
31
33
  "spec/data/embedded_ruby_example.odt",
32
34
  "spec/data/embedded_ruby_example.pdf",
33
35
  "spec/data/escaped_text_example.odt",
34
36
  "spec/data/escaped_text_example.pdf",
35
37
  "spec/data/multiple_contexts_example.odt",
36
38
  "spec/data/multiple_contexts_example.pdf",
39
+ "spec/data/picture_example.odt",
40
+ "spec/data/picture_example.pdf",
37
41
  "spec/data/plain_text_example.odt",
38
42
  "spec/data/plain_text_example.pdf",
39
43
  "spec/spec_helper.rb"
@@ -55,15 +59,18 @@ Gem::Specification.new do |s|
55
59
  if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
56
60
  s.add_runtime_dependency(%q<rubyzip>, ["= 0.9.4"])
57
61
  s.add_runtime_dependency(%q<rghost>, ["= 0.8.7.2"])
62
+ s.add_runtime_dependency(%q<nokogiri>, ["= 1.4.1"])
58
63
  s.add_development_dependency(%q<bacon>, [">= 1.1.0"])
59
64
  else
60
65
  s.add_dependency(%q<rubyzip>, ["= 0.9.4"])
61
66
  s.add_dependency(%q<rghost>, ["= 0.8.7.2"])
67
+ s.add_dependency(%q<nokogiri>, ["= 1.4.1"])
62
68
  s.add_dependency(%q<bacon>, [">= 1.1.0"])
63
69
  end
64
70
  else
65
71
  s.add_dependency(%q<rubyzip>, ["= 0.9.4"])
66
72
  s.add_dependency(%q<rghost>, ["= 0.8.7.2"])
73
+ s.add_dependency(%q<nokogiri>, ["= 1.4.1"])
67
74
  s.add_dependency(%q<bacon>, [">= 1.1.0"])
68
75
  end
69
76
  end
@@ -1,6 +1,7 @@
1
1
  require 'ftools'
2
2
  require 'digest/md5'
3
3
  require 'tempfile'
4
+ require 'nokogiri'
4
5
  require 'zip/zip'
5
6
  require 'clamsy/tenjin'
6
7
  require 'rghost'
@@ -47,9 +48,10 @@ module Clamsy
47
48
 
48
49
  def render(context)
49
50
  @context_id = Digest::MD5.hexdigest(context.to_s)
50
- Zip::ZipFile.open(working_odt.path) do |zip|
51
- zip.select {|entry| entry.file? && entry.to_s =~ /\.xml$/ }.each do |entry|
52
- zip.get_output_stream(entry.to_s) {|io| io.write(workers[entry].render(context)) }
51
+ Zip::ZipFile.open(working_odt.path) do |@zip|
52
+ @zip.select {|entry| entry.file? && entry.to_s =~ /\.xml$/ }.each do |entry|
53
+ @zip.get_output_stream(entry.to_s) {|io| io.write(workers[entry].render(context)) }
54
+ replace_pictures(entry, context[:_pictures] || {})
53
55
  end
54
56
  end
55
57
  working_odt
@@ -57,6 +59,14 @@ module Clamsy
57
59
 
58
60
  private
59
61
 
62
+ def replace_pictures(entry, pictures)
63
+ xpaths = lambda {|name| %\//drawframe[@drawname="#{name}"]/drawimage/@xlinkhref\ }
64
+ doc = Nokogiri::XML(entry.get_input_stream.read.gsub(':','')) # Hack to avoid namespace error
65
+ pictures.each do |name, path|
66
+ (node = doc.xpath(xpaths[name])[0]) && @zip.replace(node.value, path)
67
+ end
68
+ end
69
+
60
70
  def working_odt
61
71
  (@working_odts ||= {})[@context_id] ||=
62
72
  begin
@@ -1,15 +1,17 @@
1
- require 'spec_helper'
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
2
 
3
3
  describe "Clamsy" do
4
4
 
5
5
  behaves_like 'has standard files support'
6
6
 
7
7
  before do
8
- @check_processing_yields_text = lambda do |contexts, example|
8
+ @check_processing_yields_content = lambda do |contexts, example|
9
9
  generated_pdf = tmp_file('clamsy_pdf').path
10
- expected_content = comparable_content(expected_pdf(example))
11
10
  Clamsy.process(contexts, template_odt(example), generated_pdf)
12
- comparable_content(generated_pdf).should.equal expected_content
11
+ expected_content = comparable_content(expected_pdf(example))
12
+ generated_content = comparable_content(generated_pdf)
13
+ generated_content.size.should.equal expected_content.size
14
+ (generated_content - expected_content).should.equal []
13
15
  end
14
16
  end
15
17
 
@@ -17,15 +19,22 @@ describe "Clamsy" do
17
19
  trash_tmp_files
18
20
  end
19
21
 
22
+ it 'should do picture replacement for matching <draw:frame draw:name="..." />' do
23
+ @check_processing_yields_content[
24
+ context = {:_pictures => {:to_be_replaced_pic => data_file('clamsy.png')}},
25
+ example = :picture
26
+ ]
27
+ end
28
+
20
29
  it 'should do #{...} plain text replacement' do
21
- @check_processing_yields_text[
30
+ @check_processing_yields_content[
22
31
  context = {:someone => 'Peter', :mood => 'Happy'},
23
32
  example = :plain_text
24
33
  ]
25
34
  end
26
35
 
27
36
  it 'should do ${...} escaped (santized) replacement' do
28
- @check_processing_yields_text[
37
+ @check_processing_yields_content[
29
38
  context = {:someone => '<Peter>', :mood => '<Happy>'},
30
39
  example = :escaped_text
31
40
  ]
@@ -38,14 +47,14 @@ describe "Clamsy" do
38
47
  @name, @mood = name, mood
39
48
  end
40
49
  end
41
- @check_processing_yields_text[
50
+ @check_processing_yields_content[
42
51
  context = {:everyone => [@someone.new('Peter','Happy'), @someone.new('Jane','Sad')]},
43
52
  example = :embedded_ruby
44
53
  ]
45
54
  end
46
55
 
47
56
  it 'should concat multiple contexts processing to a single pdf' do
48
- @check_processing_yields_text[
57
+ @check_processing_yields_content[
49
58
  contexts = [{:someone => 'Peter', :mood => 'Happy'}, {:someone => 'Jane', :mood => 'Sad'}],
50
59
  example = :multiple_contexts
51
60
  ]
Binary file
Binary file
@@ -10,25 +10,37 @@ require 'clamsy'
10
10
 
11
11
  shared 'has standard files support' do
12
12
  class << self
13
+
14
+ INSIGNIFICANT_AND_UNMATCHABLE_PATTERNS = [
15
+ /^q\[\-?\d+\.?\d*( \-?\d+\.?\d*){5}\]concat\n$/,
16
+ /^\d+\.?\d*( \d+\.?\d*){3} re\n$/
17
+ ]
18
+
13
19
  def data_file(file)
14
20
  File.join(File.dirname(__FILE__), 'data', file)
15
21
  end
22
+
16
23
  def template_odt(name)
17
24
  data_file("#{name}_example.odt")
18
25
  end
26
+
19
27
  def expected_pdf(name)
20
28
  data_file("#{name}_example.pdf")
21
29
  end
30
+
22
31
  def comparable_content(file)
23
32
  RGhost::Convert.new(file).to(:ps).read.grep(/^[^%][^%]?/).
24
- reject {|line| line =~ /^q\[\-?\d+( \-?\d+){5}\]concat\n$/ }
33
+ reject {|line| INSIGNIFICANT_AND_UNMATCHABLE_PATTERNS.any? {|regexp| regexp =~ line } }
25
34
  end
35
+
26
36
  def trash_tmp_files
27
37
  (@trashable_tmp_files || []).select {|f| f.path }.map(&:unlink)
28
38
  end
39
+
29
40
  def tmp_file(file_name)
30
41
  ((@trashable_tmp_files ||= []) << Tempfile.new(file_name))[-1]
31
42
  end
43
+
32
44
  end
33
45
  end
34
46
 
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 0
8
- - 2
9
- version: 0.0.2
8
+ - 3
9
+ version: 0.0.3
10
10
  platform: ruby
11
11
  authors:
12
12
  - NgTzeYang
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-04-22 00:00:00 +08:00
17
+ date: 2010-04-25 00:00:00 +08:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -47,9 +47,23 @@ dependencies:
47
47
  type: :runtime
48
48
  version_requirements: *id002
49
49
  - !ruby/object:Gem::Dependency
50
- name: bacon
50
+ name: nokogiri
51
51
  prerelease: false
52
52
  requirement: &id003 !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - "="
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 1
58
+ - 4
59
+ - 1
60
+ version: 1.4.1
61
+ type: :runtime
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: bacon
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
53
67
  requirements:
54
68
  - - ">="
55
69
  - !ruby/object:Gem::Version
@@ -59,7 +73,7 @@ dependencies:
59
73
  - 0
60
74
  version: 1.1.0
61
75
  type: :development
62
- version_requirements: *id003
76
+ version_requirements: *id004
63
77
  description: ""
64
78
  email: ngty77@gmail.com
65
79
  executables: []
@@ -81,12 +95,16 @@ files:
81
95
  - lib/clamsy.rb
82
96
  - lib/clamsy/tenjin.rb
83
97
  - spec/clamsy_spec.rb
98
+ - spec/data/clamsy.png
99
+ - spec/data/clamsy2.png
84
100
  - spec/data/embedded_ruby_example.odt
85
101
  - spec/data/embedded_ruby_example.pdf
86
102
  - spec/data/escaped_text_example.odt
87
103
  - spec/data/escaped_text_example.pdf
88
104
  - spec/data/multiple_contexts_example.odt
89
105
  - spec/data/multiple_contexts_example.pdf
106
+ - spec/data/picture_example.odt
107
+ - spec/data/picture_example.pdf
90
108
  - spec/data/plain_text_example.odt
91
109
  - spec/data/plain_text_example.pdf
92
110
  - spec/spec_helper.rb