photo-cook 1.1.2 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +16 -0
  3. data/Gemfile +3 -13
  4. data/README.md +2 -2
  5. data/Rakefile +2 -15
  6. data/app/assets/javascripts/photo-cook/photo-cook.coffee +90 -0
  7. data/app/helpers/photo_cook_helper.rb +14 -0
  8. data/lib/photo-cook/device-pixel-ratio-spy.rb +17 -0
  9. data/lib/photo-cook/device-pixel-ratio.rb +67 -0
  10. data/lib/photo-cook/engine.rb +15 -2
  11. data/lib/photo-cook/events.rb +15 -0
  12. data/lib/photo-cook/logger.rb +37 -0
  13. data/lib/photo-cook/optimization/__api__.rb +21 -0
  14. data/lib/photo-cook/optimization/carrierwave.rb +10 -0
  15. data/lib/photo-cook/optimization/image-optim.rb +131 -0
  16. data/lib/photo-cook/optimization/job.rb +12 -0
  17. data/lib/photo-cook/optimization/logging.rb +21 -0
  18. data/lib/photo-cook/pixels.rb +56 -0
  19. data/lib/photo-cook/resize/__api__.rb +93 -0
  20. data/lib/photo-cook/resize/assemble.rb +125 -0
  21. data/lib/photo-cook/resize/calculations.rb +63 -0
  22. data/lib/photo-cook/resize/carrierwave.rb +22 -0
  23. data/lib/photo-cook/resize/command.rb +28 -0
  24. data/lib/photo-cook/resize/logging.rb +25 -0
  25. data/lib/photo-cook/resize/magick-photo.rb +34 -0
  26. data/lib/photo-cook/resize/middleware.rb +108 -0
  27. data/lib/photo-cook/resize/mode.rb +36 -0
  28. data/lib/photo-cook/resize/resizer.rb +88 -0
  29. data/lib/photo-cook/utils.rb +71 -0
  30. data/lib/photo-cook/version.rb +3 -2
  31. data/lib/photo-cook.rb +32 -23
  32. data/photo-cook.gemspec +14 -8
  33. data/srcset.rb +34 -0
  34. data/test/fixtures/dog.png +0 -0
  35. data/test/helper.rb +12 -0
  36. data/test/test-photo-cook-assemble.rb +23 -0
  37. data/test/test-photo-cook-calculations.rb +45 -0
  38. data/test/test-photo-cook-resize.rb +62 -0
  39. metadata +86 -25
  40. data/app/assets/javascripts/photo-cook/photo-cook.js.erb +0 -85
  41. data/lib/photo-cook/abstract-optimizer.rb +0 -9
  42. data/lib/photo-cook/assemble.rb +0 -97
  43. data/lib/photo-cook/carrierwave.rb +0 -15
  44. data/lib/photo-cook/command.rb +0 -49
  45. data/lib/photo-cook/dimensions.rb +0 -36
  46. data/lib/photo-cook/dirs.rb +0 -8
  47. data/lib/photo-cook/image-optim.rb +0 -24
  48. data/lib/photo-cook/logging.rb +0 -85
  49. data/lib/photo-cook/magick-photo.rb +0 -6
  50. data/lib/photo-cook/middleware.rb +0 -139
  51. data/lib/photo-cook/optimization-api.rb +0 -20
  52. data/lib/photo-cook/optimization-job.rb +0 -9
  53. data/lib/photo-cook/pixel-ratio-spy.rb +0 -19
  54. data/lib/photo-cook/pixel-ratio.rb +0 -36
  55. data/lib/photo-cook/resize-api.rb +0 -94
  56. data/lib/photo-cook/resizer.rb +0 -111
  57. data/lib/photo-cook/size-formatting.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6005409b8dc3c640b98055fdf5632229ba5f4f1c
4
- data.tar.gz: 39172ae4df3922a0e45a95971b2851a0ae58351a
3
+ metadata.gz: 54e3edc9dd1cca1b0bb175a4d1015b220f01d12e
4
+ data.tar.gz: 83bd26d3887b920c1a289d92cc4a274bada08c8b
5
5
  SHA512:
