carrierwave-meta 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +2 -1
- data/.travis.yml +7 -0
- data/README.rdoc +113 -45
- data/carrierwave-meta.gemspec +5 -0
- data/lib/carrierwave-meta.rb +3 -1
- data/lib/carrierwave-meta/active_record.rb +22 -0
- data/lib/carrierwave-meta/meta.rb +55 -21
- data/lib/carrierwave-meta/model_delegate_attribute.rb +16 -6
- data/lib/carrierwave-meta/version.rb +1 -1
- data/spec/fixtures/corrupted.eps +0 -0
- data/spec/fixtures/corrupted.pdf +0 -0
- data/spec/fixtures/flash.swf +0 -0
- data/spec/fixtures/sample.eps +0 -0
- data/spec/fixtures/sample.pdf +0 -0
- data/spec/modules/active_record_spec.rb +31 -0
- data/spec/modules/meta_spec.rb +131 -99
- data/spec/modules/model_delegate_attribute_spec.rb +13 -13
- data/spec/spec_helper.rb +22 -11
- data/spec/support/current_processor.rb +18 -0
- data/spec/support/remote.rb +27 -0
- data/spec/support/schema.rb +7 -12
- data/spec/support/test_blank_uploader.rb +1 -6
- data/spec/support/test_composed_model.rb +7 -0
- data/spec/support/test_delegate_uploader.rb +7 -9
- data/spec/support/test_model.rb +6 -5
- data/spec/support/test_uploader.rb +3 -9
- metadata +126 -48
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5eb83f3482998f4a6d77e2f0eee81aac037cc2b4
|
4
|
+
data.tar.gz: c01a5c5d1c24f5ac61b7d7f02f082c37fa9ee22f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f3030e8b0b781f98cd64b6ab5bde51d70f80ad1551e7fadb6d8a508893df233c708ff72a03dcd64fe3d7ef506c6c7a83848872ccc0478857d76864a799715a3d
|
7
|
+
data.tar.gz: 80276a9f29cec556543907344b0b0d7f87ad8e5f53c651880a60f67da3545cad6b963af7323602a5d7443d38903301a7e9065980b1312bb122749ef1c9e3aac2
|
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/README.rdoc
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
= carrierwave-meta
|
2
2
|
|
3
|
+
{<img src="https://secure.travis-ci.org/gzigzigzeo/carrierwave-meta.png" alt="Build Status" />}[http://travis-ci.org/gzigzigzeo/carrierwave-meta]
|
4
|
+
{<img src="https://codeclimate.com/github/gzigzigzeo/carrierwave-meta.png" />}[https://codeclimate.com/github/gzigzigzeo/carrierwave-meta]
|
5
|
+
|
3
6
|
= Installation
|
4
7
|
|
5
8
|
Add the following line to your Gemfile:
|
@@ -10,56 +13,59 @@ Add the following line to your Gemfile:
|
|
10
13
|
class TestUploader < CarrierWave::Uploader::Base
|
11
14
|
include CarrierWave::RMagick
|
12
15
|
include CarrierWave::Meta
|
13
|
-
|
14
|
-
storage :file
|
15
16
|
|
16
|
-
process :store_meta
|
17
|
+
process :store_meta => [{md5sum: true}]
|
17
18
|
version :version do
|
18
19
|
process :resize_to_fill => [200, 200]
|
19
20
|
process :store_meta
|
20
21
|
end
|
21
22
|
end
|
22
|
-
|
23
|
+
|
23
24
|
file = File.open('test.jpg') # JPEG 500x300, 20000 bytes
|
24
25
|
uploader = TestUploader.new
|
25
26
|
uploader.store!(file)
|
26
|
-
|
27
|
+
|
27
28
|
uploader.width # 500
|
28
29
|
uploader.height # 300
|
29
30
|
uploader.image_size # [500, 300]
|
30
|
-
uploader.
|
31
|
+
uploader.file_size # 20000
|
31
32
|
uploader.content_type # "image/jpeg"
|
32
|
-
|
33
|
+
uploader.md5sum # "fuuaasdfasdf...."
|
34
|
+
|
33
35
|
uploader.version.width # 200
|
34
|
-
uploader.version.height # 200
|
35
|
-
uploader.version.image_size # [200, 200]
|
36
|
+
uploader.version.height # 200
|
37
|
+
uploader.version.image_size # [200, 200]
|
36
38
|
uploader.version.file_zie # less than 20000
|
37
39
|
uploader.version.content_type # "image/jpeg"
|
40
|
+
uploader.version.md5sum # nil
|
38
41
|
|
39
42
|
= Saving values to database
|
40
43
|
|
41
|
-
Simply create database columns to hold metadata in your model's table. Currently
|
42
|
-
image_size ([width, height]), content_type
|
44
|
+
Simply create database columns to hold metadata in your model's table. Currently
|
45
|
+
gem supports width, height, image_size ([width, height]), content_type,
|
46
|
+
file_size and MD5 fields. Versions are supported too.
|
43
47
|
|
44
48
|
class TestModel
|
45
|
-
attr_accessor :image_width
|
49
|
+
attr_accessor :image_width
|
46
50
|
attr_accessor :image_height
|
47
|
-
attr_accessor :image_image_size
|
51
|
+
attr_accessor :image_image_size
|
48
52
|
attr_accessor :image_content_type
|
49
53
|
attr_accessor :image_file_size
|
54
|
+
attr_accessor :image_md5sum
|
50
55
|
|
51
|
-
attr_accessor :image_version_width
|
56
|
+
attr_accessor :image_version_width
|
52
57
|
attr_accessor :image_version_height
|
53
58
|
attr_accessor :image_version_image_size
|
54
59
|
attr_accessor :image_version_content_type
|
55
|
-
attr_accessor :image_version_file_size
|
60
|
+
attr_accessor :image_version_file_size
|
61
|
+
attr_accessor :image_version_md5sum
|
56
62
|
end
|
57
63
|
|
58
64
|
file = File.open('test.jpg')
|
59
65
|
model = TestModel.new
|
60
66
|
uploader = TestUploader.new(model, :image)
|
61
67
|
uploader.store!(file)
|
62
|
-
|
68
|
+
|
63
69
|
uploader.width # 500
|
64
70
|
model.image_width # 500
|
65
71
|
model.image_height # 300
|
@@ -67,38 +73,66 @@ image_size ([width, height]), content_type and file_size fields. Versions are su
|
|
67
73
|
|
68
74
|
When columns are available in the model instance, metadata is stored in that columns.
|
69
75
|
|
76
|
+
= Saving values into single column
|
77
|
+
|
78
|
+
For now, works only with ActiveRecord.
|
79
|
+
|
80
|
+
class TestModel < ActiveRecord::Base
|
81
|
+
extend CarrierWave::Meta::ActiveRecord
|
82
|
+
|
83
|
+
mount_uploader :image, TestUploader
|
84
|
+
serialize :image_meta, OpenStruct
|
85
|
+
carrierwave_meta_composed :image_meta,
|
86
|
+
:image, image_version: [:width, :height, :md5sum]
|
87
|
+
end
|
88
|
+
|
89
|
+
model = TestModel.new
|
90
|
+
model.image.store!('test.jpg')
|
91
|
+
model.image_width # 200
|
92
|
+
model.image_version_width # 200
|
93
|
+
model.image_meta # {image_width: 200, image_height: 200, ...}
|
94
|
+
|
95
|
+
All you need is image_meta column, all other attributes are virtual. Note
|
96
|
+
that carrierwave_meta_composed should be called after mounting uploader.
|
97
|
+
|
70
98
|
= Behind the scenes
|
71
99
|
|
72
|
-
After the file is retrieved from store or cache metadata is recalculated
|
73
|
-
|
100
|
+
After the file is retrieved from store or cache metadata is recalculated
|
101
|
+
unless uploader has attached model instance. If uploader has attached
|
102
|
+
model instance values are read from that instance.
|
74
103
|
|
75
104
|
uploader = TestUploader.new
|
76
105
|
uploader.retrieve_from_store!('test.jpg')
|
77
|
-
|
78
106
|
uploader.version.width # 200
|
79
107
|
|
108
|
+
model = TestModel.new
|
109
|
+
model.image.store!('test.jpg')
|
110
|
+
model.image_width # 200
|
111
|
+
model.image.width # 200, actually read from image_width
|
112
|
+
|
80
113
|
= model_delegate_attribute
|
81
114
|
|
82
|
-
|
115
|
+
Is used to synchronize data between uploader and mounted model instance.
|
116
|
+
Model's instance is used like value cache.
|
83
117
|
|
84
118
|
class DelegateTestModel
|
85
119
|
attr_accessor :processed
|
86
120
|
attr_accessor :a_processed
|
87
121
|
attr_accessor :a_b_processed
|
88
122
|
end
|
89
|
-
|
123
|
+
|
90
124
|
class DelegateTestUploader < CarrierWave::Uploader::Base
|
91
125
|
model_delegate_attribute :uploaded
|
92
|
-
|
126
|
+
|
93
127
|
set_processed
|
94
|
-
|
128
|
+
|
95
129
|
version :a do
|
96
130
|
set_processed
|
97
131
|
version :b do
|
98
132
|
set_processed
|
99
133
|
end
|
100
134
|
end
|
101
|
-
|
135
|
+
|
102
136
|
def set_processed
|
103
137
|
self.processed = true
|
104
138
|
end
|
@@ -107,23 +141,25 @@ Used to synchronize data between uploader and mounted model instance. Model's in
|
|
107
141
|
model = DelegateTestModel.new
|
108
142
|
uploader = DelegateTestUploader.new(model, :image)
|
109
143
|
file = File.open('test.jpg')
|
110
|
-
|
144
|
+
|
111
145
|
uploader.store!(file)
|
112
|
-
|
146
|
+
|
113
147
|
model.processed # true
|
114
|
-
model.a_processed # true
|
148
|
+
model.a_processed # true
|
115
149
|
model.a_b_processed # true
|
116
|
-
|
150
|
+
|
117
151
|
model.a_processed = false
|
118
|
-
|
152
|
+
|
119
153
|
uploader.processed # true
|
120
154
|
uploader.a_processed # false
|
121
155
|
uploader.a_b_processed # true
|
122
156
|
|
123
157
|
When model is mounted to uploader:
|
124
158
|
|
125
|
-
1. If attribute is assigned inside uploader then corresponding property
|
126
|
-
|
159
|
+
1. If attribute is assigned inside uploader then corresponding property
|
160
|
+
in model is also assigned.
|
161
|
+
2. If attribute is retrieved from uploader, uploader checks that value is
|
162
|
+
defined in model and returns it. Otherwise returns uploader's instance variable.
|
127
163
|
3. If file is deleted, value becomes nil.
|
128
164
|
|
129
165
|
Otherwise acts as regular uploader's instance variables.
|
@@ -143,7 +179,7 @@ The uploader:
|
|
143
179
|
version :crop_source do
|
144
180
|
process :resize_to_fit => [300, 300]
|
145
181
|
process :store_meta
|
146
|
-
|
182
|
+
|
147
183
|
# This is the cropped version of parent image. Let crop to 50x50 square.
|
148
184
|
version :crop do
|
149
185
|
process :crop_to => [50, 50]
|
@@ -151,7 +187,7 @@ The uploader:
|
|
151
187
|
end
|
152
188
|
|
153
189
|
# Defines crop area dimensions.
|
154
|
-
# This should be assigned before #store! and #cache! called and should be saved in the model's instance.
|
190
|
+
# This should be assigned before #store! and #cache! called and should be saved in the model's instance.
|
155
191
|
# Otherwise cropped image would be lost after #recreate_versions! is called.
|
156
192
|
# If crop area dimensions are'nt assigned, uploader calculates crop area dimensions inside the
|
157
193
|
# parent image and creates the default image.
|
@@ -177,13 +213,13 @@ The uploader:
|
|
177
213
|
cropped_img = img.crop(x, y, width, height)
|
178
214
|
new_img = cropped_img.resize_to_fill(new_width, new_height)
|
179
215
|
destroy_image(cropped_img)
|
180
|
-
destroy_image(img)
|
216
|
+
destroy_image(img)
|
181
217
|
new_img
|
182
218
|
end
|
183
219
|
end
|
184
220
|
|
185
221
|
# Creates the default crop image.
|
186
|
-
# Here the original crop area dimensions are restored and assigned to the model's instance.
|
222
|
+
# Here the original crop area dimensions are restored and assigned to the model's instance.
|
187
223
|
def resize_to_fill_and_save_dimensions(new_width, new_height)
|
188
224
|
manipulate! do |img|
|
189
225
|
width, height = img.columns, img.rows
|
@@ -194,20 +230,20 @@ The uploader:
|
|
194
230
|
h_ratio = height.to_f / new_height.to_f
|
195
231
|
|
196
232
|
ratio = [w_ratio, h_ratio].min
|
197
|
-
|
233
|
+
|
198
234
|
self.w = ratio * new_width
|
199
235
|
self.h = ratio * new_height
|
200
236
|
self.x = (width - self.w) / 2
|
201
237
|
self.y = (height - self.h) / 2
|
202
|
-
|
238
|
+
|
203
239
|
new_img
|
204
240
|
end
|
205
241
|
end
|
206
|
-
|
242
|
+
|
207
243
|
private
|
208
244
|
def crop_args
|
209
245
|
%w(x y w h).map { |accessor| send(accessor).to_i }
|
210
|
-
end
|
246
|
+
end
|
211
247
|
end
|
212
248
|
|
213
249
|
# Post should have :crop_source_version_x, :crop_source_version_y, :crop_source_version_h, :crop_source_version_w columns
|
@@ -221,14 +257,14 @@ The uploader:
|
|
221
257
|
post.save!
|
222
258
|
|
223
259
|
post.image.crop_source.width # 300
|
224
|
-
post.image.crop_source.height # 200
|
260
|
+
post.image.crop_source.height # 200
|
225
261
|
post.image.crop_source.crop.width # 50
|
226
262
|
post.image.crop_source.crop.height # 50
|
227
|
-
|
263
|
+
|
228
264
|
# Default crop area coordinates within the limits of big image dimensions: square at the center of an image
|
229
265
|
post.image.crop_source.crop.x # 50
|
230
266
|
post.image.crop_source.crop.y # 50
|
231
|
-
post.image.crop_source.crop.w # 200
|
267
|
+
post.image.crop_source.crop.w # 200
|
232
268
|
post.image.crop_source.crop.h # 200
|
233
269
|
|
234
270
|
# Let user change the crop area with JCrop script. Pass new crop area parameters to the model.
|
@@ -238,19 +274,51 @@ The uploader:
|
|
238
274
|
post.crop_source_crop_h = 100
|
239
275
|
|
240
276
|
post.save! # Crop image is reprocessed
|
241
|
-
|
277
|
+
|
242
278
|
post.image.crop_source.crop.width # 50
|
243
279
|
post.image.crop_source.crop.height # 50
|
244
280
|
|
281
|
+
= PDF/GhostScript support
|
282
|
+
|
283
|
+
If you want to use this plugin with PDF/PostScript files than you should install
|
284
|
+
GhostScript and rebuild ImageMagick with GhostScript support:
|
285
|
+
|
286
|
+
brew install ghostscript
|
287
|
+
brew install imagemagick --with-ghostscript
|
288
|
+
gem uninstall rmagick && gem install rmagick
|
289
|
+
|
290
|
+
To switch on PDF/EPS processing you should enable GhostScript somewhere in your
|
291
|
+
app's initializer:
|
292
|
+
|
293
|
+
CarrierWave::Meta.ghostscript_enabled = true
|
294
|
+
|
245
295
|
= A note about testing
|
246
296
|
|
297
|
+
@SergeyKishenin added specs for EPS/GhostScript files. They run for image_magick
|
298
|
+
or mini_magick processor by default. To make specs work please install
|
299
|
+
GhostScript as described above. To run specs WITHOUT PDF/EPS do:
|
300
|
+
|
301
|
+
PDF_EPS=false bundle exec rspec
|
302
|
+
|
247
303
|
@fschwahn added support for mini-magick. To run tests with mini-magick do:
|
248
304
|
|
249
|
-
bundle exec
|
305
|
+
PROCESSOR=mini_magick bundle exec rspec
|
306
|
+
|
307
|
+
@skord added support for ImageSorcery. To run specs do:
|
308
|
+
|
309
|
+
PROCESSOR=image_sorcery bundle exec rspec
|
310
|
+
|
311
|
+
To run specs against VIPS processor do:
|
312
|
+
|
313
|
+
PROCESSOR=vips bundle exec rspec
|
314
|
+
|
315
|
+
To run specs against with Fog (Amazon S3) simulation:
|
316
|
+
|
317
|
+
STORAGE=fog bundle exec rspec
|
250
318
|
|
251
319
|
= TODO
|
252
320
|
|
253
321
|
1. I do not know how it would work with S3 and other remote storages. Should be tested.
|
254
322
|
2. Write integration guide for JCrop.
|
255
323
|
3. A notice about content-type.
|
256
|
-
|
324
|
+
|
data/carrierwave-meta.gemspec
CHANGED
@@ -26,4 +26,9 @@ Gem::Specification.new do |s|
|
|
26
26
|
s.add_development_dependency(%q<rmagick>)
|
27
27
|
s.add_development_dependency(%q<mini_magick>)
|
28
28
|
s.add_development_dependency(%q<mime-types>)
|
29
|
+
s.add_development_dependency(%q<carrierwave-imagesorcery>)
|
30
|
+
s.add_development_dependency(%q<carrierwave-vips>)
|
31
|
+
s.add_development_dependency(%q<fog>, '~> 1.3.1')
|
32
|
+
s.add_development_dependency(%q<simplecov>)
|
33
|
+
s.add_development_dependency(%q<activerecord>, '>= 3.0')
|
29
34
|
end
|
data/lib/carrierwave-meta.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'active_support/concern'
|
2
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
2
3
|
require 'mime/types'
|
3
|
-
require
|
4
|
+
require 'carrierwave-meta/version'
|
4
5
|
require 'carrierwave-meta/model_delegate_attribute'
|
5
6
|
require 'carrierwave-meta/meta'
|
7
|
+
require 'carrierwave-meta/active_record'
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module CarrierWave
|
2
|
+
module Meta
|
3
|
+
module ActiveRecord
|
4
|
+
ALLOWED = %w(width height md5sum image_size file_size content_type)
|
5
|
+
|
6
|
+
def carrierwave_meta_composed(single_attribute, *args)
|
7
|
+
defined_attrs = args.map do |arg|
|
8
|
+
name, to_define = if arg.is_a?(Symbol)
|
9
|
+
[arg, ALLOWED]
|
10
|
+
elsif arg.is_a?(Hash)
|
11
|
+
[arg.keys.first, arg.values.first]
|
12
|
+
end
|
13
|
+
|
14
|
+
to_define.map do |attr|
|
15
|
+
delegate :"#{name}_#{attr}", to: single_attribute, allow_nil: true
|
16
|
+
delegate :"#{name}_#{attr}=", to: single_attribute, allow_nil: true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -2,6 +2,9 @@ module CarrierWave
|
|
2
2
|
module Meta
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
|
+
mattr_accessor :ghostscript_enabled
|
6
|
+
self.ghostscript_enabled = false
|
7
|
+
|
5
8
|
included do
|
6
9
|
include CarrierWave::ModelDelegateAttribute
|
7
10
|
include CarrierWave::MimeTypes
|
@@ -10,7 +13,7 @@ module CarrierWave
|
|
10
13
|
|
11
14
|
after :retrieve_from_cache, :set_content_type
|
12
15
|
after :retrieve_from_cache, :call_store_meta
|
13
|
-
after :retrieve_from_store, :set_content_type
|
16
|
+
after :retrieve_from_store, :set_content_type unless storage.name =~ /Fog/
|
14
17
|
after :retrieve_from_store, :call_store_meta
|
15
18
|
|
16
19
|
model_delegate_attribute :content_type, ''
|
@@ -18,9 +21,10 @@ module CarrierWave
|
|
18
21
|
model_delegate_attribute :image_size, []
|
19
22
|
model_delegate_attribute :width, 0
|
20
23
|
model_delegate_attribute :height, 0
|
24
|
+
model_delegate_attribute :md5sum, ''
|
21
25
|
end
|
22
26
|
|
23
|
-
def store_meta
|
27
|
+
def store_meta(options = {})
|
24
28
|
if self.file.present?
|
25
29
|
dimensions = get_dimensions
|
26
30
|
width, height = dimensions
|
@@ -29,41 +33,71 @@ module CarrierWave
|
|
29
33
|
self.image_size = dimensions
|
30
34
|
self.width = width
|
31
35
|
self.height = height
|
36
|
+
if options[:md5sum]
|
37
|
+
self.md5sum = Digest::MD5.hexdigest(File.read(self.file.path))
|
38
|
+
end
|
32
39
|
end
|
33
40
|
end
|
34
41
|
|
35
|
-
def set_content_type(file = nil)
|
36
|
-
set_content_type(true)
|
37
|
-
end
|
38
|
-
|
39
42
|
def image_size_s
|
40
43
|
image_size.join('x')
|
41
44
|
end
|
42
45
|
|
43
46
|
private
|
44
47
|
def call_store_meta(file = nil)
|
45
|
-
# Re-retrieve metadata for a file only if model is not present OR
|
46
|
-
|
48
|
+
# Re-retrieve metadata for a file only if model is not present OR
|
49
|
+
# model is not saved.
|
50
|
+
if model.nil? || (model.respond_to?(:new_record?) && model.new_record?)
|
51
|
+
processor_options = processors.
|
52
|
+
find { |p| p.first == :store_meta }.
|
53
|
+
try(:[], 1)
|
54
|
+
|
55
|
+
store_meta(*processor_options)
|
56
|
+
end
|
47
57
|
end
|
48
58
|
|
49
59
|
def get_dimensions
|
50
60
|
[].tap do |size|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
61
|
+
is_image = file.content_type =~ /image/
|
62
|
+
is_pdf =
|
63
|
+
file.content_type =~ /postscript|pdf/ &&
|
64
|
+
CarrierWave::Meta.ghostscript_enabled
|
65
|
+
|
66
|
+
is_dimensionable = is_image || is_pdf
|
67
|
+
|
68
|
+
manipulate! do |img|
|
69
|
+
if processor?(:rmagick, img) && is_dimensionable
|
70
|
+
size << img.columns
|
71
|
+
size << img.rows
|
72
|
+
elsif processor?(:mini_magick, img) && is_dimensionable
|
73
|
+
size << img['width']
|
74
|
+
size << img['height']
|
75
|
+
elsif processor?(:socrecy, img) && is_image
|
76
|
+
size << img.dimensions[:x].to_i
|
77
|
+
size << img.dimensions[:y].to_i
|
78
|
+
elsif processor?(:vips, img) && is_image
|
79
|
+
size << img.x_size
|
80
|
+
size << img.y_size
|
81
|
+
else
|
82
|
+
raise "Unsupported file type/image processor (use RMagick, MiniMagick, ImageSorcery, VIPS)"
|
63
83
|
end
|
84
|
+
img
|
64
85
|
end
|
65
86
|
end
|
66
87
|
rescue CarrierWave::ProcessingError
|
67
88
|
end
|
89
|
+
|
90
|
+
def processor?(processor, img)
|
91
|
+
processor = PROCESSORS[processor]
|
92
|
+
processor_class = processor.constantize rescue nil
|
93
|
+
processor_class.present? && img.is_a?(processor_class)
|
94
|
+
end
|
95
|
+
|
96
|
+
PROCESSORS = {
|
97
|
+
rmagick: 'Magick::Image',
|
98
|
+
mini_magick: 'MiniMagick::Image',
|
99
|
+
socrecy: 'ImageSorcery',
|
100
|
+
vips: 'VIPS::Image'
|
101
|
+
}
|
68
102
|
end
|
69
|
-
end
|
103
|
+
end
|