mork 0.10.0 → 0.11.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4aab0200b0c68dc3f3720fef8b8ae239d1c3d521
4
- data.tar.gz: 7b1dcdfea624a8d99f98230ee3899c51ce5f90ed
3
+ metadata.gz: 23ab6d5cb32c6c9474e304f619305b1841afb6a7
4
+ data.tar.gz: 021b401aeb958368de5bdaaf64908bbc42dc1e14
5
5
  SHA512:
6
- metadata.gz: 4002e21d4604887c06c75dedfd6689316364766276f3dff453cd7dd774ae752a9293098d18a72a236ce607eb81ac8b53d65d985b83b67b157f844e2f1706de5a
7
- data.tar.gz: d9720227584eef5a59d842cd4b713b6c021b7eac3d6398124c6affbf65f21e2d0d44751ba436e0aee5d305721fb49d27441e5b2aa48a5d1539b5d12c780dc108
6
+ metadata.gz: be6f8857ff7c2559204d40a5d7ac743a1b64cc2f6b9a84ee7c4b54956db55754180d07d3ecf0a36fc77e838f4758e55d99b710740328e36e76b1b58b2e1834d3
7
+ data.tar.gz: 3bef5ff687f155a1094ff2a384e7b0cb4b504b1cdc7c47e68b5766cdb714b4214752a24154e7f9170ba4b3a96c2a1bc6b544a6e1adc319d33498935c5fef3664
data/README.md CHANGED
@@ -3,11 +3,11 @@
3
3
  A ruby [optical mark recognition](http://en.wikipedia.org/wiki/Optical_mark_recognition) (OMR) library aimed at accomplishing two tasks in the context of paper-based, multiple-choice tests and surveys:
4
4
 
5
5
  1. generating [response sheets](/spec/samples/sheet.jpg) in PDF format
6
- 2. capturing the responses provided on the [printed sheet](/spec/samples/sample_gray.jpg) by a human with a pen or a pencil.
6
+ 2. detecting the responses provided on the [printed sheet](/spec/samples/sample_gray.jpg) by a human with a pen or a pencil.
7
7
 
8
- ## First, a word of caution
8
+ ## Roadmap
9
9
 
10
- __Please note that this library is under active development. Until v.1.0 is reached, the API should be regarded as unstable and bound to change without notice.__
10
+ This library is under active development. __Until v.1.0 is reached, the API should be regarded as unstable and bound to change without notice.__
11
11
 
12
12
  ## Assumptions and limitations
13
13
 
@@ -29,7 +29,7 @@ Mork is a low-level library, and very much work in progress. It is not, and will
29
29
 
30
30
  ## Getting started
31
31
 
32
- First, make sure that ImageMagick is installed in your system. Typing `convert in a terminal shell should print out a long help message. If instead you get an error, you will need to install ImageMagick.
32
+ First, make sure that ImageMagick is installed in your system. Typing `convert in a terminal shell should print out a long help message. If instead you get a “command not found”-like error, you will need to install ImageMagick.
33
33
 
34
34
  In OS X, `brew` is an excellent package manager:
35
35
 
@@ -57,10 +57,14 @@ Edit the file `mork_test.rb` with the code snippets below, then execute the foll
57
57
 
58
58
  ## Generating response sheets with `SheetPDF`
59
59
 
60
- Response sheets are created through the `Mork::SheetPDF` class. Two pieces of information must be provided to the class constructor to produce a meaningful sheet:
60
+ Response sheets are created through the `Mork::SheetPDF` class. Two pieces of information must be provided to the class constructor to produce a meaningful sheet or, more commonly, a set of similar sheets:
61
61
 
62
- - **content**: what to place on the sheet, such as how many response items and choices, what to write in the header, the number to print as a barcode
63
- - **layout**: sizes, margins, spacing, fonts, etc., of each of the printed elements
62
+ - **content**: what to place specifically on each sheet, such as how many response items and choices, what to write in the header, the number to print as a barcode
63
+ - **layout**: the sheet description in terms of element positioning, sizes, margins, spacing, fonts, etc. Layout settings apply equally to all generated sheets
64
+
65
+ ```ruby
66
+ s = SheetPDF.new content, layout
67
+ ```
64
68
 
65
69
  Let’s look at each argument in turn.
66
70
 
@@ -70,16 +74,16 @@ The `content` argument should be a hash like the following:
70
74
 
71
75
  ```ruby
72
76
  content = {
73
- # the response sheet's unique identifier; it is printed
74
- # in binary form as a barcode at the bottom of the sheet
75
- barcode: 123456,
76
77
  # number of items and number of choices per item
77
78
  # in this case: 100 items with 5 choices each
78
79
  choices: [5] * 100,
80
+ # the response sheet's unique identifier; it is printed
81
+ # in binary form as a barcode at the bottom of the sheet
82
+ barcode: 123456,
79
83
  # stuff to print in the header
80
84
  header: {
81
85
  name: 'John Doe UI354320',
82
- title: 'Anatomy and Physiology test, Sept 20, 2014',
86
+ title: 'A serious, difficult test - 31 December 1999',
83
87
  code: '201.48',
84
88
  signature: 'Signature'
85
89
  }
@@ -89,86 +93,131 @@ content = {
89
93
  These are the key-value pairs that the `content` hash may contain:
90
94
 
91
95
  - **choices**: an array of integers, specifiying the number of items in the test (the length of the array) and the number of choices available for each item (the array values); if omitted, the maximum number of items and choices per item allowed by the layout (see below) are printed
92
- - **header**: optional; a (sub)hash of key-value pairs defining the content of named header elements; in each pair, the key is the name of one header element, while the value is the rendered content; the actually available elements are defined in the `layout` (see below)
93
- - **barcode**: optional; an integer number, the sheet's unique identifier to be printed as a binary barcode along the bottom edge of the sheet
96
+ - **barcode**: an integer number, the sheet's unique identifier to be printed as a binary barcode along the bottom edge of the sheet; if omitted, no barcode is printed
97
+ - **header**: a (sub)hash defining header elements, where each key is the element’s name and each value is the content to be rendered; you can place an arbitrary number of elements in the header, as long as the same element names are also defined in the `layout` (see below); if omitted, no header elements are printed
98
+
99
+ In most situations, you will want to generate several sheets at once. To do that, simply pass an array of content hashes to the constructor.
100
+
101
+ ```ruby
102
+ content = [
103
+ {
104
+ barcode: 1001,
105
+ header: { name: 'John Doe UI354320', code: '1001'}
106
+ },
107
+ {
108
+ barcode: 1002,
109
+ header: { name: 'Jane Roe UI354321', code: '1002'}
110
+ }
111
+ ]
112
+ ```
113
+
114
+ The `content` can also be specified by passing a string with the path/name of a YAML file like the following:
115
+
116
+ ```yaml
117
+ - barcode: 1001
118
+ header:
119
+ name: 'John Doe UI354320'
120
+ code: '1001'
121
+ - barcode: 1002
122
+ header:
123
+ name: 'Jane Roe UI354321'
124
+ code: '1002'
125
+ ```
94
126
 
95
127
  ### layout
96
128
 
97
- The layout is also defined as a hash, but because of its length and since the layout often stays identical across many response sheets, it is usually more convenient to write the information in a YAML file and pass its path/filename to the `SheetPDF` constructor instead. Here is the YAML version of a standard layout hash. Please note that the parameters with an (*) in the comment have no effect on PDF production, but are relevant to OMR scan (see further below).
129
+ The layout specifies exactly the size and location of choice cells, header elements, and barcode bits; it also specifies parameters to fine tune detection of marked cells and of registration marks (see below).
130
+
131
+ The layout hash is put together in 3 steps during `SheetPDF` initialization:
132
+
133
+ 1. at first, the layout is generated based on a set of built-in options
134
+ 2. next, if a file named `layout.yml` exists in the current path, Mork automatically parses it; any values found override the built-in ones
135
+ 3. finally, values specified in the constructor argument override the existing ones
136
+
137
+ This is the list of built-in values that the layout is initially based on. Please note that the parameters with an (*) in the comment have no effect on PDF production, but are relevant to OMR scan (see further below).
98
138
 
99
139
  ```yaml
100
- page_size: # all measurements in mm
101
- width: 210 # width of the paper sheet
102
- height: 297 # height of the paper sheet
103
- reg_marks:
104
- margin: 10 # distance from each page border to registration mark center
105
- radius: 3 # registration mark radius
106
- crop: 20 # size of the registration mark search area (*)
107
- offset: 2 # distance between the search area and each page border (*)
108
- blur: 2 # size of a gaussian blur filter to smooth overly pixelated registration marks (0 to skip) (*)
109
- dilate: 5 # size of a “dilate” filter to get rid of stray noise (0 to skip) (*)
110
- contrast: 20 # minimum contrast between registration mark circles and the surrounding white paper (*)
111
- header:
112
- name: # ‘name’ is just a label; you can add arbitrary header elements
113
- top: 5 # margin relative to registration frame top side
114
- left: 15 # margin relative to registration frame left side
115
- width: 160 # text will be fitted to this width
116
- height: 7 # ...and this height
117
- size: 14 # font size
118
- title:
119
- top: 15
120
- left: 15
121
- width: 160
122
- height: 12
123
- size: 12
124
- code:
125
- top: 35
126
- left: 130
127
- width: 57
128
- height: 10
129
- size: 14
130
- signature:
131
- top: 30
132
- left: 15
133
- width: 120
134
- height: 15
135
- size: 7
136
- box: true # header element will be enclosed in a box
140
+ page_size: # all measurements in mm
141
+ width: 210 # width of the paper sheet
142
+ height: 297 # height of the paper sheet
143
+ reg_marks:
144
+ margin: 10 # distance from each page border to registration mark center
145
+ radius: 3 # registration mark radius
146
+ offset: 2 # distance between the search area and each page border (*)
147
+ crop: 20 # size of the registration mark search area (*)
148
+ dilate: 5 # size of a “dilate” filter to get rid of stray noise (0 to skip) (*)
149
+ blur: 2 # size of a gaussian blur filter to smooth overly pixelated registration marks (0 to skip) (*)
150
+ contrast: 20 # minimum contrast between registration mark circles and the surrounding white paper (*)
151
+ header:
152
+ title: # ‘title’ is a label of your choosing; you can add arbitrary header elements
153
+ top: 15 # margin relative to registration frame top side
154
+ left: 15 # margin relative to registration frame left side
155
+ width: 160 # text will be fitted to this width
156
+ height: 12 # text will be fitted to this height
157
+ size: 12 # font size
158
+ box: false # if true, header element will be enclosed in a frame
137
159
  items:
138
- threshold: 0.75 # mark detection threshold (*)
139
- top: 55 # response area margin, relative to reg frame
140
- left: 11 # response area margin, relative to reg frame
141
- rows: 30 # number of items per column
142
- columns: 4 # number of columns
143
- column_width: 44 #
144
- x_spacing: 7 # horizontal distance between ajacent cell centers
145
- y_spacing: 7 # vertical distance between ajacent cell centers
146
- cell_width: 6 # width of each choice and calibration cell
147
- cell_height: 5 # height of each choice and calibration cell
148
- max_cells: 5 # maximum number of choices per item
149
- font_size: 9 # size of both the item number and choice cell letter
150
- number_width: 8 #
151
- number_margin: 2 # margin between
160
+ threshold: 0.75 # mark detection threshold (*)
161
+ columns: 4 # number of columns
162
+ column_width: 44 #
163
+ rows: 30 # number of items per column
164
+ left: 11 # response area margin, relative to reg frame
165
+ top: 55 # response area margin, relative to reg frame
166
+ x_spacing: 7 # horizontal distance between ajacent cell centers
167
+ y_spacing: 7 # vertical distance between ajacent cell centers
168
+ cell_width: 6 # width of each choice and calibration cell
169
+ cell_height: 5 # height of each choice and calibration cell
170
+ max_cells: 5 # the maximum number of choices per question
171
+ font_size: 9 # for the question number and choice letters
172
+ number_width: 8 # width of question number text box
173
+ number_margin: 2 # distance between right side of q num and left side of first choice cell
152
174
  barcode:
153
- bits: 40 # the maximum sheet identifier is 2 to the power or bits
154
- left: 15 # distance between registration frame side and the first barcode bit
155
- width: 3 # width of each barcode bit
156
- height: 3 # height of each barcode bit from the registration frame bottom side
157
- spacing: 4 # horizontal distance between adjacent barcode bit centers
175
+ bits: 38 # the maximum sheet identifier is 2 to the power or bits
176
+ left: 15 # distance between registration frame side and the first barcode bit
177
+ width: 3 # width of each barcode bit
178
+ height: 3 # height of each barcode bit from the registration frame bottom side
179
+ spacing: 4 # horizontal distance between adjacent barcode bit centers
180
+ ```
181
+
182
+ A `layout.yml` file may be used on top of the above to always apply settings that you would consider your own defaults. A good example might be to override the built-in A4 paper size with Letter paper width and height. For example, if everthing in the built-in layout fits your needs except for paper size, the `layout.yml` file should contain just the following:
183
+
184
+ ```yaml
185
+ page_size:
186
+ width: 215.9
187
+ height: 279.4
158
188
  ```
159
189
 
160
- Assuming that the above text is written in a file named `layout.yml`, here's how to create a `SheetPDF` object and write the PDF file to disk:
190
+ Finally, the `layout` argument into the `SheetPDF` constructor can be either a hash or a string indicating the path/name of a YAML file.
191
+
192
+ The specification of header elements is slightly different than all other settings, in that you are free to add any number of elements with arbitrary names, as long as each element is given the following properties: `top`, `left`, `width`, `height`, and `size` (see the built-in title element above). The `box` property is optional. As already noted, header elements can be used in the `content` only if their properties are defined in the `layout`.
193
+
194
+ In summary, the following sample code shows how to create a 2-page PDF document:
161
195
 
162
196
  ```ruby
163
- s = SheetPDF.new content, 'layout.yml'
197
+ content = [
198
+ {
199
+ barcode: 1001,
200
+ header: { name: 'John Doe UI354320', title: 'Final exam', code: '1001'}
201
+ },
202
+ {
203
+ barcode: 1002,
204
+ header: { name: 'Jane Roe UI354321', title: 'Final exam', code: '1002'}
205
+ }
206
+ ]
207
+
208
+ layout = {
209
+ header: {
210
+ name: { top: 5, left: 15, width: 160, height: 7, size: 14 },
211
+ title: { top: 15, left: 15, width: 160, height: 12, size: 12 },
212
+ code: { top: 15, left: 155, width: 35, height: 10, size: 14 }
213
+ }
214
+ }
215
+
216
+ sheet = SheetPDF.new content, layout
164
217
  s.save 'sheet.pdf'
165
218
  system 'open sheet.pdf' # this works in OSX
166
219
  ```
167
220
 
168
- If the `layout` argument is omitted, Mork will search for a file named `layout.yml` and load it. If such file cannot be found, Mork will fall back to a default, boilerplate layout (incidentally, this is the layout shown above).
169
-
170
- Importantly, if `content` is an array of hashes, Mork will produce one sheet for each hash, all based on the same layout but each containing unique information (the barcode, names, dates, a specific number of questions/choices, etc.)
171
-
172
221
  ## Analyzing response sheets with `SheetOMR`
173
222
 
174
223
  ### Preparing a `SheetOMR` object
@@ -176,13 +225,13 @@ Importantly, if `content` is an array of hashes, Mork will produce one sheet for
176
225
  Assuming that a person has filled out a response sheet by darkening with a pen the selected choices, and that the sheet has been acquired as an image file, response scoring is performed by the `Mork::SheetOMR` class. Two pieces of information must be provided to the object constructor:
177
226
 
178
227
  - **path**: mandatory path/filename of the bitmap image (accepts JPG, JPEG, PNG, PDF extensions; a resolution of 150-200 dpi is usually more than sufficient to obtain accurate readings)
179
- - **layout_file**: same as for the `SheetPDF` class
228
+ - **layout**: same as for the `SheetPDF` class
180
229
 
181
230
  The following code shows how to create a SheetOMR based on a bitmap file named `image.jpg`:
182
231
 
183
232
  ```ruby
184
233
  # instantiating the object
185
- s = SheetOMR.new 'image.jpg', 'layout.yml'
234
+ s = SheetOMR.new 'image.jpg', 'mylayout.yml'
186
235
  ```
187
236
 
188
237
  When the object is initialized, Mork attempts to register the image, a necessary step before marking can be performed. To find out if registration succeeded:
@@ -208,18 +257,18 @@ We are now ready to mark the sheet:
208
257
  mc = s.marked_choices
209
258
  ```
210
259
 
211
- Since the function `set_choices` returns true only if the sheet is properly registered, it can replace the call to `valid?`, allowing you to write something like:
260
+ Since the function `set_choices` returns true only if the sheet is properly registered, you can write something like the following (instead of calling `valid?`):
212
261
 
213
262
  ```ruby
214
- s = SheetOMR.new 'image.jpg', 'layout.yml'
263
+ s = SheetOMR.new 'image.jpg'
215
264
  if s.set_choices [5] * 50
216
- marked = s.marked_choices
265
+ puts s.marked_choices
217
266
  else
218
267
  puts "The sheet is not registered!"
219
268
  end
220
269
  ```
221
270
 
222
- If all goes well, the `marked` array will contain 100 sub-arrays, each containing the list of marked choices for that item, where the first cell is indicated by a 0, the second by a 1, etc.
271
+ If all goes well, the `marked` array will contain 50 sub-arrays, each containing the list of marked choices for that item, where the first cell is indicated by a 0, the second by a 1, etc.
223
272
 
224
273
  Read the [API documentation](http://www.rubydoc.info/gems/mork) to learn about additional methods to extract marked responses.
225
274
 
@@ -247,21 +296,14 @@ Scoring can only be performed if the sheet gets properly registered, which in tu
247
296
 
248
297
  ### Improving sheet registration and marking
249
298
 
250
- For Mork to be able to register the response sheet, the acquired image should be of _sufficient_ resolution and contrast, and it should be straight, with some white margin around the 4 registration circles.
299
+ For Mork to be able to register the response sheet, the acquired image must be of _sufficient_ resolution and contrast, and it should be _reasonably_ straight, with some white margin around the 4 registration circles.
251
300
 
252
301
  Mork tries to be tolerant of variations in the above parameters, but you should experiment with your own layout, actual printout, and actual scans.
253
302
 
254
- Check for object “validity” to make sure that registration succeeded:
255
-
256
- ```ruby
257
- s = SheetOMR.new 'image.jpg', 'layout.yml'
258
- s.valid?
259
- ```
260
-
261
303
  When registration fails, it is possible to get some information by displaying the status and by applying a dedicated overlay on the original image:
262
304
 
263
305
  ```ruby
264
- s = SheetOMR.new 'image.jpg', 'layout.yml'
306
+ s = SheetOMR.new 'image.jpg', 'mylayout.yml'
265
307
  unless s.valid?
266
308
  s.save_registration 'unregistered.jpg'
267
309
  end
data/lib/mork.rb CHANGED
@@ -2,3 +2,4 @@ require 'mork/version'
2
2
  require 'mork/extensions'
3
3
  require 'mork/sheet_omr'
4
4
  require 'mork/sheet_pdf'
5
+ require 'yaml'
@@ -1,19 +1,3 @@
1
- # @private
2
- class Array
3
- def mean
4
- @the_sample_mean ||= inject(:+)/length.to_f
5
- end
6
-
7
- def sample_variance
8
- sum = inject(0){|accum, i| accum + (i-mean)**2 }
9
- sum/(length - 1).to_f
10
- end
11
-
12
- def stdev
13
- Math.sqrt sample_variance
14
- end
15
- end
16
-
17
1
  # @private
18
2
  class Fixnum
19
3
  def mm
@@ -27,3 +11,29 @@ class Float
27
11
  self * 2.83464566929134
28
12
  end
29
13
  end
14
+
15
+ module Mork
16
+ # @private
17
+ module Extensions
18
+ def symbolize(obj)
19
+ return obj.inject({}){|memo,(k,v)| memo[k.to_sym] = symbolize(v); memo} if obj.is_a? Hash
20
+ return obj.inject([]){|memo,v | memo << symbolize(v); memo} if obj.is_a? Array
21
+ return obj
22
+ end
23
+ end
24
+ end
25
+
26
+ # # @private
27
+ # class Array
28
+ # def mean
29
+ # @the_sample_mean ||= inject(:+)/length.to_f
30
+ # end
31
+ # def sample_variance
32
+ # sum = inject(0){|accum, i| accum + (i-mean)**2 }
33
+ # sum/(length - 1).to_f
34
+ # end
35
+ # def stdev
36
+ # Math.sqrt sample_variance
37
+ # end
38
+ # end
39
+
data/lib/mork/grid.rb CHANGED
@@ -1,4 +1,3 @@
1
- require 'yaml'
2
1
  require 'mork/grid_const'
3
2
 
4
3
  module Mork
@@ -7,6 +6,7 @@ module Mork
7
6
  # It knows nothing about the actual scanned image.
8
7
  # All returned values are in the arbitrary units given in the configuration file
9
8
  class Grid
9
+ include Extensions
10
10
  # Calling Grid.new without arguments creates the default boilerplate Grid
11
11
  def initialize(options=nil)
12
12
  @params = default_grid
@@ -67,11 +67,11 @@ module Mork
67
67
 
68
68
  # recursively turn hash keys into symbols. pasted from
69
69
  # http://stackoverflow.com/questions/800122/best-way-to-convert-strings-to-symbols-in-hash
70
- def symbolize(obj)
71
- return obj.inject({}){|memo,(k,v)| memo[k.to_sym] = symbolize(v); memo} if obj.is_a? Hash
72
- return obj.inject([]){|memo,v | memo << symbolize(v); memo} if obj.is_a? Array
73
- return obj
74
- end
70
+ # def symbolize(obj)
71
+ # return obj.inject({}){|memo,(k,v)| memo[k.to_sym] = symbolize(v); memo} if obj.is_a? Hash
72
+ # return obj.inject([]){|memo,v | memo << symbolize(v); memo} if obj.is_a? Array
73
+ # return obj
74
+ # end
75
75
 
76
76
  # cell_y(q)
77
77
  #
@@ -1,5 +1,6 @@
1
1
  module Mork
2
2
  class Grid
3
+ # @private
3
4
  # this is the default grid!
4
5
  # default units are millimiters
5
6
  def default_grid
@@ -26,7 +27,8 @@ module Mork
26
27
  left: 15,
27
28
  width: 160,
28
29
  height: 12,
29
- size: 12
30
+ size: 12,
31
+ box: false
30
32
  }
31
33
  },
32
34
  # questions and answers
@@ -43,8 +45,8 @@ module Mork
43
45
  cell_height: 5, # choice cell size
44
46
  max_cells: 5, # the maximum number of choices per question
45
47
  font_size: 9, # for the question number and choice letters
46
- number_width: 8, # distance between right side of q num and left side of first choice cell
47
- number_margin: 2 # width of question number text box
48
+ number_width: 8, # width of question number text box
49
+ number_margin: 2 # distance between right side of q num and left side of first choice cell
48
50
  },
49
51
  # unique sheet ID as a binary barcode
50
52
  barcode: {
data/lib/mork/mimage.rb CHANGED
@@ -4,6 +4,8 @@ require 'mork/magicko'
4
4
  module Mork
5
5
  # @private
6
6
  class Mimage
7
+ include Extensions
8
+
7
9
  attr_reader :rm
8
10
  attr_reader :choxq # choices per question
9
11
 
@@ -137,7 +139,8 @@ module Mork
137
139
  end
138
140
 
139
141
  def cal_cell_mean
140
- @grom.calibration_cell_areas.collect { |c| reg_pixels.average c }.mean
142
+ m = @grom.calibration_cell_areas.collect { |c| reg_pixels.average c }
143
+ m.inject(:+) / m.length.to_f
141
144
  end
142
145
 
143
146
  def darkest_cell_mean
@@ -45,11 +45,11 @@ module Mork
45
45
  #
46
46
  # @return [Boolean] True if the sheet is properly registered and ready to
47
47
  # be marked; false otherwise.
48
- def set_choices(cho)
48
+ def set_choices(choices)
49
49
  return false unless valid?
50
- @mim.set_ch case cho
51
- when Fixnum; @mim.choxq[0...cho]
52
- when Array; cho
50
+ @mim.set_ch case choices
51
+ when Fixnum; @mim.choxq[0...choices]
52
+ when Array; choices
53
53
  else raise ArgumentError, 'Invalid choice set'
54
54
  end
55
55
  true
@@ -6,17 +6,24 @@ module Mork
6
6
  # Generating response sheets as PDF files.
7
7
  # See the README file for usage
8
8
  class SheetPDF < Prawn::Document
9
+ include Extensions
9
10
  def initialize(content, layout=nil)
10
- @grip = case layout
11
- when NilClass; GridPDF.new
12
- when String, Hash; GridPDF.new layout
13
- when Mork::GridPDF; layout
14
- else raise ArgumentError, 'Invalid initialization parameter'
15
- end
11
+ @content =
12
+ case content
13
+ when Array; content
14
+ when Hash; [content]
15
+ when String
16
+ raise IOError, "File '#{content}' not found" unless File.exists? content
17
+ symbolize YAML.load_file(content)
18
+ end
19
+ @grip =
20
+ case layout
21
+ when NilClass; GridPDF.new
22
+ when String, Hash; GridPDF.new layout
23
+ when Mork::GridPDF; layout
24
+ else raise ArgumentError, 'Invalid initialization parameter'
25
+ end
16
26
  super my_page_params
17
- # @content should be an array of hashes, one per page;
18
- # convert to array if a single hash was passed
19
- @content = content.class == Hash ? [content] : content
20
27
  process
21
28
  end
22
29
 
data/lib/mork/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Mork
2
- VERSION = '0.10.0'
2
+ VERSION = '0.11.1'
3
3
  end
@@ -52,7 +52,7 @@ module Mork
52
52
 
53
53
  describe '#max_choices_per_question' do
54
54
  it 'returns the maximum number of choice cells per question' do
55
- @grom.max_choices_per_question.should == 5
55
+ expect(@grom.max_choices_per_question).to eq 5
56
56
  end
57
57
  end
58
58
 
@@ -1,7 +1,16 @@
1
1
  require 'spec_helper'
2
+ include Mork::Extensions
2
3
 
3
4
  module Mork
4
5
  describe Grid do
6
+ let(:base) { symbolize YAML.load_file('spec/samples/base_layout.yml') }
7
+
8
+ describe 'hash vs yaml' do
9
+ it 'makes sure that the default grid and the base_layout.yml are equivalent' do
10
+ expect(base).to eq(Grid.new.default_grid)
11
+ end
12
+ end
13
+
5
14
  context 'init params' do
6
15
  it 'does not work with an integer' do
7
16
  expect {Grid.new 1}.to raise_error ArgumentError
@@ -9,97 +18,17 @@ module Mork
9
18
  end
10
19
 
11
20
  context 'default grid' do
12
- before(:all) do
13
- @grid = Grid.new 'spec/samples/layout.yml'
14
- end
15
-
16
21
  describe '#max_questions' do
17
22
  it 'returns the maximum number of questions in a sheet' do
18
- @grid.max_questions.should == 120
23
+ expect(Grid.new.max_questions).to eq base[:items][:columns]*base[:items][:rows]
19
24
  end
20
25
  end
21
26
 
22
27
  describe '#barcode_bits' do
23
28
  it 'returns the number of bits used to define the form barcode' do
24
- @grid.send(:barcode_bits).should == 40
29
+ expect(Grid.new.send(:barcode_bits)).to eq base[:barcode][:bits]
25
30
  end
26
31
  end
27
32
  end
28
33
  end
29
34
  end
30
-
31
- # describe "#question_area" do
32
- # before(:each) do
33
- # grid.reg_marks(@image)
34
- # end
35
- # it "returns a hash" do
36
- # grid.question_area(1).should be_an_instance_of(Hash)
37
- # end
38
- #
39
- # it "returns the location in pixels of the first question patch" do
40
- # c = grid.question_area(1)
41
- # c[:x].should be_within(4).of(90)
42
- # c[:y].should be_within(4).of(388)
43
- # end
44
- # it "returns the location in pixels of the 40th question patch" do
45
- # c = grid.question_area(40)
46
- # c[:x].should be_within(4).of(90)
47
- # c[:y].should be_within(4).of(3120)
48
- # end
49
- #
50
- # it "returns the location in pixels of the 121th question patch" do
51
- # c = grid.question_area(121)
52
- # c[:x].should be_within(4).of(1887)
53
- # c[:y].should be_within(4).of(388)
54
- # end
55
- #
56
- # it "returns the location in pixels of the last question patch" do
57
- # c = grid.question_area(160)
58
- # c[:x].should be_within(4).of(1887)
59
- # c[:y].should be_within(4).of(3120)
60
- # end
61
- # end
62
-
63
- # describe '#ctrl_area_dark' do
64
- # it 'returns the coordinates of the control cell used to set the darkened threshold' do
65
- # @grid.ctrl_area_dark.should == {:x=>1479, :y=>329, :w=>51, :h=>41}
66
- # end
67
- # end
68
-
69
- # describe '#ctrl_area_light' do
70
- # it 'returns the coordinates of the control cell used to set the darkened threshold' do
71
- # @grid.ctrl_area_light.should == {:x=>1538, :y=>329, :w=>51, :h=>41}
72
- # end
73
- # end
74
-
75
- # describe "#cell_x" do
76
- # context "for 1st-column questions" do
77
- # it "returns the distance from the registration frame of the left edge of the 1st choice" do
78
- # grid.send(:cell_x,0,0).should == 7.5
79
- # end
80
- #
81
- # it "returns the distance from the registration frame of the left edge of the 2nd choice" do
82
- # grid.send(:cell_x,0,1).should == 14.5
83
- # end
84
- # end
85
- #
86
- # context "for 4th-column questions" do
87
- # it "returns the distance from the registration frame of the left edge of the 1st choice" do
88
- # grid.send(:cell_x,120,0).should == 157.5
89
- # end
90
- #
91
- # it "returns the distance from the registration frame of the left edge of the 2nd choice" do
92
- # grid.send(:cell_x,120,1).should == 164.5
93
- # end
94
- # end
95
- # end
96
- #
97
- # describe "#cell_y" do
98
- # it "returns the distance from the registration frame of the top edge of the 1st row of cells" do
99
- # grid.send(:cell_y,0).should == 33.5
100
- # end
101
- #
102
- # it "returns the distance from the registration frame of the 40th row of cells" do
103
- # grid.send(:cell_y,39).should == 267.5
104
- # end
105
- # end
@@ -7,39 +7,45 @@ module Mork
7
7
  barcode: 183251937962,
8
8
  choices: [5] * 120,
9
9
  header: {
10
- title: 'A serious, difficult test - 31 December 1999',
10
+ title: 'A serious, difficult test - 31 December 1999'
11
11
  }
12
12
  }
13
13
  }
14
+ def sp(cnt: {title: 'Hello world'}, grip: nil)
15
+ SheetPDF.new cnt, grip
16
+ end
14
17
 
15
18
  it 'assigns the grid to @grid' do
16
- s = SheetPDF.new(content)
17
- s.instance_variable_get('@grip').should be_a GridPDF
19
+ expect(sp.instance_variable_get('@grip')).to be_a GridPDF
20
+ end
21
+
22
+ it 'uses a yaml file as content' do
23
+ s = 'spec/samples/content.yml'
24
+ c = sp(cnt: s).instance_variable_get('@content')[0][:choices]
25
+ expect(c).to eq [5,5,5,4,5,5,5]
18
26
  end
19
27
 
20
28
  it 'creates a grid by loading the specified file' do
21
- s = SheetPDF.new(content, 'spec/samples/layout.yml')
22
- s.instance_variable_get('@grip').should be_a GridPDF
29
+ s = 'spec/samples/layout.yml'
30
+ expect(sp(grip: s).instance_variable_get('@grip')).to be_a GridPDF
23
31
  end
24
32
 
25
33
  it 'raises an error with an invalid init parameter' do
26
- lambda { SheetPDF.new(content, 2) }.should raise_error ArgumentError
34
+ expect { SheetPDF.new(content, 2) }.to raise_error ArgumentError
27
35
  end
28
36
 
29
37
  it 'raises an error if a header part is not described in the layout' do
30
- lambda {
31
- SheetPDF.new({header: {dummy: 'yes I am'}})
32
- }.should raise_error ArgumentError
38
+ expect {
39
+ sp(cnt: {header: {dummy: 'yes I am'}})
40
+ }.to raise_error ArgumentError
33
41
  end
34
42
 
35
43
  it 'assigns an array to @content' do
36
- s = SheetPDF.new(content)
37
- s.instance_variable_get('@content').should be_an Array
44
+ expect(sp.instance_variable_get('@content')).to be_an Array
38
45
  end
39
46
 
40
47
  it 'assigns an array of hashes to @content' do
41
- s = SheetPDF.new(content)
42
- s.instance_variable_get('@content').first.should be_a Hash
48
+ expect(sp.instance_variable_get('@content').first).to be_a Hash
43
49
  end
44
50
 
45
51
  it 'creates a minimal PDF sheet' do
@@ -53,6 +59,17 @@ module Mork
53
59
  end
54
60
 
55
61
  it 'creates a PDF sheet with several boxed header elements' do
62
+ h = {
63
+ name: 'John Doe UI354320',
64
+ title: 'A serious, difficult test - 31 December 1999',
65
+ code: '201.48',
66
+ signature: 'Signature'
67
+ }
68
+ s = SheetPDF.new({header: h, barcode: 2384685871}, 'spec/samples/standard.yml')
69
+ s.save dest 'standard'
70
+ end
71
+
72
+ it 'creates a PDF sheet with several header elements' do
56
73
  h = {
57
74
  name: lorem,
58
75
  title: lorem,
@@ -75,8 +92,8 @@ module Mork
75
92
  end
76
93
 
77
94
  it 'creates a PDF sheet with 160 items' do
78
- s = SheetPDF.new(content.merge({choices: [5] * 160}), 'spec/samples/grid160.yml')
79
- s.save dest 'i160'
95
+ c = { header: {title: '160 items, tighter layout'}, choices: [5]*160}
96
+ sp(cnt: c, grip: 'spec/samples/grid160.yml').save dest 'i160'
80
97
  end
81
98
 
82
99
  it 'creates a PDF sheet with unequal choices per item' do
@@ -0,0 +1,40 @@
1
+ page_size: # all measurements in mm
2
+ width: 210 # width of the paper sheet
3
+ height: 297 # height of the paper sheet
4
+ reg_marks:
5
+ margin: 10 # distance from each page border to registration mark center
6
+ radius: 3 # registration mark radius
7
+ offset: 2 # distance between the search area and each page border (*)
8
+ crop: 20 # size of the registration mark search area (*)
9
+ dilate: 5 # size of a “dilate” filter to get rid of stray noise (0 to skip) (*)
10
+ blur: 2 # size of a gaussian blur filter to smooth overly pixelated registration marks (0 to skip) (*)
11
+ contrast: 20 # minimum contrast between registration mark circles and the surrounding white paper (*)
12
+ header:
13
+ title: # ‘title’ is a label of your choosing; you can add arbitrary header elements
14
+ top: 15 # margin relative to registration frame top side
15
+ left: 15 # margin relative to registration frame left side
16
+ width: 160 # text will be fitted to this width
17
+ height: 12 # text will be fitted to this height
18
+ size: 12 # font size
19
+ box: false # if true, header element will be enclosed in a box
20
+ items:
21
+ threshold: 0.75 # mark detection threshold (*)
22
+ columns: 4 # number of columns
23
+ column_width: 44 #
24
+ rows: 30 # number of items per column
25
+ left: 11 # response area margin, relative to reg frame
26
+ top: 55 # response area margin, relative to reg frame
27
+ x_spacing: 7 # horizontal distance between ajacent cell centers
28
+ y_spacing: 7 # vertical distance between ajacent cell centers
29
+ cell_width: 6 # width of each choice and calibration cell
30
+ cell_height: 5 # height of each choice and calibration cell
31
+ max_cells: 5 # the maximum number of choices per question
32
+ font_size: 9 # for the question number and choice letters
33
+ number_width: 8 # width of question number text box
34
+ number_margin: 2 # distance between right side of q num and left side of first choice cell
35
+ barcode:
36
+ bits: 38 # the maximum sheet identifier is 2 to the power or bits
37
+ left: 15 # distance between registration frame side and the first barcode bit
38
+ width: 3 # width of each barcode bit
39
+ height: 3 # height of each barcode bit from the registration frame bottom side
40
+ spacing: 4 # horizontal distance between adjacent barcode bit centers
@@ -0,0 +1,9 @@
1
+ - choices: [5,5,5,4,5,5,5]
2
+ barcode: 122
3
+ header:
4
+ title: 'A difficult test'
5
+
6
+ - choices: [2]
7
+ barcode: 123
8
+ header:
9
+ title: 'An easier test'
Binary file
@@ -0,0 +1,27 @@
1
+ header:
2
+ name: # ‘name’ is just a label; you can add arbitrary header elements
3
+ top: 5 # margin relative to registration frame top side
4
+ left: 15 # margin relative to registration frame left side
5
+ width: 160 # text will be fitted to this width
6
+ height: 7 # text will be fitted to this height
7
+ size: 14 # font size
8
+ title:
9
+ top: 15
10
+ left: 15
11
+ width: 160
12
+ height: 12
13
+ size: 12
14
+ code:
15
+ top: 30
16
+ left: 152
17
+ width: 35
18
+ height: 10
19
+ size: 14
20
+ align: right
21
+ signature:
22
+ top: 30
23
+ left: 15
24
+ width: 120
25
+ height: 15
26
+ size: 7
27
+ box: true
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mork
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Giuseppe Bertini
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-17 00:00:00.000000000 Z
11
+ date: 2016-06-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: narray
@@ -197,7 +197,6 @@ files:
197
197
  - mork.gemspec
198
198
  - mork.sublime-project
199
199
  - spec/mork/coord_spec.rb
200
- - spec/mork/extensions_spec.rb
201
200
  - spec/mork/grid_omr_spec.rb
202
201
  - spec/mork/grid_spec.rb
203
202
  - spec/mork/magicko_spec.rb
@@ -216,7 +215,9 @@ files:
216
215
  - spec/samples/angolo.jpg
217
216
  - spec/samples/angolo2.jpg
218
217
  - spec/samples/angolo3.jpg
218
+ - spec/samples/base_layout.yml
219
219
  - spec/samples/boxy.yml
220
+ - spec/samples/content.yml
220
221
  - spec/samples/grid.yml
221
222
  - spec/samples/grid160.yml
222
223
  - spec/samples/grid_omr_layout.yml
@@ -226,15 +227,6 @@ files:
226
227
  - spec/samples/jdoe/JohnDoe3.jpeg
227
228
  - spec/samples/jdoe/layout.yml
228
229
  - spec/samples/layout.yml
229
- - spec/samples/lucrezia/border1.pdf
230
- - spec/samples/lucrezia/border2.pdf
231
- - spec/samples/lucrezia/bw1.pdf
232
- - spec/samples/lucrezia/bw2.pdf
233
- - spec/samples/lucrezia/gray1.pdf
234
- - spec/samples/lucrezia/gray2.pdf
235
- - spec/samples/marisol/marisol1.jpg
236
- - spec/samples/marisol/marisol2.jpg
237
- - spec/samples/marisol/marisol3.jpg
238
230
  - spec/samples/reg_mark.jpg
239
231
  - spec/samples/rm00.jpeg
240
232
  - spec/samples/rm01.jpeg
@@ -242,6 +234,8 @@ files:
242
234
  - spec/samples/rm03.jpeg
243
235
  - spec/samples/rm04.jpeg
244
236
  - spec/samples/rm05.jpeg
237
+ - spec/samples/standard.png
238
+ - spec/samples/standard.yml
245
239
  - spec/samples/syst/barr0.jpg
246
240
  - spec/samples/syst/barr1.jpg
247
241
  - spec/samples/syst/barr2.jpg
@@ -1,10 +0,0 @@
1
- require 'spec_helper'
2
-
3
- module Mork
4
- describe Array do
5
- it 'computes the average' do
6
- a = [ 20, 23, 23, 24, 25, 22, 12, 21, 28 ]
7
- expect(a.mean).to eq 22
8
- end
9
- end
10
- end
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file