Skip to content

Fix V4+ Style Format for Aegisub Compatibility#133

Open
tassa-yoniso-manasi-karoto wants to merge 2 commits into
asticode:masterfrom
tassa-yoniso-manasi-karoto:fix-style-format-order
Open

Fix V4+ Style Format for Aegisub Compatibility#133
tassa-yoniso-manasi-karoto wants to merge 2 commits into
asticode:masterfrom
tassa-yoniso-manasi-karoto:fix-style-format-order

Conversation

@tassa-yoniso-manasi-karoto
Copy link
Copy Markdown

Problem 1: Field Order

When writing ASS (V4+) files, updateFormat() builds the Styles Format line by adding fields in alphabetical order (Alignment, AlphaLevel, Angle, BackColour, Bold, ...). However, Aegisub ignores the Format line entirely and parses style values by fixed position. Other parsers (libass, VSFilter) likely follow Aegisub's conventions as the de facto standard.

This causes a mismatch: the Format line says position 2 is "Alignment", but parsers expect position 2 to be "Fontname". When Aegisub tries to parse a font name as an integer (for the Bold field later in the sequence), it throws:

Malformed style: bad int field

Problem 2: Missing Fields

Even after fixing field order, updateFormat() only adds fields that have non-nil values. If a field like StrikeOut is nil (e.g., after a deep copy operation), it's omitted from the Format line entirely. This shifts all subsequent field positions, causing the same parse errors.

Additionally, string() outputs empty strings for nil fields, which is invalid (e.g., empty string where Aegisub expects "0" or "1" for a boolean).

Commit Scope Clarification

Commit 1 (Field Order): Affects all users. Any ASS file written by go-astisub currently fails to parse in Aegisub due to alphabetical field order.

Commit 2 (Nil Defaults): Affects edge cases where style fields are nil, such as deep copying with reflection-based libraries or programmatically creating styles without initializing all fields. Under typical read-modify-write workflows, fields remain populated.

I encountered this via jinzhu/copier for deep copying, where pointer fields like *bool became nil. Regardless of how common this scenario is, go-astisub should not output corrupt ASS files when fields are nil. It should use sensible defaults rather than omitting fields or outputting empty strings.

Design Decision: V4+ (ASS) Only

This PR intentionally targets V4+ (ASS) only and removes support for writing legacy V4 (SSA) format:

  • AlphaLevel (V4-only field) is no longer conditionally output
  • The hardcoded field order is the V4+ canonical order

Rationale is that according to Gemini 3 Pro:

  1. Age: V4+ (ASS) is over 20 years old (created ~2002)
  2. Prevalence: It is the overwhelming standard; virtually every "SSA" file found in the wild since 2005 is actually V4+
  3. Legacy: Original V4 (SSA) is effectively extinct; it lacks features (Shadow, Outline, MarginV) that modern renderers expect
  4. Mislabeling: Files with .ssa extension often contain V4+ content anyway

If a rare legacy V4 file is encountered, reading still works (unchanged), but output is always upgraded to V4+. There is no benefit to writing old V4 files today.

Spec vs Reality

The written SSA V4+ (ASS) specification states that the Format line "allows new fields to be added... even if the field order is changed." However, the reference implementation (Aegisub) does not implement this flexibility: it parses style values by fixed position, ignoring the Format line entirely. This PR aligns with real-world behavior. See gist for Aegisub source code evidence.

References

* reorder updateFormat() fields to canonical V4+ order
* Aegisub and other parsers ignore the Format line and parse by position
* previous alphabetical order caused "bad int field" parse errors
* always include all 22 canonical fields regardless of nil values
* provide Aegisub defaults for nil fields instead of empty strings
* fixes "bad int field" errors from missing fields in format line
@asticode
Copy link
Copy Markdown
Owner

asticode commented Jan 6, 2026

First off thanks for the PR ❤️

  1. I don't mind forcing to v4+ when writing, however I'd rather have more things cleaned up in order to do so:

    • you can remove this
    • you can keep only this
    • no need for this logic (you need to remove all other functions used only here also) but you can create the format variable statically as done below
  2. Good idea, I'll review it completely once 1. is done 👍

Don't forget to fix tests if they're not passing anymore 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants