javascriptadobeadobe-illustrator

Paint by number Illustrator script


I am from supercoloring and we decided to convert our vector illustrations in color to color by number worksheets. Our input files are color and outline images in svg format. Outline version (like a coloring page) + Color version

outline version and color version

What we want to get is the following

result

We would like that a color palette is generated under the outline version of the image based on the color data from the color version of the image. Moreover, numbers corresponding to this palette are placed inside each color space of the outlined version. I understand that no script in the world would do this properly, but at least I am striving to reduce the time spent by the editor (person) to put these numbers manually in the Illustrator. I understand that our color vector images may have too many colors and shades so we need somehow to limit the result colors of the palette ( to fuse them into large groups of basic colors).

I searched all over the stackoverflow solutions and found some ingenious like Paint with numbers with Adobe Illustrator Javascript and I'm looking to create an automated numbering system for custom paint by number kits in photoshop (Kudos to Yuri Khristich). However, they are not exactly adapted to our needs. Most of scripts on the web generate outlined images from color version, but the quality is compromised. We have already a proper outline version that we want to use as a base for color by number worksheet.


Solution

  • Here is the script to make a 'color palette' for selected artwork.

    And here, as you know, is the script to add color names to all filled areas.

    So I took the two script, made a couple of minimal tweaks and get almost the result you want. All you need after the scripts is to copy the layer with numbers and 'palette' from a colored artwork to a outline version.

    Script #1

    // Modified version
    // https://stackoverflow.com/questions/75344674/paint-by-number-illustrator-script
    
    // Original: 
    // https://productivista.com/make-a-list-of-colors-from-your-selection/
    
    /*
      Date: July, 2020
      Author: Katja Bjerrum, email: katja@productivista.com, www.productivista.com
      ============================================================================
      NOTICE:
      This script is provided "as is" without warranty of any kind.
      Free to use, not for sale.
      ============================================================================
      Released under the MIT license.
      http://opensource.org/licenses/mit-license.php
      ============================================================================
    
    */
    
    //@target illustrator
    
    var doc = app.activeDocument;
    var myLayer = doc.activeLayer;
    app.coordinateSystem = CoordinateSystem.ARTBOARDCOORDINATESYSTEM;
    var swGrps = doc.swatchGroups;
    var mainSwGr = doc.swatchGroups[0];
    var sel = doc.selection;
    
    var actionSet = 'CreateSwatchGroup';
    var actionName = 'ColourGroup';
    var actionPath = Folder.myDocuments + '/Adobe Scripts/';
    
    if (!Folder(actionPath).exists) Folder(actionPath).create();
    //app.doScript("Colorgroup", "ToSwatchScript"); // Action, that creates swatch group
    
    var actionDoc =
      [ '/version 3',
        '/name [' + actionSet.length  + ' ' + ascii2Hex(actionSet) + ']',
        '/isOpen 1',
        '/actionCount 1',
        '/action-1 {',
        '/name [' + actionName.length + ' ' + ascii2Hex(actionName) + ']',
        '   /keyIndex 0',
        '   /colorIndex 0',
        '   /isOpen 1',
        '   /eventCount 1',
        '   /event-1 {',
        '     /useRulersIn1stQuadrant 0',
        '     /internalName (ai_plugin_swatches)',
        '     /localizedName [ 8',
        '       5377617463686573',
        '     ]',
        '     /isOpen 0',
        '     /isOn 1',
        '     /hasDialog 1',
        '     /showDialog 1',
        '     /parameterCount 1',
        '     /parameter-1 {',
        '       /key 1835363957',
        '       /showInPalette 4294967295',
        '       /type (enumerated)',
        '       /name [ 15',
        '         4e657720436f6c6f722047726f7570',
        '       ]',
        '      /value 17',
        '     }',
        '   }',
        '}'].join('');
    
    createAction(actionDoc, actionName, actionPath);
    
    app.redraw();
    app.doScript (actionName, actionSet);
    app.redraw();
    app.unloadAction(actionSet, '');
    
    
    var convMM = 2.8346456692; // initialization of the variable to convert points to mm
    
    var colorgroup = doc.swatchGroups[doc.swatchGroups.length - 1]; // Choose the last swatch group
    var stY = -200; //
    var stX = 20;
    var recW = 25;
    var recH = 25;
    
    var offX = recW / 5;
    var offY = recH / 4;
    var textoffY = recH / 4;
    var rows = 4;
    var cols = 4;
    
    var black = new GrayColor();
    black.gray = 80;
    var white = new GrayColor() ;
    white.gray = 0;
    var noStroke = doc.swatches[0].color;
    
    if (swGrps.length <=1){
        alert ("Please create swatch group from your selection");
    }
    else if (sel <= 0){
        //docRef.placedItems[0].selected == false;
        alert ("Please make a selection");
        delSwatchGr(colorgroup); //delete swatch group
    }
    else{
        swatchGroupList(colorgroup, stY, stX);//create corlor list
        // delSwatchGr(colorgroup);//delete swatch group
    }
    
    
    //Function, that creates color list
    function swatchGroupList(swatchGroup, stY, stX) {
    
        // Groups everything in the list
        var mainGroup = myLayer.groupItems.add();
        mainGroup.name = "Colors";
        mainGroup.moveToBeginning(myLayer);
    
        //Name of the color list
        var nameText = myLayer.textFrames.add();
        nameText.contents = swatchGroup.name; // the name of the swatch group
        nameText.position = [stX, stY + recH];
    
        var nameStyle = nameText.textRange.characterAttributes;
        nameStyle.size = 12;//size in punkt
        //nameStyle.textFont = textFonts.getByName("Avenir-Book");//the font
        nameStyle.capitalization = FontCapsOption.ALLCAPS;//ALL CAPITALS
    
        var swatches = swatchGroup.getAllSwatches();
        var swatchArray = [];
    
        for (i = swatches.length-1; i>=0; i--) {
            var mySwatch = swatches[i];
            mySwatch.name = i + 1;
            var subGroup = createSwatchGroup(mySwatch, textoffY);
            swatchArray.push(subGroup);
        }
    
        nameText.moveToEnd(mainGroup);
        var myGroup = swatchArray;
        var maxW = maxWidth(myGroup);
    
    
        for (var j = 0; j < myGroup.length; j++) {
            var mySubGroup = myGroup[j];
            mySubGroup.moveToBeginning(mainGroup);
        }
    
        for (var i = 0; i < mainGroup.groupItems.length; i++) {
            var mySubGroup = mainGroup.groupItems[i];
    
            if (mainGroup.groupItems.length > 7) {
                rows = 7;
                var c = i%rows;
                var r = Math.floor(i/rows);
                mySubGroup.position = [stX + r * (maxW + 10), stY - c * (recH + offY)];
            }
            else {
                rows = 7;
                var c = i % rows;
                var r = Math.floor(i / rows);
                mySubGroup.position = [stX, stY - c * (recH + offY)];
            }
        }
        // textSwatch.moveToBeginning(SubGroup);
        // path_ref.moveToBeginning(SubGroup);
        // SubGroup.position = [stX + c * 140, stY - r * (path_ref.height + offY)];
        subGroup.moveToBeginning(mainGroup);
    
    }
    
    function lightColor(c){
        if(c.typename)
        {
           switch(c.typename)
           {
                case "CMYKColor":
                return (c.black>=10 || c.cyan>10 || c.magenta>10 || c.yellow > 10) ? true : false;
                case "RGBColor":
                return (c.red<230  || c.green<230 || c.blue<230) ? true : false;
                case "GrayColor":
                return c.gray >= 10 ? true : false;
                case "SpotColor":
                return lightColor(c.spot.color);
    
                //return false;
           }
       }
    }
    
    function fitItem(item, itemW, itemH, diff) {
        var oldWidth = item.width
        var oldHeight = item.height
    
        if (item.width > item.height) {
          // landscape, scale height using ratio from width
          item.width = itemW - diff.deltaX
          var ratioW = item.width / oldWidth
          item.height = oldHeight * ratioW
        } else {
          // portrait, scale width using ratio from height
          item.height = itemH - diff.deltaY
          var ratioH = item.height / oldHeight
          item.width = oldWidth * ratioH
        }
    
      }
    
      function itemBoundsDiff(item) {
        var itemVB = item.visibleBounds
    
        var itemVW = itemVB[2] - itemVB[0] // right - left
        var itemVH = itemVB[1] - itemVB[3] // top - bottom
    
        var itemGB = item.geometricBounds
    
        var itemGW = itemGB[2] - itemGB[0] // right - left
        var itemGH = itemGB[1] - itemGB[3] // top - bottom
    
        var deltaX = itemVW - itemGW
        var deltaY = itemVH - itemGH
    
        var diff = { deltaX: deltaX, deltaY: deltaY }
    
        return diff
      }
    
      function delSwatchGr(swGr){
    
            var swGrSws = swGr.getAllSwatches();
            for (var j = 0; j < swGrSws.length; j++){
                var sw = swGrSws[j];
                sw.color = new CMYKColor();
            }
            swGr.remove();
    
    }
    
    //Function finds the max group width
    function maxWidth(myGroup) {
        var maxFound = 0;
        for (var j = 0; j < myGroup.length; j++) {
            var GrWidth = myGroup[j].width;
            //var Widthmax = GrWidth.width;
            maxFound = Math.max(maxFound, GrWidth);
        }
        return maxFound;
    }
    
    function createSwatchGroup(sw, myOffset) {
        //Is "MyForm" path exists?
        try{
            var path_ref_ori = app.activeDocument.pathItems.getByName("MyForm" || "myform" || "MYFORM");
        }
        catch(e) {
            var path_ref_ori = false;
        }
    
        if (path_ref_ori) {
            myPath = path_ref_ori.duplicate();
            var boundsDiff = itemBoundsDiff(myPath);
            fitItem(myPath, recW, recH, boundsDiff);
            myPath.name = "NewForm";
            myPath.position = [0, 0];
        }
        else {
            var myPath = createMyPath()
        }
    
        myPath.fillColor = sw.color;
        myPath.stroked = true;
        myPath.strokeWidth = 0.3;
        myPath.strokeColor = lightColor(myPath.fillColor) ? noStroke : black;
    
        var textSwatch = myLayer.textFrames.add(); //swatch text
        textSwatch.contents = sw.name;
    
        textSwatch.position = [myPath.width + 1.3 * convMM, -myOffset];
        var textSwStyle = textSwatch.textRange.characterAttributes;
        textSwStyle.size = 10; //size in punkt
        //textSwStyle.textFont = textFonts.getByName("MyriadPro-Semibold"); //the font
    
        var SubGroup = myLayer.groupItems.add(); //groups path and text
        SubGroup.name = sw.name;
        SubGroup.position = [0, 0];
    
        textSwatch.moveToBeginning(SubGroup);
        myPath.moveToBeginning(SubGroup);
    
        return SubGroup;
    }
    
    
    function createMyPath(){
    //Is "MyForm" path exists?
        try{
            var path_ref_ori = app.activeDocument.pathItems.getByName("MyForm" || "myform" || "MYFORM");
        }
        catch(e) {
            var path_ref_ori = false;
        }
    
        if (path_ref_ori) {
            path_ref = path_ref_ori.duplicate();
            var boundsDiff = itemBoundsDiff(path_ref);
            fitItem(path_ref, recW, recH, boundsDiff);
            path_ref.name = "NewForm";
            path_ref.position = [0, 0];
        }
        else {
            var path_ref = myLayer.pathItems.rectangle(0, 0, recW, recH); //swatch path item
        }
    
        return path_ref
    };
    
    function createAction(str, set, path) {
        var f = new File('' + path + '/' + set + '.aia');
        f.open('w');
        f.write(str);
        f.close();
        app.loadAction(f);
        f.remove();
    };
    
    function ascii2Hex(hex) {
        return hex.replace(/./g, function (a) { return a.charCodeAt(0).toString(16) });
    };
    

    Input (after select the artwork and run the script):

    enter image description here

    Result (added the global swatches and the 'color palette' at the bottom):

    enter image description here

    Script #2

    // Based on:
    // https://stackoverflow.com/questions/73705368/paint-with-numbers-with-adobe-illustrator-javascript
    
    var doc = app.activeDocument,
        lays = doc.layers,
        WORK_LAY = lays.add(),
        NUM_LAY = lays.add(),
        i = lays.length - 1,
        lay;
    
    // main working loop
    for (; i > 1; i--) {
        //process each layer
        lay = lays[i];
        lay.name = lay.name + " Num:" + (i - 1); // i-1 as 2 layers beed added.
        process(lay.pathItems, false);
        process(lay.compoundPathItems, true); // if any
    }
    //clean up
    NUM_LAY.name = "Numbers";
    WORK_LAY.remove();
    
    function process(items, isCompound) {
        var j = 0,
            b, xy, s, p, op;
    
        for (; j < items.length; j++) {
            // process each pathItem
            op = items[j];
            try { color = op.fillColor.spot.name } catch(e) { continue } // <-- HERE
            // add stroke
            if (isCompound) {
                // strokeComPath(op);
            } else {
                // !op.closed && op.closed = true;
                // op.filled = false;
                // op.stroked = true;
            };
            b = getCenterBounds(op);
            xy = [b[0] + (b[2] - b[0]) / 2, b[1] + (b[3] - b[1]) / 2];
            s = (
                Math.min(op.height, op.width) < 20 ||
                (op.area && Math.abs(op.area) < 150)
                ) ? 20 : 40; // adjust font size for small area paths.
            add_nums(color, xy, s); // <--- HERE
        }
    }
    
    function getMinVisibleSize(b) {
        var s = Math.min(b[2] - b[0], b[1] - b[3]);
        return Math.abs(s);
    }
    
    function getGeometricCenter(p) {
        var b = p.geometricBounds;
        return [(b[0] + b[2]) / 2, (b[1] + b[3]) / 2];
    }
    
    // returns square of distance between p1 and p2
    function getDist2(p1, p2) {
        return Math.pow(p1[0] + p2[0], 2) + Math.pow(p1[1] + p2[1], 2);
    }
    
    // returns visibleBounds of a path in a compoundPath p
    // which is closest to center of the original path op
    function findBestBounds(op, p) {
        var opc = getGeometricCenter(op);
        var idx = 0,
            d;
        var minD = getDist2(opc, getGeometricCenter(p.pathItems[0]));
        for (var i = 0, iEnd = p.pathItems.length; i < iEnd; i++) {
            d = getDist2(opc, getGeometricCenter(p.pathItems[i]));
            if (d < minD) {
                minD = d;
                idx = i;
            }
        }
        return p.pathItems[idx].visibleBounds;
    }
    
    function applyOffset(op, checkBounds) {
        var p = op.duplicate(WORK_LAY, ElementPlacement.PLACEATBEGINNING),
            // offset value the small the better, but meantime more slow.
            offset = function() {
                var minsize = Math.min(p.width, p.height);
                if (minsize >= 50) {
                    return '-1'
                } else if (20 < minsize && minsize < 50) {
                    return '-0.5'
                } else {
                    return '-0.2' // 0.2 * 2 (both side ) * 50 (Times) = 20
                }
            },
            xmlstring = '<LiveEffect name="Adobe Offset Path"><Dict data="I jntp 2 R mlim 4 R ofst #offset"/></LiveEffect>'
            .replace('#offset', offset()),
            TIMES = 100; // if shapes are too large, should increase the value.
    
        if (checkBounds) {
            // check its size only if it needs, because it's too slow
            while (TIMES-- && getMinVisibleSize(p.visibleBounds) > 3) p.applyEffect(xmlstring);
        } else {
            while (TIMES--) p.applyEffect(xmlstring);
        }
        return p;
    }
    
    function getCenterBounds(op) {
        var originalMinSize = getMinVisibleSize(op.visibleBounds);
    
        var p = applyOffset(op, false);
    
        if (getMinVisibleSize(p.visibleBounds) > originalMinSize) {
            // in some cases, path p becomes larger for some unknown reason
            p.remove();
            p = applyOffset(op, true);
        }
    
        var b = p.visibleBounds;
    
        if (getMinVisibleSize(b) > 10) {
            activeDocument.selection = [p];
            executeMenuCommand("expandStyle");
            p = activeDocument.selection[0];
            if (p.typename == "CompoundPathItem") {
                b = findBestBounds(op, p);
            }
        }
    
        p.remove();
        return b;
    }
    
    function add_nums(n, xy, s) {
        var txt = NUM_LAY.textFrames.add();
    
        txt.contents = n;
        txt.textRange.justification = Justification.CENTER;
        txt.textRange.characterAttributes.size = s;
        txt.position = [xy[0] - txt.width / 2, xy[1] + txt.height / 2];
    }
    
    function strokeComPath(compoundPath) {
        var p = compoundPath.pathItems,
            l = p.length,
            i = 0;
    
        for (; i < l; i++) {
            // !p[i].closed && p[i].closed = true;
            // p[i].stroked = true;
            // p[i].filled = false;
        }
    };
    

    Result (added the layer with numbers after run the script):

    enter image description here

    Final outlined version with numbers and the 'color palette'

    enter image description here

    Note: you have to ungroup and unmask the color artwork before you run the Script #2.

    Here is the results for the rest examples:

    enter image description here

    enter image description here

    As you can see the 'final' artwork still need a quite amount of additional manual work: to move or remove extra numbers.

    And it makes sense to reduce the number of colors in original color artworks (perhaps it's possible to do with a script to some extent, as well).