Skip to main content
_wckdTall_
Inspiring
April 22, 2023
Question

Detect different black tones Math advice - Illustrator

  • April 22, 2023
  • 2 replies
  • 3604 views

I'm working on replacing some irregular black tones in a document and I'm curious if anyone has math advice on this. It seems #000000 reads as c:75 m:68 y:67 k:90 and has a brightness of 0%. 100% K is R:35 G:31 B:32 and has a brightness of 13%, however there doesn't seem to be way to get brightness info anyway. I'm thinking there are 2 methods to judge what I want to be perceived as black.

 

1: CMYK approach
Check if K is greater than 85%

Check if C M Y are greater than 66%

I checked vs Pantone Blacks(though not arguably gray at best), and most are above these ranges, seems a higher Cyan value is also helpful here not to stay in gray territory.

2: RGB approach
Check if R G B are lower than 35
Determine if they're within 10 points of eachother

This arguably entering gray territory, 35 is the highest value we get with 100% K, and pushing any values away further away from eachother starts to become a color.

Is there simpler logic than this?

This topic has been closed for replies.

2 replies

m1b
Community Expert
Community Expert
June 17, 2023

Hi @_wckdTall_, did you get this working in your case? I'm happy to fix any bugs you find. - Mark

m1b
Community Expert
Community Expert
April 22, 2023

Hi @_wckdTall_, Hmm. Sorry if you know all this, but I need to start here before we can get going. 🙂

 

A useful way to approach color is by stating your goal at the outset. What is the color for? If it is for printing pigment, then CMYK or PANTONE etc are appropriate. If it is for screen, or light projection, then RGB is appropriate. And that is before we worry about the characteristics and capabilities of the coloring device, whether printer or monitor or projector, etc.

 

Mixing color spaces in the same document probably isn't wise, but it is common to mix color spaces at different stages of a job, eg. retouching in RGB, and exporting as CMYK (but keeping the RGB as master).

 

So, in your case, is your basic color space clear now? If not, you will probably want to use RGB and not mess around with CMYK until you need to print with pigments.

 

With that out of the way, it sounds like you are writing a script that aims to "snap" black-like colors to "actual" black. If so, does it need to work with CMYK colors or RGB colors? Or is it a general script? Also, you must decide what "actual" black is for your purpose. #000000 is as black as you can get in RGB, but won't be appropriate for many purposes (for example, it might be too dark for video).

- Mark

_wckdTall_
Inspiring
April 22, 2023

Color mode is a good point, but not something I can control, if I could I'd just start with the black tone I want. I basically received a bunch of BW AI files created by various users with different color spaces, and assets from different documents. Depending on how they've been created some will color with 100% K C:0 M:0 Y: 0 K:100, #000000 or just dragging the color to black in the color picker. Likely it's using Registration, in CMYK it's 100 100 100 100, but converted to RGB mode it's 75 68 67 90 or default Black in a new file CMYK 0 0 0 100 where in RGB mode it's 70 67 64 74. What does appear to stay the same in these cases are the RGB values across color modes.

In essence, I'm trying to script what Edit Colors does when it determines black, but I don't need it to be that sophisticated, just to assume black vs color when I trigger the script. 

m1b
Community Expert
Community Expert
April 24, 2023

Yes, the forum seems to be deleting my comments with code in them so let's try a screengrab of code. The logic at the end is where I'm trying to determine what constitutes as black:


Hi @_wckdTall_, that's good for me to understand what you have. You should be able to post code in the </> button so let me know if that doesn't work and I will alert a forum manager.

 

I think before we worry too much about the maths, etc. I think we should start with the structure you have. Finding and changing colors is harder than one would think, and since I've had a crack at a similar project recently, I already had some functions written that do some heavy lifting.

 

