Repository: SubtitleEdit/subtitleedit (2026-06-27T17:09:20Z)

Issue:
EBU STL Save/Save As creates invalid 14-byte file on macOS ARM
## EBU STL file saved as invalid 14-byte file after editing and using Save / Save As on macOS ARM

### Description

When working with EBU STL subtitle files in Subtitle Edit 5.0.0 on macOS, there seems to be a bug when saving an edited EBU STL file.

If an EBU STL file is created using **File → Export**, the exported file is valid and can be opened again correctly. However, after opening/editing this EBU STL file and then using **Save** or **Save As**, Subtitle Edit saves only a **14-byte file**. The resulting file is invalid and cannot be opened again.

### Steps to reproduce

1. Open or create subtitles in Subtitle Edit.
2. Export them as **EBU STL** using **File → Export**.
3. Verify that the exported EBU STL file can be opened correctly.
4. Edit the subtitle file.
5. Click **Save** or **Save As**.
6. Check the saved file size.

### Actual result

The saved file is only **14 bytes** and is not a valid EBU STL file. Subtitle Edit cannot open it afterwards.

### Expected result

The edited EBU STL file should be saved as a valid EBU STL file and should remain readable by Subtitle Edit.

### Environment

* Subtitle Edit version: **5.0.0**
* Operating system: **macOS**
* Hardware: **Apple MacBook Pro with Apple Silicon M1**
* File format: **EBU STL**

### Additional notes

The issue does not happen during the initial export via **File → Export**. The problem appears only when saving an already exported/opened EBU STL file using **Save** or **Save As**.



Discussion:

Patch:
diff --git a/src/ui/Features/Main/MainViewModel.cs b/src/ui/Features/Main/MainViewModel.cs
index abe28c8a1c..a1c545983b 100644
--- a/src/ui/Features/Main/MainViewModel.cs
+++ b/src/ui/Features/Main/MainViewModel.cs
@@ -15992,6 +15992,14 @@ private async Task<bool> SaveSubtitle(bool isAutoSave = false)
             AutoTrimWhiteSpaces();
         }
 
+        // Binary formats (EBU STL, PAC, Cavena, ...) can't be written as text: their ToText() returns
+        // a stub (EBU's is literally "Not supported!", the 14-byte file in #11910). Persist them via
+        // their binary writer instead of WriteAllText.
+        if (SelectedSubtitleFormat is IBinaryPersistableSubtitle binaryFormat)
+        {
+            return await SaveBinarySubtitle(binaryFormat, isAutoSave);
+        }
+
         var text = GetUpdateSubtitle(true).ToText(SelectedSubtitleFormat);
 
         if (Se.Settings.General.ForceCrLfOnSave)
@@ -16044,6 +16052,53 @@ await MessageBox.Show(Window!, Se.Language.General.Error, message, MessageBoxBut
         return true;
     }
 
