12 Commits

Author SHA1 Message Date
Ishotihadus 28df8f2271 version 3.9.1 2017-08-08 02:40:47 +09:00
Ishotihadus 9731fcde26 Revert "Modify console script"
This reverts commit d2ef991e76.
2017-08-08 02:39:40 +09:00
Ishotihadus 93b3b5789c Fix installation guide 2017-08-08 02:37:24 +09:00
Ishotihadus d2ef991e76 Modify console script 2017-08-08 02:37:07 +09:00
Ishotihadus ed1870920d Documentation & change permission of some methods and attributes 2017-08-08 02:31:27 +09:00
Ishotihadus 0007810984 Make faster half float conversion 2017-07-16 16:23:32 +09:00
Ishotihadus 1c6a2099ab Fix image decoding bug & Add some other image formats 2017-07-16 15:18:42 +09:00
Ishotihadus df03ce3d44 Add YAML mode to mikunyan-json 2017-07-09 01:11:45 +09:00
Ishotihadus d864b9f734 Add installation guide of development build to README 2017-07-08 22:03:13 +09:00
Ishotihadus cbedf9a749 Fix aligning fault while parsing object 2017-07-08 21:59:39 +09:00
Ishotihadus f4127166ac Correct syntax error in README 2017-07-08 21:24:57 +09:00
Ishotihadus 3cd7efdf5c Add ATSC output support 2017-07-08 21:23:51 +09:00
11 changed files with 674 additions and 196 deletions
+16 -5
View File
@@ -18,6 +18,14 @@ Or install it yourself as:
$ gem install mikunyan
If you want to install development build:
$ git clone https://github.com/Ishotihadus/mikunyan
$ cd mikunyan
$ bundle install
$ rake build
$ gem install -l pkg/mikunyan-3.9.x.gem
## Usage
### Basic Usage
@@ -96,7 +104,7 @@ obj.key
You can get png file directly from Texture2D asset. Output object's class is `ChunkyPNG::Image`.
Acceptable format is basic texture formats (1, 2, 3, 4, 5, 7 and 13) and ETC_RGB4 (34).
Only some basic texture formats (1--5, 7, 9, 13--20, 22, 62, and 63) and ETC_RGB4 (34) are available.
```ruby
require 'mikunyan/decoders'
@@ -111,16 +119,19 @@ img = Mikunyan::ImageDecoder.decode_object(obj)
img.save('mikunyan.png')
```
### Json Outputer
Mikunyan cannot decode ASTC files. Use `Mikunyan::ImageDecoder.create_astc_file` instead.
`mikunyan-json` is the executable command for converting unity3d to json.
### Json / YAML Outputer
`mikunyan-json` is an executable command for converting unity3d to json.
$ mikunyan-json bundle.unity3d > bundle.json
Available options:
- `--as-asset` (`-a`): interpret input file as not AssetBudnle but Asset
- `--pretty` (`-p`): prettify output json
- `--pretty` (`-p`): prettify output json (`mikunyan-json` only)
- `--yaml` (`-y`): YAML mode
## Dependencies
@@ -129,7 +140,7 @@ Available options:
- [bin_utils](https://rubygems.org/gems/bin_utils)
- [chunky_png](https://rubygems.org/gems/chunky_png)
Mikunyan use [oily_png](https://rubygems.org/gems/oily_png) instead of chunky_png if available.
Mikunyan uses [oily_png](https://rubygems.org/gems/oily_png) instead of chunky_png if available.
## FAQ
+19 -8
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env ruby
require 'mikunyan'
require 'json'
require 'base64'
def obj64(obj)
@@ -19,7 +18,7 @@ def obj64(obj)
end
end
opts = {:as_asset => false, :pretty => false}
opts = {:as_asset => false, :pretty => false, :yaml => false}
arg = nil
i = 0
while i < ARGV.count
@@ -29,9 +28,10 @@ while i < ARGV.count
opts[:as_asset] = true
when '--pretty', '-p'
opts[:pretty] = true
when '--yaml', '-y'
opts[:yaml] = true
else
warn("Unknown option: #{ARGV[i]}")
exit(1)
end
else
arg = ARGV[i] unless arg
@@ -39,6 +39,10 @@ while i < ARGV.count
i += 1
end
if option[:pretty] && option[:yaml]
warn("Option --pretty is ignored if --yaml is specified.")
end
unless File.file?(arg)
warn("File not found: #{arg}")
exit(1)
@@ -51,7 +55,7 @@ if opts[:as_asset]
objs = []
asset.path_ids.each do |e|
obj = asset.parse_object_simple(e)
objs << obj64(obj) if obj
objs << obj
end
assets[asset.name] = objs
else
@@ -60,14 +64,21 @@ else
objs = []
asset.path_ids.each do |e|
obj = asset.parse_object_simple(e)
objs << obj64(obj) if obj
objs << obj
end
assets[asset.name] = objs
end
end
if opts[:pretty]
puts JSON.pretty_generate(assets)
if opts[:yaml]
require 'yaml'
puts YAML.dump(assets)
else
puts JSON.generate(assets)
require 'json'
assets = assets.map{|k, v| [k, obj64(v)]}.to_h
if opts[:pretty]
puts JSON.pretty_generate(assets)
else
puts JSON.generate(assets)
end
end
+4
View File
@@ -6,3 +6,7 @@ require "mikunyan/binary_reader"
require "mikunyan/object_value"
require "mikunyan/type_tree"
require "mikunyan/constants"
# Module for deserializing Unity Assets and AssetBundles
module Mikunyan
end
+156 -99
View File
@@ -1,21 +1,133 @@
module Mikunyan
# Class for representing Unity Asset
# @attr_reader [String] name Asset name
# @attr_reader [Integer] format file format number
# @attr_reader [String] generator_version version string of generator
# @attr_reader [Integer] target_platform target platform number
# @attr_reader [Symbol] endian data endianness (:little or :big)
# @attr_reader [Array<Mikunyan::Asset::Klass>] klasses defined classes
# @attr_reader [Array<Mikunyan::Asset::ObjectData>] objects included objects
# @attr_reader [Array<Integer>] add_ids ?
# @attr_reader [Array<Mikunyan::Asset::Reference>] references reference data
class Asset
attr_accessor :name, :format, :generator_version, :target_platform, :endian, :klasses, :objects, :add_ids, :references
attr_reader :name, :format, :generator_version, :target_platform, :endian, :klasses, :objects, :add_ids, :references
# Struct for representing Asset class definition
# @attr [Integer] class_id class ID
# @attr [Integer,nil] script_id script ID
# @attr [String] hash hash value (16 or 32 bytes)
# @attr [Mikunyan::TypeTree, nil] type_tree given TypeTree
Klass = Struct.new(:class_id, :script_id, :hash, :type_tree)
# Struct for representing Asset object information
# @attr [Integer] path_id path ID
# @attr [Integer] offset data offset
# @attr [Integer] size data size
# @attr [Integer,nil] type_id type ID
# @attr [Integer,nil] class_id class ID
# @attr [Integer,nil] class_idx class definition index
# @attr [Boolean] destroyed? destroyed or not
# @attr [String] data binary data of object
ObjectData = Struct.new(:path_id, :offset, :size, :type_id, :class_id, :class_idx, :destroyed?, :data)
# Struct for representing Asset reference information
# @attr [String] path path
# @attr [String] guid GUID (16 bytes)
# @attr [Integer] type ?
# @attr [String] file_path Asset name
Reference = Struct.new(:path, :guid, :type, :file_path)
# Load Asset from binary string
# @param [String] bin binary data
# @param [String] name Asset name
# @return [Mikunyan::Asset] deserialized Asset object
def self.load(bin, name)
r = Asset.new(name)
r.send(:load, bin)
r
end
# Load Asset from file
# @param [String] file file name
# @param [String] name Asset name (automatically generated if not specified)
# @return [Mikunyan::Asset] deserialized Asset object
def self.file(file, name=nil)
name = File.basename(name, '.*') unless name
Asset.load(File.binread(file), name)
end
# Returns list of all path IDs
# @return [Array<Integer>] list of all path IDs
def path_ids
@objects.map{|e| e.path_id}
end
# Returns list of containers
# @return [Array<Hash>,nil] list of all containers
def containers
obj = parse_object(1)
return nil unless obj && obj.m_Container && obj.m_Container.array?
obj.m_Container.value.map do |e|
{:name => e.first.value, :preload_index => e.second.preloadIndex.value, :path_id => e.second.asset.m_PathID.value}
end
end
# Parse object of given path ID
# @param [Integer,ObjectData] path_id path ID or object
# @return [Mikunyan::ObjectValue,nil] parsed object
def parse_object(path_id)
if path_id.class == Integer
obj = @objects.find{|e| e.path_id == path_id}
return nil unless obj
elsif path_id.class == ObjectData
obj = path_id
else
return nil
end
klass = (obj.class_idx ? @klasses[obj.class_idx] : @klasses.find{|e| e.class_id == obj.class_id} || @klasses.find{|e| e.class_id == obj.type_id})
type_tree = Asset.parse_type_tree(klass)
return nil unless type_tree
parse_object_private(BinaryReader.new(obj.data, @endian), type_tree)
end
# Parse object of given path ID and simplify it
# @param [Integer,ObjectData] path_id path ID or object
# @return [Hash,nil] parsed object
def parse_object_simple(path_id)
Asset.object_simplify(parse_object(path_id))
end
# Returns object type name string
# @param [Integer,ObjectData] path_id path ID or object
# @return [String,nil] type name
def object_type(path_id)
if path_id.class == Integer
obj = @objects.find{|e| e.path_id == path_id}
return nil unless obj
elsif path_id.class == ObjectData
obj = path_id
else
return nil
end
klass = (obj.class_idx ? @klasses[obj.class_idx] : @klasses.find{|e| e.class_id == obj.class_id} || @klasses.find{|e| e.class_id == obj.type_id})
if klass && klass.type_tree && klass.type_tree.nodes[0]
klass.type_tree.nodes[0].type
elsif klass
Mikunyan::CLASS_ID[klass.class_id]
else
nil
end
end
private
def initialize(name)
@name = name
@endian = :big
end
def self.file(file, name)
r = Asset.new(name)
r.load(File.binread(file))
r
end
def load(bin)
br = BinaryReader.new(bin)
metadata_size = br.i32u
@@ -108,97 +220,6 @@ module Mikunyan
end
end
def path_ids
@objects.map{|e| e.path_id}
end
def containers
obj = parse_object(1)
return nil unless obj && obj.m_Container && obj.m_Container.array?
obj.m_Container.value.map do |e|
{:name => e.first.value, :preload_index => e.second.preloadIndex.value, :path_id => e.second.asset.m_PathID.value}
end
end
def parse_object(path_id)
if path_id.class == Integer
obj = @objects.find{|e| e.path_id == path_id}
return nil unless obj
elsif path_id.class == ObjectData
obj = path_id
else
return nil
end
klass = (obj.class_idx ? @klasses[obj.class_idx] : @klasses.find{|e| e.class_id == obj.class_id} || @klasses.find{|e| e.class_id == obj.type_id})
type_tree = Asset.parse_type_tree(klass)
return nil unless type_tree
parse_object_private(BinaryReader.new(obj.data, @endian), type_tree)
end
def parse_object_simple(path_id)
Asset.object_simplify(parse_object(path_id))
end
def object_type(path_id)
if path_id.class == Integer
obj = @objects.find{|e| e.path_id == path_id}
return nil unless obj
elsif path_id.class == ObjectData
obj = path_id
else
return nil
end
klass = (obj.class_idx ? @klasses[obj.class_idx] : @klasses.find{|e| e.class_id == obj.class_id} || @klasses.find{|e| e.class_id == obj.type_id})
if klass && klass.type_tree && klass.type_tree.nodes[0]
klass.type_tree.nodes[0].type
elsif klass
Mikunyan::CLASS_ID[klass.class_id]
else
nil
end
end
def self.parse_type_tree(klass)
return nil unless klass.type_tree
nodes = klass.type_tree.nodes
tree = {}
stack = []
nodes.each do |node|
this = {:name => node.name, :node => node, :children => []}
if node.depth == 0
tree = this
else
stack[node.depth - 1][:children] << this
end
stack[node.depth] = this
end
tree
end
def self.object_simplify(obj)
if obj.class != ObjectValue
obj
elsif obj.type == 'pair'
[object_simplify(obj['first']), object_simplify(obj['second'])]
elsif obj.type == 'map' && obj.array?
obj.value.map{|e| [object_simplify(e['first']), object_simplify(e['second'])] }.to_h
elsif obj.value?
object_simplify(obj.value)
elsif obj.array?
obj.value.map{|e| object_simplify(e)}
else
hash = {}
obj.keys.each do |key|
hash[key] = object_simplify(obj[key])
end
hash
end
end
private
def parse_object_private(br, type_tree)
r = nil
node = type_tree[:node]
@@ -230,7 +251,6 @@ module Mikunyan
children.each do |child|
r[child[:name]] = parse_object_private(br, child)
end
br.jmp(pos + node.size)
else
pos = br.pos
value = nil
@@ -268,5 +288,42 @@ module Mikunyan
br.align(4) if node.flags & 0x4000 != 0
r
end
def self.object_simplify(obj)
if obj.class != ObjectValue
obj
elsif obj.type == 'pair'
[object_simplify(obj['first']), object_simplify(obj['second'])]
elsif obj.type == 'map' && obj.array?
obj.value.map{|e| [object_simplify(e['first']), object_simplify(e['second'])] }.to_h
elsif obj.value?
object_simplify(obj.value)
elsif obj.array?
obj.value.map{|e| object_simplify(e)}
else
hash = {}
obj.keys.each do |key|
hash[key] = object_simplify(obj[key])
end
hash
end
end
def self.parse_type_tree(klass)
return nil unless klass.type_tree
nodes = klass.type_tree.nodes
tree = {}
stack = []
nodes.each do |node|
this = {:name => node.name, :node => node, :children => []}
if node.depth == 0
tree = this
else
stack[node.depth - 1][:children] << this
end
stack[node.depth] = this
end
tree
end
end
end
+18 -8
View File
@@ -1,19 +1,33 @@
require 'extlz4'
module Mikunyan
# Class for representing Unity AssetBundle
# @attr_reader [String] signature file signature (UnityRaw or UnityFS)
# @attr_reader [Integer] format file format number
# @attr_reader [String] unity_version version string of Unity to use this AssetBundle
# @attr_reader [String] generator_version version string of generator
# @attr_reader [Array<Mikunyan::Asset>] assets included Assets
class AssetBundle
attr_accessor :signature, :format, :unity_version, :generator_version, :assets
attr_reader :signature, :format, :unity_version, :generator_version, :assets
# Load AssetBundle from binary string
# @param [String] bin binary data
# @return [Mikunyan::AssetBundle] deserialized AssetBundle object
def self.load(bin)
r = AssetBundle.new
r.load(bin)
r.send(:load, bin)
r
end
# Load AssetBundle from file
# @param [String] file file name
# @return [Mikunyan::AssetBundle] deserialized AssetBundle object
def self.file(file)
AssetBundle.load(File.binread(file))
end
private
def load(bin)
br = BinaryReader.new(bin)
@signature = br.cstr
@@ -31,8 +45,6 @@ module Mikunyan
end
end
private
def load_unity_raw(br)
@assets = []
@@ -48,8 +60,7 @@ module Mikunyan
asset_size = br.i32u
br.jmp(asset_pos + asset_header_size - 4)
asset_data = br.read(asset_size)
asset = Asset.new(asset_name)
asset.load(asset_data)
asset = Asset.load(asset_data, asset_name)
@assets << asset
end
end
@@ -77,8 +88,7 @@ module Mikunyan
blocks.each{|b| raw_data << uncompress(br.read(b[:c]), b[:u], b[:f])}
asset_blocks.each do |b|
asset = Asset.new(b[:name])
asset.load(raw_data.byteslice(b[:offset], b[:size]))
asset = Asset.load(raw_data.byteslice(b[:offset], b[:size]), b[:name])
@assets << asset
end
end
+36 -2
View File
@@ -1,9 +1,16 @@
require 'bin_utils'
module Mikunyan
# Class for manipulating binary string
# @attr [Symbol] endian endianness
# @attr [Integer] pos position
# @attr [Integer] length data size
class BinaryReader
attr_accessor :endian, :pos, :length
# Constructor
# @param [String] data binary string
# @param [Symbol] endian endianness
def initialize(data, endian = :big)
@data = data
@pos = 0
@@ -11,104 +18,131 @@ module Mikunyan
@endian = endian
end
# Returns whether little endian or not
# @return [Boolean]
def little?
@endian == :little
end
def jmp(pos = 0)
# Jump to given position
# @param [Integer] pos position
def jmp(pos=0)
@pos = pos
end
def adv(size = 0)
# Advance position given size
# @param [Integer] size size
def adv(size=0)
@pos += size
end
# Round up position to multiple of given size
# @param [Integer] size size
def align(size)
@pos = (@pos + size - 1) / size * size
end
# Read given size of binary string and seek
# @param [Integer] size size
# @return [String] data
def read(size)
data = @data.byteslice(@pos, size)
@pos += size
data
end
# Read string until null character
# @return [String] string
def cstr
r = @data.unpack("@#{pos}Z*")[0]
@pos += r.bytesize + 1
r
end
# Read 8bit signed integer
def i8
i8s
end
# Read 8bit signed integer
def i8s
r = BinUtils.get_sint8(@data, @pos)
@pos += 1
r
end
# Read 8bit unsigned integer
def i8u
r = BinUtils.get_int8(@data, @pos)
@pos += 1
r
end
# Read 16bit signed integer
def i16
i16s
end
# Read 16bit signed integer
def i16s
r = little? ? BinUtils.get_sint16_le(@data, @pos) : BinUtils.get_sint16_be(@data, @pos)
@pos += 2
r
end
# Read 16bit unsigned integer
def i16u
r = little? ? BinUtils.get_int16_le(@data, @pos) : BinUtils.get_int16_be(@data, @pos)
@pos += 2
r
end
# Read 32bit signed integer
def i32
i32s
end
# Read 32bit signed integer
def i32s
r = little? ? BinUtils.get_sint32_le(@data, @pos) : BinUtils.get_sint32_be(@data, @pos)
@pos += 4
r
end
# Read 32bit unsigned integer
def i32u
r = little? ? BinUtils.get_int32_le(@data, @pos) : BinUtils.get_int32_be(@data, @pos)
@pos += 4
r
end
# Read 64bit signed integer
def i64
i64s
end
# Read 64bit signed integer
def i64s
r = little? ? BinUtils.get_sint64_le(@data, @pos) : BinUtils.get_sint64_be(@data, @pos)
@pos += 8
r
end
# Read 64bit unsigned integer
def i64u
r = little? ? BinUtils.get_int64_le(@data, @pos) : BinUtils.get_int64_be(@data, @pos)
@pos += 8
r
end
# Read 32bit floating point value
def float
r = little? ? @data.byteslice(@pos, 4).unpack('e')[0] : @data.byteslice(@pos, 4).unpack('g')[0]
@pos += 4
r
end
# Read 64bit floating point value
def double
r = little? ? @data.byteslice(@pos, 8).unpack('E')[0] : @data.byteslice(@pos, 8).unpack('G')[0]
@pos += 8
+1
View File
@@ -1,4 +1,5 @@
module Mikunyan
private
STRING_TABLE = {
0=>'AABB',
5=>'AnimationClip',
+363 -69
View File
@@ -1,11 +1,13 @@
begin; require 'oily_png'; rescue LoadError; require 'chunky_png'; end
require 'bin_utils'
require 'fiddle'
module Mikunyan
# Class for image decoding tools
class ImageDecoder
Etc1ModifierTable = [[2, 8], [5, 17], [9, 29], [13, 42], [18, 60], [24, 80], [33, 106], [47, 183]]
Etc1SubblockTable = [[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1]]
# Decode image from Mikunyan::ObjectValue
# @param [Mikunyan::ObjectValue] object object to decode
# @return [ChunkyPNG::Image,nil] decoded image
def self.decode_object(object)
return nil unless object.class == ObjectValue
@@ -27,100 +29,364 @@ module Mikunyan
when 2
decode_argb4444(width, height, bin, endian)
when 3
decode_rgb888(width, height, bin, endian)
decode_rgb888(width, height, bin)
when 4
decode_rgba8888(width, height, bin, endian)
decode_rgba8888(width, height, bin)
when 5
decode_argb8888(width, height, bin, endian)
decode_argb8888(width, height, bin)
when 7
decode_rgb565(width, height, bin, endian)
when 9
decode_r16(width, height, bin)
when 13
decode_rgba4444(width, height, bin, endian)
when 14
decode_bgra8888(width, height, bin)
when 15
decode_rhalf(width, height, bin, endian)
when 16
decode_rghalf(width, height, bin, endian)
when 17
decode_rgbahalf(width, height, bin, endian)
when 18
decode_rfloat(width, height, bin, endian)
when 19
decode_rgfloat(width, height, bin, endian)
when 20
decode_rgbafloat(width, height, bin, endian)
when 22
decode_rgb9e5float(width, height, bin, endian)
when 34
decode_etc1(width, height, bin)
when 62
decode_rg16(width, height, bin)
when 63
decode_r8(width, height, bin)
else
nil
end
end
def self.decode_etc1(width, height, bin)
bw = (width + 3) / 4
bh = (height + 3) / 4
pixels = "\0" * (64 * bw * bh)
pixels.force_encoding('ascii-8bit')
bh.times do |by|
bw.times do |bx|
block = decode_etc1_block(BinUtils.get_sint64_be(bin, (bx + by * bw) * 8)).pack('N16')
pixels[( bx * 4 * bh + by) * 16, 16] = block.byteslice( 0, 16)
pixels[((bx * 4 + 1) * bh + by) * 16, 16] = block.byteslice(16, 16)
pixels[((bx * 4 + 2) * bh + by) * 16, 16] = block.byteslice(32, 16)
pixels[((bx * 4 + 3) * bh + by) * 16, 16] = block.byteslice(48, 16)
end
# Decode image from RGBA4444 binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @param [Symbol] endian endianness of binary
# @return [ChunkyPNG::Image] decoded image
def self.decode_rgba4444(width, height, bin, endian = :big)
mem = String.new(capacity: width * height * 4)
(width * height).times do |i|
c = endian == :little ? BinUtils.get_int16_le(bin, i*2) : BinUtils.get_int16_be(bin, i*2)
c = ((c & 0xf000) << 12) | ((c & 0x0f00) << 8) | ((c & 0x00f0) << 4) | (c & 0x000f)
BinUtils.append_int32_be!(mem, c << 4 | c)
end
ChunkyPNG::Image.from_rgba_stream(bh * 4, bw * 4, pixels).rotate_right!.crop!(0, 0, width, height)
ChunkyPNG::Image.from_rgba_stream(width, height, mem)
end
def self.decode_a8(width, height, bin)
pixels = bin.unpack('C*').map do |c|
c << 24 | c << 16 | c << 8 | 0xff
# Decode image from ARGB4444 binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @param [Symbol] endian endianness of binary
# @return [ChunkyPNG::Image] decoded image
def self.decode_argb4444(width, height, bin, endian = :big)
mem = String.new(capacity: width * height * 4)
(width * height).times do |i|
c = endian == :little ? BinUtils.get_int16_le(bin, i*2) : BinUtils.get_int16_be(bin, i*2)
c = ((c & 0x0f00) << 16) | ((c & 0x00f0) << 12) | ((c & 0x000f) << 8) | ((c & 0xf000) >> 12)
BinUtils.append_int32_be!(mem, c << 4 | c)
end
ChunkyPNG::Image.new(width, height, pixels)
ChunkyPNG::Image.from_rgba_stream(width, height, mem)
end
# Decode image from RGB565 binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @param [Symbol] endian endianness of binary
# @return [ChunkyPNG::Image] decoded image
def self.decode_rgb565(width, height, bin, endian = :big)
pixels = bin.unpack(endian == :little ? 'v*' : 'n*').map do |c|
mem = String.new(capacity: width * height * 3)
(width * height).times do |i|
c = endian == :little ? BinUtils.get_int16_le(bin, i*2) : BinUtils.get_int16_be(bin, i*2)
r = (c & 0xf800) >> 8
g = (c & 0x07e0) >> 3
b = (c & 0x001f) << 3
r = r | r >> 5
g = g | g >> 6
b = b | b >> 5
r << 24 | g << 16 | b << 8 | 0xff
BinUtils.append_int8!(mem, r | r >> 5, g | g >> 6, b | b >> 5)
end
ChunkyPNG::Image.new(width, height, pixels)
ChunkyPNG::Image.from_rgb_stream(width, height, mem)
end
def self.decode_rgb888(width, height, bin, endian = :big)
if endian == :little
ChunkyPNG::Image.from_bgr_stream(width, height, bin)
# Decode image from A8 binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @return [ChunkyPNG::Image] decoded image
def self.decode_a8(width, height, bin)
mem = String.new(capacity: width * height * 3)
(width * height).times do |i|
c = BinUtils.get_int8(bin, i)
BinUtils.append_int8!(mem, c, c, c)
end
ChunkyPNG::Image.from_rgb_stream(width, height, mem)
end
# Decode image from R8 binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @return [ChunkyPNG::Image] decoded image
def self.decode_r8(width, height, bin)
decode_a8(width, height, bin)
end
# Decode image from RG16 binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @return [ChunkyPNG::Image] decoded image
def self.decode_rg16(width, height, bin)
mem = String.new(capacity: width * height * 3)
(width * height).times do |i|
BinUtils.append_int16_int8_be!(mem, BinUtils.get_int16_be(bin, i*2), 0)
end
ChunkyPNG::Image.from_rgb_stream(width, height, mem)
end
# Decode image from RGB24 binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @return [ChunkyPNG::Image] decoded image
def self.decode_rgb24(width, height, bin)
ChunkyPNG::Image.from_rgb_stream(width, height, bin)
end
# Decode image from RGBA32 binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @return [ChunkyPNG::Image] decoded image
def self.decode_rgba32(width, height, bin)
ChunkyPNG::Image.from_rgba_stream(width, height, bin)
end
# Decode image from ARGB32 binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @return [ChunkyPNG::Image] decoded image
def self.decode_argb32(width, height, bin)
mem = String.new(capacity: width * height * 4)
(width * height).times do |i|
c = BinUtils.get_int32_be(bin, i*4)
BinUtils.append_int32_be!(mem, ((c & 0x00ffffff) << 8) | ((c & 0xff000000) >> 24))
end
ChunkyPNG::Image.from_rgba_stream(width, height, mem)
end
# Decode image from BGRA32 binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @return [ChunkyPNG::Image] decoded image
def self.decode_bgra32(width, height, bin)
mem = String.new(capacity: width * height * 4)
(width * height).times do |i|
c = BinUtils.get_int32_le(bin, i*4)
BinUtils.append_int32_be!(mem, ((c & 0x00ffffff) << 8) | ((c & 0xff000000) >> 24))
end
ChunkyPNG::Image.from_rgba_stream(width, height, mem)
end
# Decode image from R16 binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @param [Symbol] endian endianness of binary
# @return [ChunkyPNG::Image] decoded image
def self.decode_r16(width, height, bin, endian = :big)
mem = String.new(capacity: width * height * 3)
(width * height).times do |i|
c = endian == :little ? BinUtils.get_int16_le(bin, i*2) : BinUtils.get_int16_be(bin, i*2)
c = f2i(r / 65535.0)
BinUtils.append_int8!(mem, c, c, c)
end
ChunkyPNG::Image.from_rgb_stream(width, height, mem)
end
# Decode image from RGB9e5 binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @param [Symbol] endian endianness of binary
# @return [ChunkyPNG::Image] decoded image
def self.decode_rgb9e5float(width, height, bin, endian = :big)
mem = String.new(capacity: width * height * 3)
(width * height).times do |i|
n = endian == :little ? BinUtils.get_int32_le(bin, i*4) : BinUtils.get_int32_be(bin, i*4)
e = (n & 0xf8000000) >> 27
r = (n & 0x7fc0000) >> 9
g = (n & 0x3fe00) >> 9
b = n & 0x1ff
r = (r / 512r + 1) * (2**(e-15))
g = (g / 512r + 1) * (2**(e-15))
b = (b / 512r + 1) * (2**(e-15))
BinUtils.append_int8!(mem, f2i(r), f2i(g), f2i(b))
end
ChunkyPNG::Image.from_rgb_stream(width, height, mem)
end
# Decode image from R Half-float binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @param [Symbol] endian endianness of binary
# @return [ChunkyPNG::Image] decoded image
def self.decode_rhalf(width, height, bin, endian = :big)
mem = String.new(capacity: width * height * 3)
(width * height).times do |i|
c = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i*2) : BinUtils.get_int16_be(bin, i*2)))
BinUtils.append_int8!(mem, c, c, c)
end
ChunkyPNG::Image.from_rgb_stream(width, height, mem)
end
# Decode image from RG Half-float binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @param [Symbol] endian endianness of binary
# @return [ChunkyPNG::Image] decoded image
def self.decode_rghalf(width, height, bin, endian = :big)
mem = String.new(capacity: width * height * 3)
(width * height).times do |i|
r = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i*4) : BinUtils.get_int16_be(bin, i*4)))
g = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i*4+2) : BinUtils.get_int16_be(bin, i*4+2)))
BinUtils.append_int8!(mem, r, g, 0)
end
ChunkyPNG::Image.from_rgb_stream(width, height, mem)
end
# Decode image from RGBA Half-float binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @param [Symbol] endian endianness of binary
# @return [ChunkyPNG::Image] decoded image
def self.decode_rgbahalf(width, height, bin, endian = :big)
mem = String.new(capacity: width * height * 4)
(width * height).times do |i|
r = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i*8) : BinUtils.get_int16_be(bin, i*8)))
g = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i*8+2) : BinUtils.get_int16_be(bin, i*8+2)))
b = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i*8+4) : BinUtils.get_int16_be(bin, i*8+4)))
a = f2i(n2f(endian == :little ? BinUtils.get_int16_le(bin, i*8+6) : BinUtils.get_int16_be(bin, i*8+6)))
BinUtils.append_int8!(mem, r, g, b, a)
end
ChunkyPNG::Image.from_rgba_stream(width, height, mem)
end
# Decode image from R float binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @param [Symbol] endian endianness of binary
# @return [ChunkyPNG::Image] decoded image
def self.decode_rfloat(width, height, bin, endian = :big)
mem = String.new(capacity: width * height * 3)
unpackstr = endian == :little ? 'e' : 'g'
(width * height).times do |i|
c = f2i(bin.byteslice(i*4, 4).unpack(unpackstr)[0])
BinUtils.append_int8!(mem, c, c, c)
end
ChunkyPNG::Image.from_rgb_stream(width, height, mem)
end
# Decode image from RG float binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @param [Symbol] endian endianness of binary
# @return [ChunkyPNG::Image] decoded image
def self.decode_rgfloat(width, height, bin, endian = :big)
mem = String.new(capacity: width * height * 3)
unpackstr = endian == :little ? 'e2' : 'g2'
(width * height).times do |i|
r, g = bin.byteslice(i*8, 8).unpack(unpackstr)
BinUtils.append_int8!(mem, f2i(r), f2i(g), 0)
end
ChunkyPNG::Image.from_rgb_stream(width, height, mem)
end
# Decode image from RGBA float binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @param [Symbol] endian endianness of binary
# @return [ChunkyPNG::Image] decoded image
def self.decode_rgbafloat(width, height, bin, endian = :big)
mem = String.new(capacity: width * height * 4)
unpackstr = endian == :little ? 'e4' : 'g4'
(width * height).times do |i|
r, g, b, a = bin.byteslice(i*16, 16).unpack(unpackstr)
BinUtils.append_int8!(mem, f2i(r), f2i(g), f2i(b), f2i(a))
end
ChunkyPNG::Image.from_rgba_stream(width, height, mem)
end
# Decode image from ETC1 compressed binary
# @param [Integer] width image width
# @param [Integer] height image height
# @param [String] bin binary to decode
# @return [ChunkyPNG::Image] decoded image
def self.decode_etc1(width, height, bin)
bw = (width + 3) / 4
bh = (height + 3) / 4
mem = Fiddle::Pointer.malloc(bw * bh * 48)
bh.times do |by|
bw.times do |bx|
block = decode_etc1_block(BinUtils.get_sint64_be(bin, (bx + by * bw) * 8))
16.times do |i|
mem[((i / 4 + bx * 4) + (i % 4 + by * 4) * bw * 4) * 3, 3] = block[i]
end
end
end
ChunkyPNG::Image.from_rgb_stream(bw * 4, bh * 4, mem.to_str).crop!(0, 0, width, height)
end
# Create ASTC file data from ObjectValue
# @param [Mikunyan::ObjectValue,Hash] object target object
# @return [String,nil] created file
def self.create_astc_file(object)
astc_list = {
48 => 4, 49 => 5, 50 => 6, 51 => 8, 52 => 10, 53 => 12,
54 => 4, 55 => 5, 56 => 6, 57 => 8, 58 => 10, 59 => 12
}
width = object['m_Width']
height = object['m_Height']
fmt = object['m_TextureFormat']
bin = object['image data']
width = width.value if width.class == ObjectValue
height = height.value if height.class == ObjectValue
fmt = fmt.value if fmt.class == ObjectValue
bin = bin.value if bin.class == ObjectValue
if width && height && fmt && astc_list[fmt]
header = "\x13\xAB\xA1\x5C".force_encoding('ascii-8bit')
header << [astc_list[fmt], astc_list[fmt], 1].pack("C*")
header << [width].pack("V").byteslice(0, 3)
header << [height].pack("V").byteslice(0, 3)
header << "\x01\x00\x00"
header + bin
else
ChunkyPNG::Image.from_rgb_stream(width, height, bin)
nil
end
end
def self.decode_rgba4444(width, height, bin, endian = :big)
pixels = bin.unpack(endian == :little ? 'v*' : 'n*').map do |c|
c = ((c & 0xf000) << 12) | ((c & 0x0f00) << 8) | ((c & 0x00f0) << 4) | (c & 0x000f)
c << 4 | c
end
ChunkyPNG::Image.new(width, height, pixels)
end
def self.decode_argb4444(width, height, bin, endian = :big)
pixels = bin.unpack(endian == :little ? 'v*' : 'n*').map do |c|
c = ((c & 0x0f00) << 16) | ((c & 0x00f0) << 12) | ((c & 0x000f) << 8) | ((c & 0xf000) >> 12)
c << 4 | c
end
ChunkyPNG::Image.new(width, height, pixels)
end
def self.decode_rgba8888(width, height, bin, endian = :big)
if endian == :little
ChunkyPNG::Image.from_abgr_stream(width, height, bin)
else
ChunkyPNG::Image.from_rgba_stream(width, height, bin)
end
end
def self.decode_argb8888(width, height, bin, endian = :big)
pixels = bin.unpack(endian == :little ? 'V*' : 'N*').map do |c|
c = ((c & 0x00ffffff) << 8) | ((c & 0xff000000) >> 24)
end
ChunkyPNG::Image.new(width, height, pixels)
end
private
Etc1ModifierTable = [[2, 8], [5, 17], [9, 29], [13, 42], [18, 60], [24, 80], [33, 106], [47, 183]]
Etc1SubblockTable = [[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1]]
def self.decode_etc1_block(bin)
colors = []
codes = [bin >> 37 & 7, bin >> 34 & 7]
@@ -152,10 +418,38 @@ module Mikunyan
r = (color >> 16 & 0xff) + modifier
g = (color >> 8 & 0xff) + modifier
b = (color & 0xff) + modifier
r = (r > 255 ? 255 : r < 0 ? 0 : r)
g = (g > 255 ? 255 : g < 0 ? 0 : g)
b = (b > 255 ? 255 : b < 0 ? 0 : b)
r << 24 | g << 16 | b << 8 | 0xff
r.clamp(0, 255).chr + g.clamp(0, 255).chr + b.clamp(0, 255).chr
end
# convert 16bit float
def self.n2f(n)
case n
when 0x0000
0.0
when 0x8000
-0.0
when 0x7c00
Float::INFINITY
when 0xfc00
-Float::INFINITY
else
s = n & 0x8000 != 0
e = n & 0x7c00
f = n & 0x03ff
case e
when 0x7c00
Float::NAN
when 0
(s ? -f : f) * 2.0**-24
else
(s ? -1 : 1) * (f / 1024.0 + 1) * (2.0 ** ((e >> 10)-15))
end
end
end
# [0.0,1.0] -> [0,255]
def self.f2i(d)
(d * 255).round.clamp(0, 255)
end
end
end
+39 -4
View File
@@ -1,7 +1,18 @@
module Mikunyan
# Class for representing decoded object
# @attr [String] name object name
# @attr [String] type object type name
# @attr [Object] value object
# @attr [Symbol] endian endianness
# @attr [Boolean] is_struct
class ObjectValue
attr_accessor :name, :type, :value, :endian, :is_struct
# Constructor
# @param [String] name object name
# @param [String] type object type name
# @param [Symbol] endian endianness
# @param [Object] value object
def initialize(name, type, endian, value = nil)
@name = name
@type = type
@@ -11,46 +22,70 @@ module Mikunyan
@attr = {}
end
# Return whether object is array or not
# @return [Boolean]
def array?
value && value.class == Array
end
# Return whether object is value or not
# @return [Boolean]
def value?
value && value.class != Array
end
# Return whether object is struct or not
# @return [Boolean]
def struct?
is_struct
end
# Return all keys
# @return [Array] list of keys
def keys
@attr.keys
end
# Return whether object contains key
# @param [String] key
# @return [Boolean]
def key?(key)
@attr.key?(key)
end
# Return value
# @return [Object] value
def []
@value
end
def [](name)
if array? && name.class == Integer
@value[name]
# Return value of selected index or key
# @param [Integer,String] i index or key
# @return [Object] value
def [](i)
if array? && i.class == Integer
@value[i]
else
@attr[name]
@attr[i]
end
end
# Set value of selected key
# @param [String] name key
# @param [Object] value value
# @return [Object] value
def []=(name, value)
@attr[name] = value
end
# Return value of called key
# @param [String] name key
# @return [Object] value
def method_missing(name, *args)
@attr[name.to_s]
end
# Implementation of respond_to_missing?
def respond_to_missing?(symbol, include_private)
@attr.key?(symbol.to_s)
end
+20
View File
@@ -1,8 +1,22 @@
module Mikunyan
# Class for representing TypeTree
# @attr [Array<Mikunyan::TypeTree::Node>] nodes list of all nodes
class TypeTree
attr_accessor :nodes
# Struct for representing Node in TypeTree
# @attr [String] version version string
# @attr [Integer] depth depth of node (>= 0)
# @attr [Boolean] array? array node or not
# @attr [String] type type name
# @attr [String] name node (attribute) name
# @attr [Integer] index index in node list
# @attr [Integer] flags flags of node
Node = Struct.new(:version, :depth, :array?, :type, :name, :size, :index, :flags)
# Create TypeTree from binary string (new version)
# @param [Mikunyan::BinaryReader] br
# @return [Mikunyan::TypeTree] created TypeTree
def self.load(br)
nodes = []
node_count = br.i32u
@@ -28,6 +42,9 @@ module Mikunyan
r
end
# Create TypeTree from binary string (legacy version)
# @param [Mikunyan::BinaryReader] br
# @return [Mikunyan::TypeTree] created TypeTree
def self.load_legacy(br)
nodes = []
stack = [0]
@@ -49,6 +66,9 @@ module Mikunyan
r
end
# Create default TypeTree from hash string (if exists)
# @param [String] hash
# @return [Mikunyan::TypeTree,nil] created TypeTree
def self.load_default(hash)
hash_str = hash.unpack('H*')[0]
file = File.expand_path("../typetrees/#{hash_str}.dat", __FILE__)
+2 -1
View File
@@ -1,3 +1,4 @@
module Mikunyan
VERSION = "3.9.0"
# version string
VERSION = "3.9.1"
end