-
Notifications
You must be signed in to change notification settings - Fork 5
/
l0.ASSWipe.moon
446 lines (377 loc) · 20.9 KB
/
l0.ASSWipe.moon
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
export script_name = "ASSWipe"
export script_description = "Performs script cleanup, removes unnecessary tags and lines."
export script_version = "0.5.0"
export script_author = "line0"
export script_namespace = "l0.ASSWipe"
DependencyControl = require "l0.DependencyControl"
version = DependencyControl{
feed: "https://raw.githubusercontent.com/TypesettingTools/line0-Aegisub-Scripts/master/DependencyControl.json",
{
{"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"},
{"a-mo.ConfigHandler", version: "1.1.4", url: "https://github.com/TypesettingTools/Aegisub-Motion",
feed: "https://raw.githubusercontent.com/TypesettingTools/Aegisub-Motion/DepCtrl/DependencyControl.json"},
{"l0.ASSFoundation", version: "0.5.0", url: "https://github.com/TypesettingTools/ASSFoundation",
feed: "https://raw.githubusercontent.com/TypesettingTools/ASSFoundation/master/DependencyControl.json"},
{"l0.Functional", version: "0.5.0", url: "https://github.com/TypesettingTools/Functional",
feed: "https://raw.githubusercontent.com/TypesettingTools/Functional/master/DependencyControl.json"},
{"SubInspector.Inspector", version: "0.7.2", url: "https://github.com/TypesettingTools/SubInspector",
feed: "https://raw.githubusercontent.com/TypesettingTools/SubInspector/master/DependencyControl.json"},
"json"
}
}
LineCollection, ConfigHandler, ASS, Functional, SubInspector, json = version\requireModules!
ffms, min, concat, sort = aegisub.frame_from_ms, math.min, table.concat, table.sort
{:list, :math, :string, :table, :unicode, :util, :re } = Functional
logger = version\getLogger!
reportMsg = [[
Done. Processed %d lines in %d seconds.
— Cleaned %d lines (%d%%)
— Removed %d invisible lines (%d%%)
— Combined %d consecutive identical lines (%d%%)
— Filtered %d clips and %d occurences of junk data
— Purged %d invisible contours (%d in drawings, %d in clips)
— Failed to purge %d invisible contours due to rendering inconsistencies
— Converted %d drawings/clips to floating-point
— Filtered %d records of extra data
— Total space saved: %.2f KB
]]
hints = {
cleanLevel: [[
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]]
tagsToKeep: "Don't remove these tags even if they match the style defaults for the line."
filterClips: "Removes clips that don't affect the rendered output."
removeInvisible: "Deletes lines that don't generate any visible output."
combineLines: "Merges non-animated lines that render to an identical result and have consecutive times (without overlaps or gaps)."
removeJunk: "Removes any 'in-line comments' and things not starting with a \\ from tag sections."
scale2float: "Converts drawings and clips with a scale parameter to a floating-point representation."
tagSortOrder: "Determines the order cleaned tags will be ordered inside a tag section. Resets always go first, transforms last."
fixDrawings: "Removes extraneous ordinates from broken drawings to make them parseable. May or may not changed the rendered output."
purgeContoursDraw: "Removes all contours of a drawing that are not visible on the canvas."
purgeContoursClip: "Removes all contours of a clip that do not affect the appearance of the line."
stripComments: "Removes any comments encapsulated in {curly brackets}."
purgeContoursIgnoreHashMismatch: "Removes invisible contours even if it causes a SubInspector hash mismatch when comparing the result to the original line. This may or may not visually affect your drawing, so never use this option unsupervised!"
}
defaultSortOrder = [[
\an, \pos, \move, \org, \fscx, \fscy, \frz, \fry, \frx, \fax, \fay, \fn, \fs, \fsp, \b, \i, \u, \s, \bord, \xbord, \ybord,
\shad, \xshad, \yshad, \1c, \2c, \3c, \4c, \alpha, \1a, \2a, \3a, \4a, \blur, \be, \fad, \fade, clip_rect, iclip_rect,
clip_vect, iclip_vect, \q, \p, \k, \kf, \K, \ko, junk, unknown
]]
karaokeTags = table.concat table.pluck(table.filter(ASS.tagMap, (tag) -> tag.props.karaoke), "overrideName"), ", "
-- to be moved into ASSFoundation.Functional
sortWithKeys = (tbl, comparator) ->
-- shellsort written by Rici Lake
-- c/p from http://lua-users.org/wiki/LuaSorting, with index argument added to comparator
incs = { 1391376, 463792, 198768, 86961, 33936, 13776, 4592, 1968, 861, 336, 112, 48, 21, 7, 3, 1 }
n = #tbl
for h in *incs
for i = h+1, n
a = tbl[i]
for j = i-h, 1, -h
b = tbl[j]
break unless comparator a, b, i, j
tbl[i] = b
i = j
tbl[i] = a
return tbl
-- returns if we can merge line b into a while still maintain b's layer order
isMergeable = (a, b, linesByFrame) ->
for i = b.firstFrame, b.lastFrame
group = linesByFrame[i]
local pos
unless group.sorted
-- ensure line group is sorted by layer blending order
sortWithKeys group, (x, y, i, j) ->
pos or= i if x == b
return x.layer < y.layer or x.layer == y.layer and i < j
group.sorted = true
-- get line position in blending order
pos or= i for i, v in ipairs group when v == b
-- as b is merged into a, it gets a's layer number
-- so we can only merge if the new layer number does not change the blending order in any of the frames b is visible in
lower = group[pos-1]
return false unless (not lower or a.layer > lower.layer or a.layer == lower.layer and a.number > lower.number)
higher = group[pos+1]
return false unless (not higher or a.layer < higher.layer or a.layer == higher.layer and a.number < higher.number)
return true
mergeLines = (lines, start, cmbCnt, bytes, linesByFrame) ->
-- queue merged lines for deletion and collect statistics
if lines[start].merged
return lines[start], cmbCnt+1, bytes + #lines[start].raw + 1
-- merge applicable lines into first mergeable by extending its end time
-- then mark all merged lines
merged = lines[start]
for i=start+1,lines.n
line = lines[i]
break if line.merged or line.start_time != merged.end_time or not isMergeable merged, line, linesByFrame
lines[i].merged = true
merged.end_time = lines[i].end_time
-- update lines by frame index
for f = line.firstFrame, line.lastFrame
group = linesByFrame[f]
pos = i for i, v in ipairs group when v == line
group[pos] = merged
return nil, cmbCnt, bytes
removeInvisibleContoursOptCollectBounds = (contour, _, sectionContourIndex, sliceContourIndex, _, sliceRawContours, sliceSize, linePre, linePost, isAnimated, boundsBatch) ->
prevContours = concat sliceRawContours, " ", 1, sliceContourIndex-1
nextContours = sliceContourIndex <= sliceSize and concat(sliceRawContours, " ", sliceContourIndex+1) or ""
text = "#{linePre}#{prevContours}#{(#prevContours == 0 or #nextContours == 0) and "" or " "}#{nextContours}#{linePost}"
boundsBatch\add contour.parent.parent, text, sectionContourIndex, isAnimated
rawContour = sliceRawContours[sliceContourIndex]
removeInvisibleContoursOptPurge = (contour, contours, i, _, _, allBounds, orgBounds) ->
bounds, allBounds[i] = allBounds[i]
return false if orgBounds\equal bounds
removeInvisibleContoursOpt = (section, orgBounds) ->
cutOff, ass, contourCnt = false, section.parent, #section.contours
sliceSize = math.ceil 10e4 / contourCnt
if contourCnt > 100
logger\hint "Cleaning complex drawing with %d contours (slice size: %s)...", contourCnt, sliceSize
selectSurroundingSections = (sect, _, _, _, toTheLeft) ->
if toTheLeft
cutOff = true if section == sect
else
if section == sect
cutOff = false
return false
return not cutOff
lineStringPre, drwState = ass\getString nil, nil, selectSurroundingSections, false, true
lineStringPost = ass\getString nil, drwState, selectSurroundingSections, false, false
allBounds, sliceContours, sliceStartIndex = {}
isAnimated = ass\isAnimated!
for sliceStartIndex = 1, contourCnt, sliceSize
sliceEndIndex = min sliceStartIndex+sliceSize-1, contourCnt
sliceContours = [cnt\getTagParams! for cnt in *section.contours[sliceStartIndex, sliceEndIndex]]
boundsBatch = ASS.LineBoundsBatch!
section\callback removeInvisibleContoursOptCollectBounds, sliceStartIndex, sliceEndIndex, nil, nil,
sliceContours, sliceSize, lineStringPre, lineStringPost, isAnimated, boundsBatch
boundsBatch\run true, allBounds
lineStringPre ..= concat sliceContours, " "
boundsBatch = nil
collectgarbage!
_, purgeCnt = section\callback removeInvisibleContoursOptPurge, nil, nil, nil, nil, allBounds, orgBounds
return purgeCnt
stripComments = () -> false
process = (sub, sel, res) ->
ASS.config.fixDrawings = res.fixDrawings
lines = LineCollection sub, sel
linesToDelete, delCnt, linesToCombine, cmbCnt, lineCnt, debugError = {}, 0, {}, 0, #lines.lines, false
tagNames = res.filterClips and util.copy(ASS.tagNames.clips) or {}
tagNames[#tagNames+1] = res.removeJunk and "junk"
stats = { bytes: 0, junk: 0, clips: 0, start: os.time!, cleaned: 0,
scale2float: 0, contoursDraw: 0, contoursDrawSkipped: 0, contoursClip: 0, extra: 0 }
linesByFrame = {}
-- create proper tag name lists from user input which may be override tag names or mixed
res.tagsToKeep = ASS\getTagNames string.split res.tagsToKeep, ",%s", nil, false
res.tagSortOrder = ASS\getTagNames string.split res.tagSortOrder, ",%s", nil, false
res.mergeConsecutiveExcept = ASS\getTagNames string.split res.mergeConsecutiveExcept, ",%s", nil, false
res.extraDataFilter = string.split res.extraDataFilter, ",%s", nil, false
callback = (lines, line, i) ->
aegisub.cancel! if aegisub.progress.is_cancelled!
aegisub.progress.task "Cleaning %d of %d lines..."\format i, lineCnt if i%10==0
aegisub.progress.set 100*i/lineCnt
unless line.styleRef
logger\warn "WARNING: Line #%d is using undefined style '%s', skipping...\n— %s", i, line.style, line.text
return
-- filter extra data
if line.extra and res.extraDataMode != "Keep all"
removed, r = switch res.extraDataMode
when "Remove all"
removed = line.extra
line.extra = nil
removed, table.length removed
when "Remove all except"
table.removeKeysExcept line.extra, res.extraDataFilter
when "Keep all except"
table.removeKeys line.extra, res.extraDataFilter
stats.extra += r
success, data = pcall ASS\parse, line
unless success
logger\warn "Couldn't parse line #%d: %s", i, data
return
-- it is essential to run SubInspector on a ASSFoundation-built line (rather than the original)
-- because ASSFoundation rounds tag values to a sane precision, which is not visible but
-- will produce a hash mismatch compared to the original line. However we must avoid that to
-- not trigger the ASSWipe bug detection
orgText, oldBounds = line.text, data\getLineBounds false, true
orgTextParsed = oldBounds.rawText
orgTagTypes = ["#{tag.__tag.name}(#{table.concat({tag\getTagParams!}, ",")})" for tag in *data\getTags!]
removeInvisibleContours = (contour) ->
contour.disabled = true
if oldBounds\equal data\getLineBounds!
if contour.parent.class == ASS.Section.Drawing
stats.contoursDraw += 1
else stats.contoursClip += 1
return false
contour.disabled = false
-- remove invisible lines
if res.removeInvisible and oldBounds.w == 0
stats.bytes += #line.raw + 1
delCnt += 1
linesToDelete[delCnt], line.ASS = line
return
purgedContourCount = 0
if res.purgeContoursDraw or res.scale2float
cb = (section) ->
-- remove invisible contours from drawings
if res.purgeContoursDraw
purgedContourCount = removeInvisibleContoursOpt section, oldBounds
-- un-scale drawings
if res.scale2float and section.scale > 1
section.scale\set 1
stats.scale2float += 1
data\callback cb, ASS.Section.Drawing
if purgedContourCount > 0
if res.purgeContoursDraw and not res.purgeContoursIgnoreHashMismatch and not data\getLineBounds!\equal oldBounds
line.text = orgText
data = ASS\parse line
stats.contoursDrawSkipped += purgedContourCount
else
stats.contoursDraw += purgedContourCount
oldBounds = data\getLineBounds! if res.purgeContoursIgnoreHashMismatch
-- pogressively build a table of visible lines by frame
-- which is required to check mergeability of consecutive identical lines
if res.combineLines
line.firstFrame = ffms line.start_time
line.lastFrame = -1 + ffms line.end_time
for i = line.firstFrame, line.lastFrame
lbf = linesByFrame[i]
if lbf
lbf[lbf.n+1] = line
lbf.n += 1
else linesByFrame[i] = {line, n: 1}
-- collect lines to combine
unless oldBounds.animated
ltc = linesToCombine[oldBounds.firstHash]
if ltc
ltc[ltc.n+1] = line
ltc.n += 1
else linesToCombine[oldBounds.firstHash] = {line, n: 1}
mergeConsecutiveTagSections = if not res.mergeConsecutive
false
elseif #res.mergeConsecutiveExcept == 0
true
else
exceptions = list.makeSet res.mergeConsecutiveExcept
(sourceSection, targetSection) ->
predicate = (tag) -> exceptions[tag.__tag.name]
not table.find(sourceSection.tags, predicate) and not table.find targetSection.tags, predicate
-- clean tags
data\cleanTags res.cleanLevel, mergeConsecutiveTagSections, res.tagsToKeep, res.tagSortOrder
newBounds = data\getLineBounds!
if res.stripComments
data\stripComments!
--data\callback stripComments, ASS.Section.Comment
if res.filterClips or res.removeJunk
data\modTags tagNames, (tag) ->
-- remove junk
if tag.class == ASS.Tag.Unknown
stats.junk += 1
return false
if tag.class == ASS.Tag.ClipVect
-- un-scale clips
if res.scale2float and tag.scale>1
tag.scale\set 1
stats.scale2float += 1
-- purge ineffective contours from clips
if res.purgeContoursClip
tag\callback removeInvisibleContours
-- filter clips
tag.disabled = true
if data\getLineBounds!\equal newBounds
stats.clips += 1
return false
tag.disabled = false
data\commit nil, res.cleanLevel == 0
-- reclaim some memory
line.ASS, line.undoText = nil
if orgText != line.text
if not newBounds\equal oldBounds
debugError = true
logger\warn "Cleaning affected output on line #%d, rolling back...", line.humanizedNumber
logger\warn "—— Before: %s\n—— Parsed: %s\n—— After: %s\n—— Style: %s\n—— Tags: %s", orgText, orgTextParsed, line.text, line.styleRef.name, table.concat orgTagTypes, "; "
logger\warn "—— Hash Before: %s (%s); Hash After: %s (%s)\n",
oldBounds.firstHash, oldBounds.animated and "animated" or "static",
newBounds.firstHash, newBounds.animated and "animated" or "static"
line.text = orgText
elseif #line.text < #orgText
stats.cleaned += 1
stats.bytes += #orgText - #line.text
aegisub.cancel! if aegisub.progress.is_cancelled!
lines\runCallback callback, true
-- sort lines which are to be combined by time
sortFunc = (a, b) ->
return true if a.start_time < b.start_time
return false if a.start_time > b.start_time
return true if a.layer < b.layer
return false if a.layer > b.layer
return true if a.number < b.number
return false
linesToCombineSorted, l = {}, 1
for _, group in pairs linesToCombine
continue if group.n < 2
sort group, sortFunc
linesToCombineSorted[l] = group
l += 1
sort linesToCombineSorted, (a, b) -> sortFunc a[1], b[1]
-- combine lines
for group in *linesToCombineSorted
for j=1, group.n
linesToDelete[delCnt+cmbCnt+1], cmbCnt, stats.bytes = mergeLines group, j, cmbCnt, stats.bytes, linesByFrame
lines\replaceLines!
lines\deleteLines linesToDelete
logger\warn json.encode {Styles: lines.styles, Configuration: res} if debugError
logger\warn reportMsg, lineCnt, os.time!-stats.start, stats.cleaned, 100*stats.cleaned/lineCnt,
delCnt, 100*delCnt/lineCnt, cmbCnt, 100*cmbCnt/lineCnt, stats.clips, stats.junk,
stats.contoursClip+stats.contoursDraw, stats.contoursDraw, stats.contoursClip, stats.contoursDrawSkipped,
stats.scale2float, stats.extra, stats.bytes/1000
if debugError
logger\warn [[However, ASSWipe possibly encountered bugs while cleaning.
Affected lines have been rolled back to their previous state, so your script is most likely fine.
Please copy the whole log window contents and send them to line0.]]
return lines\getSelection!
showDialog = (sub, sel, res) ->
dlg = {
main: {
removeInvisible: class: "checkbox", x: 0, y: 0, width: 2, height: 1, value: true, config: true, label: "Remove invisible lines", hint: hints.removeInvisible
combineLines: class: "checkbox", x: 0, y: 1, width: 2, height: 1, value: true, config: true, label: "Combine consecutive identical lines", hint: hints.combineLines
mergeConsecutive: class: "checkbox", x: 2, y: 0, width: 12, height: 1, value: true, config: true, label: "Merge consecutive tag sections unless it contains any of:", hint: hints.mergeConsecutive
mergeConsecutiveExcept: class: "textbox", x: 2, y: 1, width: 12, height: 2, value: karaokeTags, config: true, hint: hints.mergeConsecutiveExcept
cleanLevelLabel: class: "label", x: 0, y: 4, width: 1, height: 1, label: "Tag cleanup level: "
cleanLevel: class: "intedit", x: 1, y: 4, width: 1, height: 1, min: 0, max: 4, value: 4, config: true, hint: hints.cleanLevel
tagsToKeepLabel: class: "label", x: 4, y: 4, width: 1, height: 1, label: "Keep default tags: "
tagsToKeep: class: "textbox", x: 4, y: 5, width: 10, height: 2, value: "\\pos", config:true, hint: hints.tagsToKeep
tagSortOrderLabel: class: "label", x: 4, y: 7, width: 1, height: 1, label: "Tag sort order: "
stripComments: class: "checkbox", x: 0, y: 5, width: 2, height: 1, value: true, config: true, label: "Strip comments", hint: hints.stripComments
removeJunk: class: "checkbox", x: 0, y: 6, width: 2, height: 1, value: true, config: true, label: "Remove junk from tag sections", hint: hints.removeJunk
tagSortOrder: class: "textbox", x: 4, y: 8, width: 10, height: 3, value: defaultSortOrder, config: true, hint: hints.tagSortOrder
filterClips: class: "checkbox", x: 0, y: 11, width: 2, height: 1, value: true, config: true, label: "Filter clips", hint: hints.filterClips
scale2float: class: "checkbox", x: 0, y: 12, width: 2, height: 1, value: true, config: true, label: "Un-scale drawings and clips", hint: hints.scale2float
fixDrawings: class: "checkbox", x: 0, y: 13, width: 2, height: 1, value: false, config: true, label: "Try to fix broken drawings", hint: hints.fixDrawings
purgeContoursLabel: class: "label", x: 0, y: 14, width: 2, height: 1, label: "Purge invisible contours: "
purgeContoursDraw: class: "checkbox", x: 4, y: 14, width: 3, height: 1, value: false, config: true, label: "from drawings", hint: hints.purgeContoursDraw
purgeContoursClip: class: "checkbox", x: 7, y: 14, width: 6, height: 1, value: false, config: true, label: "from clips", hint: hints.purgeContoursClip
purgeContoursIgnoreHashMismatch: class: "checkbox", x: 4, y: 15, width: 9, height: 1, value: false, config: true, label: "ignore rendering inconsistencies", hint: hints.purgeContoursIgnoreHashMismatch
extraDataLabel: class: "label", x: 0, y: 17, width: 1, height: 1, label: "Filter extra data: "
extraDataMode: class: "dropdown", x: 1, y: 17, width: 1, height: 1, value: "Keep All", config: true, items: {"Keep all", "Remove all", "Keep all except", "Remove all except"}, hint: hints.extraData
extraDataFilter: class: "textbox", x: 4, y: 17, width: 10, height: 3, value: "", config: true, hint: hints.extraData
quirksModeLabel: class: "label", x: 0, y: 21, width: 2, height: 1, label: "Quirks mode: "
quirksMode: class: "dropdown", x: 4, y: 21, width: 2, height: 1, value: "VSFilter", config: true, items: [k for k, v in pairs ASS.Quirks], hint: hints.quirksMode
}
}
options = ConfigHandler dlg, version.configFile, false, script_version, version.configDir
options\read!
options\updateInterface "main"
btn, res = aegisub.dialog.display dlg.main
if btn
options\updateConfiguration res, "main"
options\write!
ASS.config.quirks = ASS.Quirks[res.quirksMode]
process sub, sel, res
version\registerMacro showDialog, ->
if aegisub.project_properties!.video_file == ""
return false, "A video must be loaded to run #{script_name}."
else return true, script_description