masaccio / numbers-parser

Python module for parsing Apple Numbers .numbers files
MIT License
201 stars 14 forks source link

Feature: cell background color #55

Closed barnacle-carnival closed 1 year ago

barnacle-carnival commented 1 year ago

I've figured out a way to extract the cell background color by digging deep into a cells and extracting the style data. I cribbed heavily from the background image code to figure out how to get this data. It would be nice if this had a dedicated API. I wrote a very lame hack function as a proof of concept, it gets the job done... I could parse the style data, but I that's probably better done via the API. I don't need the API to guess color names, I can do that myself, I just shouldn't have to parse the string or use such a convoluted method to get it.

Here's my lame function:

def guess_color(cell):
    # The style is a json-esq string which apparently is not parsed.
    # Rather than parse it ourselves, I've decided to cheat and look for
    # the color strings.  Since it's json, order is not guaranteed, so
    # we loop through the values.  This is somewhat inflexible and hard
    # coded to our exact colors...
    colors = {
        "gray":   [ "r: 0.836030781", "g: 0.836030722", "b: 0.836030722" ],
        "yellow": [ "r: 0.968627453", "g: 0.980392158", "b: 0.858823538" ],
        "blue":   [ "r: 0.796078444", "g: 0.941176474", "b: 1" ],
    }
    if not isinstance(cell, numbers_parser.EmptyCell) and not isinstance(cell, numbers_parser.MergedCell) and cell._storage:
        try:
            style = cell._model.table_style(cell._table_id, cell._storage.cell_style_id).__str__()
            for color in colors:
                match = 0
                for rgb in colors[color]:
                    pattern = "      %s\n    " % rgb
                    if pattern in style:
                        match += 1
                if match == 3:
                    return color
            return "UNKNOWN: %s" % style
        except KeyError as e:
            # This happens when table_style() fails to return anything.
            return ""
    else:
        return ""
masaccio commented 1 year ago

The objects returned by the model are typically Python representations of Google protobufs and you can just traverse them with dot parameters, so the code becomes:

def cell_bg_color(cell: object) -> Union[Tuple, List[Tuple]]:
    if cell._storage is None or cell._storage.cell_style_id is None:
        return None

    cell_style = cell._model.table_style(cell._table_id, cell._storage.cell_style_id)
    cell_properties = cell_style.cell_properties.cell_fill

    if cell_properties.HasField("color"):
        return (
            int(cell_properties.color.r * 65536),
            int(cell_properties.color.g * 65536),
            int(cell_properties.color.b * 65536),
        )
    elif cell_properties.HasField("gradient"):
        return [
            (int(s.color.r * 65536), int(s.color.g * 65536), int(s.color.b * 65536))
            for s in cell_properties.gradient.stops
        ]

At least for a couple of variants of colours.

masaccio commented 1 year ago

And guessing on the colour depth is 16-but but might 8-bit.

masaccio commented 1 year ago

So there's a new bg_color property for cells that should return None if there isn't one. Is present on all cell types. I'll add to the docs at some point soon and publish to PyPI. Right now it's just on main.

masaccio commented 1 year ago

And now published as 3.11.0. See README for usage.