photo-cook 1.1.2 → 1.2.1

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.
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