mork 0.9.3 → 0.10.0

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: 6ad2fa0bbdc2867005194207a5b864712f960086
4
- data.tar.gz: c41bb8ee3b1d79498f3bcebd589f5ffbd1203bfb
3
+ metadata.gz: 4aab0200b0c68dc3f3720fef8b8ae239d1c3d521
4
+ data.tar.gz: 7b1dcdfea624a8d99f98230ee3899c51ce5f90ed
5
5
  SHA512:
6
- metadata.gz: 967dff2829c9fc983e77cab161dc5ed612bc977d4c3ed1e98ef81a16a60c440f5a91633a7440df77166d71b42e7cd46ae4ce3808061dcee6ce06acd0750a203b
7
- data.tar.gz: 0027ebe1c08ea06573322608a56320618ea7a57bcf0235076f9fbf9134dc62999a81a77eb7755191a99ba1b15c877c77cc00a7f9c5eeb564f18860d72b6fbf0a
6
+ metadata.gz: 4002e21d4604887c06c75dedfd6689316364766276f3dff453cd7dd774ae752a9293098d18a72a236ce607eb81ac8b53d65d985b83b67b157f844e2f1706de5a
7
+ data.tar.gz: d9720227584eef5a59d842cd4b713b6c021b7eac3d6398124c6affbf65f21e2d0d44751ba436e0aee5d305721fb49d27441e5b2aa48a5d1539b5d12c780dc108
data/.yardopts CHANGED
@@ -1 +1 @@
1
- --no-private
1
+ --no-private --markup markdown
data/README.md CHANGED
@@ -5,6 +5,10 @@ A ruby [optical mark recognition](http://en.wikipedia.org/wiki/Optical_mark_reco
5
5
  1. generating [response sheets](/spec/samples/sheet.jpg) in PDF format
6
6
  2. capturing 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
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.__
11
+
8
12
  ## Assumptions and limitations
9
13
 
10
14
  Mork is a low-level library, and very much work in progress. It is not, and will likely never be a complete OMR solution. While suggestions and contributions are more than welcome, for the time being several assumptions and restrictions apply.
@@ -84,13 +88,13 @@ content = {
84
88
 
85
89
  These are the key-value pairs that the `content` hash may contain:
86
90
 
87
- - **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 are printed
91
+ - **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
88
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)
89
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
90
94
 
91
95
  ### layout
92
96
 
93
- The layout argument 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).
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).
94
98
 
95
99
  ```yaml
96
100
  page_size: # all measurements in mm
@@ -161,31 +165,69 @@ s.save 'sheet.pdf'
161
165
  system 'open sheet.pdf' # this works in OSX
162
166
  ```
163
167
 
164
- It's easy to see that by iterating over a series of `content` hashes you can produce any number of sheets, all based on the same layout but each containing unique information (notably the barcode, but also names, dates, etc.)
165
-
166
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).
167
169
 
168
- ## Scoring response sheets with `SheetOMR`
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
+ ## Analyzing response sheets with `SheetOMR`
169
173
 
170
- 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. Three pieces of information must be provided to the object constructor:
174
+ ### Preparing a `SheetOMR` object
175
+
176
+ 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:
171
177
 
172
178
  - **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)
173
- - **choices**: a named argument (ruby-2 style) equivalent to the `choices` array of integers passed to the `SheetPDF` constructor as part of the `content` parameter (see above). If omitted, the `choices` parameter is inferred from the layout
174
179
  - **layout_file**: same as for the `SheetPDF` class
175
180
 
176
- The following code shows how to create and analyze a SheetOMR based on a bitmap file named `image.jpg`:
181
+ The following code shows how to create a SheetOMR based on a bitmap file named `image.jpg`:
177
182
 
178
183
  ```ruby
179
184
  # instantiating the object
180
- s = SheetOMR.new 'image.jpg', choices: [5]*100, layout_file: 'layout.yml'
181
- # detecting darkened choice cells for the 100 items
182
- chosen = s.marked_choices
185
+ s = SheetOMR.new 'image.jpg', 'layout.yml'
186
+ ```
187
+
188
+ 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:
189
+
190
+ ```ruby
191
+ s.valid?
192
+ ```
193
+
194
+ Next, we need to indicate the questions/choices of interest for subsequent operations. For example, the following instructs Mork to evaluate 5 choices for each of the first 50 questions:
195
+
196
+ ```ruby
197
+ choices = [5] * 50
198
+ s.set_choices choices
183
199
  ```
184
200
 
