dotfiles/.aegisub/automation/autoload/lyger.Image2ASS.lua

569 lines
15 KiB
Lua

--[[
README:
***ALSO REQUIRES(1) CONVERT.EXE STANDALONE FROM IMAGEMAGICK***
(1) Only if you want to use non-bitmap images or the resize capability.
Save the binary to your Aegisub\automation\autoload directory:
http://www.mediafire.com/download/zdxn75nte1n6cq6/convert.exe
Image to .ass
Converts a 24-bit or 32-bit bitmap image pixel-by-pixel into an .ass drawing.
Runs a basic compression algorithm on the resultant drawing. Compression
level can be adjusted from the interface (the higher the number, the more
compressed).
Time a line to the times you want the image to appear. If positioning or
alignment tags are present in the line, and you select "from line" for the
position handling, then these tags will be used on the drawing. Any other
text in the original line will be ignored.
Also allows you to use an alpha mask, which is a grayscale bitmap loaded
separately. Black represents solid and white represents transparent, just
like .ass color codes. This is inverted compared to, for example, Photoshop
alpha masks, so you may have to invert your mask before loading it.
If you're using an alpha mask, the original image should extend beyond the
mask. For example, if you have textured text on a white background as your
main image, the text will already be antialised to the white background.
When you apply the alpha mask, the antialiasing pixels will be antialiased
again, making a weird white glow. Instead, your main image should be the
solid texture, thus creating smooth antialiasing pixels when you apply the
mask.
Be aware that due to subpixel alignment errors in the current version of
xy-vsfilter, the image may appear transparent or have subpixel gaps if you
use certain alignments and positionings. Corner alignments and whole-number
positions are the most reliable.
Supports \move but you are strongly advised NOT to use it.
]]
script_name = "Image to .ass"
script_description = "Converts bitmap image to .ass lines."
script_version = "2.3.0"
script_author = "lyger"
script_namespace = "lyger.Image2ASS"
local DependencyControl = require("l0.DependencyControl")
local rec = DependencyControl{
feed = "https://raw.githubusercontent.com/TypesettingTools/lyger-Aegisub-Scripts/master/DependencyControl.json",
{ "aegisub.util", "ffi" }
}
local util, ffi = rec:requireModules()
--[[ Detect whether to use *nix or Windows style paths. ]]--
local winpaths = ffi.os == "Windows"
function make_config()
return
{
{x=0,y=0,height=1,width=1,class="label",label="Output drawing"},
{x=1,y=0,height=1,width=1,class="dropdown",name="otype",
items={"all on one line","with each row on a new line"},
value="with each row on a new line"},
{x=0,y=1,height=1,width=1,class="label",label="Position:"},
{x=1,y=1,height=1,width=1,class="dropdown",name="postype",
items={"from line","default"},value="from line"},
{x=0,y=2,height=1,width=1,class="label",label="Compression:"},
{x=1,y=2,height=1,width=1,class="intedit",name="tol",
max=3000,min=1,value=40},
{x=0,y=3,height=1,width=1,class="label",label="Resize image (%):"},
{x=1,y=3,height=1,width=1,class="floatedit",name="resize",
max=100,min=1,value=100},
{x=0,y=4,height=1,width=1,class="label",label="Pixel size:"},
{x=1,y=4,height=1,width=1,class="intedit",name="pxsize",
max=250,min=1,value=1}
}
end
--Parse out properties from a bitmap header
function parse_header(fn)
--Open
_file=io.open(fn,"rb")
--Read irrelevant data
_file:read(18)
--Read in the pixel width of the image
_width=_file:read(4)
swidth=""
for _w in _width:gmatch(".") do
swidth=string.format("%02X",string.byte(_w))..swidth
end
_iw=tonumber(swidth,16)
--Read the pixel height of the image, including its orientation
_height=_file:read(4)
sheight=""
for _h in _height:gmatch(".") do
sheight=string.format("%02X",string.byte(_h))..sheight
end
_ih=tonumber(sheight,16)
--Handle two's complement. Good god this is hacky
if _ih>tonumber("7FFFFFFF",16) then
_ih=_ih-tonumber("FFFFFFFF",16)-1
end
_file:read(2)
--Read in whether the bitmap is 24 or 32 bit (fuck handling anything less)
bitsize=string.byte(_file:read(1))
_ws=bitsize/8
_file:close()
--Return width, height, and wordsize
return _iw, _ih, _ws
end
function convert_to_bmp(filename,scale)
local cfname=filename:gsub("%.%a+$",".bmp")
local prefix=aegisub.decode_path("?data").."\\automation\\autoload\\"
--Make sure convert binary exists
local cex=io.open(prefix.."convert.exe")
if cex==nil then
aegisub.dialog.display({{x=0,y=0,width=1,height=1,class="label",
label="convert.exe not found. Make sure the\n"..
"executable is in your automation\\autoload\n"..
"directory."}},{"OK"})
aegisub.cancel()
else
cex:close()
end
--Write the self-deleting batch and run it
opts="-type TrueColor"
if scale then
opts=opts.." -resize "..scale.."%%"
cfname=cfname:gsub("%.bmp$","_"..scale..".bmp")
end
--Make sure the filenames are different
if cfname==filename then
cfname=cfname:gsub("%.bmp$","_copy.bmp")
end
local command="\""..prefix.."convert.exe\" \""
..filename.."\" "..opts.." BMP3:\""..cfname.."\""
convertfile=io.open(prefix.."image2ass_converter.bat","wb")
convertfile:write(command.."\ndel %0")
convertfile:close()
os.execute("\""..prefix.."image2ass_converter.bat\"")
return cfname
end
function run_i2a(subs,sel)
local ffilter = "Bitmap images (.bmp)|*.bmp"
if winpaths then ffilter="All images (.bmp; .jpg; .png; .gif)|*.bmp;*.jpg;*.png;*.gif" end
--Prompt for bitmap image
fname=aegisub.dialog.open("Select image","","",ffilter,false,true)
if not fname then aegisub.cancel() end
cleanfiles={}
--Convert to .bmp if not .bmp already
if not fname:lower():match("%.bmp$") then
fname=convert_to_bmp(fname)
table.insert(cleanfiles,fname)
end
--Initialize some values
dconfig=make_config()
results=nil
afname=""
alpha=false
buttons={"Convert","Add alpha mask","Cancel"}
repeat
--Show options
pressed,results=aegisub.dialog.display(dconfig,buttons)
if pressed=="Cancel" then aegisub.cancel()
elseif pressed=="Add alpha mask" then
--Prompt for bitmap image
afname=aegisub.dialog.open("Select image to use as alpha mask","","",ffilter,false,true)
if not afname then
aegisub.dialog.display({{x=0,y=0,width=1,height=1,class="label",
label="Error, invalid file."}},{"OK"})
else
alpha=true
if not afname:lower():match(".bmp$") then
afname=convert_to_bmp(afname)
table.insert(cleanfiles,afname)
end
table.insert(dconfig,{x=0,y=5,height=1,width=2,class="label",
label="Alpha mask loaded."})
table.remove(buttons,2)
end
end
until pressed=="Convert"
if results["resize"]~=100 then
fname=convert_to_bmp(fname,results["resize"])
table.insert(cleanfiles,fname)
if alpha then
afname=convert_to_bmp(afname,results["resize"])
table.insert(cleanfiles,afname)
end
end
--Parse headers
rowsize,imgheight,wordsize=parse_header(fname)
awordsize=0
if alpha then
_aiw,_aih,awordsize=parse_header(afname)
if _aiw~=rowsize or _aih~=imgheight then
aegisub.dialog.display({{x=0,y=0,width=1,height=1,class="label",
label="Error, alpha channel is not the same size\n"..
"as image."}},{"OK"})
aegisub.cancel()
end
end
--Check wordsize
if (wordsize~=3 and wordsize~=4) or (alpha and awordsize~=3 and awordsize~=4) then
aegisub.dialog.display({{x=0,y=0,width=1,height=1,class="label",
label="Error, images must be 24-bit or 32-bit bitmap."}},{"OK"})
aegisub.cancel()
end
--Compile results
tolerance=results["tol"]
px=results["pxsize"]
oneline=(results["otype"]=="all on one line")
readpos=(results["postype"]=="from line")
--Open the file
file=io.open(fname,"rb")
file:read(54)
if alpha then
afile=io.open(afname,"rb")
afile:read(54)
end
--Distance in rgb space
local function cdist(r1,g1,b1,r2,g2,b2)
return math.sqrt((r1-r2)^2+(g1-g2)^2+(b1-b2)^2)
end
--Counter variables
counter=0
bytesread=0
abytesread=0
--Stores previous color used, standard deviation, last color
_r,_g,_b=-1*tolerance-1,-1*tolerance-1,-1*tolerance-1
sr,sg,sb=_r,_g,_b
lr,lg,lb=_r,_g,_b
--Stores current and previous alphas
aval="00"
praval="00"
ppraval="00"
--Previous color code used
pcode=""
--Width of next shape to draw
width=1
--String to store each line
line=""
--Table to store processed image
imgtable={}
--Force alpha tag if alpha channel is on
if alpha then ppraval="GG" end
while true do
byte=file:read(wordsize)
bytesread=bytesread+wordsize
if byte==nil then break end
b,g,r=byte:match("^(.)(.)(.)")
if b==nil or g==nil or r==nil then break end
r=string.byte(r)
g=string.byte(g)
b=string.byte(b)
--Temporary old values of the average
_tr,_tg,_tb=_r,_g,_b
if _r>=0 then
--Keep a running average of the rgb values
_r=_r+(r-_r)/(width+1)
_g=_g+(g-_g)/(width+1)
_b=_b+(b-_b)/(width+1)
--Keep a running standard deviation or the rbg values
sr=sr+(r-_tr)*(r-_r)
sg=sg+(g-_tg)*(g-_g)
sb=sb+(b-_tb)*(b-_b)
end
--Read and average alpha channel
if alpha then
abyte=afile:read(awordsize)
abytesread=abytesread+awordsize
ab,ag,ar=abyte:match("^(.)(.)(.)")
aval=string.format("%02X",
math.floor((string.byte(ab)+string.byte(ag)+string.byte(ar))/3))
end
if ((cdist(lr,lg,lb,r,g,b)<tolerance
and cdist(sr,sg,sb,0,0,0)<tolerance and aval==praval))
or (aval=="FF" and aval==praval) then
--Increase width
width=width+1
else
--Only add the colors if this is not the first pixel in a row
if _r>=0 then
shape=string.format("m 0 0 l 0 %d %d %d %d 0",px,width*px,px,width*px)
--Add color code
code=string.format("%02X%02X%02X",_tb,_tg,_tr)
line=line.."{"
if praval~=ppraval then
line=line.."\\alpha&H"..praval.."&"
end
if code~=pcode and praval~="FF" then
line=line.."\\c&H"..code.."&"
pcode=code
end
line=line.."}"..shape
end
--Reset width and colors
width=1
_r,_g,_b=r,g,b
sr,sg,sb=0,0,0
--Set last alpha value
if alpha then
ppraval=praval
praval=aval
end
end
--Set last r,g,b values
lr,lg,lb=r,g,b
counter=counter+1
if counter%rowsize==0 then
--Read filler bytes
file:read(math.abs((4-bytesread)%4))
if alpha then afile:read(math.abs((4-abytesread)%4)) end
bytesread=0
abytesread=0
--Dump current shape on end of line
code=string.format("%02X%02X%02X",_b,_g,_r)
shape=string.format("m 0 0 l 0 %d %d %d %d 0",px,width*px,px,width*px)
line=line.."{"
if paval~=aval then
line=line.."\\alpha&H"..aval.."&"
end
if pcode~=code then
line=line.."\\c&H"..code.."&"
end
line=line.."}"..shape
--Sometimes the algorithm inserts blank tags. I can't be bothered
--to figure out why, so just remove them
while line:match("0{}m") do
line=line:gsub(
"m 0 0 l 0 "..px.." (%d+) "..px.." %d+ 0{}m 0 0 l 0 "..px.." (%d+) "..px.." %d+ 0",
function(w1,w2)
local nw=tonumber(w1)+tonumber(w2)
return string.format("m 0 0 l 0 %d %d %d %d 0",px,nw,px,nw)
end)
end
--Add line to table
if imgheight<0 then
table.insert(imgtable,line)
else
table.insert(imgtable,1,line)
end
--Progress report
rprog=math.floor(counter/rowsize)
aegisub.progress.set(rprog*100/math.abs(imgheight))
aegisub.progress.task(string.format("Processing %d/%d rows",rprog,math.abs(imgheight)))
--Reset the line
line=""
--Reset previous colors
_r=-1*tolerance-1
_g=-1*tolerance-1
_b=-1*tolerance-1
sr,sg,sb=_r,_g,_b
lr,lg,lb=_r,_g,_b
--Reset alpha if alpha channel is on
if alpha then praval="GG" end
--Reset previous code
pcode=""
--Reset width
width=1
end
end
--Close files
file:close()
if alpha then afile:close() end
aegisub.progress.task("Writing to subtitles...")--No progress bar because this should be near instant
--Read in the line
line=subs[sel[1]]
line.comment=false
--Get style info
meta,styles=karaskel.collect_head(subs,false)
lstyle=styles[line.style]
--Estimate filesize
fsize=0
--New selection
newsel={}
--If the drawing is to be written all on one line
if oneline then
oline=util.copy(line)
rtext="{"
if readpos then
if oline.text:match("\\move") then
mtag=oline.text:match("(\\move%b())")
rtext=rtext..mtag
end
if oline.text:match("\\pos") then
ptag=oline.text:match("(\\pos%b())")
rtext=rtext..ptag
end
end
if lstyle.outline~=0 then rtext=rtext.."\\bord0" end
if lstyle.shadow~=0 then rtext=rtext.."\\shad0" end
rtext=rtext.."}"
rtext=rtext:gsub("{}","")
prefix="{\\p1}"
eol="{\\p0}\\N"
for i,row in ipairs(imgtable) do
rtext=rtext..prefix..row
if i~=#imgtable then rtext=rtext..eol end
end
oline.text=rtext
fsize=#rtext+44+#oline.style+#oline.effect+#oline.actor
subs.insert(sel[1]+1,oline)
newsel={sel[1]+1}
--If the drawing is to be written across multiple lines
else
prefix="{\\p1"
pfmt="\\pos(%d,%d)"
align="\\an7"
bx,by,bx2,by2=0,0,0,0
if readpos then
if line.text:match("\\move") then
mx1,my1,mx2,my2,msuf=line.text:match(
"\\move%(([%d%.%-]+),([%d%.%-]+),([%d%.%-]+),([%d%.%-]+)([^%)]*%))")
bx=tonumber(mx1)
by=tonumber(my1)
bx2=tonumber(mx2)
by2=tonumber(my2)
pfmt="\\move(%d,%d,%d,%d"..msuf
end
if line.text:match("\\pos") then
p_x,p_y=line.text:match("\\pos%(([%d%.%-]+),([%d%.%-]+)%)")
bx=tonumber(p_x)
by=tonumber(p_y)
end
align=line.text:match("\\an?%d%d?") or align
end
prefix=prefix..align..pfmt
if lstyle.outline~=0 then prefix=prefix.."\\bord0" end
if lstyle.shadow~=0 then prefix=prefix.."\\shad0" end
prefix=prefix.."}"
inserts=1
for i,row in ipairs(imgtable) do
_,alphanum=row:gsub("\\alpha","\\alpha")
dowrite=true
if alphanum==1 then
alphavalue=row:match("\\alpha&H(..)&")
if alphavalue=="FF" then dowrite=false end
end
if dowrite then
nline=util.copy(line)
nline.text=prefix:format(bx,by+(i-1)*px,bx2,by2+(i-1)*px)
nline.text=nline.text..row
subs.insert(sel[1]+inserts,nline)
table.insert(newsel,sel[1]+inserts)
inserts=inserts+1
fsize=fsize+#nline.text+44+#nline.style+#nline.effect+#nline.actor
end
end
end
line.text=fname
line.comment=true
subs[sel[1]]=line
mbytes=string.format("%.2f",fsize/1048576):gsub("0+$",""):gsub("%.$","")
msg="Conversion finished.\nApproximate added filesize: "..mbytes.." MB."
if mbytes=="0" then
kbytes=string.format("%.2f",fsize/1025):gsub("0+$",""):gsub("%.$","")
msg="Conversion finished.\nApproximate added filesize: "..kbytes.." kB."
else
end
aegisub.dialog.display({{x=0,y=0,width=1,height=1,class="label",
label=msg}},
{"OK"})
--Delete generated bitmap, if applicable
for _,cleanf in ipairs(cleanfiles) do os.execute("del \""..cleanf.."\"") end
aegisub.set_undo_point(script_name)
return newsel
end
rec:registerMacro(run_i2a)