6
- metadata.gz: 751fd68124f6dc8c7006587354a258f5a65c654c863a7eb6fabfaf1f321a63aaabf09ea72ad53a86de292bd11b3b1a058ee3c4765dde103961fe7db47c58a9aa
7
- data.tar.gz: 8afd24b253d69b657f4308c85950f1d7f9ce6c4086cbdc0059fb2571a3c46a8f4b68a874c649ec780032c79a604f8a948507f69b9d5588f1a9a445801b449ef0
6
+ metadata.gz: fc6465991aac047ceb19817f305ade9d39949e6aa737b7fa82419b6bed5a3c5203907c29516a852472247155371112eefd823082a9d701315bf72158edd54059
7
+ data.tar.gz: a58e5f69881238958e46fd6b8379cbc49de2d67c94b417b3bccb45464558693660941d452ea2023515734acd373b5dd1146745d0f52d634685229dd9061b20e4
data/.travis.yml ADDED
@@ -0,0 +1,16 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 2.3.1
5
+
6
+ cache: bundler
7
+
8
+ branches:
9
+ only:
10
+ - master
11
+
12
+ env:
13
+ - RAILS_ENV=test RAKE_ENV=test
14
+
15
+ script:
16
+ - bundle exec rake test
data/Gemfile CHANGED
@@ -1,16 +1,6 @@
1
- source "https://rubygems.org"
1
+ source 'https://rubygems.org'
2
2
 
3
- # Declare your gem's dependencies in photo_cook.gemspec.
4
- # Bundler will treat runtime dependencies like base dependencies, and
5
- # development dependencies will be added by default to the :development group.
6
3
  gemspec
7
4
 
8
- # Declare any dependencies that are still in development here instead of in
9
- # your gemspec. These might include edge Rails or gems from your path or
10
- # Git. Remember to move these dependencies to your gemspec before releasing
11
- # your gem to rubygems.org.
12
-
13
- # To use debugger
14
- # gem 'debugger'
15
-
16
- gem 'mini_magick'
5
+ gem 'test-unit', '~> 3.1'
6
+ gem 'shoulda-context', '~> 1.2'
data/README.md CHANGED
@@ -10,6 +10,6 @@
10
10
  # uploads
11
11
  # photos
12
12
  # 1
13
- # width=auto&height=640&pixel_ratio=1&crop=yes
13
+ # fit-640x640
14
14
  # car.png
15
- ```
15
+ ```
data/Rakefile CHANGED
@@ -4,19 +4,6 @@ rescue LoadError
4
4
  puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
5
  end
6
6
 
7
- require 'rdoc/task'
8
-
9
- RDoc::Task.new(:rdoc) do |rdoc|
10
- rdoc.rdoc_dir = 'rdoc'
11
- rdoc.title = 'PhotoCook'
12
- rdoc.options << '--line-numbers'
13
- rdoc.rdoc_files.include('README.rdoc')
14
- rdoc.rdoc_files.include('lib/**/*.rb')
15
- end
16
-
17
-
18
-
19
-
20
7
  Bundler::GemHelper.install_tasks
21
8
 
22
9
  require 'rake/testtask'
@@ -24,9 +11,9 @@ require 'rake/testtask'
24
11
  Rake::TestTask.new(:test) do |t|
25
12
  t.libs << 'lib'
26
13
  t.libs << 'test'
27
- t.pattern = 'test/**/*_test.rb'
14
+ t.pattern = 'test/**/test*.rb'
28
15
  t.verbose = false
29
16
  end
30
17
 
31
-
18
+ desc 'Run test suite'
32
19
  task default: :test
