def cree_background(inLayer, inLayerF, Img):
    """Add background to inLayer"""
    newlayer = pdb.gimp_layer_copy(inLayer, True)
    newfond = pdb.gimp_layer_copy(inLayerF, True)
    layername = pdb.gimp_item_get_name(inLayer)

    pdb.gimp_item_set_visible(newlayer, True)
    pdb.gimp_image_insert_layer(Img, newlayer, None, 0)

    pdb.gimp_image_set_active_layer(Img, inLayer)
    pdb.gimp_image_insert_layer(Img, newfond, None, -1)
    pdb.gimp_image_lower_item(Img, newfond)
    pdb.gimp_item_set_visible(newfond, 1)

    pdb.gimp_context_set_sample_transparent(True)
    pdb.gimp_image_select_contiguous_color(Img, CHANNEL_OP_REPLACE, newlayer,
                                           10, 10)
    pdb.gimp_edit_clear(newfond)
    pdb.gimp_item_set_visible(inLayer, True)
    clipped_layer = pdb.gimp_image_merge_down(Img, inLayer, CLIP_TO_IMAGE)
    pdb.gimp_item_set_name(clipped_layer, layername)

    pdb.gimp_selection_invert(Img)
    pdb.gimp_context_set_foreground((117, 117, 154))
    pdb.gimp_edit_fill(newlayer, FILL_FOREGROUND)

    floating_sel = pdb.gimp_edit_paste(newlayer, 0)
    pdb.gimp_layer_set_opacity(floating_sel, 70)
    pdb.gimp_floating_sel_anchor(floating_sel)

    pdb.gimp_layer_set_opacity(newlayer, 85)
    pdb.gimp_selection_none(Img)

    layerfinal = pdb.gimp_image_get_layer_by_name(Img, layername)
    pdb.gimp_item_set_visible(layerfinal, False)
Beispiel #2
0
    def on_add(self, widget, copy=False):
        """
        Add new layer to the image and a new frame to the Timeline.
        if copy is true them the current layer will be copy.
        """
        # starting gimp undo group
        self.image.undo_group_start()

        name = "Frame " + str(len(self.frames))
        # create the layer to add
        l = None
        if not copy:
            l = gimp.Layer(self.image, name, self.image.width,
                           self.image.height, RGBA_IMAGE, 100, NORMAL_MODE)

        else:  # copy current layer to add
            l = self.frames[self.active].layer.copy()
            l.name = name

        # adding layer
        self.image.add_layer(l, len(self.image.layers) - self.active - 1)
        if self.new_layer_type == TRANSPARENT_FILL and not copy:
            pdb.gimp_edit_clear(l)

        self._scan_image_layers()
        self.on_goto(None, NEXT, True)

        if len(self.frames) == 1:
            self._toggle_enable_buttons(NO_FRAMES)

        # ending gimp undo group
        self.image.undo_group_end()
