riiif 2.0.0.beta2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +2 -11
  3. data/.rubocop_todo.yml +76 -33
  4. data/README.md +17 -2
  5. data/app/controllers/riiif/images_controller.rb +8 -2
  6. data/app/models/riiif/image.rb +15 -20
  7. data/app/models/riiif/image_information.rb +12 -24
  8. data/app/services/riiif/crop.rb +99 -30
  9. data/app/services/riiif/image_magick_info_extractor.rb +9 -2
  10. data/app/services/riiif/imagemagick_command_factory.rb +9 -4
  11. data/app/services/riiif/kakadu_command_factory.rb +2 -2
  12. data/app/services/riiif/resize.rb +72 -7
  13. data/app/transformers/riiif/kakadu_transformer.rb +25 -2
  14. data/docs/benchmark.md +75 -0
  15. data/lib/riiif/engine.rb +2 -1
  16. data/lib/riiif/routes.rb +3 -0
  17. data/lib/riiif/version.rb +1 -1
  18. data/riiif.gemspec +3 -3
  19. data/spec/controllers/riiif/images_controller_spec.rb +14 -3
  20. data/spec/models/riiif/image_information_spec.rb +2 -10
  21. data/spec/models/riiif/image_spec.rb +10 -16
  22. data/spec/services/riiif/imagemagick_command_factory_spec.rb +6 -6
  23. data/spec/services/riiif/kakadu_command_factory_spec.rb +15 -15
  24. data/spec/transformers/riiif/kakadu_transformer_spec.rb +22 -22
  25. metadata +24 -46
  26. data/app/models/riiif/transformation.rb +0 -35
  27. data/app/services/riiif/imagemagick_transformer.rb +0 -8
  28. data/app/services/riiif/option_decoder.rb +0 -88
  29. data/app/services/riiif/region/absolute.rb +0 -23
  30. data/app/services/riiif/region/full.rb +0 -23
  31. data/app/services/riiif/region/percentage.rb +0 -68
  32. data/app/services/riiif/region/square.rb +0 -45
  33. data/app/services/riiif/size/absolute.rb +0 -39
  34. data/app/services/riiif/size/best_fit.rb +0 -18
  35. data/app/services/riiif/size/full.rb +0 -17
  36. data/app/services/riiif/size/height.rb +0 -24
  37. data/app/services/riiif/size/percent.rb +0 -44
  38. data/app/services/riiif/size/width.rb +0 -24
  39. data/spec/models/riiif/transformation_spec.rb +0 -42
  40. data/spec/services/riiif/region/absolute_spec.rb +0 -17
  41. data/spec/services/riiif/size/absolute_spec.rb +0 -17
  42. data/spec/services/riiif/size/height_spec.rb +0 -13
  43. data/spec/services/riiif/size/width_spec.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: e944a8c92ae1774d9dd42515c3202a18b6a70f64
4
- data.tar.gz: a4fa5460db8fc3703f21d3c8a07a62b920e4aa5e
2
+ SHA256:
3
+ metadata.gz: b274258986b942fec11fa586bd52f13e9bf8a567725954f8ca6fd92282780207
4
+ data.tar.gz: d786222b5b9be2b1c6d514917adb239d498ed819f88f9de93d9a8f61852f1c5a
5
5
  SHA512:
6
- metadata.gz: 33d4db4a5c49e09845ecdac04bdd35d88fa3ab1526eab8fbdd55bfbde786aee6adc1843e90e4f2bf0389c83570c005cb143e1dc831c18ec664fde6919a7feb6e
7
- data.tar.gz: '05628c076ffc69365d0821f368118d0e078d7bbae5c946f4f59fafc485d8948d56b954338f0dfbdb4b8628212d48954b1a972fc937e4099985b97f67ca12ba9b'
6
+ metadata.gz: d4b7a1b45f2513f51abb150a0bc6678bc1578b4ab64da8487b7447af3a0cab2db922d8e7a49a5c737cfd9931e2abcd4f2bbeec37838862ecee49ffaeee6e9eb7
7
+ data.tar.gz: 4d06560bdbbf84747e7dba9f341fb6d5eff45dd6e1e573839aa8ec236b42dc242d7a31b8ff9c2bc4d6f31248839c9c3c0e39a76e884569904f7c36aa5aa5834b
@@ -1,12 +1,3 @@
1
1
  inherit_from: .rubocop_todo.yml