@@ -0,0 +1,90 @@
1
+ @PhotoCook =
2
+
3
+ initialize: -> try PhotoCook.persistDevicePixelRatio()
4
+
5
+ resizeCacheDir: do ->
6
+ if document? and (head = document.getElementsByTagName('head')[0])?
7
+ for el in head.getElementsByTagName('meta')
8
+ if el.getAttribute('name') is 'photo_cook:resize:cache_dir'
9
+ break if value = el.getAttribute('content')
10
+ value or 'resize-cache'
11
+
12
+ resizeCommandRegex: /^fit|fill\-\d+x\d+$/
13
+
14
+ # Returns device pixel ratio (float)
15
+ # If no ratio could be determined will return normal ratio (1.0)
16
+ devicePixelRatio: do ->
17
+ # https://gist.github.com/marcedwards/3446599
18
+ mediaQuery = [
19
+ '(-webkit-min-device-pixel-ratio: 1.3)'
20
+ '(-o-min-device-pixel-ratio: 13/10)'
21
+ 'min-resolution: 120dpi'
22
+ ].join(', ')
23
+
24
+ ratio = window?.devicePixelRatio
25
+
26
+ # If no ratio found check if screen is retina
27
+ # and if so return 2x ratio
28
+ if not ratio and window?.matchMedia?(mediaQuery).matches
29
+ ratio = 2.0
30
+
31
+ parseFloat(ratio) or 1.0
32
+
33
+ resizeMultiplier: @devicePixelRatio
34
+
35
+ persistDevicePixelRatio: ->
36
+ date = new Date()
37
+
38
+ # Expires in 1 year
39
+ date.setTime(date.getTime() + 365 * 24 * 60 * 60 * 1000)
40
+ expires = 'expires=' + date.toUTCString()
41
+ document.cookie = 'DevicePixelRatio=' + PhotoCook.devicePixelRatio + '; ' + expires
42
+ return
43
+
44
+ resize: (path, width, height, mode, options) ->
45
+ multiplier = options?.multiplier or PhotoCook.resizeMultiplier
46
+ command = "#{mode or 'fit'}-#{Math.floor(width * multiplier)}x#{Math.floor(height * multiplier)}"
47
+ pathTokens = path.split('/');
48
+ pathTokens.splice(-1, 0, PhotoCook.resizeCacheDir, command)
49
+ pathTokens.join('/')
50
+
51
+ strip: (uri) ->
52
+ sections = uri.split('/')
53
+ length = sections.length
54
+ return uri if length < 3
55
+
56
+ cacheDir = sections[length - 3];
57
+ command = sections[length - 2];
58
+
59
+ return uri unless cacheDir is PhotoCook.resizeCacheDir
60
+ return uri unless PhotoCook.resizeCommandRegex.test(command)
61
+
62
+ sections.splice(length - 3, 2)
63
+ sections.join('/')
64
+
65
+ uriRegex: /^[-a-z]+:\/\/|^(?:cid|data):|^\/\//i
66
+
67
+ # Returns true if given URL can produce request to PhotoCook middleware on server
68
+ # This is important thing you probably should override when using CDN or different assets delivery method
69
+ isServableURL: (url) ->
70
+ # By default check that URL is relative
71
+ !PhotoCook.uriRegex.test(url)
72
+
73
+ sizeToFit: (maxw, maxh, reqw, reqh, round) ->
74
+ outw = maxw
75
+ outh = maxh
76
+
77
+ scale = if outw > reqw then reqw / outw else 1.0
78
+ outw *= scale
79
+ outh *= scale
80
+
81
+ scale = if outh > reqh then reqh / outh else 1.0
82
+ outw *= scale
83
+ outh *= scale
84
+
85
+ if !round? or round # (null or undefined) OR true-value
86
+ [Math.floor(outw), Math.floor(outh)]
87
+ else
88
+ [outw, outh]
89
+
90
+ PhotoCook.initialize()
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCookHelper
3
+ def device_pixel_ratio
4
+ PhotoCook.device_pixel_ratio
5
+ end
6
+
7
+ def resize_multiplier
8
+ PhotoCook::Resize.multiplier
9
+ end
10
+
11
+ def photo_cook_cache_key
12
+ "resize_multiplier:#{resize_multiplier}"
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCook
3
+ module DevicePixelRatioSpy
4
+ extend ActiveSupport::Concern
5
+
6
+ included { before_action :pass_device_pixel_ratio }
7
+
8
+ def pass_device_pixel_ratio
9
+ ratio = PhotoCook::DevicePixelRatio.parse(cookies[:DevicePixelRatio])
10
+ PhotoCook.device_pixel_ratio = if PhotoCook::DevicePixelRatio.valid?(ratio)
11
+ ratio
12
+ else
13
+ PhotoCook::DevicePixelRatio::DEFAULT
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCook
3
+ module DevicePixelRatio
4
+ DEFAULT = 1.0.freeze
5
+ MAX = 4.0.freeze
6
+
7
+ class << self
8
+ def parse(x)
9
+ typecast(x)
10
+ end
11
+
12
+ def parse!(x)
13
+ check!(x = parse(x))
14
+ x
15
+ end
16
+
17
+ def check!(x)
18
+ x = typecast(x)
19
+ raise Invalid, x if !x.kind_of?(Integer) && (x.nan? || x.infinite?)
20
+ raise OutOfBounds, x if x < DEFAULT || x > MAX
21
+ true
22
+ end
23
+
24
+ # Do not produce various number of pixel ratios:
25
+ # 1.0 => 1
26
+ # 2.5 => 3
27
+ # 2.1 => 3
28
+ # 3.1 => 4
29
+ def unify(x)
30
+ typecast(x).ceil
31
+ end
32
+
33
+ def valid?(x)
34
+ check!(x)
35
+ rescue Invalid, OutOfBounds
36
+ false
37
+ end
38
+
39
+ def typecast(x)
40
+ x.kind_of?(Numeric) ? x : x.to_f
41
+ end
42
+ end
43
+
44
+ class Invalid < ArgumentError
45
+ def initialize(*)
46
+ super 'Device pixel ratio is invalid (NaN) or infinite number'
47
+ end
48
+ end
49
+
50
+ class OutOfBounds < ArgumentError
51
+ def initialize(*)
52
+ super 'Device pixel ratio must be positive number (integer or float) ' +
53
+ "which satisfies #{DEFAULT} <= x <= #{MAX}"
54
+ end
55
+ end
56
+ end
57
+
58
+ class << self
59
+ def device_pixel_ratio
60
+ @device_pixel_ratio || DevicePixelRatio::DEFAULT
61
+ end
62
+
63
+ def device_pixel_ratio=(x)
64
+ @device_pixel_ratio = DevicePixelRatio.parse!(x)
65
+ end
66
+ end
67
+ end
@@ -1,7 +1,20 @@
1
+ # frozen_string_literal: true
1
2
  module PhotoCook
