ass_swap_an_pos
Swap vertical Aegisub alignment tags and shift \pos or MarginV to keep subtitle text visually fixed; optionally mirror horizontally.
Instructions
Mirror \an vertically and move \pos so the text does not budge.
The classic Aegisub helper: \an1 <-> \an7, \an2 <-> \an8,
\an3 <-> \an9 (the middle row 4/5/6 has no vertical mirror and is
left alone). Because the anchor flips between the bottom and the top of the
line, \pos has to move by exactly the line height for the rendered text
to stay where it was.
The shift is computed from the real rendered size of the line, measured
with :func:asscore.measure.measure_line_render in script pixels (see
:func:_measure_geometry). height is the vertical anchor separation
(\an7 ink top minus \an1 ink top, both anchored at the same
y), i.e. the line height, and width is the advance width (left side
bearing + ink width + right side bearing, from the \an7/\an9
probes), i.e. the horizontal anchor separation. One numpad row or column
step moves the anchor by half of the corresponding separation, and the
anchor must move the other way for the ink to stay put::
row_step = new_row - row # numpad grid, +-2 for a top/bottom swap
col_step = new_col - col # +-2 for a left/right swap
dy = -(height / 2) * row_step # row 2 is the top of the screen
dx = +(width / 2) * col_step # col 2 is the right of the screenFor \an7 -> \an1 that is row_step = -2 and dy = +height: the
anchor walks down by the line height so the text stays on its screen row.
A purely vertical mirror never changes the column, so dx stays 0
there; horizontal=True additionally mirrors the column
(\an7 <-> \an9, \an1 <-> \an3, \an4 <-> \an6) and then
dx = +width for \an7 -> \an3 (numpad columns grow rightwards, so
the new right-edge anchor has to sit one width further right), which is what
makes the width term reachable.
The measured text is the line with its own \pos/\move/\an
removed, rendered with the line's style and the document's PlayRes, so
the height matches what libass does for the real line.
Indices: \an/\pos are located inside the override blocks, so no
plain-character index and no raw offset is taken as input. selection
refers to event indices (doc.events() order, 0-based), and the plain text
of every line survives byte for byte, so both index maps are unchanged.
Args:
selection: selection spelling; the lines to rewrite.
doc_id: document to edit; the current one when omitted.
margin_mode: how to treat lines that have no \pos. By default they
are skipped (there is nothing to shift). With margin_mode=True
the vertical margin is rewritten instead:
MarginV = PlayResY - MarginV - height, which keeps an
alignment/margin-positioned line in place across the mirror.
dry_run: compute and report the plan without writing (no snapshot).
horizontal: also mirror the alignment column (180 degree mirror) and
shift x by the advance width. Off by default, matching the
Aegisub helper.
Returns {"doc_id", "dry_run", "margin_mode", "horizontal", "count", "changed", "written", "geometry_source", "lines": [...]}; each line entry
has index, before, after, old_an, new_an, old_pos,
new_pos, dx, dy, margin_v, measured ({"width", "height", "ink_width", "ink_height", "left_bearing"}), changed,
skipped and reason. Retiming/render checks aside, the check to
apply is that the ink bounding box before and after is identical — see the
test suite, which asserts exactly that with
:func:asscore.measure.measure_render.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| doc_id | No | ||
| dry_run | No | ||
| selection | Yes | ||
| horizontal | No | ||
| margin_mode | No |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||