2
-
3
- AllCops:
4
- TargetRubyVersion: 2.1
5
- DisplayCopNames: true
6
-
7
- Style/IndentationConsistency:
8
- EnforcedStyle: rails
9
-
10
- Metrics/BlockLength:
11
- Exclude:
12
- - spec/**/*
2
+ inherit_gem:
3
+ bixby: bixby_default.yml
@@ -1,17 +1,21 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2017-04-11 15:07:00 -0500 using RuboCop version 0.47.1.
3
+ # on 2018-02-23 11:28:05 -0600 using RuboCop version 0.52.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
- # Offense count: 9
9
+ # Offense count: 2
10
10
  # Configuration parameters: AllowSafeAssignment.
11
11
  Lint/AssignmentInCondition:
12
12
  Exclude:
13
13
  - 'app/models/riiif/file.rb'
14
- - 'app/services/riiif/option_decoder.rb'
14
+ - 'app/resolvers/riiif/http_file_resolver.rb'
15
+
16
+ # Offense count: 1
17
+ Lint/DuplicateMethods:
18
+ Exclude:
15
19
  - 'app/resolvers/riiif/http_file_resolver.rb'
16
20
 
17
21
  # Offense count: 1
@@ -24,50 +28,89 @@ Lint/UselessAssignment:
24
28
  Exclude:
25
29
  - 'app/models/riiif/file.rb'
26
30
 
27
- # Offense count: 5
28
- Metrics/AbcSize:
29
- Max: 29
31
+ # Offense count: 21
32
+ # Configuration parameters: CountComments, ExcludedMethods.
33
+ Metrics/BlockLength:
34
+ Max: 233
30
35
 
31
36
  # Offense count: 1
32
37
  Metrics/CyclomaticComplexity:
33
- Max: 8
38
+ Max: 7
34
39
 
35
- # Offense count: 96
36
- # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
37
- # URISchemes: http, https
38
- Metrics/LineLength:
39
- Max: 117
40
-
41
- # Offense count: 6
40
+ # Offense count: 3
42
41
  # Configuration parameters: CountComments.
43
42
  Metrics/MethodLength:
44
43
  Max: 18
45
44
 
45
+ # Offense count: 1
46
+ # Configuration parameters: CountKeywordArgs.
46
47
  Metrics/ParameterLists:
48
+ Max: 6
49
+
50
+ # Offense count: 1
51
+ RSpec/DescribeClass:
52
+ Exclude:
53
+ - 'spec/routing/resize_routes_spec.rb'
54
+
55
+ # Offense count: 2
56
+ # Configuration parameters: SkipBlocks, EnforcedStyle.
57
+ # SupportedStyles: described_class, explicit
58
+ RSpec/DescribedClass:
47
59
  Exclude:
48
- - 'app/services/riiif/imagemagick_command_factory.rb'
60
+ - 'spec/models/riiif/image_spec.rb'
61
+
62
+ # Offense count: 7
63
+ # Configuration parameters: Max.
64
+ RSpec/ExampleLength:
65
+ Exclude:
66
+ - 'spec/controllers/riiif/images_controller_spec.rb'
67
+ - 'spec/models/riiif/http_file_resolver_spec.rb'
68
+ - 'spec/transformers/riiif/kakadu_transformer_spec.rb'
49
69
 
50
70
  # Offense count: 1
51
- # Configuration parameters: EnforcedStyle, SupportedStyles.
52
- # SupportedStyles: nested, compact
53
- Style/ClassAndModuleChildren:
71
+ RSpec/LeadingSubject:
54
72
  Exclude:
55
- - 'lib/riiif/rails/routes.rb'
73
+ - 'spec/transformers/riiif/kakadu_transformer_spec.rb'
74
+
75
+ # Offense count: 43
76
+ # Configuration parameters: .
77
+ # SupportedStyles: have_received, receive
78
+ RSpec/MessageSpies:
79
+ EnforcedStyle: receive
56
80
 