def load_caches(args):
    inColorFondR, inColorFondG, inColorFondB, outDir = args.split(" ")
    inDir, inNameLayerFleche = GRAPH_PATH + "layers/", "layer_fleche-1.png"
    inColorFondR, inColorFondG, inColorFondB = int(inColorFondR), int(
        inColorFondG), int(inColorFondB)

    nb, listimg = pdb.file_glob(inDir + "*.png", 1)
    baseimage = pdb.gimp_image_new(10, 10, RGB)
    fondcolor = (inColorFondR, inColorFondG, inColorFondB)

    for filename in listimg:
        layer = pdb.gimp_file_load_layer(baseimage, filename)
        pdb.gimp_image_insert_layer(baseimage, layer, None, 0)

    pdb.gimp_image_resize_to_layers(baseimage)
    pdb.gimp_message("Layers chargés")
    pdb.gimp_selection_all(baseimage)

    layerfond = pdb.gimp_image_get_layers(baseimage)[1][0]
    layerfond = gimp.Item.from_id(layerfond)
    fond = pdb.gimp_layer_copy(layerfond, 1)
    _, _, _, xmax, ymax = pdb.gimp_selection_bounds(baseimage)

    pdb.gimp_item_set_name(fond, "layer_fond.png")
    pdb.gimp_image_insert_layer(baseimage, fond, None, 0)

    pdb.gimp_edit_clear(fond)
    pdb.gimp_image_select_round_rectangle(baseimage, CHANNEL_OP_REPLACE, 0, 0,
                                          xmax, ymax, 35, 35)
    pdb.gimp_selection_shrink(baseimage, 3)
    pdb.gimp_selection_feather(baseimage, 20)
    pdb.gimp_context_set_foreground(fondcolor)
    pdb.gimp_edit_fill(fond, FILL_FOREGROUND)
    pdb.gimp_image_lower_item_to_bottom(baseimage, fond)
    pdb.plug_in_hsv_noise(baseimage, fond, 5, 38, 63, 74)
    pdb.gimp_selection_none(baseimage)
    pdb.gimp_message("Fond créé")
    caches(baseimage, inNameLayerFleche, "layer_fond.png")
    pdb.gimp_message("Cache créé")

    layercache = pdb.gimp_image_get_layer_by_name(baseimage, "cache.png")
    layerfond = pdb.gimp_image_get_layer_by_name(baseimage, "layer_fond.png")
    layerfleche = pdb.gimp_image_get_layer_by_name(baseimage,
                                                   inNameLayerFleche)

    pdb.gimp_item_set_visible(layerfond, True)
    pdb.gimp_image_merge_down(baseimage, layercache, CLIP_TO_IMAGE)
    pdb.gimp_item_set_visible(layerfleche, True)
    pdb.gimp_image_merge_down(baseimage, layerfleche, CLIP_TO_IMAGE)

    pdb.gimp_image_scale(baseimage, 900, 550)
    # drawable = pdb.gimp_image_get_active_drawable(baseimage)
    pdb.script_fu_multiple_layer_actions(baseimage, None, 0, 0, (0, 0, 0), 4,
                                         0, 0, 0, 0, 0)
    pdb.gimp_message("Taille de l'image ajustée")

    pdb.script_fu_export_layers(baseimage, None, outDir, "~l")
    pdb.gimp_message("Layers enregistrés")
Beispiel #4
0
def clear_background(image, drawable):
    # invert selection, thus selecting the background
    pdb.gimp_selection_invert(image)

    # clear background
    pdb.gimp_edit_clear(drawable)

    # invert selection
    pdb.gimp_selection_invert(image)
