mega-quick-box 0.0.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.
@@ -0,0 +1,609 @@
1
+ require 'tempfile'
2
+ require 'stringio'
3
+ require 'pathname'
4
+ require 'uri'
5
+ require 'open-uri'
6
+
7
+ require 'mini_magick/image/info'
8
+ require 'mini_magick/utilities'
9
+
10
+ module MiniMagick
11
+ class Image
12
+
13
+ ##
14
+ # This is the primary loading method used by all of the other class
15
+ # methods.
16
+ #
17
+ # Use this to pass in a stream object. Must respond to #read(size) or be a
18
+ # binary string object (BLOB)
19
+ #
20
+ # Probably easier to use the {.open} method if you want to open a file or a
21
+ # URL.
22
+ #
23
+ # @param stream [#read, String] Some kind of stream object that needs
24
+ # to be read or is a binary String blob
25
+ # @param ext [String] A manual extension to use for reading the file. Not
26
+ # required, but if you are having issues, give this a try.
27
+ # @return [MiniMagick::Image]
28
+ #
29
+ def self.read(stream, ext = nil)
30
+ if stream.is_a?(String)
31
+ stream = StringIO.new(stream)
32
+ end
33
+
34
+ create(ext) { |file| IO.copy_stream(stream, file) }
35
+ end
36
+
37
+ ##
38
+ # Creates an image object from a binary string blob which contains raw
39
+ # pixel data (i.e. no header data).
40
+ #
41
+ # @param blob [String] Binary string blob containing raw pixel data.
42
+ # @param columns [Integer] Number of columns.
43
+ # @param rows [Integer] Number of rows.
44
+ # @param depth [Integer] Bit depth of the encoded pixel data.
45
+ # @param map [String] A code for the mapping of the pixel data. Example:
46
+ # 'gray' or 'rgb'.
47
+ # @param format [String] The file extension of the image format to be
48
+ # used when creating the image object.
49
+ # Defaults to 'png'.
50
+ # @return [MiniMagick::Image] The loaded image.
51
+ #
52
+ def self.import_pixels(blob, columns, rows, depth, map, format = 'png')
53
+ # Create an image object with the raw pixel data string:
54
+ read(blob, ".dat").tap do |image|
55
+ output_path = image.path.sub(/\.\w+$/, ".#{format}")
56
+ # Use ImageMagick to convert the raw data file to an image file of the
57
+ # desired format:
58
+ MiniMagick.convert do |convert|
59
+ convert.size "#{columns}x#{rows}"
60
+ convert.depth depth
61
+ convert << "#{map}:#{image.path}"
62
+ convert << output_path
63
+ end
64
+
65
+ image.path.replace output_path
66
+ end
67
+ end
68
+
69
+ ##
70
+ # Opens a specific image file either on the local file system or at a URI.
71
+ # Use this if you don't want to overwrite the image file.
72
+ #
73
+ # Extension is either guessed from the path or you can specify it as a
74
+ # second parameter.
75
+ #
76
+ # @param path_or_url [String] Either a local file path or a URL that
77
+ # open-uri can read
78
+ # @param ext [String] Specify the extension you want to read it as
79
+ # @param options [Hash] Specify options for the open method
80
+ # @return [MiniMagick::Image] The loaded image
81
+ #
82
+ def self.open(path_or_url, ext = nil, **options)
83
+ if path_or_url.to_s =~ %r{\A(https?|ftp)://}
84
+ uri = URI(path_or_url)
85
+ ext ||= File.extname(uri.path).sub(/:.*/, '') # handle URL including a colon
86
+ uri.open(options) { |file| read(file, ext) }
87
+ else
88
+ pathname = Pathname(path_or_url)
89
+ ext ||= File.extname(pathname.to_s)
90
+ pathname.open(binmode: true, **options) { |file| read(file, ext) }
91
+ end
92
+ end
93
+
94
+ ##
95
+ # Used to create a new Image object data-copy. Not used to "paint" or
96
+ # that kind of thing.
97
+ #
98
+ # Takes an extension in a block and can be used to build a new Image
99
+ # object. Used by both {.open} and {.read} to create a new object. Ensures
100
+ # we have a good tempfile.
101
+ #
102
+ # @param ext [String] Specify the extension you want to read it as
103
+ # @yield [Tempfile] You can #write bits to this object to create the new
104
+ # Image
105
+ # @return [MiniMagick::Image] The created image
106
+ #
107
+ def self.create(ext = nil, &block)
108
+ tempfile = MiniMagick::Utilities.tempfile(ext.to_s.downcase, &block)
109
+
110
+ new(tempfile.path, tempfile)
111
+ end
112
+
113
+ ##
114
+ # @private
115
+ # @!macro [attach] attribute
116
+ # @!attribute [r] $1
117
+ #
118
+ def self.attribute(name, key = name.to_s)
119
+ define_method(name) do |*args|
120
+ if args.any? && name != :resolution
121
+ mogrify { |b| b.send(name, *args) }
122
+ else
123
+ @info[key, *args]
124
+ end
125
+ end
126
+ end
127
+
128
+ ##
129
+ # @return [String] The location of the current working file
130
+ #
131
+ attr_reader :path
132
+ ##
133
+ # @return [Tempfile] The underlying temporary file
134
+ #
135
+ attr_reader :tempfile
136
+
137
+ ##
138
+ # Create a new {MiniMagick::Image} object.
139
+ #
140
+ # _DANGER_: The file location passed in here is the *working copy*. That
141
+ # is, it gets *modified*. You can either copy it yourself or use {.open}
142
+ # which creates a temporary file for you and protects your original.
143
+ #
144
+ # @param input_path [String, Pathname] The location of an image file
145
+ # @yield [MiniMagick::Tool] If block is given, {#combine_options}
146
+ # is called.
147
+ #
148
+ def initialize(input_path, tempfile = nil, &block)
149
+ @path = input_path.to_s
150
+ @tempfile = tempfile
151
+ @info = MiniMagick::Image::Info.new(@path)
152
+
153
+ combine_options(&block) if block
154
+ end
155
+
156
+ def ==(other)
157
+ self.class == other.class && signature == other.signature
158
+ end
159
+ alias eql? ==
160
+
161
+ def hash
162
+ signature.hash
163
+ end
164
+
165
+ ##
166
+ # Returns raw image data.
167
+ #
168
+ # @return [String] Binary string
169
+ #
170
+ def to_blob
171
+ File.binread(path)
172
+ end
173
+
174
+ ##
175
+ # Checks to make sure that MiniMagick can read the file and understand it.
176
+ #
177
+ # This uses the 'identify' command line utility to check the file. If you
178
+ # are having issues with this, then please work directly with the
179
+ # 'identify' command and see if you can figure out what the issue is.
180
+ #
181
+ # @return [Boolean]
182
+ #
183
+ def valid?
184
+ validate!
185
+ true
186
+ rescue MiniMagick::Invalid
187
+ false
188
+ end
189
+
190
+ ##
191
+ # Runs `identify` on the current image, and raises an error if it doesn't
192
+ # pass.
193
+ #
194
+ # @raise [MiniMagick::Invalid]
195
+ #
196
+ def validate!
197
+ identify
198
+ rescue MiniMagick::Error => error
199
+ raise MiniMagick::Invalid, error.message
200
+ end
201
+
202
+ ##
203
+ # Returns the image format (e.g. "JPEG", "GIF").
204
+ #
205
+ # @return [String]
206
+ #
207
+ attribute :type, "format"
208
+ ##
209
+ # @return [Integer]
210
+ #
211
+ attribute :width
212
+ ##
213
+ # @return [Integer]
214
+ #
215
+ attribute :height
216
+ ##
217
+ # @return [Array<Integer>]
218
+ #
219
+ attribute :dimensions
220
+ ##
221
+ # Returns the file size of the image (in bytes).
222
+ #
223
+ # @return [Integer]
224
+ #
225
+ attribute :size
226
+ ##
227
+ # Returns the file size in a human readable format.
228
+ #
229
+ # @return [String]
230
+ #
231
+ attribute :human_size
232
+ ##
233
+ # @return [String]
234
+ #
235
+ attribute :colorspace
236
+ ##
237
+ # @return [Hash]
238
+ #
239
+ attribute :exif
240
+ ##
241
+ # Returns the resolution of the photo. You can optionally specify the
242
+ # units measurement.
243
+ #
244
+ # @example
245
+ # image.resolution("PixelsPerInch") #=> [250, 250]
246
+ # @see http://www.imagemagick.org/script/command-line-options.php#units
247
+ # @return [Array<Integer>]
248
+ #
249
+ attribute :resolution
250
+ ##
251
+ # Returns the message digest of this image as a SHA-256, hexadecimal
252
+ # encoded string. This signature uniquely identifies the image and is
253
+ # convenient for determining if an image has been modified or whether two
254
+ # images are identical.
255
+ #
256
+ # @example
257
+ # image.signature #=> "60a7848c4ca6e36b8e2c5dea632ecdc29e9637791d2c59ebf7a54c0c6a74ef7e"
258
+ # @see http://www.imagemagick.org/api/signature.php
259
+ # @return [String]
260
+ #
261
+ attribute :signature
262
+ ##
263
+ # Returns the result of converting the image to JSON format.
264
+ #
265
+ # @return [Hash]
266
+ attribute :data
267
+
268
+ ##
269
+ # Use this method if you want to access raw Identify's format API.
270
+ #
271
+ # @example
272
+ # image["%w %h"] #=> "250 450"
273
+ # image["%r"] #=> "DirectClass sRGB"
274
+ #
275
+ # @param value [String]
276
+ # @see http://www.imagemagick.org/script/escape.php
277
+ # @return [String]
278
+ #
279
+ def [](value)
280
+ @info[value.to_s]
281
+ end
282
+ alias info []
283
+
284
+ ##
285
+ # Returns layers of the image. For example, JPEGs are 1-layered, but
286
+ # formats like PSDs, GIFs and PDFs can have multiple layers/frames/pages.
287
+ #
288
+ # @example
289
+ # image = MiniMagick::Image.new("document.pdf")
290
+ # image.pages.each_with_index do |page, idx|
291
+ # page.write("page#{idx}.pdf")
292
+ # end
293
+ # @return [Array<MiniMagick::Image>]
294
+ #
295
+ def layers
296
+ layers_count = identify.lines.count
297
+ layers_count.times.map do |idx|
298
+ MiniMagick::Image.new("#{path}[#{idx}]")
299
+ end
300
+ end
301
+ alias pages layers
302
+ alias frames layers
303
+
304
+ ##
305
+ # Returns a matrix of pixels from the image. The matrix is constructed as
306
+ # an array (1) of arrays (2) of arrays (3) of unsigned integers:
307
+ #
308
+ # 1) one for each row of pixels
309
+ # 2) one for each column of pixels
310
+ # 3) three or four elements in the range 0-255, one for each of the RGB(A) color channels
311
+ #
312
+ # @example
313
+ # img = MiniMagick::Image.open 'image.jpg'
314
+ # pixels = img.get_pixels
315
+ # pixels[3][2][1] # the green channel value from the 4th-row, 3rd-column pixel
316
+ #
317
+ # @example
318
+ # img = MiniMagick::Image.open 'image.jpg'
319
+ # pixels = img.get_pixels("RGBA")
320
+ # pixels[3][2][3] # the alpha channel value from the 4th-row, 3rd-column pixel
321
+ #
322
+ # It can also be called after applying transformations:
323
+ #
324
+ # @example
325
+ # img = MiniMagick::Image.open 'image.jpg'
326
+ # img.crop '20x30+10+5'
327
+ # img.colorspace 'Gray'
328
+ # pixels = img.get_pixels
329
+ #
330
+ # In this example, all pixels in pix should now have equal R, G, and B values.
331
+ #
332
+ # @param map [String] A code for the mapping of the pixel data. Must be either
333
+ # 'RGB' or 'RGBA'. Default to 'RGB'
334
+ # @return [Array] Matrix of each color of each pixel
335
+ def get_pixels(map="RGB")
336
+ raise ArgumentError, "Invalid map value" unless ["RGB", "RGBA"].include?(map)
337
+ convert = MiniMagick.convert
338
+ convert << path
339
+ convert.depth(8)
340
+ convert << "#{map}:-"
341
+
342
+ # Do not use `convert.call` here. We need the whole binary (unstripped) output here.
343
+ shell = MiniMagick::Shell.new
344
+ output, * = shell.run(convert.command)
345
+
346
+ pixels_array = output.unpack("C*")
347
+ pixels = pixels_array.each_slice(map.length).each_slice(width).to_a
348
+
349
+ # deallocate large intermediary objects
350
+ output.clear
351
+ pixels_array.clear
352
+
353
+ pixels
354
+ end
355
+
356
+ ##
357
+ # This is used to create image from pixels. This might be required if you
358
+ # create pixels for some image processing reasons and you want to form
359
+ # image from those pixels.
360
+ #
361
+ # *DANGER*: This operation can be very expensive. Please try to use with
362
+ # caution.
363
+ #
364
+ # @example
365
+ # # It is given in readme.md file
366
+ ##
367
+ def self.get_image_from_pixels(pixels, dimension, map, depth, format)
368
+ pixels = pixels.flatten
369
+ blob = pixels.pack('C*')
370
+ import_pixels(blob, *dimension, depth, map, format)
371
+ end
372
+
373
+ ##
374
+ # This is used to change the format of the image. That is, from "tiff to
375
+ # jpg" or something like that. Once you run it, the instance is pointing to
376
+ # a new file with a new extension!
377
+ #
378
+ # *DANGER*: This renames the file that the instance is pointing to. So, if
379
+ # you manually opened the file with Image.new(file_path)... Then that file
380
+ # is DELETED! If you used Image.open(file) then you are OK. The original
381
+ # file will still be there. But, any changes to it might not be...
382
+ #
383
+ # Formatting an animation into a non-animated type will result in
384
+ # ImageMagick creating multiple pages (starting with 0). You can choose
385
+ # which page you want to manipulate. We default to the first page.
386
+ #
387
+ # If you would like to convert between animated formats, pass nil as your
388
+ # page and ImageMagick will copy all of the pages.
389
+ #
390
+ # @param format [String] The target format... Like 'jpg', 'gif', 'tiff' etc.
391
+ # @param page [Integer] If this is an animated gif, say which 'page' you
392
+ # want with an integer. Default 0 will convert only the first page; 'nil'
393
+ # will convert all pages.
394
+ # @param read_opts [Hash] Any read options to be passed to ImageMagick
395
+ # for example: image.format('jpg', page, {density: '300'})
396
+ # @yield [MiniMagick::Tool] It optionally yields the command,
397
+ # if you want to add something.
398
+ # @return [self]
399
+ #
400
+ def format(format, page = 0, read_opts={})
401
+ if @tempfile
402
+ new_tempfile = MiniMagick::Utilities.tempfile(".#{format}")
403
+ new_path = new_tempfile.path
404
+ else
405
+ new_path = Pathname(path).sub_ext(".#{format}").to_s
406
+ end
407
+
408
+ input_path = path.dup
409
+ input_path << "[#{page}]" if page && !layer?
410
+
411
+ MiniMagick.convert do |convert|
412
+ read_opts.each do |opt, val|
413
+ convert.send(opt.to_s, val)
414
+ end
415
+ convert << input_path
416
+ yield convert if block_given?
417
+ convert << new_path
418
+ end
419
+
420
+ if @tempfile
421
+ destroy!
422
+ @tempfile = new_tempfile
423
+ else
424
+ File.delete(path) unless path == new_path || layer?
425
+ end
426
+
427
+ path.replace new_path
428
+ @info.clear
429
+
430
+ self
431
+ rescue MiniMagick::Invalid, MiniMagick::Error => e
432
+ new_tempfile.unlink if new_tempfile && @tempfile != new_tempfile
433
+ raise e
434
+ end
435
+
436
+ ##
437
+ # You can use multiple commands together using this method. Very easy to
438
+ # use!
439
+ #
440
+ # @example
441
+ # image.combine_options do |c|
442
+ # c.draw "image Over 0,0 10,10 '#{MINUS_IMAGE_PATH}'"
443
+ # c.thumbnail "300x500>"
444
+ # c.background "blue"
445
+ # end
446
+ #
447
+ # @yield [MiniMagick::Command]
448
+ # @see http://www.imagemagick.org/script/mogrify.php
449
+ # @return [self]
450
+ #
451
+ def combine_options(&block)
452
+ mogrify(&block)
453
+ end
454
+
455
+ ##
456
+ # If an unknown method is called then it is sent through the mogrify
457
+ # program.
458
+ #
459
+ # @see http://www.imagemagick.org/script/mogrify.php
460
+ # @return [self]
461
+ #
462
+ def method_missing(name, *args)
463
+ mogrify do |builder|
464
+ builder.send(name, *args)
465
+ end
466
+ end
467
+
468
+ ##
469
+ # Prevents ruby from calling `#to_ary` on the image when checking if it's a
470
+ # splattable data structure in certain cases.
471
+ def respond_to_missing?(name, include_all)
472
+ false
473
+ end
474
+
475
+ ##
476
+ # Writes the temporary file out to either a file location (by passing in a
477
+ # String) or by passing in a Stream that you can #write(chunk) to
478
+ # repeatedly
479
+ #
480
+ # @param output_to [String, Pathname, #read] Some kind of stream object
481
+ # that needs to be read or a file path as a String
482
+ #
483
+ def write(output_to)
484
+ case output_to
485
+ when String, Pathname
486
+ if layer?
487
+ MiniMagick.convert do |builder|
488
+ builder << path
489
+ builder << output_to
490
+ end
491
+ else
492
+ FileUtils.copy_file path, output_to unless path == output_to.to_s
493
+ end
494
+ else
495
+ IO.copy_stream File.open(path, "rb"), output_to
496
+ end
497
+ end
498
+
499
+ ##
500
+ # @example
501
+ # first_image = MiniMagick::Image.open "first.jpg"
502
+ # second_image = MiniMagick::Image.open "second.jpg"
503
+ # result = first_image.composite(second_image) do |c|
504
+ # c.compose "Over" # OverCompositeOp
505
+ # c.geometry "+20+20" # copy second_image onto first_image from (20, 20)
506
+ # end
507
+ # result.write "output.jpg"
508
+ #
509
+ # @see http://www.imagemagick.org/script/composite.php
510
+ #
511
+ def composite(other_image, output_extension = type.downcase, mask = nil)
512
+ output_tempfile = MiniMagick::Utilities.tempfile(".#{output_extension}")
513
+
514
+ MiniMagick.composite do |composite|
515
+ yield composite if block_given?
516
+ composite << other_image.path
517
+ composite << path
518
+ composite << mask.path if mask
519
+ composite << output_tempfile.path
520
+ end
521
+
522
+ Image.new(output_tempfile.path, output_tempfile)
523
+ end
524
+
525
+ ##
526
+ # Collapse images with sequences to the first frame (i.e. animated gifs) and
527
+ # preserve quality.
528
+ #
529
+ # @param frame [Integer] The frame to which to collapse to, defaults to `0`.
530
+ # @return [self]
531
+ #
532
+ def collapse!(frame = 0)
533
+ mogrify(frame) { |builder| builder.quality(100) }
534
+ end
535
+
536
+ ##
537
+ # Destroys the tempfile (created by {.open}) if it exists.
538
+ #
539
+ def destroy!
540
+ if @tempfile
541
+ FileUtils.rm_f @tempfile.path.sub(/mpc$/, "cache") if @tempfile.path.end_with?(".mpc")
542
+ @tempfile.unlink
543
+ end
544
+ end
545
+
546
+ ##
547
+ # Runs `identify` on itself. Accepts an optional block for adding more
548
+ # options to `identify`.
549
+ #
550
+ # @example
551
+ # image = MiniMagick::Image.open("image.jpg")
552
+ # image.identify do |b|
553
+ # b.verbose
554
+ # end # runs `identify -verbose image.jpg`
555
+ # @return [String] Output from `identify`
556
+ # @yield [MiniMagick::Tool]
557
+ #
558
+ def identify
559
+ MiniMagick.identify do |builder|
560
+ yield builder if block_given?
561
+ builder << path
562
+ end
563
+ end
564
+
565
+ def mogrify(page = nil)
566
+ MiniMagick.mogrify do |builder|
567
+ yield builder if block_given?
568
+ if builder.args.include?("-format")
569
+ fail MiniMagick::Error, "you must call #format on a MiniMagick::Image directly"
570
+ end
571
+ builder << (page ? "#{path}[#{page}]" : path)
572
+ end
573
+
574
+ @info.clear
575
+
576
+ self
577
+ end
578
+
579
+ def layer?
580
+ path =~ /\[\d+\]$/
581
+ end
582
+
583
+ ##
584
+ # Compares if image width
585
+ # is greater than height
586
+ # ============
587
+ # | |
588
+ # | |
589
+ # ============
590
+ # @return [Boolean]
591
+ def landscape?
592
+ width > height
593
+ end
594
+
595
+ ##
596
+ # Compares if image height
597
+ # is greater than width
598
+ # ======
599
+ # | |
600
+ # | |
601
+ # | |
602
+ # | |
603
+ # ======
604
+ # @return [Boolean]
605
+ def portrait?
606
+ height > width
607
+ end
608
+ end
609
+ end
@@ -0,0 +1,53 @@
1
+ require "open3"
2
+
3
+ module MiniMagick
4
+ ##
5
+ # Sends commands to the shell (more precisely, it sends commands directly to
6
+ # the operating system).
7
+ #
8
+ # @private
9
+ #
10
+ class Shell
11
+
12
+ def run(command, errors: MiniMagick.errors, warnings: MiniMagick.warnings, **options)
13
+ stdout, stderr, status = execute(command, **options)
14
+
15
+ if status != 0
16
+ if stderr.include?("time limit exceeded")
17
+ fail MiniMagick::TimeoutError, "`#{command.join(" ")}` has timed out"
18
+ elsif errors
19
+ fail MiniMagick::Error, "`#{command.join(" ")}` failed with status: #{status.inspect} and error:\n#{stderr}"
20
+ end
21
+ end
22
+
23
+ $stderr.print(stderr) if warnings
24
+
25
+ [stdout, stderr, status]
26
+ end
27
+
28
+ def execute(command, stdin: "", timeout: MiniMagick.timeout)
29
+ env = MiniMagick.restricted_env ? ENV.to_h.slice("HOME", "PATH", "LANG") : {} # Using #to_h for Ruby 2.5 compatibility.
30
+ env.merge!(MiniMagick.cli_env)
31
+ env["MAGICK_TIME_LIMIT"] = timeout.to_s if timeout
32
+
33
+ stdout, stderr, status = log(command.join(" ")) do
34
+ Open3.capture3(env, *command, stdin_data: stdin, unsetenv_others: MiniMagick.restricted_env)
35
+ end
36
+
37
+ [stdout, stderr, status&.exitstatus]
38
+ rescue Errno::ENOENT, IOError
39
+ ["", "executable not found: \"#{command.first}\"", 127]
40
+ end
41
+
42
+ private
43
+
44
+ def log(command, &block)
45
+ time_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
46
+ value = block.call
47
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - time_start
48
+ MiniMagick.logger.debug "[%.2fs] %s" % [duration, command]
49
+ value
50
+ end
51
+
52
+ end
53
+ end