artbase 0.0.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,306 @@
1
+
2
+ module Artbase
3
+ class Base ## "abstract" Base collection - check -use a different name - why? why not?
4
+
5
+
6
+ def convert_images( overwrite: )
7
+ image_dir = "./#{slug}/token-i"
8
+ Image.convert( image_dir, from: 'jpg', to: 'png', overwrite: overwrite )
9
+ Image.convert( image_dir, from: 'gif', to: 'png', overwrite: overwrite )
10
+ Image.convert( image_dir, from: 'svg', to: 'png', overwrite: overwrite )
11
+ end
12
+
13
+
14
+
15
+ def make_strip
16
+ composite_count = @count - @excludes.size
17
+
18
+ composite = ImageComposite.new( 9, 1,
19
+ width: @width,
20
+ height: @height )
21
+
22
+ i = 0
23
+ each_image do |img, id|
24
+ puts "==> [#{i+1}/9] #{id}"
25
+ composite << img
26
+
27
+ i += 1
28
+ break if i >= 9
29
+ end
30
+
31
+
32
+ composite.save( "./#{@slug}/tmp/#{@slug}-strip.png" )
33
+ end
34
+
35
+
36
+
37
+ def make_composite
38
+ ### use well-known / pre-defined (default) grids
39
+ ## (cols x rows) for now - why? why not?
40
+
41
+ composite_count = @count - @excludes.size
42
+ cols, rows = case composite_count
43
+ when 99 then [10, 10]
44
+ when 100 then [10, 10]
45
+ when 150 then [15, 10]
46
+ when 314 then [15, 21]
47
+ when 500 then [25, 20]
48
+ when 1000 then [25, 40]
49
+ when 2200 then [50, 44]
50
+ when 2222 then [50, 45]
51
+ when 2469 then [50, 50]
52
+ when 3000 then [100, 30] ## or use 50*60 - why? why not?
53
+ when 3500 then [100, 35] ## or use 50*x ??
54
+ when 3979 then [100, 40]
55
+ when 4000 then [100, 40] ## or use 50x80 - why? why not?
56
+ when 4444 then [100, 45] ## or use 50x??
57
+ when 5000 then [100, 50] ## or use 50x100 - why? why not?
58
+ when 5555 then [100, 56] # 5600 (45 left empty)
59
+ when 6666 then [100, 67] # 6700 (34 left empty)
60
+ when 6688 then [100, 67] # 6700 (12 left empty)
61
+ when 6969 then [100, 70] # 7000 (31 left empty)
62
+ when 8888 then [100, 89]
63
+ when 9969 then [100,100]
64
+ when 10000 then [100,100]
65
+ else
66
+ raise ArgumentError, "sorry - unknown composite count #{composite_count}/#{@count} for now"
67
+ end
68
+
69
+ composite = ImageComposite.new( cols, rows,
70
+ width: @width,
71
+ height: @height )
72
+
73
+ each_image do |img, id|
74
+ puts "==> #{id}"
75
+ composite << img
76
+ end
77
+
78
+
79
+
80
+ composite.save( "./#{@slug}/tmp/#{@slug}-#{@width}x#{@height}.png" )
81
+
82
+ if composite_count < 1000
83
+ composite.zoom(2).save( "./#{@slug}/tmp/#{@slug}-#{@width}x#{@height}@2x.png" )
84
+ end
85
+ end
86
+
87
+
88
+
89
+
90
+ def calc_attribute_counters ## todo/check: use a different name _counts/_stats etc - why? why not?
91
+
92
+ attributes_by_count = { count: 0,
93
+ by_count: Hash.new(0)
94
+ }
95
+ counter = {}
96
+
97
+
98
+ each_meta do |meta, id| ## todo/fix: change id to index
99
+ traits = meta.traits
100
+ # print "#{traits.size} - "
101
+ # pp traits
102
+
103
+ print "#{id}.." if id % 100 == 0 ## print progress report
104
+
105
+ attributes_by_count[ :count ] +=1
106
+ attributes_by_count[ :by_count ][ traits.size ] += 1
107
+
108
+ traits.each do |trait_type, trait_value|
109
+ trait_type = _normalize_trait_type( trait_type )
110
+ trait_value = _normalize_trait_value( trait_value )
111
+
112
+
113
+ rec = counter[ trait_type ] ||= { count: 0,
114
+ by_type: Hash.new(0)
115
+ }
116
+ rec[ :count ] +=1
117
+ rec[ :by_type ][ trait_value ] += 1
118
+ end
119
+ end
120
+
121
+ print "\n"
122
+ puts
123
+
124
+ ## return all-in-one hash
125
+ {
126
+ total: attributes_by_count,
127
+ traits: counter,
128
+ }
129
+ end
130
+
131
+
132
+ def dump_attributes
133
+ stats = calc_attribute_counters
134
+
135
+ total = stats[:total]
136
+ counter = stats[:traits]
137
+
138
+ puts
139
+ puts "attribute usage / counts:"
140
+ pp total
141
+ puts
142
+
143
+ puts "#{counter.size} attribute(s):"
144
+ counter.each do |trait_name, trait_rec|
145
+ puts " #{trait_name} #{trait_rec[:count]} (#{trait_rec[:by_type].size} uniques)"
146
+ end
147
+
148
+ puts
149
+ pp counter
150
+ end
151
+
152
+
153
+
154
+
155
+ ## order - allow "custom" attribute order export
156
+ ## renames - allow renames of attributes
157
+ def export_attributes(
158
+ order: [],
159
+ renames: {}
160
+ )
161
+
162
+ ## step 1: get counters
163
+ stats = calc_attribute_counters
164
+
165
+ total = stats[:total]
166
+ counter = stats[:traits]
167
+
168
+ puts
169
+ puts "attribute usage / counts:"
170
+ pp total
171
+ puts
172
+
173
+ puts "#{counter.size} attribute(s):"
174
+ counter.each do |trait_name, trait_rec|
175
+ puts " #{trait_name} #{trait_rec[:count]} (#{trait_rec[:by_type].size} uniques)"
176
+ end
177
+
178
+
179
+ trait_names = []
180
+ trait_names += order ## get attributes if any in pre-defined order
181
+ counter.each do |trait_name, _|
182
+ if trait_names.include?( trait_name )
183
+ next ## skip already included
184
+ else
185
+ trait_names << trait_name
186
+ end
187
+ end
188
+
189
+
190
+ recs = []
191
+
192
+
193
+ ## step 2: get tabular data
194
+ each_meta do |meta, id| ## todo/fix: change id to index
195
+
196
+ traits = meta.traits
197
+ # print "#{traits.size} - "
198
+ # pp traits
199
+
200
+ print "#{id}.." if id % 100 == 0 ## print progress report
201
+
202
+ ## setup empty hash table (with all attributes)
203
+ rec = {}
204
+
205
+ ## note: use __Slug__& __Name__
206
+ ## to avoid conflict with attribute names
207
+ ## e.g. attribute with "Name" will overwrite built-in and so on
208
+
209
+ rec['__Slug__'] = if respond_to?( :_meta_slugify )
210
+ _meta_slugify( meta, id )
211
+ else
212
+ ## default to id (six digits) as string with leading zeros
213
+ ## for easy sorting using strings
214
+ ## e.g. 1 => '000001'
215
+ ## 2 => '000002'
216
+ '%06d' % id
217
+ end
218
+
219
+ rec['__Name__'] = meta.name
220
+
221
+ ## add all attributes/traits names/keys
222
+ trait_names.reduce( rec ) { |h,value| h[value] = []; h }
223
+ ## pp rec
224
+
225
+ ## note: use an array (to allow multiple values for attributes)
226
+ traits.each do |trait_type, trait_value|
227
+ trait_type = _normalize_trait_type( trait_type )
228
+ trait_value = _normalize_trait_value( trait_value )
229
+
230
+ values = rec[ trait_type ]
231
+ values << trait_value
232
+ end
233
+ recs << rec
234
+ end
235
+ print "\n"
236
+
237
+ ## pp recs
238
+
239
+ ## flatten recs
240
+ data = []
241
+ recs.each do |rec|
242
+ row = rec.values.map do |value|
243
+ if value.is_a?( Array )
244
+ value.join( ' / ' )
245
+ else
246
+ value
247
+ end
248
+ end
249
+ data << row
250
+ end
251
+
252
+
253
+ ## sort by slug
254
+ data = data.sort {|l,r| l[0] <=> r[0] }
255
+ pp data
256
+
257
+ ### save dataset
258
+ ## note: change first colum Slug to ID - only used for "internal" sort etc.
259
+ headers = ['ID', 'Name']
260
+ headers += trait_names.map do |trait_name| ## check for renames
261
+ renames[trait_name] || trait_name
262
+ end
263
+
264
+
265
+ path = "./#{@slug}/tmp/#{@slug}.csv"
266
+ dirname = File.dirname( path )
267
+ FileUtils.mkdir_p( dirname ) unless Dir.exist?( dirname )
268
+
269
+ File.open( path, 'w:utf-8' ) do |f|
270
+ f.write( headers.join( ', ' ))
271
+ f.write( "\n" )
272
+ ## note: replace ID with our own internal running (zero-based) counter
273
+ data.each_with_index do |row,i|
274
+ f.write( ([i]+row[1..-1]).join( ', '))
275
+ f.write( "\n" )
276
+ end
277
+ end
278
+ end
279
+
280
+
281
+
282
+
283
+ #############
284
+ # "private" helpers
285
+
286
+ def _normalize_trait_type( trait_type )
287
+ if @patch && @patch[:trait_types]
288
+ @patch[:trait_types][ trait_type ] || trait_type
289
+ else
290
+ trait_type
291
+ end
292
+ end
293
+
294
+ def _normalize_trait_value( trait_value )
295
+ if @patch && @patch[:trait_values]
296
+ @patch[:trait_values][ trait_value ] || trait_value
297
+ else
298
+ trait_value
299
+ end
300
+ end
301
+
302
+
303
+
304
+
305
+ end # class Base
306
+ end # module Artbase
@@ -0,0 +1,39 @@
1
+
2
+
3
+ class ImageCollection
4
+
5
+ attr_reader :slug, :count
6
+
7
+ def initialize( slug, count,
8
+ image_base: ) # check: rename count to items or such - why? why not?
9
+ @slug = slug
10
+ @count = count
11
+ @image_base = image_base
12
+ end
13
+
14
+ def download_images( range=(0...@count) )
15
+ start = Time.now
16
+ delay_in_s = 0.3
17
+
18
+ range.each do |offset|
19
+ image_src = @image_base.sub( '{id}', offset.to_s )
20
+
21
+ puts "==> #{offset} - #{@slug}..."
22
+
23
+ ## note: will auto-add format file extension (e.g. .png, .jpg)
24
+ ## depending on http content type!!!!!
25
+ copy_image( image_src, "./#{@slug}/image-i/#{offset}" )
26
+
27
+ stop = Time.now
28
+ diff = stop - start
29
+
30
+ mins = diff / 60 ## todo - use floor or such?
31
+ secs = diff % 60
32
+ puts "up #{mins} mins #{secs} secs (total #{diff} secs)"
33
+
34
+ puts "sleeping #{delay_in_s}s..."
35
+ sleep( delay_in_s )
36
+ end
37
+ end
38
+ end # class ImageCollection
39
+
@@ -0,0 +1,297 @@
1
+
2
+
3
+ class Collection ## todo/check - change to OpenseaCollection or such - why? why not?
4
+
5
+ attr_reader :slug, :count
6
+
7
+ # check: rename count to items or such - why? why not?
8
+ # default format to '24x24' - why? why not?
9
+ def initialize( slug, count,
10
+ meta_slugify: nil,
11
+ image_pixelate: nil,
12
+ patch: nil,
13
+ exclude: [],
14
+ format:,
15
+ source: )
16
+ @slug = slug
17
+ @count = count
18
+
19
+ @meta_slugify = meta_slugify
20
+ @image_pixelate = image_pixelate
21
+
22
+ @patch = patch
23
+
24
+ @exclude = exclude
25
+
26
+ @width, @height = _parse_dimension( format )
27
+
28
+
29
+ ## note: allow multiple source formats / dimensions
30
+ ### e.g. convert 512x512 into [ [512,512] ]
31
+ ##
32
+ source = [source] unless source.is_a?( Array )
33
+ @sources = source.map { |dimension| _parse_dimension( dimension ) }
34
+ end
35
+
36
+ ## e.g. convert dimension (width x height) "24x24" or "24 x 24" to [24,24]
37
+ def _parse_dimension( str )
38
+ str.split( /x/i ).map { |str| str.strip.to_i }
39
+ end
40
+
41
+
42
+ def _image_pixelate( img )
43
+ if @image_pixelate
44
+ @image_pixelate.call( img )
45
+ else
46
+ @sources.each do |source_width, source_height|
47
+ if img.width == source_width && img.height == source_height
48
+ from = "#{source_width}x#{source_height}"
49
+ to = "#{@width}x#{@height}"
50
+ steps = (Image::DOwNSAMPLING_STEPS[ to ] || {})[ from ]
51
+ if steps.nil?
52
+ puts "!! ERROR - no sampling steps defined for #{from} to #{to}; sorry"
53
+ exit 1
54
+ end
55
+
56
+ return img.pixelate( steps )
57
+ end
58
+ end
59
+
60
+ puts "!! ERROR - unknown image dimension #{img.width}x#{img.height}; sorry"
61
+ puts " supported source dimensions include: #{@sources.inspect}"
62
+ exit 1
63
+ end
64
+ end
65
+
66
+
67
+
68
+
69
+ def download_meta( range=(0...@count) )
70
+ self.class.download_meta( range, @slug )
71
+ end
72
+
73
+ def download_images( range=(0...@count) )
74
+ self.class.download_images( range, @slug )
75
+ end
76
+
77
+ def download( range=(0...@count) )
78
+ download_meta( range )
79
+ download_images( range )
80
+ end
81
+
82
+
83
+
84
+
85
+
86
+ def _meta_slugify_match( regex, meta, index )
87
+ if m=regex.match( meta.name )
88
+ captures = m.named_captures ## get named captures in match data as hash (keys as strings)
89
+ # e.g.
90
+ #=> {"num"=>"3"}
91
+ #=> {"num"=>"498", "name"=>"Doge"}
92
+ pp captures
93
+
94
+ num = captures['num'] ? captures['num'].to_i( 10 ) : nil ## note: add base 10 (e.g. 015=>15)
95
+ name = captures['name'] ? captures['name'].strip : nil
96
+
97
+ slug = ''
98
+ if num
99
+ slug << "%06d" % num ## todo/check: always fill/zero-pad with six 000000's - why? why not?
100
+ end
101
+
102
+ if name
103
+ slug << "-" if num ## add separator
104
+ slug << slugify( name )
105
+ end
106
+ slug
107
+ else
108
+ nil ## note: return nil if no match / slug
109
+ end
110
+ end
111
+
112
+ def _do_meta_slugify( meta_slugify, meta, index )
113
+ if meta_slugify.is_a?( Regexp )
114
+ _meta_slugify_match( meta_slugify, meta, index )
115
+ elsif meta_slugify.is_a?( Proc )
116
+ meta_slugify.call( meta, index )
117
+ else
118
+ raise ArgumentError, "meta_slugify - unsupported type: #{meta_slugify.class.name}"
119
+ end
120
+ end
121
+
122
+
123
+ def _meta_slugify( meta, index )
124
+ slug = nil
125
+
126
+ if @meta_slugify.is_a?( Array )
127
+ @meta_slugify.each do |meta_slugify|
128
+ slug = _do_meta_slugify( meta_slugify, meta, index )
129
+ return slug if slug ## note: short-circuit on first match
130
+ ## use break instead of return - why? why not?
131
+ end
132
+ else ## assume object e.g. Regexp, Proc, etc.
133
+ slug = _do_meta_slugify( @meta_slugify, meta, index )
134
+ end
135
+
136
+ ## do nothing
137
+ if slug.nil?
138
+ puts "!! ERROR - cannot find id in >#{meta.name}<:"
139
+ pp meta
140
+ exit 1
141
+ end
142
+
143
+ slug
144
+ end
145
+
146
+
147
+
148
+ def each_meta( range=(0...@count),
149
+ exclude: true, &blk )
150
+ range.each do |id| ## todo/fix: change id to index
151
+ meta = OpenSea::Meta.read( "./#{@slug}/meta/#{id}.json" )
152
+
153
+ ####
154
+ # filter out/skip
155
+ if exclude && @exclude.include?( meta.name )
156
+ puts " skipping / exclude #{id} >#{meta.name}<..."
157
+ next
158
+ end
159
+
160
+ blk.call( meta, id )
161
+ end
162
+ end
163
+
164
+
165
+
166
+
167
+ def pixelate( range=(0...@count) )
168
+
169
+ meta_slugs = Hash.new( 0 ) ## deduplicate (auto-add counter if duplicate)
170
+
171
+ ### todo/fix: must read slugs starting at 0
172
+ ### to work for deduplicate!!!!!!
173
+
174
+
175
+ range.each do |id|
176
+ meta = OpenSea::Meta.read( "./#{@slug}/meta/#{id}.json" )
177
+
178
+ ####
179
+ # filter out/skip
180
+ if @exclude.include?( meta.name )
181
+ puts " skipping / exclude #{id} >#{meta.name}<..."
182
+ next
183
+ end
184
+
185
+ puts meta.name
186
+
187
+
188
+ meta_slug = _meta_slugify( meta, id )
189
+ count = meta_slugs[ meta_slug ] += 1
190
+
191
+ meta_slug = "#{meta_slug}_(#{count})" if count > 1
192
+
193
+
194
+ img = Image.read( "./#{@slug}/i/#{id}.png" )
195
+
196
+ pix = _image_pixelate( img )
197
+
198
+ path = "./#{@slug}/ii/#{meta_slug}.png"
199
+ puts " saving to >#{path}<..."
200
+ pix.save( path )
201
+ end
202
+ end
203
+
204
+
205
+
206
+ ################################
207
+ # private (static) helpers
208
+ #
209
+
210
+ def self.download_images( range, collection,
211
+ original: false )
212
+ start = Time.now
213
+ delay_in_s = 0.3
214
+
215
+ range.each do |offset|
216
+ meta = OpenSea::Meta.read( "./#{collection}/meta/#{offset}.json" )
217
+
218
+ puts "==> #{offset}.json - #{meta.name}"
219
+
220
+ image_src = if original
221
+ meta.image_original_url
222
+ else
223
+ meta.image_url
224
+ end
225
+
226
+ puts " >#{image_src}<"
227
+ if image_src.nil?
228
+ puts "!! ERROR - no image url found (use original: #{original}):"
229
+ pp meta
230
+ exit 1
231
+ end
232
+
233
+ ## note: use a different directory to avoid size confusion!!!
234
+ img_slug = if original
235
+ 'i_org'
236
+ else
237
+ 'i'
238
+ end
239
+
240
+ ## note: will auto-add format file extension (e.g. .png, .jpg)
241
+ ## depending on http content type!!!!!
242
+ copy_image( image_src, "./#{collection}/#{img_slug}/#{offset}" )
243
+
244
+ stop = Time.now
245
+ diff = stop - start
246
+
247
+ mins = diff / 60 ## todo - use floor or such?
248
+ secs = diff % 60
249
+ puts "up #{mins} mins #{secs} secs (total #{diff} secs)"
250
+
251
+ puts "sleeping #{delay_in_s}s..."
252
+ sleep( delay_in_s )
253
+ end
254
+ end
255
+
256
+
257
+ def self.download_meta( range, collection )
258
+ start = Time.now
259
+ delay_in_s = 0.3
260
+
261
+ range.each do |offset|
262
+
263
+ dest = "./#{collection}/meta/#{offset}.json"
264
+ meta = nil
265
+
266
+ puts "==> #{offset} / #{collection} (#{dest})..."
267
+
268
+ data = OpenSea.assets( collection: collection,
269
+ offset: offset )
270
+ meta = OpenSea::Meta.new( data )
271
+ puts " name: >#{meta.name}<"
272
+ puts " image_url: >#{meta.image_url}<"
273
+
274
+
275
+ ## make sure path exists
276
+ dirname = File.dirname( dest )
277
+ FileUtils.mkdir_p( dirname ) unless Dir.exist?( dirname )
278
+
279
+ File.open( dest, "w:utf-8" ) do |f|
280
+ f.write( JSON.pretty_generate( data ) )
281
+ end
282
+
283
+
284
+ stop = Time.now
285
+ diff = stop - start
286
+
287
+ mins = diff / 60 ## todo - use floor or such?
288
+ secs = diff % 60
289
+ puts "up #{mins} mins #{secs} secs (total #{diff} secs)"
290
+
291
+ puts " sleeping #{delay_in_s}s..."
292
+ sleep( delay_in_s )
293
+ end
294
+ end
295
+
296
+
297
+ end # class Collection