So I've written a script that, I hope, will, if nothing else, give you a broader understanding of the issues of getting colors and a couple of approaches to testing the colors. I have structured the code so you can write as many filter functions as you want (see the filters array) they can do anything you want in terms of maths etc, and only have to return true when you want the color change to happen. The second filter in my examples is probably a better way to go than the first, unless you find some clever maths to bring to bear. But there's no reason you can make special case filters if you find it doesn't catch something.

 

By the way, I use the term colorable just to mean an object that accepts a color (although not literally a Color object in every case, eg. RasterItems don't accept color the way pathItems do.

 

See how you go, and let me know. I wrote it quite quickly (aside from some of the utility functions) so it'll be likely to have bugs. Let me know if you find any and I'll update the code here.

- Mark

 

/**
 * Collects "colorables" from selected items,
 * then filters them with supplied functions and
 * finally re-colors colorables.
 *
 * Example replacement color is a Rich Black and
 * match color filters attempt to find various blacks.
 *
 * USAGE INSTRUCTIONS:
 * adjust the settings as needed, but pay particular
 * attention to customizing the `matchColorFilters`
 * to suit your needs.
 *
 * IMPORTANT NOTES:
 * - can only work with an item's *basic* appearance,
 *   meaning its first fillColor and strokeColor.
 * - cannot modify colors in objects that aren't accessible
 *   via scripting, eg. blends.
 * @author m1b
 * @discussion https://community.adobe.com/t5/illustrator-discussions/detect-different-black-tones-math-advice-illustrator/m-p/13744282
 */

(function () {

    var settings = {

        // swatch name
        replacementColorName: 'Rich Black',

        // CMYK breakdown (only used
        // if swatch doesn't exist)
        replacementColorBreakdown: [60, 40, 40, 100],

        // these functions, given a breakdown
        // will return true if match
        // you can adjust them, or make your own
        // note: all of these functions will be
        // checked, so remove any you don't want
        matchColorFilters: [
            matchByConvertingToGrayScale,
            matchCustomBreakdowns
        ],

        keepTints: true,
        showResults: true,
    };

    var doc = app.activeDocument,
        items = itemsInsideGroupItems(doc.selection);

    if (items.length == 0) {
        alert('Please select one or more page items and try again.');
        return;
    }

    var replacementColor = getOrMakeCMYKSwatch(doc, settings.replacementColorName, settings.replacementColorBreakdown),
        results = replaceColors(items, doc, settings.matchColorFilters, replacementColor, settings.keepTints);

    if (settings.showResults)
        alert('Replace Colors\nRe-colored ' + results.conversionCount + ' colorables to "' + replacementColor.name + '".');

})();



/**
 * Returns a swatch with name. If swatch doesn't
 * exist, will make one with supplied breakdown.
 * @param {String} name
 * @param {Array<Number>} breakdown - the color breakdown [C, M, Y, K].
 * @returns {Swatch}
 */
function getOrMakeCMYKSwatch(doc, name, breakdown) {

    var sw = getByName(doc.swatches, name);

    if (sw == undefined) {
        var c = doc.spots.add();
        c.name = name;
        c.colorType = ColorModel.SPOT;
        c.color = new CMYKColor();
        c.color.cyan = breakdown[0];
        c.color.magenta = breakdown[1];
        c.color.yellow = breakdown[2];
        c.color.black = breakdown[3];
        var sp = new SpotColor();
        sp.spot = c;
        sw = getByName(doc.swatches, name);
    }

    return sw;

};


/**
 * Re-colors the items basic fill and stroke colors
 * according to the filters applied.
 * Note on `keepTints` param:
 * When given a SpotColor with 50% tint,
 * we have two options:
 * 1. convert to 100% of a color that matches
 *    the 50% tinted breakdown (keepTints == false).
 * 2. convert to 50% of a color that matches
 *    the untinted breakdown (keepTints == true).
 * @author m1b
 * @version 2023-06-21
 * @param {Array<PageItem>} items - Illustrator page items.
 * @param {Document} doc - the Illustrator Document to convert.
 * @param {Array<Function>} filters - array of functions to filter colorables.
 * @param {Swatch} replacementColorSwatch - the color to apply to filtered colorables.
 * @param {Boolean} [keepTints] - whether to keep tints of the converted colors - see note above (default: true).
 */
function replaceColors(items, doc, filters, replacementColorSwatch, keepTints) {

    var colorables = [],
        colorablesCounter = 0,
        changeColorCounter = 0,
        replacementColorName = replacementColorSwatch.name;

    keepTints = keepTints !== false;

    itemsLoop:
    for (var i = 0; i < items.length; i++)
        colorables = colorables.concat(getBasicColorablesFromItem(doc, items[i]));

    colorablesCounter += colorables.length;

    colorablesLoop:
    for (var i = 0; i < colorables.length; i++) {

        keysLoop:
        for (var key in colorables[i]) {

            if (key == 'color')
                continue keysLoop;

            var item = colorables[i][key],
                col = colorables[i].color,
                tintFactor = 1, // 100%
                tint = 100,
                name = undefined;

            // if spot color (or global color)
            // handle tinting
            if (col.constructor.name == 'SpotColor') {

                if (keepTints)
                    tint = col.tint;
                else
                    tintFactor = col.tint / 100;

                name = col.spot.name;
                col = col.spot.color;

            }

            var breakdown = getColorBreakdown(col, tintFactor);

            if (
                breakdown == undefined
                || name === replacementColorName
            )
                continue keysLoop;

            // run filters

            for (var j = 0; j < filters.length; j++) {

                if (filters[j](breakdown, col, item) == true) {

                    // the filter has picked this colorable
                    changeColorCounter++;

                    if (
                        keepTints
                        && replacementColorSwatch.color.hasOwnProperty('tint')
                    ) {
                        replacementColorSwatch.color.tint = tint;
                    }

                    if (key === 'colorize') {
                        // only way to set the raster item's color to SpotColor
                        var originalSelection = doc.selection;
                        doc.selection = [item];
                        doc.defaultFillColor = replacementColorSwatch.color;
                        doc.selection = originalSelection;
                    }

                    else
                        item[key] = replacementColorSwatch.color;

                    // no need to run any more filters
                    continue keysLoop;
                }

            }

        }

    }

    return {
        colorableCount: colorablesCounter,
        conversionCount: changeColorCounter,
    };

};



/**
 * Returns array of swatches or colors
 * found in fill or stroke of page item.
 * @author m1b
 * @version 2022-10-11
 * @param {PageItem} item - an Illustrator page item.
 * @returns {Object} -  {fillColors: Array<Color>, strokeColors: Array<Color>}
 */
function getBasicColorablesFromItem(doc, item) {

    if (item == undefined)
        throw Error('getBasicColorablesFromItem: No `item` supplied.');

    var colorables = [];

    // collect all the colorables
    if (item.constructor.name == 'PathItem') {
        colorables = colorables.concat(getBasicColorables(item));
    }

    else if (
        item.constructor.name == 'CompoundPathItem'
        && item.pathItems
    ) {
        colorables = colorables.concat(getBasicColorables(item.pathItems[0]));
    }

    else if (
        item.constructor.name == 'RasterItem'
    ) {
        // only way to get the raster item's color
        var originalSelection = doc.selection;
        doc.selection = [item];
        colorables.push({
            color: doc.defaultFillColor,
            colorize: item,
        });
        doc.selection = originalSelection;
    }

    else if (
        item.constructor.name == 'TextFrame'
        && item.textRanges
    ) {
        var ranges = getTextStyleRanges(item.textRange, ['fillColor', 'strokeColor']);
        for (var i = ranges.length - 1; i >= 0; i--)
            colorables = colorables.concat(getBasicColorables(ranges[i]));
    }

    else if (item.constructor.name == 'GroupItem') {
        for (var i = 0; i < item.pageItems.length; i++)
            colorables = colorables.concat(getBasicColorables(item.pageItems[i]));

    }

    return colorables;

};



/**
 * Returns array of colorable things.
 * @date 2023-04-20
 * @param {PageItem} item - an Illustrator page item.
 * @returns {Array<Object>}
 */
function getBasicColorables(item) {

    var colorables = [],
        noColor = "[NoColor]";

    if (
        item.hasOwnProperty('fillColor')
        && item.fillColor != noColor
        && (
            !item.hasOwnProperty('filled')
            || item.filled == true
        )
        && item.fillColor != undefined
    )
        colorables.push({
            color: item.fillColor,
            fillColor: item
        });

    if (
        item.hasOwnProperty('strokeColor')
        && item.strokeColor != noColor
        && (
            !item.hasOwnProperty('stroked')
            || item.stroked == true
        )
        && item.strokeColor != undefined
    )
        colorables.push({
            color: item.strokeColor,
            strokeColor: item
        });

    return colorables;

};


/**
 * Returns an array of TextRanges,
 * determined by mapping changes
 * in supplied keys. For example,
 * supplying the 'fillColor' key
 * will return ranges divided when
 * the text's fillColor changes.
 * @author m1b
 * @version 2023-04-26
 * @param {TextRange} textRange - an Illustrator TextRange.
 * @param {Array<String>} keys - an array of keys to match, eg ['fillColor'].
 */
function getTextStyleRanges(textRange, keys) {

    if (
        textRange == undefined
        || textRange.constructor.name != 'TextRange'
    )
        throw Error('getTextStyleRanges: bad `textRange` supplied.');

    if (
        keys == undefined
        || keys.constructor.name != 'Array'
    )
        throw Error('getTextStyleRanges: bad `textRange` supplied.');

    // check keys are valid
    for (var j = 0; j < keys.length; j++)
        if (!textRange.characterAttributes.hasOwnProperty(keys[j]))
            throw Error('getTextStyleRanges: bad key supplied ("' + keys[j] + '")');

    var ranges = [],
        start = 0,
        currentValues = {};

    charactersLoop:
    for (var i = 0; i < textRange.length; i++) {

        var tr = textRange.textRanges[i],
            matches = true;

        // check each key
        keysLoop:
        for (var j = 0; j < keys.length; j++) {

            if (i == 0)
                currentValues[keys[j]] = tr.characterAttributes[keys[j]];

            else if (stringify(tr.characterAttributes[keys[j]]) !== stringify(currentValues[keys[j]])) {
                matches = false;
                break keysLoop;
            }
        }

        currentValues[keys[j]] = tr.characterAttributes[keys[j]];

        if (
            i == textRange.length - 1
            || !matches
        ) {
            // start a new range
            var newTextRange = textRange.textRanges[start];
            newTextRange.end = i == textRange.length - 1 ? i + 1 : i;
            ranges.push(newTextRange);
            start = i;
        }

    }

    return ranges;

};


/**
 * Stringify tailored for the purpose
 * of identifying identical Swatches.
 * @author m1b
 * @version 2023-04-26
 * @param {Object} obj - the object to stringify.
 * @returns {String}
 */
function stringify(obj) {

    var str = obj.toString();

    for (var key in obj) {

        if (!obj.hasOwnProperty(key))
            continue;

        if (
            key == 'spot'
            || key == 'color'
        )
            str += stringify(obj[key]);

        else
            str += obj[key];

    }

    return str;

};


/**
 * Returns array containing items, including
 * items found inside GroupItems
 * @author m1b
 * @version 2022-05-23
 * Example usage:
 * var items = itemsInsideGroupItems(doc.selection, ['PathItem', 'CompoundPathItem']);
 * @param {Array<PageItem>} items - array or collection of Illustrator page items.
 * @param {String|Array<String>} [typenames] - item constructor names to target (default: target all typenames).
 * @param {Number} [level] - recursion level (private parameter).
 * @returns {Array<PathItem>} the path items found.
 */
function itemsInsideGroupItems(items, typenames, level) {

    try {

        if (level == undefined)
            level = 0;

        var found = [];

        for (var i = 0; i < items.length; i++) {

            var item = items[i];

            if (
                item.uuid == undefined
                || item.parent.typename == 'CompoundPathItem'
            )
                continue;

            if (item.typename == 'GroupItem') {
                found = found.concat(itemsInsideGroupItems(item.pageItems, typenames, level + 1));
            }

            else if (typenames === undefined || itemIsType(item, typenames)) {
                found.push(item);
            }

        }

        return found;

    } catch (err) {
        alert('itemsInsideGroupItems: ' + err)
    }

};


/**
 * Returns true if item.typename
 * matches any of the typenames supplied.
 * @param {PageItem} item - an Illustrator page item.
 * @param {String|Array<String>} typenames - the typenames to check against.
 * @returns {Boolean}
 */
function itemIsType(item, typenames) {

    if (!typenames.constructor.name == 'Array')
        typenames = [typenames];

    var matched = false;
    for (var i = 0; i < typenames.length; i++) {
        if (typenames[i] == item.typename) {
            matched = true;
            break;
        }
    }

    return matched;

};


/**
 * Gets a thing by name from array of named things.
 * @param {Array<any>} things - array of named things.
 * @param {String} name - the name to look for.
 * @returns {Any} - the found thing.
 */
function getByName(things, name) {
    for (var i = 0; i < things.length; i++)
        if (things[i].name == name)
            return things[i];
};


/**
 * Returns an array of color channel values.
 * @param {Swatch|Color} col - an Illustrator Swatch or Color.
 * @param {Number} [tintFactor] - a number in range 0..1 (default: 1).
 * @returns {Array<Number>}
 */
function getColorBreakdown(col, tintFactor) {

    tintFactor = tintFactor || 1;

    if (col.hasOwnProperty('color'))
        col = col.color;

    if (col.constructor.name == 'SpotColor')
        col = col.spot.color;

    if (col.constructor.name === 'CMYKColor')
        return [col.cyan * tintFactor, col.magenta * tintFactor, col.yellow * tintFactor, col.black * tintFactor];

    else if (col.constructor.name === 'RGBColor')
        return [col.red * tintFactor, col.green * tintFactor, col.blue * tintFactor];

    else if (col.constructor.name === 'GrayColor')
        return [col.gray * tintFactor];

};


/**
 * Basic check for dark color
 * Example of basic matching.
 */
function matchCustomBreakdowns(breakdown, col, item) {
    return (
        (   // CMYK
            breakdown.length === 4
            && breakdown[3] > 95
        )
        || ( // RGB
            breakdown.length === 3
            && breakdown[0] < 35
            && breakdown[1] < 35
            && breakdown[2] < 35
        )
        || ( // Gray
            breakdown.length === 1
            && breakdown[0] > 95
        )
    );
};


/**
 * Convert to GrayScale and check gray level.
 */
function matchByConvertingToGrayScale(breakdown, col, item) {

    var gray;

    if (breakdown.length === 4)
        gray = app.convertSampleColor(ImageColorSpace.CMYK, breakdown, ImageColorSpace.GrayScale, ColorConvertPurpose.defaultpurpose);

    else if (breakdown.length === 3)
        gray = app.convertSampleColor(ImageColorSpace.RGB, breakdown, ImageColorSpace.GrayScale, ColorConvertPurpose.defaultpurpose);

    return gray > 95;

};

 

 

Edit 2023-04-25: minor fix. Changed literal true for variable keepTints.

Edit 2023-04-25: added check to stop changing the replacement color to itself.

Edit 2023-04-26: improved getTextStyleRanges so it doesn't rely on json3.js.

Edit 2023-05-02: fixed bug in getOrMakeCMYKSwatch() so that returns a Swatch, not sometimes a SpotColor. Fixed bug in the first basic matchColorFilter where it matched GrayScale grays < 95 (should be > 95).

Edit 2023-06-21: fixed bug that stopped re-coloring in some cases.