185
- If all goes well, the `chosen` 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. It is also possible to show the scoring graphically by applying an overlay on top of the scanned image.
201
+ `choices` is equivalent to the array of integers passed to the `SheetPDF` constructor as part of the `content` parameter (see above).
202
+
203
+ ### Marking the response sheet
204
+
205
+ We are now ready to mark the sheet:
206
+
207
+ ```ruby
208
+ mc = s.marked_choices
209
+ ```
210
+
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:
212
+
213
+ ```ruby
214
+ s = SheetOMR.new 'image.jpg', 'layout.yml'
215
+ if s.set_choices [5] * 50
216
+ marked = s.marked_choices
217
+ else
218
+ puts "The sheet is not registered!"
219
+ end
220
+ ```
221
+
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.
223
+
224
+ Read the [API documentation](http://www.rubydoc.info/gems/mork) to learn about additional methods to extract marked responses.
225
+
226
+ ### Applying overlays on the original image
227
+
228
+ It is also possible to show the scoring graphically by applying an overlay on top of the scanned image.
186
229
 
187
230
  ```ruby
188
- s = SheetOMR.new 'image.jpg', choices: [5]*100, layout_file: 'layout.yml'
189
231
  s.overlay :check, :marked
190
232
  s.save 'marked_choices.jpg'
191
233
  system 'open marked_choices.jpg' # this works in OSX
@@ -194,14 +236,13 @@ system 'open marked_choices.jpg' # this works in OSX
194
236
  More than one overlay can be applied on a sheet. For example, in addition to checking the marked choice cells, the expected choices can be outlined to show correct vs. wrong responses:
195
237
 
196
238
  ```ruby
197
- ...
198
239
  correct = [[3], [0], [2], [1], [2]] # and so on...
199
- s.overlay :check, :marked
200
240
  s.overlay :outline, correct
201
- ...
241
+ s.overlay :check, :marked
242
+ s.save 'marked_choices_and_outlines.jpg'
243
+ system 'open marked_choices_and_outlines.jpg' # this works in OSX
202
244
  ```
203
245
 
204
-
205
246
  Scoring can only be performed if the sheet gets properly registered, which in turn depends on the quality of the scanned image.
206
247
 
207
248
  ### Improving sheet registration and marking
@@ -213,14 +254,14 @@ Mork tries to be tolerant of variations in the above parameters, but you should
213
254
  Check for object “validity” to make sure that registration succeeded:
214
255
 
215
256
  ```ruby
216
- s = SheetOMR.new 'image.jpg', choices: [5]*100, layout_file: 'layout.yml'
257
+ s = SheetOMR.new 'image.jpg', 'layout.yml'
217
258
  s.valid?
218
259
  ```
219
260
 
220
261
  When registration fails, it is possible to get some information by displaying the status and by applying a dedicated overlay on the original image:
221
262
 
222
263
  ```ruby
223
- s = SheetOMR.new 'image.jpg', choices: [5]*100, layout_file: 'layout.yml'
264
+ s = SheetOMR.new 'image.jpg', 'layout.yml'
224
265
  unless s.valid?
225
266
  s.save_registration 'unregistered.jpg'
226
267
  end
data/lib/mork/grid.rb CHANGED
@@ -9,7 +9,7 @@ module Mork
9
9
  class Grid
10
10
  # Calling Grid.new without arguments creates the default boilerplate Grid
11
11
  def initialize(options=nil)
12
- @params = DGRID
12
+ @params = default_grid
13
13
  if File.exists?('layout.yml')
14
14
  @params.deeper_merge! symbolize YAML.load_file('layout.yml')
15
15
  end
@@ -1,86 +1,60 @@
1
1
  module Mork
2
- # this is the default grid!
3
- # default units are millimiters
4
- DGRID = {
5
- # size of the paper sheet
6
- page_size: {
7
- # this is A4
8
- width: 210,
9
- height: 297
10
- },
11
- # size, location, search parameters of registration marks
12
- reg_marks: {
13
- margin: 10,
14
- radius: 3,
15
- offset: 2, # distance between page edge and registraton mark search area
16
- crop: 20, # size of square where the regmark should be located
17
- dilate: 5, # set to >0 to apply a dilate IM operation
18
- blur: 2, # set to >0 to apply a blur IM operation
19
- contrast: 20 # minimum contrast between registration mark circles and the white paper
20
- },
21
- header: {
22
- name: {
23
- top: 5,
24
- left: 15,
25
- width: 160,
26
- height: 7,
27
- size: 14
28
- },
29
- title: {
30
- top: 15,
31
- left: 15,
32
- width: 160,
33
- height: 12,
34
- size: 12
35
- },
36
- code: {
37
- top: 35,
38
- left: 130,
39
- width: 57,
40
- height: 10,
41
- size: 14,
42
- align: :right
43
- },
44
- signature: {
45
- top: 30,
46
- left: 15,
47
- width: 120,
48
- height: 15,
49
- size: 7,
50
- box: true
2
+ class Grid
3
+ # this is the default grid!
4
+ # default units are millimiters
5
+ def default_grid
6
+ {
7
+ # size of the paper sheet
8
+ page_size: {
9
+ width: 210, # A4
10
+ height: 297
11
+ },
12
+ # size, location, search parameters of registration marks
13
+ reg_marks: {
14
+ margin: 10, # from sheet edge to registration mark center
15
+ radius: 3, # of the registration circle
16
+ offset: 2, # distance between page edge and registraton mark search area
17
+ crop: 20, # size of square where the regmark should be located
18
+ dilate: 5, # set to >0 to apply a dilate IM operation
19
+ blur: 2, # set to >0 to apply a blur IM operation
20
+ contrast: 20 # minimum contrast between registration mark circles and the white paper
21
+ },
22
+ # you can place multiple elements in the header; title is the only default
23
+ header: {
24
+ title: {
25
+ top: 15,
26
+ left: 15,
27
+ width: 160,
28
+ height: 12,
29
+ size: 12
30
+ }
31
+ },
32
+ # questions and answers
33
+ items: {
34
+ threshold: 0.75, # how much darker a marked cell should be compared to cal cells
35
+ columns: 4,
36
+ column_width: 44,
37
+ rows: 30,
38
+ left: 11, # distance from the top-left registration mark...
39
+ top: 55, # ...to the center of the first choice cell
40
+ x_spacing: 7, # between choices
41
+ y_spacing: 7, # between rows
42
+ cell_width: 6, # choice cell size
43
+ cell_height: 5, # choice cell size
44
+ max_cells: 5, # the maximum number of choices per question
45
+ 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
+ },
49
+ # unique sheet ID as a binary barcode
50
+ barcode: {
51
+ bits: 38,
52
+ left: 15,
53
+ width: 3,
54
+ height: 3,
55
+ spacing: 4
56
+ }
51
57
  }
52
- }, # header end
53
- items: {
54
- threshold: 0.75,
55
- columns: 4,
56
- column_width: 44,
57
- rows: 30,
58
- # from the top-left registration mark
59
- # to the center of the first choice cell
60
- left: 11,
61
- top: 55,
62
- # between choices
63
- x_spacing: 7,
64
- # between rows
65
- y_spacing: 7,
66
- # darkened area
67
- cell_width: 6,
68
- cell_height: 5,
69
- # the maximum number of choices per question
70
- max_cells: 5,
71
- # font size for the question number and choice letters
72
- font_size: 9,
73
- # distance between right side of q num and left side of first choice cell
74
- number_width: 8,
75
- # width of question number text box
76
- number_margin: 2
77
- }, # items end
78
- barcode: {
79
- bits: 40,
80
- left: 15,
81
- width: 3,
82
- height: 3,
83
- spacing: 4
84
- } # barcode end
85
- }
58
+ end
59
+ end
86
60
  end
data/lib/mork/grid_pdf.rb CHANGED
@@ -70,6 +70,10 @@ module Mork
70
70
  @cround ||= [width_of_cell, height_of_cell].min / 2
71
71
  end
72
72
 
73
+ def missing_header?(k)
74
+ @params[:header][k].nil?
75
+ end
76
+
73
77
  def page_size() [page_width.mm, page_height.mm] end
74
78
  def margins() reg_margin.mm end
75
79
  def qnum_margin() @params[:items][:number_margin].to_f.mm end
data/lib/mork/mimage.rb CHANGED
@@ -5,12 +5,13 @@ module Mork
5
5
  # @private
6
6
  class Mimage
7
7
  attr_reader :rm
8
+ attr_reader :choxq # choices per question
8
9
 
9
- def initialize(path, nitems, grom)
10
- @mack = Magicko.new path
11
- @nitems = nitems
12
- @grom = grom.set_page_size @mack.width, @mack.height
13
- @rm = {} # registration mark centers
10
+ def initialize(path, grom)
11
+ @mack = Magicko.new path
12
+ @grom = grom.set_page_size @mack.width, @mack.height
13
+ @choxq = [grom.max_choices_per_question] * grom.max_questions
14
+ @rm = {} # registration mark centers
14
15
  @valid = register
15
16
  end
16
17
 
@@ -27,17 +28,19 @@ module Mork
27
28
  }
28
29
  end
29
30
 
30
- def marked
31
- @logical_array_of_marked_cells ||= begin # memoization necessary?
32
- itemator { |q, c| shade_of(q, c) < choice_threshold }
33
- end
31
+ def set_ch(cho)
32
+ @choxq = cho
33
+ # if set_ch is called more than once, discard memoization
34
+ @marked_choices = nil
34
35
  end
35
36
 
36
- def marked_int
37
- marked.map do |q|
38
- [].tap do |choices|
39
- q.each_with_index do |choice, idx|
40
- choices << idx if choice
37
+ def marked
38
+ @marked_choices ||= begin
39
+ @choxq.map.with_index do |ncho, q|
40
+ [].tap do |choices|
41
+ ncho.times do |c|
42
+ choices << c if shade_of(q, c) < choice_threshold
43
+ end
41
44
  end
42
45
  end
43
46
  end
@@ -58,11 +61,12 @@ module Mork
58
61
  when :cal
59
62
  @grom.calibration_cell_areas
60
63
  when :marked
61
- choice_cell_areas marked_int
64
+ choice_cell_areas marked
62
65
  when :all
63
66
  all_choice_cell_areas
64
67
  when :max
65
- @grom.max_questions.times.map { |i| (0...@grom.max_choices_per_question).to_a }
68
+ choice_cell_areas [@grom.max_choices_per_question] * @grom.max_questions
69
+ # @grom.max_questions.times.map { |i| (0...@grom.max_choices_per_question).to_a }
66
70
  when Array
67
71
  choice_cell_areas where
68
72
  else
@@ -90,7 +94,7 @@ module Mork
90
94
  private #
91
95
  # ============================================================#
92
96
 
93
- def itemator(items=@nitems)
97
+ def itemator(items=@choxq)
94
98
  items.map.with_index do |cho, q|
95
99
  if cho.is_a? Fixnum
96
100
  cho.times.map { |c| yield q, c }
@@ -105,7 +109,7 @@ module Mork
105
109
  end
106
110
 
107
111
  def all_choice_cell_areas
108
- @all_choice_cell_areas ||= choice_cell_areas(@nitems)
112
+ @all_choice_cell_areas ||= choice_cell_areas(@choxq)
109
113
  end
110
114
 
111
115
  def each_corner
@@ -122,10 +126,10 @@ module Mork
122
126
  end
123
127
  end
124
128
 
125
- # TODO: 0.75 should be a parameter
126
129
  def choice_threshold
127
- # puts "CT #{@grom.choice_threshold.inspect}"
128
- @choice_threshold ||= (cal_cell_mean - darkest_cell_mean) * @grom.choice_threshold + darkest_cell_mean
130
+ @choice_threshold ||= begin
131
+ (cal_cell_mean-darkest_cell_mean) * @grom.choice_threshold + darkest_cell_mean
132
+ end
129
133
  end
130
134
 
131
135
  def barcode_threshold
@@ -258,3 +262,13 @@ end
258
262
  # puts "BL: #{@rm[:bl].inspect}"
259
263
 
260
264
  # puts "REG #{@grom.rm_blur} - #{@grom.rm_dilate} - C #{c.inspect}"
265
+
266
+ # def marked_int
267
+ # marked.map do |q|
268
+ # [].tap do |choices|
269
+ # q.each_with_index do |choice, idx|
270
+ # choices << idx if choice
271
+ # end
272
+ # end
273
+ # end
274
+ # end
data/lib/mork/npatch.rb CHANGED
@@ -10,8 +10,6 @@ module Mork
10
10
  def initialize(source, width, height)
11
11
  @patch = NArray.float(width, height)
12
12
  @patch[true] = source
13
- # @width = width
14
- # @height = height
15
13
  end
16
14
 
17
15
  def average(coord)
@@ -22,11 +20,6 @@ module Mork
22
20
  @patch[coord.x_rng, coord.y_rng].stddev
23
21
  end
24
22
 
25
- # def length
26
- # # is this only going to be used for testing purposes?
27
- # @patch.length
28
- # end
29
-
30
23
  def centroid
31
24
  xp = @patch.sum(1).to_a
32
25
  yp = @patch.sum(0).to_a
@@ -59,3 +52,8 @@ end
59
52
  # # tested with the few examples: spec/samples/rm0x.jpeg
60
53
  # p.stddev > 20
61
54
  # end
55
+
56
+ # def length
57
+ # # is this only going to be used for testing purposes?
58
+ # @patch.length
59
+ # end
@@ -14,26 +14,14 @@ module Mork
14
14
  class SheetOMR
15
15
  # @param path [String] the required path/filename to the saved image
16
16
  # (.jpg, .jpeg, .png, or .pdf)
17
- # @param choices [Fixnum, Array] the questions/choices we want scored, as
18
- # an optional named argument. Scoring is done on all available questions,
19
- # if the argument is omitted, on the first N questions, If an integer
20
- # is passed, or on the indicated questions, if the argument is an array.
21
- # @param layout [String, Hash] the sheet description. Use a string to
22
- # specify the path/filename of a YAML file containing the parameters,
23
- # or directly a hash of parameters. See the README file for a full listing
17
+ # @param layout [String, Hash] the sheet description. Send a hash of
18
+ # parameters or a string to specify the path/filename of a YAML file
19
+ # containing the parameters. See the README file for a full listing
24
20
  # of the available parameters.
25
- def initialize(path, choices: nil, layout: nil)
21
+ def initialize(path, layout=nil)
26
22
  raise IOError, "File '#{path}' not found" unless File.exists? path
27
- grom = GridOMR.new layout
28
- nitems = case choices
29
- when NilClass
30
- [grom.max_choices_per_question] * grom.max_questions
31
- when Fixnum
32
- [grom.max_choices_per_question] * choices
33
- when Array
34
- choices
35
- end
36
- @mim = Mimage.new path, nitems, grom
23
+ grom = GridOMR.new layout
24
+ @mim = Mimage.new path, grom
37
25
  end
38
26
 
39
27
  # True if sheet registration completed successfully
@@ -43,6 +31,30 @@ module Mork
43
31
  @mim.valid?
44
32
  end
45
33
 
34
+ # Setting the choices/questions to analyze. If this function is not called,
35
+ # the maximum number of choices/questions allowed by the layout will be
36
+ # evaluated.
37
+ #
38
+ # @param choices [Fixnum, Array] the questions/choices we want subsequent
39
+ # scoring/overlaying to apply to. Normally, `choices` should be an array
40
+ # of integers, with each element indicating the number of available
41
+ # choices for the corresponding question (i.e. `choices.length` is the
42
+ # number of questions). As a shortcut, `choices` can also be a single
43
+ # integer value, indicating the number of questions; in such case, the
44
+ # maximum number of choices allowed by the layout will be considered.
45
+ #
46
+ # @return [Boolean] True if the sheet is properly registered and ready to
47
+ # be marked; false otherwise.
48
+ def set_choices(cho)
49
+ return false unless valid?
50
+ @mim.set_ch case cho
51
+ when Fixnum; @mim.choxq[0...cho]
52
+ when Array; cho
53
+ else raise ArgumentError, 'Invalid choice set'
54
+ end
55
+ true
56
+ end
57
+
46
58
  # Registration status for each of the four corners
47
59
  #
48
60
  # @return [Hash] { tl: Symbol, tr: Symbol, br: Symbol, bl: Symbol } where
@@ -78,7 +90,7 @@ module Mork
78
90
  # @return [Boolean]
79
91
  def marked?(question, choice)
80
92
  return if not_registered
81
- @mim.marked[question][choice]
93
+ marked_choices[question].find {|x| x==choice} ? true : false
82
94
  end
83
95
 
84
96
  # The set of choice indices marked on the response sheet
@@ -89,12 +101,22 @@ module Mork
89
101
  # indicates that the responder has marked the first choice for the first
90
102
  # question, none for the second, and the fourth and fifth choices for the
91
103
  # third question.
92
- #
93
- # Note that only the questions/choices indicated via the `choices` argument
94
- # during object creation are evaluated.
95
104
  def marked_choices
96
105
  return if not_registered
97
- @mim.marked_int
106
+ @mim.marked
107
+ end
108
+
109
+ # The set of choice indices marked on the response sheet. If more than one
110
+ # choice was marked for a question, the response is regarded as invalid and
111
+ # treated as if it had been left blank.
112
+ #
113
+ # @return [Array] an array of integers; each element contains
114
+ # the (zero-based) marked choice for the corresponding question.
115
+ def marked_choices_unique
116
+ return if not_registered
117
+ marked_choices.map do |c|
118
+ c.length == 1 ? c.first : nil
119
+ end
98
120
  end
99
121
 
100
122
  # The set of letters marked on the response sheet. At this time, only the
@@ -102,9 +124,6 @@ module Mork
102
124
  #
103
125
  # @return [Array] an array of arrays of 1-character strings; each element
104
126
  # contains the list of letters marked for the corresponding question.
105
- #
106
- # Note that only the questions/choices indicated via the `choices` argument
107
- # during object creation are evaluated.
108
127
  def marked_letters
109
128
  return if not_registered
110
129
  marked_choices.map do |q|
@@ -112,13 +131,17 @@ module Mork
112
131
  end
113
132
  end
114
133
 
115
- # Marked choices as boolean values
134
+ # The set of letters marked on the response sheet. At this time, only the
135
+ # latin sequence 'A, B, C...' is supported. If more than one choice was
136
+ # marked for an item, the response is regarded as invalid and treated as if
137
+ # it had been left blank.
116
138
  #
117
- # @return [Array] an array of arrays of true/false values corresponding to
118
- # marked vs unmarked choice cells.
119
- def marked_logicals
139
+ # @return [Array] an array of 1-character strings
140
+ def marked_letters_unique
120
141
  return if not_registered
121
- @mim.marked
142
+ marked_choices_unique.map do |c|
143
+ c.nil?? '' : (65+c).chr
144
+ end
122
145
  end
123
146
 
124
147
  # Apply an overlay on the image
@@ -131,8 +154,8 @@ module Mork
131
154
  # specified by the `choices` argument during object creation
132
155
  # (this is the default); `:all`: all cells in `choices`;
133
156
  # `:max`: maximum number of cells allowed by the layout (can be larger
134
- # than `:all`); `:barcode`: the dark barcode elements; `:cal` the
135
- # calibration cells
157
+ # than `:all`); `:barcode`: the dark barcode elements; `:cal` the
158
+ # calibration cells
136
159
  def overlay(what, where=:marked)
137
160
  return if not_registered
138
161
  @mim.overlay what, where
@@ -276,3 +299,26 @@ end
276
299
  # def barcode_bit_string(i)
277
300
  # @mim.barcode_bit?(i) ? "1" : "0"
278
301
  # end
302
+
303
+ # def validate_choices(ch=nil)
304
+ # return false unless valid?
305
+ # cho = case ch
306
+ # when NilClass; [@mc] * @mq
307
+ # when Fixnum; [@mc] * [[ch, @mq].min, 1].max
308
+ # when Array; ch
309
+ # else raise ArgumentError, 'Invalid choice set'
310
+ # end
311
+ # @marked_choices = @mim.marked cho
312
+ # true
313
+ # end
314
+
315
+ # # Marked choices as boolean values
316
+ # #
317
+ # # @return [Array] an array of arrays of true/false values corresponding to
318
+ # # marked vs unmarked choice cells.
319
+ # def marked_logicals
320
+ # return if not_registered
321
+ # # this is the only marking function calling the mimage object
322
+ # @marked_choices ||= @mim.marked
323
+ # end
324
+
@@ -90,8 +90,11 @@ module Mork
90
90
  @grip.barcode_xy_for(code).each { |c| stamp_at 'barcode', c }
91
91
  end
92
92
 
93
- def header(content)
94
- content.each do |k,v|
93
+ def header(elements)
94
+ elements.each do |k,v|
95
+ if @grip.missing_header? k
96
+ raise ArgumentError, "The header element '#{k}' is not described in the layout"
97
+ end
95
98
  font_size @grip.header_size(k) do
96
99
  align = @grip.header_align(k).nil?? :left : @grip.header_align(k).to_sym
97
100
  if @grip.header_boxed?(k)
data/lib/mork/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Mork
2
- VERSION = '0.9.3'
2
+ VERSION = '0.10.0'
3
3
  end
@@ -7,7 +7,7 @@ module Mork
7
7
  context 'John Doe' do
8
8
  let(:img) { sample_img 'jdoe1' }
9
9
  let(:fn) { File.basename(img.image_path) }
10
- let(:mim) { Mimage.new img.image_path, [img.nchoices]*img.nitems, GridOMR.new(img.grid_path) }
10
+ let(:mim) { Mimage.new img.image_path, GridOMR.new(img.grid_path) }
11
11
  describe 'basics' do
12
12
  it 'should be valid' do
13
13
  expect(mim.valid?).to be_truthy
@@ -6,7 +6,7 @@ module Mork
6
6
  context 'with a valid response sheet' do
7
7
  let(:img) { sample_img 'jdoe1' }
8
8
  let(:fn) { File.basename(img.image_path) }
9
- let(:omr) { SheetOMR.new img.image_path, choices: [img.nchoices]*img.nitems, layout: img.grid_path }
9
+ let(:omr) { SheetOMR.new img.image_path, layout: img.grid_path }
10
10
  describe '#new' do
11
11
  it 'creates a SheetOMR object' do
12
12
  expect(omr).to be_a SheetOMR
@@ -17,6 +17,12 @@ module Mork
17
17
  end
18
18
  end
19
19
 
20
+ describe '#set_choices' do
21
+ it 'returns true if all goes well' do
22
+ expect(omr.set_choices(10)).to be_truthy
23
+ end
24
+ end
25
+
20
26
  describe '#valid?' do
21
27
  it 'registers correctly' do
22
28
  expect(omr.valid?).to be_truthy
@@ -50,29 +56,18 @@ module Mork
50
56
  end
51
57
  end
52
58
 
53
- describe '#marked_choices' do
54
- it 'returns an array of marked choices as position indices' do
55
- expect(omr.marked_choices).to eq standard_mark_array(24)
56
- end
57
- end
58
-
59
59
  describe '#marked_choices' do
60
60
  it 'returns an array of marked choices as position indexes' do
61
61
  expect(omr.marked_choices ).to eq standard_mark_array(24)
62
62
  end
63
63
 
64
- let(:om2) { SheetOMR.new img.image_path, choices: [5, 4, 3, 2, 1], layout: img.grid_path }
64
+ let(:om2) { SheetOMR.new img.image_path, layout: img.grid_path }
65
65
  it 'returns marked choices only for existing choice cells' do
66
+ om2.set_choices [5, 4, 3, 2, 1]
66
67
  expect(om2.marked_choices).to eq [[0], [1], [2], [], []]
67
68
  end
68
69
  end
69
70
 
70
- describe '#marked_logicals' do
71
- it 'returns an array of logicals for the marked choices' do
72
- expect(omr.marked_logicals).to eq standard_mark_logical_array(24)
73
- end
74
- end
75
-
76
71
  describe '#marked_letters' do
77
72
  it 'returns an array of characters for the marked choices' do
78
73
  expect(omr.marked_letters).to eq standard_mark_char_array(24)
@@ -96,8 +91,15 @@ module Mork
96
91
  end
97
92
 
98
93
  it 'highlights all choice cells' do
94
+ omr.set_choices [5] * 32
99
95
  omr.overlay :highlight, :all
100
- omr.save "spec/out/highlight/#{fn}"
96
+ omr.save "spec/out/highlight/all-#{fn}"
97
+ end
98
+
99
+ it 'highlights all possible choice cells' do
100
+ omr.set_choices [5] * 30
101
+ omr.overlay :highlight, :max
102
+ omr.save "spec/out/highlight/max-#{fn}"
101
103
  end
102
104
 
103
105
  it 'highlights marked cells' do
@@ -110,6 +112,12 @@ module Mork
110
112
  omr.save "spec/out/mark/#{fn}"
111
113
  end
112
114
 
115
+ it 'checks the first 32 marked cells' do
116
+ omr.set_choices [5] * 32
117
+ omr.overlay :check, :marked
118
+ omr.save "spec/out/mark/part-#{fn}"
119
+ end
120
+
113
121
  it 'outlines and crosses marked cells' do
114
122
  omr.overlay :outline, standard_mark_array(24)
115
123
  omr.overlay :check
@@ -4,13 +4,10 @@ module Mork
4
4
  describe SheetPDF do
5
5
  let(:content) {
6
6
  {
7
- barcode: 1234566,
7
+ barcode: 183251937962,
8
8
  choices: [5] * 120,
9
9
  header: {
10
- name: 'John Doe UI01234',
11
- title: 'A really serious and difficult test - 18 January 2013',
12
- code: '201.48',
13
- signature: 'Signature'
10
+ title: 'A serious, difficult test - 31 December 1999',
14
11
  }
15
12
  }
16
13
  }
@@ -29,6 +26,12 @@ module Mork
29
26
  lambda { SheetPDF.new(content, 2) }.should raise_error ArgumentError
30
27
  end
31
28
 
29
+ 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
33
+ end
34
+
32
35
  it 'assigns an array to @content' do
33
36
  s = SheetPDF.new(content)
34
37
  s.instance_variable_get('@content').should be_an Array
@@ -39,62 +42,67 @@ module Mork
39
42
  s.instance_variable_get('@content').first.should be_a Hash
40
43
  end
41
44
 
42
- it 'creates a basic PDF sheet' do
43
- s = SheetPDF.new(content)
44
- s.save('spec/out/pdf/sheet.pdf')
45
+ it 'creates a minimal PDF sheet' do
46
+ s = SheetPDF.new({})
47
+ s.save dest 'minimal'
48
+ end
49
+
50
+ it 'creates a PDF sheet with a big barcode' do
51
+ s = SheetPDF.new({barcode: 183251937962})
52
+ s.save dest 'bigbarcode'
45
53
  end
46
54
 
47
- it 'creates a boxed PDF sheet' do
55
+ it 'creates a PDF sheet with several boxed header elements' do
48
56
  h = {
49
57
  name: lorem,
50
58
  title: lorem,
51
59
  code: '1000.10.100',
52
60
  signature: 'Signature'
53
61
  }
54
- s = SheetPDF.new(content.merge({header: h}), 'spec/samples/boxy.yml')
55
- s.save('spec/out/pdf/boxy.pdf')
62
+ s = SheetPDF.new({header: h}, 'spec/samples/boxy.yml')
63
+ s.save dest 'boxy'
56
64
  end
57
65
 
58
- it 'creates a basic PDF sheet with a code of 15' do
59
- s = SheetPDF.new(content.merge({barcode: 15}))
60
- s.save('spec/out/pdf/sheet16.pdf')
61
- end
62
-
63
- it 'creates a basic PDF sheet with a code of 666666666666' do
64
- s = SheetPDF.new(content.merge({barcode: 666666666666}))
65
- s.save('spec/out/pdf/sheet666.pdf')
66
- end
67
-
68
- it 'creates a PDF sheet with the maximum possible barcode' do
69
- s = SheetPDF.new(content.merge({barcode: 1099511627775}))
70
- s.save('spec/out/pdf/maxcode.pdf')
66
+ it 'creates a PDF sheet with the maximum possible barcode for 38 bits' do
67
+ c = {
68
+ barcode: 274877906943,
69
+ header: {
70
+ title: 'The maximum barcode for 38 bits is 274877906943'
71
+ }
72
+ }
73
+ s = SheetPDF.new(c)
74
+ s.save dest 'maxcode'
71
75
  end
72
76
 
73
77
  it 'creates a PDF sheet with 160 items' do
74
78
  s = SheetPDF.new(content.merge({choices: [5] * 160}), 'spec/samples/grid160.yml')
75
- s.save('spec/out/pdf/i160.pdf')
79
+ s.save dest 'i160'
76
80
  end
77
81
 
78
82
  it 'creates a PDF sheet with unequal choices per item' do
79
- s = SheetPDF.new(content.merge({choices: [5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1]}), 'spec/samples/layout.yml')
80
- s.save('spec/out/pdf/uneq.pdf')
83
+ c = {
84
+ choices: [5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1, 5, 4, 3, 2, 1],
85
+ header: {
86
+ title: 'Each question can have an arbitrary number of choices'
87
+ }
88
+ }
89
+ SheetPDF.new(c).save dest('uneq')
81
90
  end
82
91
 
83
92
  it 'creates 20 PDF sheets' do
84
- c = content
85
- s = SheetPDF.new([c,c,c,c,c,c,c,c,c,c,c,c,c,c,c,c,c,c,c,c])
86
- s.save('spec/out/pdf/p20.pdf')
87
- end
88
-
89
- it 'creates a pdf with minimal content' do
90
- s = SheetPDF.new({choices: [4] * 30})
91
- s.save 'spec/out/pdf/mincont.pdf'
93
+ c = 20.times.collect do |x|
94
+ content.merge({ header: {title: "Test #{x+1}"}, barcode: x})
95
+ end
96
+ SheetPDF.new(c).save dest('p20')
92
97
  end
93
98
 
94
99
  it 'creates a PDF with the maximum possible choice cells if none are specified' do
95
100
  ct = [{}, {choices: [3]*20}]
96
- s = SheetPDF.new ct
97
- s.save 'spec/out/pdf/nocontent.pdf'
101
+ SheetPDF.new(ct).save dest('nocontent')
102
+ end
103
+
104
+ def dest(fname)
105
+ "spec/out/pdf/#{fname}.pdf"
98
106
  end
99
107
  end
100
108
  end
@@ -1,13 +1,3 @@
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
9
- blur: 2
10
- dilate: 5
11
1
  header:
12
2
  name: # ‘name’ is just a label; you can add arbitrary header elements
13
3
  top: 5 # margin relative to registration frame top side
@@ -15,7 +5,7 @@ header:
15
5
  width: 160 # text will be fitted to this width
16
6
  height: 7 # text will be fitted to this height
17
7
  size: 14 # font size
18
- # box: true
8
+ box: true
19
9
  title:
20
10
  top: 15
21
11
  left: 15
@@ -38,23 +28,3 @@ header:
38
28
  height: 15
39
29
  size: 7
40
30
  box: true # header element will be enclosed in a box
41
- items:
42
- top: 55.5 # response area margin, relative to reg frame
43
- left: 10.5 # response area margin, relative to reg frame
44
- rows: 30 # number of items per column
45
- columns: 4 # number of columns
46
- column_width: 44 #
47
- x_spacing: 7 # horizontal distance between ajacent cell centers
48
- y_spacing: 7 # vertical distance between ajacent cell centers
49
- cell_width: 6 # width of each choice and calibration cell
50
- cell_height: 5 # height of each choice and calibration cell
51
- max_cells: 5 # maximum number of choices per item
52
- font_size: 9 # size of both the item number and choice cell letter
53
- number_width: 8 #
54
- number_margin: 2 # margin between
55
- barcode:
56
- bits: 40 # the maximum sheet identifier is 2 to the power or bits
57
- left: 15 # distance between registration frame side and the first barcode bit
58
- width: 3 # width of each barcode bit
59
- height: 2.5 # height of each barcode bit from the registration frame bottom side
60
- spacing: 4 # horizontal distance between adjacent barcode bit centers
@@ -4,7 +4,7 @@ jdoe1:
4
4
  nchoices: 5
5
5
  nitems: 120
6
6
  barcode_int: 1234566
7
- barcode_str: '0000000000000000000100101101011010000110'
7
+ barcode_str: '00000000000000000100101101011010000110'
8
8
  width: 1240
9
9
  height: 1754
10
10
  mark_array: [[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4],[0],[1],[2],[3],[4]]
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.9.3
4
+ version: 0.10.0
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-15 00:00:00.000000000 Z
11
+ date: 2016-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: narray