Introduction
ASSFoundation, henceforth called assf, is a module that aims to make working with subtitle object efficient. It does most of the heavy lifting so that we can do more with less lines of code in our script. No need to use messy regular expressions, define the types of tags and the nature of their values. No need to reinvent the wheel and write functions to work in subtitle object or write your own module for common things.
This guide assumes that you already know how to write Aegisub scripts and know the basics of Moonscript.
Here's the very basics:
export script_name = "name of the script"
export script_description = "description of your script"
export script_version = "0.0.1"
export script_author = "you"
export script_namespace = "namespace of your script"
DependencyControl = require "l0.DependencyControl"
depctrl = DependencyControl{
feed: "",
{
{"a-mo.LineCollection", version: "1.3.0", url: "https://github.com/TypesettingTools/Aegisub-Motion",
feed: "https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json"},
{"l0.ASSFoundation", version: "0.4.0", url: "https://github.com/TypesettingTools/ASSFoundation",
feed: "https://raw.githubusercontent.com/TypesettingTools/ASSFoundation/master/DependencyControl.json"}
}
}
LineCollection, ASS = depctrl\requireModules!
logger = depctrl\getLogger!
functionName = (sub, sel, act) ->
-- stuff goes here
depctrl\registerMacro functionName
This is the framework that all your scripts will have. Here we import LineCollection and assf and define a function called functionName. This function name is what we register in aegisub in the last line. Everything we do below will go inside the function where --stuff goes here
is written. We also define logger for logging purposes but if you don't need to log anything, you can remove that line.
LineCollection
LineCollection is actually not a part of assf but almost anything that assf does will act on the line table generated by LineCollection. The line table generated by LineCollection will have all the elements of a normal line table like start_time, actor, end_time etc but in addition, it also adds other elements. Apart from the basic fields of line table, following fields are now available to you.:
Fields | Meaning | Type |
---|---|---|
duration | duration of line in ms | integer |
startFrame | start frame of a line | integer |
endFrame | end frame of a line | integer |
styleRef | style table | table |
number | number of line in subtitle file | integer |
humanizedNumber | number of line as seen in Aegisub | integer |
Some of the methods LineCollection provides to us to modify subtitles are:
Method | Usage | Meaning |
---|---|---|
LineCollection | lines = LineCollection sub, sel | Add all the selected lines to a variable named lines . It ignores commented lines. |
replaceLines | lines\replaceLines! | Any change you make to lines will be put back to the subtitle |
deleteLines | lines\deleteLines! | Delete all the lines |
lines\deleteLines tbl | Provide a table of line to delete those lines only | |
insertLines | lines\insertLines! | insert lines to subtitle file |
newLines\insertLines | Insert a new set of lines that you defined called newLines | |
addline | lines\addLine | add line to subtitle file |
You can actually work in the line table generated by LineCollection without using assf as shown in an example below where we change the effect of the line to "Actor".
functionName = (sub, sel) ->
lines = LineCollection sub, sel
for line in *lines
line.effect = "Actor"
lines\replaceLines!
But we'll use LineCollection alongside assf to get the most out of both of them.
Logger
Logger is a logging module from dependency control that you can use to log messages. If you do not pass log level, default log level is 2. By default, Aegisub's log level is set to 3 which means that the message above 3 wont be seen by end user unless they set the log level higher themself. The script exits after showing message if the log level is below 2.
logger\log "A simple message inside quotes"
logger\log 4, "A simple message inside quotes but with log level 4"
-- dump is the most useful part of logger as far as debugging goes.
-- You can pass a table and it'll show you a nice formatted view with all it's keys and values.
logger\dump table
-- With predefined log levels
logger\fatal "message" -- log level 0
logger\error "message" -- log level 1
logger\warn "message" -- log level 2
logger\hint "message" -- log level 3
logger\debug "message" -- log level 4
logger\trace "message" -- log level 5
logger\assert condition, "Show this message if the condition is false" -- log level 1
ASSFoundation
Name of tags as understood by assf
Tag | Assf name |
---|---|
\fscx | scale_x |
\fscy | scale_y |
\an | align |
\frz | angle |
\fry | angle_y |
\frx | angle_x |
\bord | outline |
\xbord | outline_x |
\ybord | outline_y |
\shad | shadow |
\xshad | shadow_x |
\yshad | shadow_y |
\r | reset |
\pos | position |
\move | move |
\org | origin |
\alpha | alpha |
\1a | alpha1 |
\2a | alpha2 |
\3a | alpha3 |
\4a | alpha4 |
\1c | color1 |
\2c | color2 |
\3c | color3 |
\4c | color4 |
\clip | clip_vect |
\iclip | iclip_vect |
\clip | clip_rect |
\iclip | iclip_rect |
\p | drawing |
\be | blur_edges |
\blur | blur |
\fax | shear_x |
\fay | shear_y |
\b | bold |
\i | italic |
\u | underline |
\s | strikeout |
\fsp | spacing |
\fs | fontsize |
\fn | fontname |
\k | k_fill |
\kf | k_sweep |
\ko | k_bord |
\q | wrapstyle |
\fad | fade_simple |
\fade | fade |
\t | transform |
Loop through all selected lines
This loops through all selected lines. If the number of lines is 0, exits out. Here line
is the line table for the current line and i
is the index of line i.e. the first selected line has index 1 and second has index 2 and so on.
Line Data
This creates a line data for each line. Everything assf does will be acted on this line data.
lines = LineCollection sub, sel
return if #lines.lines == 0
lines\runCallback (lines, line, i) ->
data = ASS\parse line
Sections
In assf, a single line can have four different types of sections. Their names make them self-explanatory so I'll only list them.
- ASS.Section.Text
- ASS.Section.Tag
- ASS.Section.Drawing
- ASS.Section.Comment
Work in different sections of a line
lines = LineCollection sub, sel
return if #lines.lines == 0
lines\runCallback (lines, line, i) ->
data = ASS\parse line
data\callback (section) ->
if section.class == ASS.Section.Tag
-- do stuff to tags
elseif section.class == ASS.Section.Text
-- do stuff to text
elseif section.class == ASS.Section.Comment
-- do stuff to comment
elseif section.class == ASS.Section.Drawing
-- do stuff to drawing
If you care, you can download this example script and try this in different types of lines (with/without tags, comments, drawings, text, gbc etc.) to get a idea of how assf treats different sections of a line.
If you want to work in an individual section, you can do the following:
Example: Working with only text of line
data = ASS\parse line
data\callback ((section) ->
--obtain the text
text = section.value
-- do stuff to text of line
), ASS.Section.Text
Example: Working with only tags of line
data = ASS\parse line
data\callback ((section) ->
for tags in *section\getTags!
-- do stuff to tags of line
), ASS.Section.Tag
Modifying text
Change whole text
data = ASS\parse line
data\callback ((section) ->
section.value = "Change the text to this."
), ASS.Section.Text
data\commit!
Append text to the existing text
string\append str, sep
str = string you want to append to the text
sep = string that seperates your text and str
data = ASS\parse line
data\callback ((section) ->
section\append "string you want to append"
), ASS.Section.Text
data\commit!
Prepend text to the existing text
data = ASS\parse line
data\callback ((section) ->
section\prepend "string you want to prepend"
), ASS.Section.Text
data\commit!
Replace text
string\replace pattern, replacement, plainMatch, useRegex
If useRegex
is true, regular expressions (re module) can be used else gsub is used for replacement.
If plainMatch
is true, then useRegex
is automatically set to false and any thing that must be escaped in the pattern is escaped and any thing that must be escaped in the pattern is escaped for gsub to work.
data = ASS\parse line
data\callback ((section) ->
section\replace "pattern you want to replace", "replacment for pattern"
), ASS.Section.Text
data\commit!
Modifying Tags
getDefaultTags
data = ASS\parse line
styleTags = data\getDefaultTags!
-- Then we can access a table for each tag as such
angleTable = styleTags.tags.angle
-- We can directly get the value as:
angle= styleTags.tags.angle\get!
insertDefaultTags
Parameters | Meaning | Type |
---|---|---|
tagnames | names of tag or table of tag's name | string or table |
index | index of section i.e. 1 = first section and so on | integer |
sectionPostion | position where tag is inserted ({\1\2\3...}) | integer |
direct | if true, index considers all sections and errors if index is not tag section else only considers tag sections | boolean |
data = ASS\parse line
data\insertDefaultTags "align" -- Insert a single tag
data\insertDefaultTags {"scale_x", "scale_y", "blur"} -- Insert multiple tags
data\insertDefaultTags "fontname", 2 -- Insert tag at second tag block
data\commit!
A way to change the value of the default tag you inserted is:
getEffectiveTags
Parameters | Meaning | Type |
---|---|---|
index | index of section of which effective tag is being required (It considers all types of sections) | integer |
includeDefault | include style tag value if override tag is not found | boolean |
includePrevious | consider the tag value of tag section before current section | boolean |
copyTags | boolean |
data = ASS\parse line
-- Get effective tag values for last section
tags = (data\getEffectiveTags -1, true, true, false).tags
-- Then you can insert tags to line in following way
data\removeTags "align"
data\insertTags tags.align
data\commit!
removeTags
Remove tags already present in the line
Parameters | Meaning | Type |
---|---|---|
tagnames | names of tag or table of tag's name | string or table |
start | index of section from where to start removing tags | integer |
end | index of selection upto which to remove tags | integer |
relative | boolean |
data = ASS\parse line
-- You can pass a single tag name to delete it
data\removeTags "outline"
-- You can pass a table of tag names to delete them all
data\removeTags {"align", "blur"}
-- Removes first instance of the tag blur
data\removeTags "blur", 1, 1
-- Removes second to fifth instance of the tag blur
data\removeTags "blur", 2, 5
data\commit!
NOTE: After you remove tags using removeTags
, there might be stray '{}' left if that's the only tag. It is recommended that you use cleanTags to remove them.
TODO: link the guide portion of clean
It is very useful to get a value of a tag to a variable using removeTags
as well
insertTags
Insert tags to current line.
Parameters | Meaning | Type |
---|---|---|
tag | tag instance you want to insert | assf tag instance |
start | index of section from where to start removing tags | integer |
end | index of selection upto which to remove tags | integer |
relative | if true, only tag section is considered (i.e. 2 = second tag section not literal 2nd section in line) | boolean |
data = ASS\parse line
tags = (data\getEffectiveTags -1, true, true, false).tags
data\insertTags tags.shadow
data\insertTags tags.scale_x, 2
data\insertTags tags.scale_y, -1
createTag
Create a new instance of tags.
data = ASS\parse line
data\removeTags "position"
pos = ASS\createTag 'position', 5, 50
data\insertTags pos
data\commit!
Alternatively, you can direct insert a tag without assigning it to a variable.
data = ASS\parse line
data\insertTags {ASS\createTag 'position', 5, 50} -- \pos(5,50)
data\insertTags {ASS\createTag 'outline', 5} -- \bord5
data\insertTags {ASS\createTag 'blur', 0.8} -- \blur0.8
data\insertTags {ASS\createTag 'move', 0, 0, 50, 50} -- \move(0,0,50,50)
data\insertTags {ASS\createTag 'move', 0, 0, 50, 50, 25, 500} -- \move(0,0,50,50,25,500)
data\insertTags {ASS\createTag 'clip_rect', 50, 50, 500, 500} -- \clip(50,50,500,500)
data\insertTags {ASS\createTag 'drawing', 1} -- \p1
data\insertTags {ASS\createTag "transform", {tags}, t1, t2} -- transfrom all tags inside {tags} from time t1 to t2
-- creating Vectorial Clip
m = ASS.Draw.Move
l = ASS.Draw.Line
data\insertTags {ASS\createTag 'clip_vect', {m(0,0), l(500,500), l(700,100)}} -- \clip( m 0 0 l 500 500 700 100)
data\commit!
Some tags like color and alpha can have special parameters
data\insertTags {ASS\createTag 'color1', 15, 34, 22} -- b, g, r
data\insertTags {ASS\createTag 'alpha', 110}
-- You can also give hex as a parameter
data\insertTags {ASS\createTag 'alpha', "6E"}
replaceTags
Replace tags present in a line.
Parameters | Meaning | Type |
---|---|---|
tagList | tag instance you want to replace | assf tag instance |
start | index of section from where to start replacing tags | integer |
end | index of selection upto which to replace tags | integer |
relative | if true, only tag section is considered (i.e. 2 = second tag section not literal 2nd section in line) | boolean |
insertRemaining | if the tag you're replacing does not already exist in the line, it adds the tag to the line. | boolean |
data = ASS\parse line
bord = ASS\createTag "outline", 5
data\replaceTags bord -- Replace all bord tags
data\replaceTags bord, 2, 5, true -- Replace bord from 2nd to 5th tag block
data\commit!
You can also create a new instance of tag and replace it in a single line
getTags
Obtain the tags present in the line.
Parameters | Meaning | Type |
---|---|---|
tag name | name of the tag | string or table |
start | index of section from where to look for tags | integer |
end | index of selection upto which to look for tags | integer |
relative | if true, only tag section is considered (i.e. 2 = second tag section not literal 2nd section in line) | boolean |
data = ASS\parse line
bord = data\getTags "outline" -- A table that has all the border values in this line
first = bord[1]\get! -- To get the first border value
first = bord[1].value -- Another way to get value
nth = bord[n]\get! -- Get nth border value
for b in *bord -- To get all values of border tags in a line
logger\dump b\get!
To get multiple tag values
data = ASS\parse line
for tag in *data\getTags {"outline", "scale_x"}
tagname = tag.__tag.name
tagvalue = tag\getTagParams!
logger\log "#{tagname}(#{tagvalue})"
tag = data\getTags "tagname", 1 -- Get first instance of tag value
tag = data\getTags "tagname", 1, 3 -- Get from first to third instance of tag value
tag = data\getTags "tagname", -1, -3 -- Get last instance of tag value
modTags
Modify tags already present in the line. If the tag is not found in the line, it does nothing.
Parameters | Meaning | Type |
---|---|---|
tag name | name of the tag | string or table |
callback | callback | |
start | index of section from where to modify tags | integer |
end | index of selection upto which to modify tags | integer |
relative | boolean |
data = ASS\parse line
data\modTags "outline", (tag) -> tag\add 1 -- Add 1 to all \bord in the line
data\modTags {"outline"}, ((tag) -> tag\add 1), 1, 3 -- Add 1 to \bord in 1st to 3rd tag block
data\commit!
You can have multiple lines for modifying tags, run functions inside them and so on.
data = ASS\parse line
data\modTags {"scale_x"}, (tag) ->
old_value = tag.value
new_value = old_value * 5
tag.value = new_value
data\commit!
getPosition
Get position, alignment or org.
Parameters | Meaning | Type |
---|---|---|
style | line style | string |
align | alignment | integer |
forceDefault | ignore override \pos | boolean |
pos, align, org = data\getPosition!
-- Get position of the line if alignent were 7
pos, align, org = data\getPosition nil, 7
-- To get the actual value {\bord1do the following
pos_x, pos_y = pos.x, pos.y
org_x, org_y = org.x, org,y
an = align
-- Alternatively, you can also do something like this if you need value
x, y = data\getPosition!\getTagParams!
getLineBounds
Then to get co-ordinate of the bounding box:
x1, y1 = bound[1].x, bound[1].y -- To get co-ordinate of top left boundary
x2, y2 = bound[2].x, bound[2].y -- To get co-ordinate of bottom right boundary
To get the dimension of the bounding box():
An easy way to check if the text is visible in the screen or not is to check if height or width is 0 or not
To check if the text is animated or not (transform, move etc)
Then you can also loop over all the fbf lines to get their bounding box. This is also the same number of lines you'd get if you do line2fbf so you could probably do other interesting things by using this loop.
getString
To loop over tag blocks and get their string.
Something similar for text
cleanTags
Clean/Sort/Merge tags in the line.
Parameters | Meaning | Type | Default |
---|---|---|---|
level | integer | 3 | |
mergeConsecutiveSections | level for cleanTags | boolean | true |
defaultToKeep | adjust position after splitting | table | |
tagSortOrder | add origin if needed to maintain appearance | table |
Clean Level:
- 0: no cleaning
- 1: remove empty tag sections
- 2: deduplicate tags inside sections
- 3: deduplicate tags globally,
- 4: remove tags matching the style defaults and otherwise ineffective tags
data = ASS\parse line
data\cleanTags!
data\cleanTags 1
data\cleanTags nil, nil, nil, tagSortOrder -- where tagSortOrder is the table of tags
Note: Make sure the tagSortOrder has all the tags in the table if you want to run it. There might be some unwanted results if done otherwise. Also the tags in the tagSortOrder must be the names of the tags as understood by assf.
Splitting Stuff
splitAtIntervals
Parameters | Meaning | Type | Default |
---|---|---|---|
callback | callback | ||
cleanLevel | level for cleanTags | integer | 3 |
reposition | adjust position after splitting | boolean | true |
writeOrigin | add origin if needed to maintain appearance | boolean |
data = ASS\parse line
char = data\splitAtIntervals 1, 4, false -- split by characters
char = data\splitAtIntervals 2, 4, true, true -- split by 2 characters
-- get nth split
n_char = char[n]
Working with split characters:
lines = LineCollection sub, sel
-- define newLines where we'll be adding each split characters
newLines = LineCollection sub
lines\runCallback (lines, line, i) ->
data = ASS\parse line
charLines = data\splitAtIntervals 2, 4, false -- split by characters
-- Looping through each interval
for char in *charLines
charData = char.ASS
-- get effective tag for each split
effTags = charData.sections[1]\getEffectiveTags(true,true).tags
-- do stuff to each split here like adding new tags
-- don't forget to add their position as well
-- commit changes to the split
charData\commit!
newLines\addLine char
-- add new lines that contains all the splits
newLines\insertLines!
-- remove orignal lines
lines\deleteLines!
repositionSplitLines
After using any other split methods of assf, if you want the splits to maintain position, use this.
Parameters | Meaning | Type | Default |
---|---|---|---|
splitLines | split lines from other split methods | integer | 3 |
writeOrigin | add origin if needed to maintain appearance | boolean | true |
lines = LineCollection sub, sel
newLines = LineCollection sub
lines\runCallback (lines, line, i) ->
data = ASS\parse line
charLines = data\splitAtIntervals 2, 4, false -- split by 2 characters
charLines = data\repositionSplitLines charLines
for char in *charLines
charData = char.ASS
charData\commit!
newLines\addLine char
newLines\insertLines!
lines\deleteLines!
splitAtTags
Split line at each tag block and get new lines
Parameters | Meaning | Type | Default |
---|---|---|---|
cleanLevel | clean level for \cleanTags | integer | 3 |
reposition | repostion split tags | boolean | |
writeOrigin | add origin if needed to maintain appearance | boolean |
lines = LineCollection sub, sel
newLines = LineCollection sub
lines\runCallback (lines, line, i) ->
data = ASS\parse line
splitLines = data\splitAtTags nil, true, true
for char in *splitLines
charData = char.ASS
-- Here you can do stuff to each line before committing if you desire
charData\commit!
newLines\addLine char
newLines\insertLines!
lines\deleteLines!
Stripping Stuff
stripTags
Remove all tags present in the line.
stripText
Remove all text present in the line.
stripComments
Remove all comments present in the line.
stripDrawings
Remove all drawings as well as \\p
tags present in the line.
Miscellaneous
trim
Trim whitespace in the beginning or end of the text.
getTagCount
Get the total number of tags present in the line. Start as well as inline override tags - it counts all of them.
getTextExtents
Get the width of the text no matter it's orientation.
getTextMetrics
metrics = data\getTextMetrics!
-- Access different metrics
ascent = metrics.ascent
descent = metrics.descent
internal_leading = metrics.internal_leading
external_leading = metrics.external_leading
height = metrics.height
width = metrics.width
getSectionCount
Get the exact count of the type of section
tagSectionCount = data\getSectionCount ASS.Section.Tag
textSectionCount = data\getSectionCount ASS.Section.Text
drawingSectionCount = data\getSectionCount ASS.Section.Drawing
commentSectionCount = data\getSectionCount ASS.Section.Comment
getTextLength
Get the total number of characters in the text. Letters, spaces and punctuations.
isAnimated
Check if the text is animated or not. Checks if there are transforms, move, fade or karaoke tags. It's intelligent enough to know that a single frame line can't be animated so it'll return false even if a single frame line has those tags.
reverse
It reverses the text of the current line. It's huge.
will become .eguh s'tI
. This is sometimes useful but the most useful part of this function is that it keeps the tags intact. So for example if you run this in gbc, the gbc will remain intact while the text will be reversed.
data\reverse!
-- you can also save it to a variable and do some stuff to it before committing it
rev = data\reverse!
Working with move
Check if move exists in a line
data = ASS\parse line
pos = data\getPosition!
if pos.class == ASS.Tag.Move
logger\log "Move exists"
else
logger\log "Move does not exist"
Check if the move is simple or not
-- if move is simple "\move(x1,y1,x2,y2)"
pos.__tag.signature == "simple"
-- else "\move(x1,y1,x2,y2,t1,t2)"
pos.__tag.signature == "default"
Obtain values of move tag
if pos.class == ASS.Tag.Move and pos.__tag.signature == "simple"
x1, y1, x2, y2 = pos\getTagParams!
elseif pos.class == ASS.Tag.Move and pos.__tag.signature == "default"
x1, y1, x2, y2, t1, t2 = pos\getTagParams!
It is also possible to obtain each value individually if you desire.
Working with transform
There are 3 types of transform
- "accel" - \t(accel,style modifiers)
- "default" - \t(t1,t2,accel,style modifiers)
- "time" - \t(t1,t2,style modifiers)
To check if tags are transformed
data = ASS\parse line
tags = data\getEffectiveTags -1, true, true, false
transformed = tags\checkTransformed!
-- Get the list of all the tags that were transformed
for key, _ in pairs transformed
logger\log key
To get the idea of how transform table is structured, you can run the following script
Obtain all parameters of transforms
data = ASS\parse line
transforms = data\getTags "transform"
for tr in *transforms
t1, t2, tags = tr\getTagParams!
Change the times or accel in transforms (Here adding 100 to start time, 200 to end time and 5 to accel of all transforms)
data = ASS\parse line
transforms = data\getTags "transform"
for index, tr in ipairs transforms
tr.startTime\add 100
tr.endTime\add 200
tr.accel\add 5
data\commit!
Modify tags inside transforms (In this case add 50 to fscx as an example)
data = ASS\parse line
transforms = data\getTags "transform"
for index, tr in ipairs transforms
for tag in *tr.tags\getTags!
if tag.__tag.name == "scale_x"
tag\add 50
data\commit!
Working with fade
There are two types of fade. Simple fade (\fad) and complex fade (\fade).
-- Simple fade
fad = data\getTags "fade_simple" -- Get simple fade
t1, t2 = fad\getTagParams! -- Get parameters of simple fade
-- Complex fade
fade = data\getTags "fade" -- Get complex fade
a1, a2, a3, t1, t2, t3, t4 = fade\getTagParams! -- Get parameters of simple fade
Parameters of fade tags:
Parameters in assf | Equivalent fade parameters |
---|---|
inAlpha | a1 |
midAlpha | a2 |
outAlpha | a3 |
inStartTime | t1 |
inDuration | t2 |
outStartTime | t3 |
outDuration | t4 |
Modifying fades