Last active
July 27, 2022 13:39
-
-
Save NightFeather/7e04f873c8776acdd84c68ce80f04a61 to your computer and use it in GitHub Desktop.
Some dirty script dumps info in ClipStudioPaint file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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