+    // Writes a binary subtitle format (e.g. EBU STL) via its IBinaryPersistableSubtitle writer,
+    // in batch mode so it re-uses the existing/auto-detected header without a dialog. Fixes the
+    // 14-byte invalid file produced when these formats went through the text save path (#11910).
+    private async Task<bool> SaveBinarySubtitle(IBinaryPersistableSubtitle binaryFormat, bool isAutoSave)
+    {
+        try
+        {
+            // EBU STL needs a UI helper to resolve its header; reuse the same one as File > Export.
+            if (binaryFormat is Ebu)
+            {
+                Ebu.EbuUiHelper ??= new UiEbuSaveHelper();
+            }
+
+            using var ms = new MemoryStream();
+            if (!binaryFormat.Save(_subtitleFileName, ms, GetUpdateSubtitle(true), batchMode: true))
+            {
+                if (!isAutoSave)
+                {
+                    ShowStatus(string.Format(Se.Language.General.CouldNotSaveFileXErrorY, _subtitleFileName, string.Empty));
+                }
+
+                return false;
+            }
+
+            await File.WriteAllBytesAsync(_subtitleFileName, ms.ToArray());
+        }
+        catch (Exception ex)
+        {
+            var message = string.Format(Se.Language.General.CouldNotSaveFileXErrorY, _subtitleFileName, ex.Message);
+            if (isAutoSave)
+            {
+                ShowStatus(message);
+                return false;
+            }
+
+            await MessageBox.Show(Window!, Se.Language.General.Error, message, MessageBoxButtons.OK, MessageBoxIcon.Error);
+            return false;
+        }
+
+        _changeSubtitleHash = GetFastHash();
+        _lastOpenSaveFormat = SelectedSubtitleFormat;
+
+        new BookmarkPersistence(GetUpdateSubtitle(), _subtitleFileName).Save();
+
+        return true;
+    }
+
     private async Task<bool> SaveSubtitleOriginal(bool isAutoSave = false)
     {
         if (Subtitles == null || !Subtitles.Any())


Test patch:
diff --git a/tests/libse/SubtitleFormats/EbuTest.cs b/tests/libse/SubtitleFormats/EbuTest.cs
new file mode 100644
index 0000000000..823e60b364
--- /dev/null
+++ b/tests/libse/SubtitleFormats/EbuTest.cs
@@ -0,0 +1,68 @@
+using Nikse.SubtitleEdit.Core.Common;
+using Nikse.SubtitleEdit.Core.Interfaces;
+using Nikse.SubtitleEdit.Core.SubtitleFormats;
+using System.IO;
+
+namespace LibSETests.SubtitleFormats;
+
+public class EbuTest
+{
+    // Minimal headless stand-in for the UI's EBU save helper so the binary writer can run in a test.
+    private sealed class TestEbuUiHelper : Ebu.IEbuUiHelper
+    {
+        public byte JustificationCode { get; set; }
+        public void Initialize(Ebu.EbuGeneralSubtitleInformation header, byte justificationCode, string fileName, Subtitle subtitle) { }
+        public bool ShowDialogOk() => true;
+    }
+
+    private static Subtitle MakeSubtitle()
+    {
+        var subtitle = new Subtitle();
+        subtitle.Paragraphs.Add(new Paragraph("Hello world", 1000, 3000));
+        subtitle.Paragraphs.Add(new Paragraph("Second line", 4000, 6000));
+        return subtitle;
+    }
+
+    // Regression for #11910: EBU STL Save produced a 14-byte invalid file ("Not supported!")
+    // because the binary format went through the text save path. The binary writer must emit a real
+    // EBU file (1024-byte GSI header + TTI blocks) that reads back.
+    [Fact]
+    public void EbuStl_BinarySave_ProducesValidFileNotFourteenByteStub()
+    {
+        Ebu.EbuUiHelper = new TestEbuUiHelper();
+        var subtitle = MakeSubtitle();
+
+        using var ms = new MemoryStream();
+        var ok = ((IBinaryPersistableSubtitle)new Ebu()).Save("test.stl", ms, subtitle, batchMode: true);
+        var bytes = ms.ToArray();
+
+        Assert.True(ok);
+        Assert.True(bytes.Length >= 1024, $"EBU file is only {bytes.Length} bytes (regression #11910)");
+    }
+
+    [Fact]
+    public void EbuStl_BinarySave_RoundTripsParagraphs()
+    {
+        Ebu.EbuUiHelper = new TestEbuUiHelper();
+        var subtitle = MakeSubtitle();
+
+        using var ms = new MemoryStream();
+        ((IBinaryPersistableSubtitle)new Ebu()).Save("test.stl", ms, subtitle, batchMode: true);
+
+        var loaded = new Subtitle();
+        new Ebu().LoadSubtitle(loaded, ms.ToArray());
+
+        Assert.Equal(2, loaded.Paragraphs.Count);
+        Assert.Contains("Hello world", loaded.Paragraphs[0].Text);
+        Assert.Contains("Second line", loaded.Paragraphs[1].Text);
+    }
+
+    [Fact]
+    public void EbuStl_ToText_IsNotUsableForSaving()
+    {
+        // Guards the root cause: ToText is a stub for this binary format, so the save path must use
+        // the IBinaryPersistableSubtitle writer instead (#11910).
+        Assert.IsAssignableFrom<IBinaryPersistableSubtitle>(new Ebu());
+        Assert.True(new Ebu().ToText(MakeSubtitle(), string.Empty).Length < 20);
+    }
+}