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,372 @@
1
+
2
+ class TokenCollection < Artbase::Base
3
+
4
+
5
+ #############
6
+ # (nested) Meta classes
7
+ # read meta data into struct
8
+ class Meta
9
+ def self.read( path )
10
+ new( read_json( path ))
11
+ end
12
+
13
+
14
+ def initialize( data )
15
+ @data = data
16
+ end
17
+
18
+
19
+ def name
20
+ ## note: name might be an integer number e.g. 0/1/2 etc.
21
+ ## e.g. see crypto pudgy punks and others?
22
+ ## always auto-convert to string
23
+ @name ||= _normalize( @data['name'].to_s )
24
+ end
25
+
26
+
27
+ def description
28
+ @description ||= _normalize( @data['description'] )
29
+ end
30
+
31
+ ## note: auto-convert "" (empty string) to nil
32
+ def image() _blank( @data['image'] ); end
33
+ alias_method :image_url, :image ## add image_url alias - why? why not?
34
+
35
+
36
+ def attributes
37
+ @attributes ||= begin
38
+ traits = []
39
+ ## keep traits as (simple)
40
+ ## ordered array of pairs for now
41
+ ##
42
+ ## in a step two make lookup via hash table
43
+ ## or such easier / "automagic"
44
+
45
+ @data[ 'attributes' ].each do |t|
46
+ trait_type = t['trait_type'].strip
47
+ trait_value = t['value'].strip
48
+ traits << [trait_type, trait_value]
49
+ end
50
+
51
+ traits
52
+ end
53
+ end
54
+ alias_method :traits, :attributes ## keep traits alias - why? why not?
55
+
56
+ ### "private" convenience / helper methods
57
+ def _normalize( str )
58
+ return if str.nil? ## check: check for nil - why? why not?
59
+
60
+ ## normalize string
61
+ ## remove leading and trailing spaces
62
+ ## collapse two and more spaces into one
63
+ ## change unicode space to ascii
64
+ str = str.gsub( "\u{00a0}", ' ' )
65
+ str = str.strip.gsub( /[ ]{2,}/, ' ' )
66
+ str
67
+ end
68
+
69
+ def _blank( o ) ## auto-convert "" (empty string) into nil
70
+ if o && o.strip.empty?
71
+ nil
72
+ else
73
+ o
74
+ end
75
+ end
76
+ end # (nested) class Meta
77
+
78
+
79
+
80
+
81
+ attr_reader :slug, :count
82
+
83
+ def initialize( slug, count,
84
+ token_base:,
85
+ image_base: nil,
86
+ image_base_id_format: nil,
87
+ format:,
88
+ source:,
89
+ top_x: 0,
90
+ top_y: 0,
91
+ center_x: true,
92
+ center_y: true,
93
+ excludes: [],
94
+ offset: 0 ) # check: rename count to items or such - why? why not?
95
+ @slug = slug
96
+ @count = count
97
+ @offset = offset ## starting by default at 0 (NOT 1 or such)
98
+
99
+ @token_base = token_base
100
+ @image_base = image_base
101
+ @image_base_id_format = image_base_id_format
102
+
103
+ @width, @height = _parse_dimension( format )
104
+
105
+
106
+ ## note: allow multiple source formats / dimensions
107
+ ### e.g. convert 512x512 into [ [512,512] ]
108
+ ##
109
+ source = [source] unless source.is_a?( Array )
110
+ @sources = source.map { |dimension| _parse_dimension( dimension ) }
111
+
112
+ @top_x = top_x ## more (down)sampling / pixelate options
113
+ @top_y = top_y
114
+ @center_x = center_x
115
+ @center_y = center_y
116
+
117
+ @excludes = excludes
118
+ end
119
+
120
+
121
+ ## e.g. convert dimension (width x height) "24x24" or "24 x 24" to [24,24]
122
+ def _parse_dimension( str )
123
+ str.split( /x/i ).map { |str| str.strip.to_i }
124
+ end
125
+
126
+
127
+ def _range( offset: 0 ) ## return "default" range - make "private" helper public - why? why not?
128
+ ## note: range uses three dots (...) exclusive (NOT inclusive) range
129
+ ## e.g. 0...100 => [0,..,99]
130
+ ## 1...101 => [1,..,100]
131
+ ##
132
+ ## note: allow offset argument
133
+ ## (to start with different offset - note: in addition to builtin 0/1 offset)
134
+
135
+ (0+@offset+offset...@count+@offset)
136
+ end
137
+
138
+
139
+
140
+ def each_image( range=_range,
141
+ exclude: true, &blk )
142
+ range.each do |id|
143
+ ####
144
+ # filter out/skip
145
+ # if exclude && @excludes.include?( id )
146
+ # puts " skipping / exclude #{id}..."
147
+ # next
148
+ # end
149
+
150
+ puts "==> #{id}"
151
+ img = Image.read( "./#{@slug}/#{@width}x#{@height}/#{id}.png" )
152
+ blk.call( img, id )
153
+ end
154
+ end
155
+
156
+
157
+ def each_meta( range=_range,
158
+ exclude: true, &blk )
159
+ range.each do |id| ## check: change/rename id to index - why? why not?
160
+ meta = Meta.read( "./#{@slug}/token/#{id}.json" )
161
+
162
+ ####
163
+ # filter out/skip
164
+ # if exclude && @excludes.include?( meta.name )
165
+ # puts " skipping / exclude #{id} >#{meta.name}<..."
166
+ # next
167
+ # end
168
+
169
+ blk.call( meta, id )
170
+ end
171
+ end
172
+
173
+
174
+
175
+
176
+
177
+ def pixelate( range=_range, exclude: true,
178
+ force: false,
179
+ debug: false,
180
+ zoom: nil )
181
+
182
+ range.each do |id|
183
+
184
+ if exclude && @excludes.include?( id )
185
+ puts " skipping #{id}; listed in excludes #{@excludes.inspect}"
186
+ next
187
+ end
188
+
189
+ outpath = "./#{@slug}/#{@width}x#{@height}/#{id}.png"
190
+ if !force && File.exist?( outpath )
191
+ next ## note: skip if file already exists
192
+ end
193
+
194
+ center_x = if @center_x.is_a?( Proc ) then @center_x.call( id ); else @center_x; end
195
+ center_y = if @center_y.is_a?( Proc ) then @center_y.call( id ); else @center_y; end
196
+
197
+
198
+
199
+ puts "==> #{id} - reading / decoding #{id} ..."
200
+ start = Time.now
201
+
202
+ img = Image.read( "./#{@slug}/token-i/#{id}.png" )
203
+
204
+ stop = Time.now
205
+ diff = stop - start
206
+
207
+ puts " in #{diff} sec(s)\n"
208
+
209
+
210
+ source = nil
211
+ @sources.each do |source_width, source_height|
212
+ if img.width == source_width && img.height == source_height
213
+ source = [source_width, source_height]
214
+ break
215
+ end
216
+ end
217
+
218
+
219
+ if source
220
+ source_width = source[0]
221
+ source_height = source[1]
222
+
223
+ steps_x = Image.calc_sample_steps( source_width-@top_x, @width, center: center_x )
224
+ steps_y = Image.calc_sample_steps( source_height-@top_y, @height, center: center_y )
225
+
226
+ pix = if debug
227
+ img.pixelate_debug( steps_x, steps_y,
228
+ top_x: @top_x,
229
+ top_y: @top_y )
230
+ else
231
+ img.pixelate( steps_x, steps_y,
232
+ top_x: @top_x,
233
+ top_y: @top_y )
234
+ end
235
+ ## todo/check: keep usingu slug e.g. 0001.png or "plain" 1.png - why? why not?
236
+ ## slug = "%04d" % id
237
+ pix.save( outpath )
238
+
239
+ if zoom
240
+ outpath = "./#{@slug}/#{@width}x#{@height}/#{id}@#{zoom}x.png"
241
+ pix.zoom( zoom ).save( outpath )
242
+ end
243
+ else
244
+ puts "!! ERROR - unknown/unsupported dimension - #{img.width}x#{img.height}; sorry - tried:"
245
+ pp @sources
246
+ exit 1
247
+ end
248
+ end
249
+ end
250
+
251
+
252
+
253
+ def meta_url( id: )
254
+ src = @token_base.gsub( '{id}', id.to_s )
255
+
256
+ ## quick ipfs (interplanetary file system) hack - make more reusabele!!!
257
+ src = handle_ipfs( src )
258
+ src
259
+ end
260
+ alias_method :token_url, :meta_url
261
+
262
+
263
+ def image_url( id:,
264
+ direct: @image_base ? true : false )
265
+ src = if direct && @image_base
266
+ ###
267
+ ## todo/fix:
268
+ ## change image_base_id_format
269
+ ## to image_base proc with para id and call proc!!!!
270
+ if @image_base_id_format
271
+ @image_base.gsub( '{id}', @image_base_id_format % id )
272
+ else
273
+ @image_base.gsub( '{id}', id.to_s )
274
+ end
275
+ else
276
+ ## todo/check - change/rename data to meta - why? why not?
277
+ data = Meta.read( "./#{@slug}/token/#{id}.json" )
278
+
279
+ meta_name = data.name
280
+ meta_image = data.image
281
+
282
+ puts "==> #{id} - #{@slug}..."
283
+ puts " name: #{meta_name}"
284
+ puts " image: #{meta_image}"
285
+ meta_image
286
+ end
287
+ src
288
+
289
+ ## quick ipfs (interplanetary file system) hack - make more reusabele!!!
290
+ src = handle_ipfs( src )
291
+ src
292
+ end
293
+
294
+
295
+
296
+ def download_meta( range=_range, force: false )
297
+ start = Time.now
298
+ delay_in_s = 0.3
299
+
300
+ range.each do |id|
301
+ outpath = "./#{@slug}/token/#{id}.json"
302
+ if !force && File.exist?( outpath )
303
+ next ## note: skip if file already exists
304
+ end
305
+
306
+ dirname = File.dirname( outpath )
307
+ FileUtils.mkdir_p( dirname ) unless Dir.exist?( dirname )
308
+
309
+ puts "==> #{id} - #{@slug}..."
310
+
311
+ token_src = meta_url( id: id )
312
+ copy_json( token_src, outpath )
313
+
314
+ stop = Time.now
315
+ diff = stop - start
316
+ puts " download token metadata in #{diff} sec(s)"
317
+
318
+ mins = diff / 60 ## todo - use floor or such?
319
+ secs = diff % 60
320
+ puts "up #{mins} mins #{secs} secs (total #{diff} secs)"
321
+
322
+ puts "sleeping #{delay_in_s}s..."
323
+ sleep( delay_in_s )
324
+ end
325
+ end
326
+
327
+
328
+ ## note: default to direct true if image_base present/availabe
329
+ ## otherwise to false
330
+ ## todo/check: change/rename force para to overwrite - why? why not?
331
+ def download_images( range=_range, force: false,
332
+ direct: @image_base ? true : false )
333
+ start = Time.now
334
+ delay_in_s = 0.3
335
+
336
+ range.each do |id|
337
+
338
+ ## note: skip if (downloaded) file already exists
339
+ skip = false
340
+ if !force
341
+ ['png', 'gif', 'jgp', 'svg'].each do |format|
342
+ if File.exist?( "./#{@slug}/token-i/#{id}.#{format}" )
343
+ skip = true
344
+ break
345
+ end
346
+ end
347
+ end
348
+ next if skip
349
+
350
+ image_src = image_url( id: id, direct: direct )
351
+
352
+ ## note: will auto-add format file extension (e.g. .png, .jpg)
353
+ ## depending on http content type!!!!!
354
+ start_copy = Time.now
355
+ copy_image( image_src, "./#{@slug}/token-i/#{id}" )
356
+
357
+ stop = Time.now
358
+
359
+ diff = stop - start_copy
360
+ puts " download image in #{diff} sec(s)"
361
+
362
+ diff = stop - start
363
+ mins = diff / 60 ## todo - use floor or such?
364
+ secs = diff % 60
365
+ puts "up #{mins} mins #{secs} secs (total #{diff} secs)"
366
+
367
+ puts "sleeping #{delay_in_s}s..."
368
+ sleep( delay_in_s )
369
+ end
370
+ end
371
+
372
+ end # class TokenCollection
@@ -0,0 +1,12 @@
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+ require_relative 'collection/base'
9
+ require_relative 'collection/token'
10
+ require_relative 'collection/image'
11
+ require_relative 'collection/opensea'
12
+
@@ -0,0 +1,169 @@
1
+
2
+
3
+
4
+ def slugify( name )
5
+ name.downcase.gsub( /[^a-z0-9 ()$_-]/ ) do |_|
6
+ puts " !! WARN: asciify - found (and removing) non-ascii char >#{Regexp.last_match}<"
7
+ '' ## remove - use empty string
8
+ end.gsub( ' ', '_')
9
+ end
10
+
11
+
12
+
13
+ =begin
14
+ moved/ use Image.convert !!! remove here
15
+ def convert_images( collection, from: 'jpg',
16
+ to: 'png',
17
+ dir: 'i',
18
+ overwrite: true )
19
+ files = Dir.glob( "./#{collection}/#{dir}/*.#{from}" )
20
+ puts "==> converting #{files.size} image(s) from #{from} to #{to}"
21
+
22
+ files.each_with_index do |file,i|
23
+ dirname = File.dirname( file )
24
+ extname = File.extname( file )
25
+ basename = File.basename( file, extname )
26
+
27
+ ## skip convert if target / dest file already exists
28
+ next if overwrite == false && File.exist?( "#{dirname}/#{basename}.#{to}" )
29
+
30
+
31
+ cmd = "magick convert #{dirname}/#{basename}.#{from} #{dirname}/#{basename}.#{to}"
32
+
33
+ puts " [#{i+1}/#{files.size}] - #{cmd}"
34
+ system( cmd )
35
+
36
+ if from == 'gif'
37
+ ## assume multi-images for gif
38
+ ## save image-0.png to image.png
39
+ path0 = "#{dirname}/#{basename}-0.#{to}"
40
+ path = "#{dirname}/#{basename}.#{to}"
41
+
42
+ puts " saving #{path0} to #{path}..."
43
+
44
+ blob = File.open( path0, 'rb' ) { |f| f.read }
45
+ File.open( path, 'wb' ) { |f| f.write( blob ) }
46
+ end
47
+ end
48
+ end
49
+ =end
50
+
51
+
52
+
53
+
54
+
55
+
56
+
57
+ def copy_json( src, dest )
58
+ uri = URI.parse( src )
59
+
60
+ http = Net::HTTP.new( uri.host, uri.port )
61
+
62
+ puts "[debug] GET #{uri.request_uri} uri=#{uri}"
63
+
64
+ headers = { 'User-Agent' => "ruby v#{RUBY_VERSION}" }
65
+
66
+
67
+ request = Net::HTTP::Get.new( uri.request_uri, headers )
68
+ if uri.instance_of? URI::HTTPS
69
+ http.use_ssl = true
70
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
71
+ end
72
+
73
+ response = http.request( request )
74
+
75
+ if response.code == '200'
76
+ puts "#{response.code} #{response.message}"
77
+ puts " content_type: #{response.content_type}, content_length: #{response.content_length}"
78
+
79
+ text = response.body.to_s
80
+ text = text.force_encoding( Encoding::UTF_8 )
81
+
82
+ data = JSON.parse( text )
83
+
84
+ File.open( dest, "w:utf-8" ) do |f|
85
+ f.write( JSON.pretty_generate( data ) )
86
+ end
87
+ else
88
+ puts "!! error:"
89
+ puts "#{response.code} #{response.message}"
90
+ exit 1
91
+ end
92
+ end
93
+
94
+
95
+ def copy_image( src, dest,
96
+ dump_headers: false )
97
+ uri = URI.parse( src )
98
+
99
+ http = Net::HTTP.new( uri.host, uri.port )
100
+
101
+ puts "[debug] GET #{uri.request_uri} uri=#{uri}"
102
+
103
+ headers = { 'User-Agent' => "ruby v#{RUBY_VERSION}" }
104
+
105
+ request = Net::HTTP::Get.new( uri.request_uri, headers )
106
+ if uri.instance_of? URI::HTTPS
107
+ http.use_ssl = true
108
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
109
+ end
110
+
111
+ response = http.request( request )
112
+
113
+ if response.code == '200'
114
+ puts "#{response.code} #{response.message}"
115
+
116
+ content_type = response.content_type
117
+ content_length = response.content_length
118
+ puts " content_type: #{content_type}, content_length: #{content_length}"
119
+
120
+ if dump_headers ## for debugging dump headers
121
+ headers = response.each_header.to_h
122
+ puts "htttp respone headers:"
123
+ pp headers
124
+ end
125
+
126
+
127
+ format = if content_type =~ %r{image/jpeg}i
128
+ 'jpg'
129
+ elsif content_type =~ %r{image/png}i
130
+ 'png'
131
+ elsif content_type =~ %r{image/gif}i
132
+ 'gif'
133
+ elsif content_type =~ %r{image/svg}i
134
+ 'svg'
135
+ else
136
+ puts "!! error:"
137
+ puts " unknown image format content type: >#{content_type}<"
138
+ exit 1
139
+ end
140
+
141
+ ## make sure path exits - autocreate dirs
142
+ ## make sure path exists
143
+ dirname = File.dirname( "#{dest}.#{format}" )
144
+ FileUtils.mkdir_p( dirname ) unless Dir.exist?( dirname )
145
+
146
+ if format == 'svg'
147
+ ## save as text (note: assume utf-8 encoding for now)
148
+ text = response.body.to_s
149
+ text = text.force_encoding( Encoding::UTF_8 )
150
+
151
+ File.open( "#{dest}.svg", 'w:utf-8' ) do |f|
152
+ f.write( text )
153
+ end
154
+ else
155
+ ## save as binary
156
+ File.open( "#{dest}.#{format}", 'wb' ) do |f|
157
+ f.write( response.body )
158
+ end
159
+ end
160
+ else
161
+ puts "!! error:"
162
+ puts "#{response.code} #{response.message}"
163
+ exit 1
164
+ end
165
+ end
166
+
167
+
168
+
169
+
@@ -0,0 +1,31 @@
1
+ module Pixelart
2
+
3
+ class Image
4
+
5
+ ###
6
+ # add common
7
+ # pixel(ate) steps/offsets (for re/down/sampling)
8
+ DOwNSAMPLING_STEPS = {
9
+ '24x24' => {
10
+ '269x269' => Image.calc_sample_steps( 269, 24 ), # width (269px), new_width (24px)
11
+ '512x512' => Image.calc_sample_steps( 512, 24 ), # width (512px), new_width (24px)
12
+ },
13
+ '32x32' => {
14
+ '320x320' => Image.calc_sample_steps( 320, 32 ),
15
+ '512x512' => Image.calc_sample_steps( 512, 32 ),
16
+ },
17
+ '35x35' => {
18
+ '512x512' => Image.calc_sample_steps( 512, 35 ),
19
+ },
20
+ '60x60' => {
21
+ '512x512' => Image.calc_sample_steps( 512, 60 ),
22
+ },
23
+ '80x80' => {
24
+ '512x512' => Image.calc_sample_steps( 512, 80 ),
25
+ },
26
+ }
27
+
28
+ end # class Image
29
+ end # module Pixelart
30
+
31
+
@@ -0,0 +1,31 @@
1
+ ######################
2
+ # pixelart image extensions
3
+ # move upstream!!!!!
4
+
5
+
6
+ module Pixelart
7
+ class ImageComposite
8
+ def add_glob( glob )
9
+ files = Dir.glob( glob )
10
+ puts "#{files.size} file(s) found matching >#{glob}<"
11
+
12
+
13
+ files = files.sort
14
+ ## puts files.inspect
15
+
16
+ files.each_with_index do |file,i|
17
+ puts "==> [#{i+1}/#{files.size}] - #{file}"
18
+ img = Image.read( file )
19
+
20
+ self << img ## todo/check: use add alias - why? why not?
21
+ end
22
+ end
23
+ end # class ImageComposite
24
+ end # module Pixelart
25
+
26
+
27
+
28
+ require_relative 'image/sample' ## check - change to downsample/pixelate - why? why not?
29
+
30
+
31
+
@@ -0,0 +1,41 @@
1
+
2
+ ## for more ideas on retry
3
+ ## see https://github.com/ooyala/retries
4
+
5
+
6
+ ## todo/check: use a different name than retry - why? why not?
7
+ def retry_on_error( max_tries: 3, &block )
8
+ errors = []
9
+ delay = 3 ## 3 secs
10
+
11
+ begin
12
+ block.call
13
+
14
+ ## note: add more exception here (separated by comma) like
15
+ ## rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED => e
16
+ rescue Net::ReadTimeout => e
17
+ ## (re)raise (use raise with arguments or such - why? why not?)
18
+ raise if errors.size >= max_tries
19
+
20
+ ## ReadTimeout, a subclass of Timeout::Error, is raised if a chunk of the response cannot be read within the read_timeout.
21
+ ## subclass of RuntimeError
22
+ ## subclass of StandardError
23
+ ## subclass of Exception
24
+ puts "!! ERROR - #{e}:"
25
+ pp e
26
+
27
+ errors << e
28
+
29
+ puts
30
+ puts "==> retrying (count=#{errors.size}, max_tries=#{max_tries}) in #{delay} sec(s)..."
31
+ sleep( delay )
32
+ retry
33
+ end
34
+
35
+ if errors.size > 0
36
+ puts " #{errors.size} retry attempt(s) on error(s):"
37
+ pp errors
38
+ end
39
+
40
+ errors
41
+ end