texel 0.1.0

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.
data/lib/texel.rb ADDED
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "texel/version"
4
+ require_relative "texel/errors"
5
+ require_relative "texel/info"
6
+ require_relative "texel/pixel_data"
7
+ require_relative "texel/image"
8
+ require_relative "texel/color_transform"
9
+ require_relative "texel/native"
10
+ require_relative "texel/resampler"
11
+ require_relative "texel/image_transformations"
12
+ require_relative "texel/codec"
13
+ require_relative "texel/registry"
14
+ require_relative "texel/source_reader"
15
+ require_relative "texel/codecs/qoi"
16
+ require_relative "texel/codecs/stb"
17
+
18
+ module Texel
19
+ class << self
20
+ def load(source, channels: nil, dtype: nil, flip_y: false, color_space: nil)
21
+ validate_channels!(channels) if channels
22
+ validate_dtype!(dtype) if dtype
23
+ validate_color_space!(color_space) if color_space
24
+
25
+ input = SourceReader.read(source)
26
+ format = detect_format!(input)
27
+ codec = Registry.codec_for!(format, operation: :decode)
28
+ image = codec.decode(input.bytes, channels: channels, dtype: dtype, format: format)
29
+ image = image.dup_with(color_space: color_space) if color_space
30
+ flip_y ? image.flip_y : image
31
+ end
32
+
33
+ def info(source)
34
+ input = SourceReader.read(source)
35
+ format = detect_format!(input)
36
+ Registry.codec_for!(format, operation: :decode).info(input.bytes, format: format)
37
+ end
38
+
39
+ def encode(image, format, **options)
40
+ raise FormatError, "expected a Texel::Image" unless image.is_a?(Image)
41
+
42
+ normalized = normalize_format(format)
43
+ encoded = String(Registry.codec_for!(normalized, operation: :encode).encode(image, normalized, **options))
44
+ encoded = encoded.dup.force_encoding(Encoding::BINARY) unless encoded.encoding == Encoding::BINARY
45
+ encoded.freeze
46
+ end
47
+
48
+ def save(image, destination, format: nil, **options)
49
+ resolved_format = format || destination_format(destination)
50
+ bytes = encode(image, resolved_format, **options)
51
+ return write_io(destination, bytes) if destination.respond_to?(:write)
52
+
53
+ File.binwrite(destination, bytes)
54
+ destination
55
+ rescue SystemCallError => e
56
+ raise EncodeError, "could not write image: #{e.message}"
57
+ end
58
+
59
+ def load_all(paths, threads: 4, **options)
60
+ unless threads.is_a?(Integer) && threads.positive?
61
+ raise FormatError, "threads must be a positive integer"
62
+ end
63
+
64
+ sources = paths.to_a
65
+ return [] if sources.empty?
66
+
67
+ queue = Queue.new
68
+ sources.each_index { |index| queue << index }
69
+ results = Array.new(sources.length)
70
+ errors = Array.new(sources.length)
71
+ workers = [threads, sources.length].min.times.map do
72
+ Thread.new do
73
+ loop do
74
+ index = queue.pop(true)
75
+ results[index] = load(sources[index], **options)
76
+ rescue ThreadError
77
+ break
78
+ rescue StandardError => e
79
+ errors[index] = e
80
+ end
81
+ end
82
+ end
83
+ workers.each(&:join)
84
+ raise errors.compact.first if errors.any?
85
+
86
+ results
87
+ end
88
+
89
+ private
90
+
91
+ def detect_format!(input)
92
+ Registry.detect(input.bytes.byteslice(0, 32).to_s, extension: input.extension) ||
93
+ raise(FormatError, "unknown image format")
94
+ end
95
+
96
+ def destination_format(destination)
97
+ unless destination.respond_to?(:to_path) || destination.is_a?(String)
98
+ raise FormatError, "format is required when saving to an IO"
99
+ end
100
+
101
+ path = destination.respond_to?(:to_path) ? destination.to_path : destination
102
+ extension = SourceReader.extension_for_path(path)
103
+ raise FormatError, "destination has no image format extension" unless extension
104
+
105
+ normalize_format(extension)
106
+ end
107
+
108
+ def normalize_format(format)
109
+ normalized = format.to_s.delete_prefix(".").downcase.to_sym
110
+ normalized == :jpeg ? :jpg : normalized
111
+ end
112
+
113
+ def write_io(io, bytes)
114
+ written = io.write(bytes)
115
+ raise EncodeError, "IO wrote #{written} of #{bytes.bytesize} bytes" if written.is_a?(Integer) && written != bytes.bytesize
116
+
117
+ io
118
+ end
119
+
120
+ def validate_channels!(channels)
121
+ return if channels.is_a?(Integer) && channels.between?(1, 4)
122
+
123
+ raise FormatError, "channels must be between 1 and 4"
124
+ end
125
+
126
+ def validate_dtype!(dtype)
127
+ return if Image::DTYPE_SIZES.key?(dtype)
128
+
129
+ raise FormatError, "unsupported dtype #{dtype.inspect}"
130
+ end
131
+
132
+ def validate_color_space!(color_space)
133
+ return if Image::COLOR_SPACES.include?(color_space)
134
+
135
+ raise FormatError, "unsupported color space #{color_space.inspect}"
136
+ end
137
+ end
138
+
139
+ Registry.register(:png, magic: "\x89PNG\r\n\x1a\n".b, codec: Codecs::Stb)
140
+ Registry.register(:jpg, magic: "\xff\xd8\xff".b, codec: Codecs::Stb, extensions: %w[jpg jpeg])
141
+ Registry.register(:gif, magic: ["GIF87a".b, "GIF89a".b], codec: Codecs::Stb)
142
+ Registry.register(:bmp, magic: "BM".b, codec: Codecs::Stb)
143
+ Registry.register(
144
+ :tga,
145
+ magic: lambda do |bytes|
146
+ bytes.bytesize >= 18 && [0, 1].include?(bytes.getbyte(1)) &&
147
+ [1, 2, 3, 9, 10, 11].include?(bytes.getbyte(2)) &&
148
+ [8, 15, 16, 24, 32].include?(bytes.getbyte(16)) &&
149
+ bytes.byteslice(12, 4).unpack("S<2").all?(&:positive?)
150
+ end,
151
+ codec: Codecs::Stb
152
+ )
153
+ Registry.register(:hdr, magic: ["#?RADIANCE".b, "#?RGBE".b], codec: Codecs::Stb)
154
+ Registry.register(:psd, magic: "8BPS".b, codec: Codecs::Stb)
155
+ Registry.register(:qoi, magic: "qoif".b, codec: Codecs::Qoi)
156
+ Registry.register(:ktx2, magic: "\xabKTX 20\xbb\r\n\x1a\n".b, codec: nil, required_gem: "texel-ktx")
157
+ Registry.register(:exr, magic: "\x76\x2f\x31\x01".b, codec: nil, required_gem: "texel-exr")
158
+ Registry.register(
159
+ :webp,
160
+ magic: ->(bytes) { bytes.start_with?("RIFF".b) && bytes.byteslice(8, 4) == "WEBP".b },
161
+ codec: nil,
162
+ required_gem: "texel-webp"
163
+ )
164
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: texel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yudai Takada
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |-
13
+ Texel provides a compact immutable image container, stb-based codecs, QOI,
14
+ color and channel conversion, mipmap generation, and GPU upload helpers.
15
+ email:
16
+ - t.yudai92@gmail.com
17
+ executables: []
18
+ extensions:
19
+ - ext/texel/extconf.rb
20
+ extra_rdoc_files: []
21
+ files:
22
+ - LICENSE.txt
23
+ - README.md
24
+ - Rakefile
25
+ - ext/texel/extconf.rb
26
+ - ext/texel/shim.c
27
+ - ext/texel/shim.h
28
+ - ext/texel/texel_native.c
29
+ - ext/texel/vendor/README.md
30
+ - ext/texel/vendor/stb_image.h
31
+ - ext/texel/vendor/stb_image_resize2.h
32
+ - ext/texel/vendor/stb_image_write.h
33
+ - lib/texel.rb
34
+ - lib/texel/codec.rb
35
+ - lib/texel/codecs/qoi.rb
36
+ - lib/texel/codecs/stb.rb
37
+ - lib/texel/color_transform.rb
38
+ - lib/texel/errors.rb
39
+ - lib/texel/gl.rb
40
+ - lib/texel/half_float.rb
41
+ - lib/texel/image.rb
42
+ - lib/texel/image_transformations.rb
43
+ - lib/texel/info.rb
44
+ - lib/texel/native.rb
45
+ - lib/texel/pixel_data.rb
46
+ - lib/texel/registry.rb
47
+ - lib/texel/resampler.rb
48
+ - lib/texel/source_reader.rb
49
+ - lib/texel/version.rb
50
+ - lib/texel/wgpu.rb
51
+ homepage: https://github.com/ydah/texel
52
+ licenses:
53
+ - MIT
54
+ metadata:
55
+ source_code_uri: https://github.com/ydah/texel
56
+ changelog_uri: https://github.com/ydah/texel/releases
57
+ rubygems_mfa_required: 'true'
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: 3.2.0
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubygems_version: 4.0.6
73
+ specification_version: 4
74
+ summary: Texture-oriented image decoding, conversion, and GPU upload
75
+ test_files: []