Beispiel #5
0
def MDFade(image, layer, srcmode, dstmode, fademode):
    srclut = SelectSrcLUT(srcmode)
    dstlut = SelectDstLUT(dstmode)
    gimp.progress_init("Generating palette fade...")
    pdb.gimp_image_undo_group_start(image)
    # Get the layer position.
    pos = FindLayer(image, layer)
    srcWhite = FindColor(255, srcmode)
    if (fademode == FadeMode.CurrentToBlack):
        conv = lambda jj, ss: (jj * (15 - ss)) // 15
    elif (fademode == FadeMode.BlackToCurrent):
        conv = lambda jj, ss: (jj * ss) // 15
    elif (fademode == FadeMode.CurrentToWhite):
        conv = lambda jj, ss: (jj * (15 - ss) + srcWhite * ss) // 15
    else:  #if (fademode == FadeMode.WhiteToCurrent):
        conv = lambda jj, ss: (jj * ss + srcWhite * (15 - ss)) // 15
    finalLayer = None
    for step in xrange(15):
        lut = {
            chr(ii): chr(dstlut[srclut[conv(ii, step)]])
            for ii in xrange(256)
        }
        # Create a new layer to save the results (otherwise is not possible to undo the operation).
        newLayer = layer.copy()
        image.add_layer(newLayer, pos)
        # Clear the new layer.
        pdb.gimp_edit_clear(newLayer)
        newLayer.flush()
        # Calculate the number of tiles.
        tile_width = gimp.tile_width()
        tile_height = gimp.tile_height()
        # Ceiling division
        width_tiles = -(-layer.width // tile_width)
        height_tiles = -(-layer.height // tile_height)
        # Iterate over the tiles.
        for col in xrange(width_tiles):
            for row in xrange(height_tiles):
                # Update the progress bar.
                gimp.progress_update(
                    float(step * width_tiles * height_tiles +
                          col * height_tiles + row) /
                    float(15 * width_tiles * height_tiles))
                ConvertTileNoSHL(layer.get_tile(False, row, col),
                                 newLayer.get_tile(False, row, col), lut)
        # Update the new layer.
        newLayer.flush()
        newLayer.merge_shadow(True)
        newLayer.update(0, 0, layer.width, layer.height)
        finalLayer = newLayer
    # Remove the old layer.
    if finalLayer is not None:
        layerName = layer.name
        image.remove_layer(layer)
        finalLayer.name = layerName
    gimp.displays_flush()
    pdb.gimp_image_undo_group_end(image)
def nobox(image=None):
    if not image:
        image = gimp.image_list()[0]
    pdb.gimp_context_set_sample_transparent(True)
    image_size = (image.width, image.height)
    for layer in image.layers:
        if not pdb.gimp_drawable_has_alpha(layer):
            layer.add_alpha()
        if layer.offsets != (0, 0) or (layer.width,
                                       layer.height) != image_size:
            pdb.gimp_layer_resize_to_image_size(layer)
        (xmax, ymax) = (image.width - 1, image.height - 1)
        for (x, y) in [(0, 0), (xmax, 0), (0, ymax), (xmax, ymax)]:
            _num_channels, pixel = pdb.gimp_drawable_get_pixel(layer, x, y)
            if all(p > 240 for p in pixel):
                pdb.gimp_image_select_contiguous_color(image, 2, layer, x, y)
                pdb.gimp_edit_clear(layer)
Beispiel #7
0
def cookie_cutter_letter(img, substrate, right, font, letter):
    '''Cut text shaped like letter out of the given layer.'''

    temp_layer = gpdb.gimp_text_fontname(img, substrate, right, 0, letter, 1,
                                         False, FONT_HEIGHT, PIXELS, font)
    gpdb.gimp_selection_layer_alpha(temp_layer)

    angle = random.uniform(*ANGLE_RANGE)
    xaxis = right
    yaxis = 15
    # srcX = float(xaxis)
    # dstX = float(srcX + random.uniform(0, 25))
    # srcY = float(yaxis)
    # dstY = float(srcY + random.uniform(0, 25))
    # scaleX = scaleY = float(100)

    # We need to save the selection as a channel so we can mess with
    # the letter form.
    shape = gpdb.gimp_selection_save(img)
    gpdb.gimp_selection_none(img)
    gpdb.gimp_floating_sel_remove(temp_layer)

    # Distort the form of the individual letter:
    shape = gpdb.gimp_item_transform_rotate(shape, angle, 0, xaxis, yaxis)

    # We aren't doing any letter warping now, but if we were, this is the
    # point where it should be done. We want to warp the shape of textLayer,
    # which later serves as a cutout for the dark noise layer. If we warp the
    # dark noise layer directly we will end up with warped dots.
    #
    # gpdb.gimp_context_set_transform_resize(TRANSFORM_RESIZE_CROP)
    # shape = gpdb.gimp_item_transform_2d(shape, srcX, srcY, angle,
    #                                     scaleX, scaleY, dstX, dstY)
    gpdb.gimp_selection_load(shape)
    img.remove_channel(shape)

    # Note the bounding box of the letter form so we can figure out
    # where the next one should go.
    bounds = gpdb.gimp_selection_bounds(img)
    new_right = bounds[3] + LETTER_SPACING

    # Actually cut the letter form out of the substate.
    gpdb.gimp_selection_invert(img)
    gpdb.gimp_edit_clear(substrate)
    gpdb.gimp_selection_none(img)
    return new_right
Beispiel #8
0
def change_num(image, inNumero):
    layer = pdb.gimp_image_get_active_layer(image)

    pdb.gimp_context_set_sample_transparent(True)
    pdb.gimp_image_select_contiguous_color(image, CHANNEL_OP_REPLACE,layer, 1, 1)
    pdb.gimp_selection_invert(image)
    _, x, y, x2, y2  = pdb.gimp_selection_bounds(image)
    xthird=x  + ((x2 - x) // 4  * 3)

    y = _find_black_not_transparent(image, layer, xthird, y)

    pdb.gimp_context_set_sample_transparent(False)
    pdb.gimp_image_select_contiguous_color(image, CHANNEL_OP_REPLACE, layer, xthird, y)
    pdb.gimp_edit_clear(layer)
                    
    if inNumero > 1:        
        _, x, y, x2, y2  = pdb.gimp_selection_bounds(image)
        ymiddle = y + ( (y2 - y) // 2)

        keep = True
        while keep:
            if not _is_transparent(image, layer, x, ymiddle):
                keep = False
                pdb.gimp_context_set_sample_transparent(False)
                pdb.gimp_context_set_sample_threshold(1)
                pdb.gimp_image_select_contiguous_color(image, CHANNEL_OP_REPLACE, layer, x+1, ymiddle)
                pdb.gimp_context_set_sample_threshold(0.1)
 
            x += 1
        pdb.gimp_edit_clear(layer)

        _, x1, y1, x2, y2  = pdb.gimp_selection_bounds(image)
        
        pdb.gimp_image_select_rectangle(image, CHANNEL_OP_REPLACE, x1, y1, x2-x1, y2-y1)
        
        filename = GRAPH_PATH + str(inNumero) + ".png"

        imagechiffre = pdb.gimp_file_load(filename, filename)
        layerchiffre = pdb.gimp_image_get_active_layer(imagechiffre)
        
        pdb.gimp_selection_all(imagechiffre)
        pdb.gimp_edit_copy(layerchiffre)
        pdb.gimp_image_delete(imagechiffre)
        
        floating_sel = pdb.gimp_edit_paste(layer,False)
        pdb.gimp_floating_sel_anchor(floating_sel)
Beispiel #9
0
def cookie_cutter_letter(img, substrate, right, font, letter):
    '''Cut text shaped like letter out of the given layer.'''

    temp_layer = gpdb.gimp_text_fontname(img, substrate, right, 0, letter,
                                         1, False, FONT_HEIGHT, PIXELS, font)
    gpdb.gimp_selection_layer_alpha(temp_layer)

    angle = random.uniform(*ANGLE_RANGE)
    xaxis = right
    yaxis = 15
    # srcX = float(xaxis)
    # dstX = float(srcX + random.uniform(0, 25))
    # srcY = float(yaxis)
    # dstY = float(srcY + random.uniform(0, 25))
    # scaleX = scaleY = float(100)

    # We need to save the selection as a channel so we can mess with
    # the letter form.
    shape = gpdb.gimp_selection_save(img)
    gpdb.gimp_selection_none(img)
    gpdb.gimp_floating_sel_remove(temp_layer)

    # Distort the form of the individual letter:
    shape = gpdb.gimp_item_transform_rotate(shape, angle, 0, xaxis, yaxis)

    # We aren't doing any letter warping now, but if we were, this is the
    # point where it should be done. We want to warp the shape of textLayer,
    # which later serves as a cutout for the dark noise layer. If we warp the
    # dark noise layer directly we will end up with warped dots.
    #
    # gpdb.gimp_context_set_transform_resize(TRANSFORM_RESIZE_CROP)
    # shape = gpdb.gimp_item_transform_2d(shape, srcX, srcY, angle,
    #                                     scaleX, scaleY, dstX, dstY)
    gpdb.gimp_selection_load(shape)
    img.remove_channel(shape)

    # Note the bounding box of the letter form so we can figure out
    # where the next one should go.
    bounds = gpdb.gimp_selection_bounds(img)
    new_right = bounds[3] + LETTER_SPACING

    # Actually cut the letter form out of the substate.
    gpdb.gimp_selection_invert(img)
    gpdb.gimp_edit_clear(substrate)
    gpdb.gimp_selection_none(img)
    return new_right
Beispiel #10
0
def MDColors(image, layer, srcmode, dstmode, shlmode):
    lut = BuildColorLUT(srcmode, dstmode, shlmode)
    gimp.progress_init("Converting to MD colors...")
    # Indexed images are faster
    if layer.is_indexed:
        ConvertColormap(image, lut)
    else:
        pdb.gimp_image_undo_group_start(image)
        # Get the layer position.
        pos = FindLayer(image, layer)
        # Create a new layer to save the results (otherwise is not possible to undo the operation).
        newLayer = layer.copy()
        image.add_layer(newLayer, pos)
        layerName = layer.name
        # Clear the new layer.
        pdb.gimp_edit_clear(newLayer)
        newLayer.flush()
        # Calculate the number of tiles.
        tile_width = gimp.tile_width()
        tile_height = gimp.tile_height()
        # Ceiling division
        width_tiles = -(-layer.width // tile_width)
        height_tiles = -(-layer.height // tile_height)
        # Iterate over the tiles.
        for col in xrange(width_tiles):
            for row in xrange(height_tiles):
                # Update the progress bar.
                gimp.progress_update(
                    float(col * height_tiles + row) /
                    float(width_tiles * height_tiles))
                ConvertTile(layer.get_tile(False, row, col),
                            newLayer.get_tile(False, row, col), lut)
        # Update the new layer.
        newLayer.flush()
        newLayer.merge_shadow(True)
        newLayer.update(0, 0, layer.width, layer.height)
        # Remove the old layer.
        image.remove_layer(layer)
        newLayer.name = layerName
        # Update display and finish undo group.
        gimp.displays_flush()
        pdb.gimp_image_undo_group_end(image)
Beispiel #11
0
def effet_texte(inColor, inImg):
    layer = pdb.gimp_image_get_active_layer(inImg)
    
    pdb.gimp_context_set_sample_transparent(True)
    pdb.gimp_image_select_contiguous_color(inImg, CHANNEL_OP_REPLACE, layer, 1, 1)
    pdb.gimp_selection_invert(inImg)

    _, x, _, x2, y2  = pdb.gimp_selection_bounds(inImg)
    xmiddle = x + (x2 - x) // 2
    y = y2 - 1
        
    keep = True
    while keep:
        if y < 0:
            print("Error in effet_texte for img {} : y < 0 !".format(inImg))
            return
        if (not _is_transparent(inImg, layer, xmiddle, y)) and  _is_black(inImg, layer, xmiddle, y):
            keep = False
            pdb.gimp_context_set_sample_transparent(False)
            pdb.gimp_image_select_contiguous_color(inImg, CHANNEL_OP_REPLACE, layer, xmiddle, y)
            pdb.gimp_edit_clear(layer)
                
        y -= 1

    _, x1, y1, x2, y2  = pdb.gimp_selection_bounds(inImg)
    newlayer = pdb.gimp_layer_copy(layer, True)

    pdb.gimp_image_select_rectangle(inImg, CHANNEL_OP_REPLACE, x1, y1, x2-x1, y2-y1)
    pdb.gimp_context_set_sample_transparent(True)
    pdb.gimp_image_select_contiguous_color(inImg, CHANNEL_OP_SUBTRACT, layer, x1, y1)
    pdb.gimp_edit_clear(layer)
    
    pdb.gimp_selection_invert(inImg)
    pdb.gimp_image_insert_layer(inImg, newlayer, None, 0)
    pdb.gimp_edit_clear(newlayer)
    
    pdb.script_fu_layerfx_outer_glow(inImg, newlayer, inColor, 75, 0, 0, 0, 0, 5, 0, 1)
    pdb.gimp_image_merge_visible_layers(inImg, CLIP_TO_IMAGE)
    
    pdb.gimp_selection_none(inImg)
    pdb.gimp_displays_flush()
Beispiel #12
0
def make_captcha(sx, sy, font_height, letter_spacing, left_margin, angle_range,
                 fonts, answer):
    """Generate a captcha consisting of the letters in answer.

    :rtype: :class:`gimp.Image`
    :returns: The CAPTCHA as a gimp-python image object.
    """
    img = gimp.Image(sx, sy, RGB_IMAGE)
    img.disable_undo()

    light_noise_layer = gimp.Layer(img, 'light noise', sx, sy, RGB_IMAGE, 100,
                                   NORMAL_MODE)
    img.add_layer(light_noise_layer, 0)

    gpdb.gimp_selection_none(img)
    gpdb.gimp_drawable_fill(light_noise_layer, WHITE_FILL)

    # plug_in_randomize_hurl at 1% 1 time is vastly superior to
    # scatter_rgb here, but has a bug where it creates an artifact at
    # the top of the image when invoked in a scripting context like
    # this.
    #
    # Future experiment: dial down the amount of noise generated by
    # scatter, then run it through levels to darken it, then
    # blur. This should be equivalent to hurl + blur.
    #gpdb.plug_in_randomize_hurl(img, light_noise_layer, 1, 1, 0, 0)
    gpdb.plug_in_scatter_hsv(img, light_noise_layer, 1, 25, 200, 180)
    gpdb.plug_in_gauss_iir(img, light_noise_layer, 1, 1, 1)
    gpdb.gimp_desaturate(light_noise_layer)

    # Next make pure black layer which we will copy repeatedly as a
    # place to cut out letters.
    blackLayer = gimp.Layer(img, 'black', sx, sy, RGB_IMAGE, 100, NORMAL_MODE)
    img.add_layer(blackLayer, 0)
    blackLayer.add_alpha()
    gpdb.gimp_layer_add_alpha(blackLayer)
    gpdb.gimp_drawable_fill(blackLayer, WHITE_FILL)
    gpdb.gimp_invert(blackLayer)

    # Loop through each letter, making it a separate black layer.
    right = left_margin
    last_substrate = None
    for letter in answer:
        font = random.choice(FONTS)
        substrate = blackLayer.copy()
        img.add_layer(substrate, 0)
        new_right = cookie_cutter_letter(img, substrate, right, font, letter)
        # look out for really narrow letters
        if new_right - right < 20:
            new_right += 5
        right = new_right
    img.remove_layer(blackLayer)

    # Hide the light noise layer, then collapse all the remaining
    # layers (all letters) into a single layer.
    light_noise_layer.visible = False
    textLayer = gpdb.gimp_image_merge_visible_layers(img, CLIP_TO_IMAGE)
    light_noise_layer.visible = True

    # Create a layer of dark noise which will display the letters.
    dark_noise_layer = gimp.Layer(img, 'dark noise', sx, sy, RGB_IMAGE, 100,
                                  MULTIPLY_MODE)
    img.add_layer(dark_noise_layer, 1)
    gpdb.gimp_drawable_fill(dark_noise_layer, WHITE_FILL)
    gpdb.plug_in_randomize_hurl(img, dark_noise_layer, 25, 1, 0, 0)
    gpdb.gimp_desaturate(dark_noise_layer)

    # These next operations are ordered carefully. Changing the order
    # dramatically affects how the output looks.

    # Here's where we do the cutout operation.
    gpdb.gimp_selection_layer_alpha(textLayer)
    gpdb.gimp_selection_invert(img)
    gpdb.gimp_edit_clear(dark_noise_layer)
    gpdb.gimp_selection_none(img)

    # After the cutout, blur the dark noise layer and then darken it:
    gpdb.plug_in_gauss_iir(img, dark_noise_layer, 1, 1, 1)
    gpdb.gimp_levels(dark_noise_layer, HISTOGRAM_VALUE, 127, 255, 0.25, 0, 255)
    textLayer.visible = False

    # If you start gimp without --no-interface with an X server, this
    # line will let you see the image looks like at this point in the
    # script, layers and all. It should be fine to move this line to
    # any problematic part of the script for debugging.
    #
    # gimp.Display(gpdb.gimp_image_duplicate(img))

    final = img.flatten()
    gpdb.gimp_image_clean_all(img)
    img.enable_undo()
    return img, final
Beispiel #13
0
def make_captcha(sx, sy, font_height, letter_spacing, left_margin,
                 angle_range, fonts, answer):
    """Generate a captcha consisting of the letters in answer.

    :rtype: :class:`gimp.Image`
    :returns: The CAPTCHA as a gimp-python image object.
    """
    img = gimp.Image(sx, sy, RGB_IMAGE)
    img.disable_undo()

    light_noise_layer = gimp.Layer(img, 'light noise', sx, sy,
                                   RGB_IMAGE, 100, NORMAL_MODE)
    img.add_layer(light_noise_layer, 0)

    gpdb.gimp_selection_none(img)
    gpdb.gimp_drawable_fill(light_noise_layer, WHITE_FILL)

    # plug_in_randomize_hurl at 1% 1 time is vastly superior to
    # scatter_rgb here, but has a bug where it creates an artifact at
    # the top of the image when invoked in a scripting context like
    # this.
    #
    # Future experiment: dial down the amount of noise generated by
    # scatter, then run it through levels to darken it, then
    # blur. This should be equivalent to hurl + blur.
    #gpdb.plug_in_randomize_hurl(img, light_noise_layer, 1, 1, 0, 0)
    gpdb.plug_in_scatter_hsv(img, light_noise_layer, 1, 25, 200, 180)
    gpdb.plug_in_gauss_iir(img, light_noise_layer, 1, 1, 1)
    gpdb.gimp_desaturate(light_noise_layer)

    # Next make pure black layer which we will copy repeatedly as a
    # place to cut out letters.
    blackLayer = gimp.Layer(img, 'black', sx, sy, RGB_IMAGE, 100, NORMAL_MODE)
    img.add_layer(blackLayer, 0)
    blackLayer.add_alpha()
    gpdb.gimp_layer_add_alpha(blackLayer)
    gpdb.gimp_drawable_fill(blackLayer, WHITE_FILL)
    gpdb.gimp_invert(blackLayer)

    # Loop through each letter, making it a separate black layer.
    right = left_margin
    last_substrate = None
    for letter in answer:
        font = random.choice(FONTS)
        substrate = blackLayer.copy()
        img.add_layer(substrate, 0)
        new_right = cookie_cutter_letter(img, substrate, right, font, letter)
        # look out for really narrow letters
        if new_right - right < 20:
            new_right += 5
        right = new_right
    img.remove_layer(blackLayer)

    # Hide the light noise layer, then collapse all the remaining
    # layers (all letters) into a single layer.
    light_noise_layer.visible = False
    textLayer = gpdb.gimp_image_merge_visible_layers(img, CLIP_TO_IMAGE)
    light_noise_layer.visible = True

    # Create a layer of dark noise which will display the letters.
    dark_noise_layer = gimp.Layer(img, 'dark noise', sx, sy,
                                  RGB_IMAGE, 100, MULTIPLY_MODE)
    img.add_layer(dark_noise_layer, 1)
    gpdb.gimp_drawable_fill(dark_noise_layer, WHITE_FILL)
    gpdb.plug_in_randomize_hurl(img, dark_noise_layer, 25, 1, 0, 0)
    gpdb.gimp_desaturate(dark_noise_layer)

    # These next operations are ordered carefully. Changing the order
    # dramatically affects how the output looks.

    # Here's where we do the cutout operation.
    gpdb.gimp_selection_layer_alpha(textLayer)
    gpdb.gimp_selection_invert(img)
    gpdb.gimp_edit_clear(dark_noise_layer)
    gpdb.gimp_selection_none(img)

    # After the cutout, blur the dark noise layer and then darken it:
    gpdb.plug_in_gauss_iir(img, dark_noise_layer, 1, 1, 1)
    gpdb.gimp_levels(dark_noise_layer, HISTOGRAM_VALUE, 127, 255, 0.25, 0, 255)
    textLayer.visible = False

    # If you start gimp without --no-interface with an X server, this
    # line will let you see the image looks like at this point in the
    # script, layers and all. It should be fine to move this line to
    # any problematic part of the script for debugging.
    #
    # gimp.Display(gpdb.gimp_image_duplicate(img))

    final = img.flatten()
    gpdb.gimp_image_clean_all(img)
    img.enable_undo()
    return img, final