2
3
  class Engine < ::Rails::Engine
3
- initializer :photo_cook_javascripts do |app|
4
+ initializer :photo_cook_root do |app|
5
+ PhotoCook.root_path = Rails.root.to_s
6
+ end
7
+
8
+ initializer :photo_cook_logger do |app|
9
+ PhotoCook.logger = Rails.logger
10
+ end
11
+
12
+ initializer :photo_cook_assets do |app|
4
13
  app.config.assets.paths << File.join(PhotoCook::Engine.root, 'app/assets/javascripts')
5
14
  end
15
+
16
+ config.before_initialize do |app|
17
+ app.config.middleware.insert_before(Rack::Sendfile, PhotoCook::Resize::Middleware)
18
+ end
6
19
  end
7
- end
20
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCook
3
+ class << self
4
+ def notify(event, *params)
5
+ blocks = @events && @events[event.to_s]
6
+ blocks && blocks.each { |blk| Utils.call_block_with_floating_arguments(blk, params) }
7
+ nil
8
+ end
9
+
10
+ def subscribe(event, &block)
11
+ ((@events ||= {})[event.to_s] ||= []) << block
12
+ nil
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCook
3
+ class << self
4
+ attr_accessor :logger
5
+ attr_accessor :logger_evaluator
6
+
7
+ def log(&block)
8
+ logger_evaluator.instance_eval do
9
+ log "\n"
10
+ log '--- PhotoCook ---'
11
+ instance_eval(&block)
12
+ log '---'
13
+ end if @logging_enabled
14
+ nil
15
+ end
16
+
17
+ def enable_logging!
18
+ @logging_enabled = true
19
+ nil
20
+ end
21
+
22
+ def disable_logging!
23
+ @logging_enabled = false
24
+ nil
25
+ end
26
+ end
27
+
28
+ class LoggerEvaluator
29
+ def log(msg)
30
+ PhotoCook.logger.info(msg)
31
+ end
32
+ end
33
+
34
+ self.logger = Logger.new(STDOUT)
35
+ self.logger_evaluator = LoggerEvaluator.new
36
+ enable_logging!
37
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCook
3
+ module Optimization
4
+ class << self
5
+ # TODO PhotoCook::Optimization.optimizer = :image_optim
6
+ attr_accessor :optimizer
7
+
8
+ def perform(path)
9
+ if File.readable?(path) && (optimizer = self.optimizer)
10
+ result, msec = PhotoCook::Utils.measure { optimizer.optimize(path) }
11
+ params = [path]
12
+ params.push(result[:before], result[:after], msec) if result
13
+ PhotoCook.notify("optimization:#{result ? 'success' : 'failure'}", *params)
14
+ !!result
15
+ else
16
+ false
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCook
3
+ module Optimization
4
+ module CarrierWave
5
+ def optimize
6
+ PhotoCook::Optimization.perform(current_path)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCook
3
+ module Optimization
4
+ class ImageOptim
5
+ include Singleton
6
+
7
+ def optimize(path)
8
+ result = image_optim.optimize_image!(path)
9
+ if result.kind_of?(::ImageOptim::ImagePath::Optimized)
10
+ { before: result.original_size, after: result.size }
11
+ else
12
+ false
13
+ end
14
+ end
15
+
16
+ protected
17
+ # https://github.com/toy/image_optim
18
+ def image_optim
19
+ @image_optim ||= ::ImageOptim.new(
20
+
21
+ # Nice level (defaults to 10)
22
+ nice: 10,
23
+
24
+ # Number of threads or disable (defaults to number of processors)
25
+ threads: begin
26
+ match = if OS.osx?
27
+ `sysctl -a | grep machdep.cpu | grep thread_count`
28
+ else
29
+ `cat /proc/cpuinfo | grep "cpu cores"`
30
+ end
31
+ $~.to_s.to_i if match.to_s =~ /\d{1,2}/
32
+ end,
33
+
34
+ # Verbose output (defaults to false)
35
+ verbose: false,
36
+
37
+ # Require image_optim_pack or disable it, by default image_optim_pack will be used if available, will turn on :skip-missing-workers unless explicitly disabled (defaults to nil)
38
+ pack: nil,
39
+
40
+ # Skip workers with missing or problematic binaries (defaults to false)
41
+ skip_missing_workers: nil,
42
+
43
+ # Allow lossy workers and optimizations (defaults to false)
44
+ allow_lossy: false,
45
+
46
+ advpng: {
47
+ # Compression level: 0 - don't compress, 1 - fast, 2 - normal, 3 - extra, 4 - extreme (defaults to 4)
48
+ level: 1
49
+ },
50
+ gifsicle: {
51
+ # Interlace: true - interlace on, false - interlace off, nil - as is in original image (defaults to running two instances, one with interlace off and one with on)
52
+ interlace: nil,
53
+
54
+ # Compression level: 1 - light and fast, 2 - normal, 3 - heavy (slower) (defaults to 3)
55
+ level: 1,
56
+
57
+ # Avoid bugs with some software (defaults to false)
58
+ careful: false
59
+ },
60
+ jhead: {},
61
+ jpegoptim: {
62
+ # List of extra markers to strip: :comments, :exif, :iptc, :icc or :all (defaults to :all)
63
+ strip: :all,
64
+
65
+ # Maximum image quality factor 0..100, ignored in default/lossless mode (defaults to 100)
66
+ max_quality: 100
67
+ },
68
+ jpegrecompress: {
69
+ # JPEG quality preset: 0 - low, 1 - medium, 2 - high, 3 - veryhigh (defaults to 3)
70
+ quality: 3
71
+ },
72
+ jpegtran: {
73
+
74
+ # Copy all chunks
75
+ copy_chunks: false,
76
+
77
+ # Create progressive JPEG file
78
+ progressive: true,
79
+
80
+ # Use jpegtran through jpegrescan, ignore progressive option
81
+ jpegrescan: false
82
+ },
83
+ optipng: {
84
+ # Optimization level preset: 0 is least, 7 is best
85
+ level: 3,
86
+
87
+ # Interlace: true - interlace on, false - interlace off, nil - as is in original image
88
+ interlace: false,
89
+
90
+ # Remove all auxiliary chunks (defaults to true)
91
+ strip: true
92
+ },
93
+ pngcrush: {
94
+ # List of chunks to remove or :alla - all except tRNS/transparency or :allb - all except tRNS and gAMA/gamma (defaults to :alla)
95
+ chunks: :alla,
96
+
97
+ # Fix otherwise fatal conditions such as bad CRCs (defaults to false)
98
+ fix: false,
99
+
100
+ # Brute force try all methods, very time-consuming and generally not worthwhile (defaults to false)
101
+ brute: false,
102
+
103
+ # Blacken fully transparent pixels (defaults to true)
104
+ blacken: true
105
+ },
106
+ pngout: {
107
+ # Copy optional chunks (defaults to false)
108
+ copy_chunks: false,
109
+
110
+ # Strategy: 0 - xtreme, 1 - intense, 2 - longest Match, 3 - huffman Only, 4 - uncompressed (defaults to 0)
111
+ strategy: 2
112
+ },
113
+ pngquant: {
114
+ # min..max - don't save below min, use less colors below max (both in range 0..100; in yaml - !ruby/range 0..100), ignored in default/lossless mode (defaults to 100..100, 0..100 in lossy mode)
115
+ quality: 100..100,
116
+
117
+ # speed/quality trade-off: 1 - slow, 3 - default, 11 - fast & rough (defaults to 3)
118
+ speed: 11
119
+ },
120
+ svgo: {
121
+ # List of plugins to disable (defaults to [])
122
+ disable_plugins: [],
123
+
124
+ # List of plugins to enable (defaults to [])
125
+ enable_plugins: []
126
+ }
127
+ )
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCook
3
+ module Optimization
4
+ class Job < ActiveJob::Base
5
+ queue_as :photo_cook
6
+
7
+ def perform(path)
8
+ PhotoCook::Optimization.perform(path)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ PhotoCook.subscribe 'optimization:success' do |path, original_size, new_size, msec|
3
+ PhotoCook.log do
4
+ diff = original_size - new_size
5
+ log "Optimization successfully performed"
6
+ log "File path: #{path}"
7
+ log "Original size: #{PhotoCook::Utils.format_size(original_size)}"
8
+ log "New size: #{PhotoCook::Utils.format_size(new_size)}"
9
+ log "Saved: #{PhotoCook::Utils.format_size(diff)} / #{diff} bytes / #{(diff / original_size.to_f * 100.0).round(2)}%"
10
+ log "Completed in: #{msec.round(1)}ms"
11
+ end
12
+ end
13
+
14
+ PhotoCook.subscribe 'optimization:failure' do |path|
15
+ PhotoCook.log do
16
+ log "Optimization failed because one of the following:"
17
+ log "1) photo is already optimized;"
18
+ log "2) some problem occurred with optimization utility."
19
+ log "Related to: #{path}"
20
+ end
21
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+ module PhotoCook
3
+ module Pixels
4
+ MAX = 4256.freeze
5
+
6
+ class << self
7
+ def parse(x)
8
+ round(typecast(x))
9
+ end
10
+
11
+ def parse!(x)
12
+ check!(x = parse(x))
13
+ x
14
+ end
15
+
16
+ def check!(x)
17
+ x = typecast(x)
18
+ raise Invalid, x if !x.kind_of?(Integer) && (x.nan? || x.infinite?)
19
+ raise OutOfBounds, x unless in_bounds?(x)
20
+ true
21
+ end
22
+
23
+ def in_bounds?(x)
24
+ x = typecast(x)
25
+ 0 < x && x <= MAX
26
+ end
27
+
28
+ # Standardize how dimensions are rounded in PhotoCook
29
+ def round(x)
30
+ x.floor
31
+ end
32
+
33
+ # Returns Imagemagick dimension-string
34
+ def to_magick_dimensions(width, height)
35
+ "#{width}x#{height}"
36
+ end
37
+
38
+ def typecast(x)
39
+ x.kind_of?(Numeric) ? x : x.to_f
40
+ end
41
+ end
42
+
43
+ class OutOfBounds < ArgumentError
44
+ def initialize(*)
45
+ super 'Size must be positive number (integer or float) ' +
46
+ "which satisfies 0 < x <= #{MAX}"
47
+ end
48
+ end
49
+
50
+ class Invalid < ArgumentError
51
+ def initialize(*)
52
+ super 'Size is invalid (NaN) or infinite number'
53
+ end
54
+ end
55
+ end
56
+ end