mork 0.9.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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