Skip to content

Instantly share code, notes, and snippets.

@NightFeather
Last active July 27, 2022 13:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save NightFeather/7e04f873c8776acdd84c68ce80f04a61 to your computer and use it in GitHub Desktop.
Save NightFeather/7e04f873c8776acdd84c68ce80f04a61 to your computer and use it in GitHub Desktop.
Some dirty script dumps info in ClipStudioPaint file.
#!/usr/bin/env ruby
require "stringio"
require "tempfile"
require "sqlite3"
module StringConvert
refine String do
def btol endianess = :BE
if endianess == :LE
unpack("Q<")[0]
elsif endianess == :BE
unpack("Q>")[0]
end
end
def btoi endianess = :BE
if endianess == :LE
unpack("L<")[0]
elsif endianess == :BE
unpack("L>")[0]
end
end
def btos endianess = :BE
if endianess == :LE
unpack("S<")[0]
elsif endianess == :BE
unpack("S>")[0]
end
end
end
end
using StringConvert
KNOWN_CHUNK = {
"CHNKHead" => lambda { |f| parse_head f },
"CHNKExta" => lambda { |f| parse_exta f },
"CHNKSQLi" => lambda { |f| parse_sqli f },
"CHNKFoot" => lambda { |f| parse_foot f }
}
def parse_head file
puts "Header @#{file.pos}:"
len = file.readpartial(8).btol
puts "Chunk size: #{len}"
body = StringIO.new file.readpartial(len)
return 0
end
# Possible exta block types:
# Data:
# |block len in byte (include) / 4 bytes|
# |sig len in word (exclude) / 2 bytes|"BlockDataBeginChunk" in ucs-2|
# |index / 4|
# |attribute? / 4|
# |attribute? / 4|
# |attribute? / 4|
# |extra block count / 4|
# |data len in word (exclude) / 4|data|
# |len in word (exclude) / 2|"BlockDataEndChunk" in ucs-2|
#
# Status
# |len in word (exclude) / 2 bytes|"BlockStatus" in ucs-2|
# |header len in word (include) / 4|
# |item count in word / 4|
# |item size in word / 4|
# |items|
#
# Checksums
# |len in word (exclude) / 2 bytes|"BlockCheckSum" in ucs-2|
# |header len in word (include) / 4|
# |item count in word / 4|
# |item size in word / 4|
# |items|
#
KNOWN_EXTA_TAGS = [
"BlockDataEndChunk".encode("UTF-16BE").bytes,
]
def parse_exta_data seg
len = seg.readpartial(4).btoi
tag = seg.readpartial(len*2)
if tag.bytes != "BlockDataBeginChunk".encode("UTF-16BE").bytes
puts " Unknown seg block tag: #{tag[0..20].inspect}"
return 1
end
index = seg.readpartial(4).btoi
attr0 = seg.readpartial(4).btoi
attr1 = seg.readpartial(4).btoi
attr2 = seg.readpartial(4).btoi
subcnt = seg.readpartial(4).btoi
if subcnt > 0
puts " Data Block[#{index}]:"
puts " attrs: #{attr0}, #{attr1}, #{attr2}; #{subcnt} objects"
subcnt.times do
elen = seg.readpartial(4).btoi
extra = seg.readpartial(elen)
puts " object: #{elen} bytes #{extra[0..16].unpack("H*")}"
end
end
len = seg.readpartial(4).btoi
tag = seg.readpartial(len*2)
if tag.bytes != "BlockDataEndChunk".encode("UTF-16BE").bytes
puts " Unknown seg block closing tag: #{tag[0..20].inspect}"
return 1
end
return 0
end
def parse_exta_checksum data
sz = data.readpartial(4).btoi - 4
cnt = data.readpartial(4).btoi
isz = data.readpartial(4).btoi
puts " Checksums[#{cnt}]: #{cnt.times.map { |_| data.readpartial(isz).unpack("H*")[0] }[0,4]}"
end
def parse_exta_status data
sz = data.readpartial(4).btoi - 4
cnt = data.readpartial(4).btoi
isz = data.readpartial(4).btoi
puts " Status[#{cnt}]: #{cnt.times.map { |_| data.readpartial(isz).unpack("H*")[0] }[0,4]}"
end
def parse_exta file
puts "ExtA @#{file.pos}:"
len = file.readpartial(8).btol
puts "Chunk size: #{len}"
chunk = StringIO.new file.readpartial len
len = chunk.readpartial(8).btol
chid = chunk.readpartial(len)
puts "Exta id: #{chid}"
len = chunk.readpartial(8).btol
puts "Data size: #{len}"
data = chunk.readpartial len
body = StringIO.new data
body.binmode
loop do
break if body.eof?
anchor = body.pos
len = body.readpartial(4).btoi
sub = body.readpartial(len*2)
if sub.bytes == "BlockCheckSum".encode("UTF-16BE").bytes
parse_exta_checksum body
elsif sub.bytes == "BlockStatus".encode("UTF-16BE").bytes
parse_exta_status body
else
body.seek anchor+4
parse_exta_data StringIO.new body.readpartial len-4
end
end
return 0
end
def parse_sqli file
puts "SQLite DB @#{file.pos}:"
len = file.readpartial(8).btol
puts "Chunk size: #{len}"
body = file.readpartial len
f = Tempfile.open "clip-parser-"
f.write body
f.close
begin
SQLite3::Database.new(f.path) do |db|
puts "Projects:"
res = db.execute('select ProjectName, DefaultPageWidth, DefaultPageHeight, DefaultPageResolution from Project')
res.each do |row|
puts " Project #{row[0]}: dim #{row[1]}x#{row[2]}, res #{row[3]}"
end
puts "Layers:"
res = db.execute(
'select Layer._PW_ID, LayerVisibility, LayerName, LayerOffsetX, LayerOffsetY' +
', CanvasWidth, CanvasHeight, CanvasResolution' +
' from Layer JOIN Canvas ON Layer.CanvasID = Canvas._PW_ID'
)
res.each do |row|
lid = row.shift
puts " #{row[0] == 1 ? 'v' : 'x'} Layer##{lid} #{row[1]}: pos #{row[2]},#{row[3]}, dim #{row[4]}x#{row[5]}, res #{row[6]}"
end
end
ensure
f.unlink
end
return 0
end
def parse_foot file
puts "Footer @#{file.pos}:"
file.readpartial(8)
unless file.eof?
puts "Extra data at the end of file"
return 1
end
return 0
end
def parse_clip file
sig = file.readpartial 8
if sig != "CSFCHUNK"
STDERR.puts "Invalid file signature"
return 1
end
len = ARGF.readpartial 8
puts "File size: #{len.unpack 'Q>'}"
len = ARGF.readpartial 8
puts "Head Offset: #{len.unpack 'Q>'}"
loop do
break if file.eof?
sig = file.readpartial 8
if KNOWN_CHUNK.key? sig
KNOWN_CHUNK[sig][file]
puts
else
STDERR.puts "Unkown block signature '#{sig}' at #{file.pos-8}"
return 1
end
end
end
parse_clip ARGF.binmode
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment