facelessuser / ColorHelper

Sublime plugin that provides helpful color previews and tooltips
https://facelessuser.github.io/ColorHelper/
MIT License
254 stars 30 forks source link

Support color codes in ASS/SSA subtitles #191

Closed jfcherng closed 3 years ago

jfcherng commented 3 years ago

The Goal

I would like to add support for color codes in ASS/SSA subtitles. The color codes are in the forms of &HAABBGGRR or &HBBGGRR such as &H80FF0000 or &HFF0000. Note that by default alpha=00 and it means opaque.

Currently, it's done in the https://packagecontrol.io/packages/Advanced%20Substation%20Alpha%20(ASS) side. But it will require users to modify their ColorHelper's settings so it would be better if it can be moved to ColorHelper.

I am willing to change the naming of files and classes. (I don't know how to name them properly actually.)

References

facelessuser commented 3 years ago

I'm happy to add your custom color class.

  1. Can you update the docstrings to silence the linter issues?

  2. I picked ahex for the alpha hex colors mainly because it is kind of a generic form that is used in multiple implementations. But this form seems very specific to ASS. Maybe give the setting a more descriptive name like ass or ssa_ass. Something that kind of describes their main use as this is a very specific color style to ASS it seems (which I guess is from SSA v4 -- based on what I read) . I'd also rename the file using a similar name.

  3. I'd also just rename the color class ColorAlphaHex object something like ColorASS or ColorSsaAss. Maybe something like that.

Overall, I think it looks fine. I haven't tested it yet, but I'm assuming you have, but I'll take a closer look and test before I merge.

I think with this, ColorHelper should support ASS out of the box. That acronym though 🙃.

facelessuser commented 3 years ago

I updated the original post, just call the file something like ass or ssa_ass should be fine along with the custom name in the settings.

facelessuser commented 3 years ago

I've tested pull, making the suggested changes locally and it seems to work well. After changes are made, I'll be happy to pull it in.

Screen Shot 2021-09-06 at 9 33 48 AM
diff --git a/color_helper.sublime-settings b/color_helper.sublime-settings
index dab27af..43cb521 100755
--- a/color_helper.sublime-settings
+++ b/color_helper.sublime-settings
@@ -211,6 +211,14 @@
             "output": [
                 {"space": "srgb", "format": {"hex": true}}
             ]
+        },
+        // for color codes like `&HAABBGGRR` and `&HBBGGRR`
+        "ass_abgr": {
+            "class": "ColorHelper.custom.ass_abgr.ColorAssABGR",
+            "filters": ["srgb"],
+            "output": [
+                {"space": "srgb", "format": {"upper": true}}
+            ]
         }
     },

@@ -379,6 +387,14 @@
                 "constant.color.w3c-standard-color-name.css",
                 "meta.property-value.css"
             ]
+        },
+        {
+            // ASS ( based on: https://packagecontrol.io/packages/Advanced%20Substation%20Alpha%20(ASS) )
+            "name": "ASS",
+            "base_scopes": ["text.ass"],
+            "scanning": ["constant.other.color"],
+            "color_class": "ass_abgr",
+            "color_trigger": "(?i)&h"
         }
     ],

diff --git a/custom/ass_abgr.py b/custom/ass_abgr.py
new file mode 100644
index 0000000..1d620e7
--- /dev/null
+++ b/custom/ass_abgr.py
@@ -0,0 +1,85 @@
+"""Custom color that looks for colors of format `&HAABBGGRR` as `#AARRGGBB`."""
+from ColorHelper.lib.coloraide import Color
+from ColorHelper.lib.coloraide import util
+from ColorHelper.lib.coloraide.spaces import _parse
+from ColorHelper.lib.coloraide.spaces.srgb.css import SRGB
+import copy
+import re
+
+
+class AssABGR(SRGB):
+    """ASS `ABGR` color space."""
+
+    MATCH = re.compile(r"&H([0-9a-fA-f]{8}|[0-9a-fA-f]{6})\b")
+
+    @classmethod
+    def match(cls, string: str, start: int = 0, fullmatch: bool = True):
+        """Match a color string."""
+
+        m = cls.MATCH.match(string, start)
+        if m is not None and (not fullmatch or m.end(0) == len(string)):
+            return cls.split_channels(m.group(1)), m.end(0)
+        return None, None
+
+    @classmethod
+    def translate_channel(cls, channel: int, value: str):
+        """Translate channel string."""
+
+        if -1 <= channel <= 2:
+            return _parse.norm_hex_channel(value)
+
+    @classmethod
+    def split_channels(cls, color: str):
+        """Split string into the appropriate channels."""
+
+        # convert `BBGGRR` to `AABBGGRR`
+        if len(color) == 6:
+            color = "00" + color
+        # deal with `AABBGGRR`
+        if len(color) == 8:
+            return cls.null_adjust(
+                (
+                    cls.translate_channel(0, "#" + color[6:]),  # RR
+                    cls.translate_channel(1, "#" + color[4:6]),  # GG
+                    cls.translate_channel(2, "#" + color[2:4]),  # BB
+                ),
+                1 - cls.translate_channel(-1, "#" + color[:2]),  # AA
+            )
+
+        raise RuntimeError("Something is wrong in code logics.")
+
+    def to_string(self, parent, *, options=None, alpha=None, precision=None, fit=True, **kwargs):
+        """Convert color to `&HAABBGGRR`."""
+
+        options = kwargs
+        a = util.no_nan(self.alpha)
+        show_alpha = alpha is not False and (alpha is True or a < 1.0)
+
+        template = "&H{:02x}{:02x}{:02x}{:02x}" if show_alpha else "&H{:02x}{:02x}{:02x}"
+        if options.get("upper"):
+            template = template.upper()
+
+        # Always fit hex
+        method = None if not isinstance(fit, str) else fit
+        coords = util.no_nan(parent.fit(method=method).coords())
+        if show_alpha:
+            value = template.format(
+                int(util.round_half_up(a * 255.0)),
+                int(util.round_half_up(coords[2] * 255.0)),
+                int(util.round_half_up(coords[1] * 255.0)),
+                int(util.round_half_up(coords[0] * 255.0)),
+            )
+        else:
+            value = template.format(
+                int(util.round_half_up(coords[2] * 255.0)),
+                int(util.round_half_up(coords[1] * 255.0)),
+                int(util.round_half_up(coords[0] * 255.0)),
+            )
+        return value
+
+
+class ColorAssABGR(Color):
+    """Color class for ASS `ABGR` colors."""
+
+    CS_MAP = copy.copy(Color.CS_MAP)
+    CS_MAP["srgb"] = AssABGR
facelessuser commented 3 years ago

@jfcherng, I basically had it done, so I've merged the changes with your name attached. It will be included in tag st-3.6.0. I may tag it today as I don't really have anything else in the queue right now for ColorHelper tasks.

jfcherng commented 3 years ago

Thanks!

FichteFoll commented 3 years ago

Thanks!

Just as an aside, the ASS format (or rather the renderer that became the reference implementation) has terrible and very lax parsing, meaning that override blocks a la {\1cffffff} are also valid color specifications (iirc) that the check for (?i)&h will miss, but that's very much discouraged to begin with. The "correct" format for that would be {\1c&Hffffff&}.

facelessuser commented 3 years ago

@FichteFoll, I'm more than happy to accept any changes to ensure that all proper forms are accepted. Internally, they all get serialized to color(srgb r g b / a). It is more controlling what is read in and what is exported out. Obviously, I would suggest the output format to probably follow the widely accepted format, but I'll let you experts in ASS work out what you most prefer.

facelessuser commented 3 years ago

This may work for identifying all colors as an input:

diff --git a/color_helper.sublime-settings b/color_helper.sublime-settings
index 43cb521..169874c 100755
--- a/color_helper.sublime-settings
+++ b/color_helper.sublime-settings
@@ -394,7 +394,7 @@
             "base_scopes": ["text.ass"],
             "scanning": ["constant.other.color"],
             "color_class": "ass_abgr",
-            "color_trigger": "(?i)&h"
+            "color_trigger": "(?i)(?:&H|(?<=\\\\c)[a-f0-9])"
         }
     ],

diff --git a/custom/ass_abgr.py b/custom/ass_abgr.py
index 1d620e7..5983dbe 100644
--- a/custom/ass_abgr.py
+++ b/custom/ass_abgr.py
@@ -10,7 +10,7 @@ import re
 class AssABGR(SRGB):
     """ASS `ABGR` color space."""

-    MATCH = re.compile(r"&H([0-9a-fA-f]{8}|[0-9a-fA-f]{6})\b")
+    MATCH = re.compile(r"(?:&H)?([0-9a-fA-f]{8}|[0-9a-fA-f]{6})\b")

     @classmethod
     def match(cls, string: str, start: int = 0, fullmatch: bool = True):