57
- # Offense count: 14
58
- Style/Documentation:
81
+ # Offense count: 23
82
+ RSpec/NamedSubject:
83
+ Exclude:
84
+ - 'spec/controllers/riiif/images_controller_spec.rb'
85
+ - 'spec/models/riiif/akubra_system_file_resolver_spec.rb'
86
+ - 'spec/models/riiif/file_system_file_resolver_spec.rb'
87
+ - 'spec/models/riiif/http_file_resolver_spec.rb'
88
+ - 'spec/models/riiif/image_spec.rb'
89
+
90
+ # Offense count: 2
91
+ RSpec/RepeatedExample:
92
+ Exclude:
93
+ - 'spec/controllers/riiif/images_controller_spec.rb'
94
+
95
+ # Offense count: 7
96
+ # Configuration parameters: IgnoreSymbolicNames.
97
+ RSpec/VerifiedDoubles:
98
+ Exclude:
99
+ - 'spec/controllers/riiif/images_controller_spec.rb'
100
+ - 'spec/services/riiif/imagemagick_command_factory_spec.rb'
101
+ - 'spec/services/riiif/kakadu_command_factory_spec.rb'
102
+
103
+ # Offense count: 1
104
+ Rails/FilePath:
105
+ Exclude:
106
+ - 'spec/models/riiif/akubra_system_file_resolver_spec.rb'
107
+
108
+ # Offense count: 4
109
+ # Cop supports --auto-correct.
110
+ # Configuration parameters: PreferredDelimiters.
111
+ Style/PercentLiteralDelimiters:
59
112
  Exclude:
60
- - 'spec/**/*'
61
- - 'test/**/*'
62
- - 'app/controllers/riiif/images_controller.rb'
63
- - 'app/models/riiif/file.rb'
64
113
  - 'app/models/riiif/image.rb'
65
- - 'app/resolvers/riiif/abstract_file_system_resolver.rb'
66
- - 'app/resolvers/riiif/akubra_system_file_resolver.rb'
67
114
  - 'app/resolvers/riiif/file_system_file_resolver.rb'
68
- - 'app/resolvers/riiif/http_file_resolver.rb'
69
- - 'app/services/riiif/nil_authorization_service.rb'
70
- - 'lib/riiif.rb'
71
- - 'lib/riiif/engine.rb'
72
- - 'lib/riiif/rails/routes.rb'
73
- - 'lib/riiif/routes.rb'
115
+ - 'spec/controllers/riiif/images_controller_spec.rb'
116
+ - 'spec/models/riiif/http_file_resolver_spec.rb'
data/README.md CHANGED
@@ -63,6 +63,14 @@ If you need to use HTTP basic authentication you can enable it like this:
63
63
  This file resolver caches the network files, so you will want to clear out the old files or the cache will expand until you run out of disk space.
64
64
  Using a script like this would be a good idea: https://github.com/pulibrary/loris/blob/607567b921404a15a2111fbd7123604f4fdec087/bin/loris-cache_clean.sh
65
65
  By default the cache is located in `tmp/network_files`. You can set the cache path like this: `Riiif::Image.file_resolver.cache_path = '/var/cache'`
66
+ ### Kakadu (for faster jp2 decoding)
67
+ To configure Riiif to use Kakadu set:
68
+
69
+ ```ruby
70
+ Riiif::Engine.config.kakadu_enabled = true
71
+ ```
72
+
73
+ See [benchmark](docs/benchmark.md) for details
66
74
 
67
75
  ### GraphicsMagick
68
76
 
@@ -167,7 +175,14 @@ Riiif::Image.info_service = lambda do |id, file|
167
175
  resp = ActiveFedora::SolrService.get("id:#{fs_id}")
168
176
  doc = resp['response']['docs'].first
169
177
  raise "Unable to find solr document with id:#{fs_id}" unless doc
170
- { height: doc['height_is'], width: doc['width_is'] }
178
+
179
+ # You’ll want default values if you make thumbnails of PDFs or other
180
+ # file types that `identify` won’t return dimensions for
181
+ {
182
+ height: doc["height_is"] || 100,
183
+ width: doc["width_is"] || 100,
184
+ format: doc["mime_type_ssi"],
185
+ }
171
186
  end
172
187
 
173
188
  def logger
@@ -179,7 +194,7 @@ end
179
194
  # Rails.cache. Some cache stores may not automatically purge expired content,
180
195
  # such as the default FileStore.
181
196
  # http://guides.rubyonrails.org/caching_with_rails.html#cache-stores
182
- Riiif::Engine.config.cache_duration_in_days = 30
197
+ Riiif::Engine.config.cache_duration = 30.days
183
198
  ```
184
199
  #### Special note for Passenger and Apache users
185
200
  If you are running riiif in Passenger under Apache, you must set the following in your virtual host definition:
@@ -2,7 +2,7 @@ module Riiif
2
2
  class ImagesController < ::ApplicationController
3
3
  before_action :link_header, only: [:show, :info]
4
4
 
5
- rescue_from Riiif::InvalidAttributeError do
5
+ rescue_from IIIF::Image::InvalidAttributeError do
6
6
  head 400
7
7
  end
8
8
 
@@ -44,6 +44,12 @@ module Riiif
44
44
  end
45
45
  end
46
46
 
47
+ # See https://fetch.spec.whatwg.org/#http-access-control-allow-headers
48
+ def info_options
49
+ response.headers['Access-Control-Allow-Headers'] = 'Authorization'
50
+ self.response_body = ''
51
+ end
52
+
47
53
  # this is a workaround for https://github.com/rails/rails/issues/25087
48
54
  def redirect
49
55
  # This was attempted with just info_path, but it gave a NoMethodError
@@ -120,7 +126,7 @@ module Riiif
120
126
  CONTEXT => CONTEXT_URI,
121
127
  ID => request.original_url.sub('/info.json', ''),
122
128
  PROTOCOL => PROTOCOL_URI,
123
- PROFILE => [LEVEL1, 'formats' => OptionDecoder::OUTPUT_FORMATS]
129
+ PROFILE => [LEVEL1, 'formats' => IIIF::Image::OptionDecoder::OUTPUT_FORMATS]
124
130
  }
125
131
  end
126
132
  end
@@ -1,27 +1,11 @@
1
1
  require 'digest/md5'
2
2
 
3
- ##
4
- # These explict requires are needed because in some contexts the Rails
5
- # autoloader can either: unload already loaded classes, or cause a lock while
6
- # trying to load a needed class.
7
- require_dependency 'riiif/region/absolute'
8
- require_dependency 'riiif/region/full'
9
- require_dependency 'riiif/region/percentage'
10
- require_dependency 'riiif/region/square'
11
-
12
- require_dependency 'riiif/size/absolute'
13
- require_dependency 'riiif/size/best_fit'
14
- require_dependency 'riiif/size/full'
15
- require_dependency 'riiif/size/height'
16
- require_dependency 'riiif/size/percent'
17
- require_dependency 'riiif/size/width'
18
-
19
3
  module Riiif
20
4
  class Image
21
5
  extend Deprecation
22
6
 
23
7
  class_attribute :file_resolver, :info_service, :authorization_service, :cache
24
- self.file_resolver = FileSystemFileResolver.new(base_path: File.join(Rails.root, 'tmp'))
8
+ self.file_resolver = FileSystemFileResolver.new(base_path: ::File.join(Rails.root, 'tmp'))
25
9
  self.authorization_service = NilAuthorizationService
26
10
  self.cache = Rails.cache
27
11
 
@@ -60,20 +44,31 @@ module Riiif
60
44
  key = Image.cache_key(id, cache_opts)
61
45
 
62
46
  cache.fetch(key, compress: true, expires_in: Image.expires_in) do
63
- file.extract(OptionDecoder.decode(args, info), info)
47
+ file.extract(IIIF::Image::OptionDecoder.decode(args), info)
64
48
  end
65
49
  end
66
50
 
67
51
  def info
68
52
  @info ||= begin
69
53
  result = info_service.call(id, file)
70
- ImageInformation.new(result[:width], result[:height])
54
+ ImageInformation.new(
55
+ width: result[:width],
56
+ height: result[:height],
57
+ format: result[:format]
58
+ )
71
59
  end
72
60
  end
73
61
 
74
62
  class << self
75
63
  def expires_in
76
- Riiif::Engine.config.cache_duration_in_days.days
64
+ if Riiif::Engine.config.respond_to?(:cache_duration_in_days)
65
+ Deprecation.warn(self,
66
+ 'Riiif::Engine.config.cache_duration_in_days is deprecated; '\
67
+ 'use #cache_duration instead and pass a fully-qualified date (e.g., `3.days`)')
68
+ Riiif::Engine.config.cache_duration_in_days.days
69
+ else
70
+ Riiif::Engine.config.cache_duration
71
+ end
77
72
  end
78
73
 
79
74
  def cache_key(id, options)
@@ -1,41 +1,29 @@
1
1
  module Riiif
2
2
  # This is the result of calling the Riiif.image_info service. It stores the height & width
3
- class ImageInformation
3
+ class ImageInformation < IIIF::Image::Dimension
4
4
  extend Deprecation
5
5
 
6
- def initialize(width, height)
7
- @width = width
8
- @height = height
6
+ def initialize(*args)
7
+ if args.size == 2
8
+ Deprecation.warn(self, 'calling initialize without kwargs is deprecated. Use named parameters.')
9
+ super(width: args.first, height: args.second)
10
+ else
11
+ @width = args.first[:width]
12
+ @height = args.first[:height]
13
+ @format = args.first[:format]
14
+ end
9
15
  end
10
-
11
- attr_reader :width, :height
16
+ attr_reader :format, :height, :width
12
17
 
13
18
  def to_h
14
- { width: width, height: height }
15
- end
16
-
17
- def aspect_ratio
18
- width.to_f / height
19
+ { width: width, height: height, format: format }
19
20
  end
20
21
 
21
- def [](key)
22
- to_h[key]
23
- end
24
- deprecation_deprecate :[] => 'Riiif::ImageInformation#[] has been deprecated ' \
25
- 'and will be removed in version 2.0. Use Riiif::ImageInformation#to_h and ' \
26
- 'call #[] on that result OR consider using #height and #width directly.'
27
-
28
22
  # Image information is only valid if height and width are present.
29
23
  # If an image info service doesn't have the value yet (not characterized perhaps?)
30
24
  # then we wouldn't want to cache this value.
31
25
  def valid?
32
26
  width.present? && height.present?
33
27
  end
34
-
35
- def ==(other)
36
- other.class == self.class &&
37
- other.width == width &&
38
- other.height == height
39
- end
40
28
  end
41
29
  end
@@ -1,54 +1,123 @@
1
1
  module Riiif
2
2
  # Represents a cropping operation
3
3
  class Crop
4
- attr_reader :image_info
4
+ # @param transformation [IIIF::Image::Region] the result the user requested
5
+ # @param image_info []
6
+ def initialize(region, image_info)
7
+ @region = region
8
+ @image_info = image_info
9
+ end
10
+
11
+ attr_reader :image_info, :region
5
12
 
6
13
  # @return [String] a region for imagemagick to decode
7
14
  # (appropriate for passing to the -crop parameter)
8
15
  def to_imagemagick
9
- "#{width}x#{height}+#{offset_x}+#{offset_y}"
16
+ case region
17
+ when IIIF::Image::Region::Full
18
+ nil
19
+ when IIIF::Image::Region::Absolute
20
+ "#{region.width}x#{region.height}+#{region.offset_x}+#{region.offset_y}"
21
+ when IIIF::Image::Region::Square
22
+ imagemagick_square
23
+ when IIIF::Image::Region::Percent
24
+ imagemagick_percent
25
+ else
26
+ raise "Unknown region #{region.class}"
27
+ end
10
28
  end
11
29
 
12
30
  # @return [String] a region for kakadu to decode
13
31
  # (appropriate for passing to the -region parameter)
14
32
  def to_kakadu
15
- "\{#{decimal_offset_y},#{decimal_offset_x}\},\{#{decimal_height},#{decimal_width}\}"
33
+ case region
34
+ when IIIF::Image::Region::Full
35
+ nil
36
+ when IIIF::Image::Region::Absolute
37
+ "\{#{decimal_offset_y(region.offset_y)},#{decimal_offset_x(region.offset_x)}\}," \
38
+ "\{#{decimal_height(region.height)},#{decimal_width(region.width)}\}"
39
+ when IIIF::Image::Region::Square
40
+ kakadu_square
41
+ when IIIF::Image::Region::Percent
42
+ kakadu_percent
43
+ else
44
+ raise "Unknown region #{region.class}"
45
+ end
16
46
  end
17
47
 
18
- attr_reader :offset_x
48
+ private
19
49
 
20
- attr_reader :offset_y
50
+ def imagemagick_percent
51
+ offset_x = (image_info.width * percentage_to_fraction(region.x_pct)).round
52
+ offset_y = (image_info.height * percentage_to_fraction(region.y_pct)).round
53
+ "#{region.width_pct}%x#{region.height_pct}+#{offset_x}+#{offset_y}"
54
+ end
21
55
 
22
- # @return [Integer] the height in pixels
23
- def height
24
- image_info.height
25
- end
56
+ def kakadu_percent
57
+ offset_x = (image_info.width * percentage_to_fraction(region.x_pct)).round
58
+ offset_y = (image_info.height * percentage_to_fraction(region.y_pct)).round
59
+ "\{#{decimal_offset_y(offset_y)},#{decimal_offset_x(offset_x)}\}," \
60
+ "\{#{percentage_to_fraction(region.height_pct)},#{percentage_to_fraction(region.width_pct)}\}"
61
+ end
26
62
 
27
- # @return [Integer] the width in pixels
28
- def width
29
- image_info.width
30
- end
63
+ def kakadu_square
64
+ min, max = [image_info.width, image_info.height].minmax
65
+ offset = (max - min) / 2
66
+ if image_info.height >= image_info.width
67
+ # Portrait
68
+ "\{#{decimal_height(offset)},0\}," \
69
+ "\{#{decimal_height(image_info.height)},#{decimal_width(image_info.height)}\}"
70
+ else
71
+ # Landscape
72
+ "\{0,#{decimal_width(offset)}\}," \
73
+ "\{#{decimal_height(image_info.width)},#{decimal_width(image_info.width)}\}"
74
+ end
75
+ end
31
76
 
32
- # @return [Float] the fractional height with respect to the original size
33
- def decimal_height(n = height)
34
- n.to_f / image_info.height
35
- end
77
+ def imagemagick_square
78
+ min, max = [image_info.width, image_info.height].minmax
79
+ offset = (max - min) / 2
80
+ if image_info.height >= image_info.width
81
+ "#{min}x#{min}+0+#{offset}"
82
+ else
83
+ "#{min}x#{min}+#{offset}+0"
84
+ end
85
+ end
36
86
 
37
- # @return [Float] the fractional width with respect to the original size
38
- def decimal_width(n = width)
39
- n.to_f / image_info.width
40
- end
87
+ # @return [Integer] the height in pixels
88
+ def height
89
+ image_info.height
90
+ end
41
91
 
42
- def decimal_offset_x
43
- offset_x.to_f / image_info.width
44
- end
92
+ # @return [Integer] the width in pixels
93
+ def width
94
+ image_info.width
95
+ end
45
96
 
46
- def decimal_offset_y
47
- offset_y.to_f / image_info.height
48
- end
97
+ # @return [Float] the fractional height with respect to the original size
98
+ def decimal_height(n = height)
99
+ n.to_f / image_info.height
100
+ end
49
101
 
50
- def maintain_aspect_ratio?
51
- (height / width) == (image_info.height / image_info.width)
52
- end
102
+ # @return [Float] the fractional width with respect to the original size
103
+ def decimal_width(n = width)
104
+ n.to_f / image_info.width
105
+ end
106
+
107
+ def decimal_offset_x(offset_x)
108
+ offset_x.to_f / image_info.width
109
+ end
110
+
111
+ def decimal_offset_y(offset_y)
112
+ offset_y.to_f / image_info.height
113
+ end
114
+
115
+ def maintain_aspect_ratio?
116
+ (height / width) == (image_info.height / image_info.width)
117
+ end
118
+
119
+ def percentage_to_fraction(n)
120
+ n / 100.0
121
+ end
53
122
  end
54
123
  end