User:AVRS/gimp-jpegpixi.py

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search

https://github.com/aleksejrs/gimp-jpegpixi

Commit 4869aa25d911ffef5327ab9e6672b2109f1637f0, 2015-03-25.

Note that the size limit is because some modes will use a lot of RAM to process "big" areas.

#!/usr/bin/env python
# jpegpixi.py - a GIMP script to use jpegpixi
#
#   Copyright 2012 Aleksej
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from os import path
from subprocess import check_call
import sys


def _(x):
    return x

try:
    from gimpfu import *
    in_gimp = True
except ImportError:
    in_gimp = False
    sys.stderr.write(
        _('This program is a GIMP plug-in and cannot be used standalone.\n'))

# The grid spacing needed to match DCT blocks (to help user choose
# a less lossy position).
#
# Wikipedia says DCT blocks are 8x8.  It also says some lossless
# operations can be performed on MCU blocks, which are usually
# 16x16.  If it's only MCU and not DCT, it would be more
# convenient to use the MCU size here, but jpegpixi's man page
# says DCT.
REQUIRED_GRID_SPACING = (8, 8)


def python_pixi(timg, tdrawable, method, direction,
                max_selection_size, rename_method, fn_sufbase):
    have_selection = pdb.gimp_selection_bounds(timg)[0]

    grid_offset = pdb.gimp_image_grid_get_offset(timg)
    grid_spacing = pdb.gimp_image_grid_get_spacing(timg)

    set_grid(timg)

    if have_selection:
        we_have_a_selection(timg, tdrawable, method, direction,
                            max_selection_size, fn_sufbase, rename_method)
    else:
        pdb.gimp_message(_("A selection is required."))


def we_have_a_selection(timg, tdrawable, method, direction,
                        max_selection_size, fn_sufbase, rename_method):

    x1, y1, sx, sy = rect_coords(pdb.gimp_selection_bounds(timg)[1:])
    selection_size = sx * sy
    max_selection_size = int(max_selection_size)

    if selection_size > max_selection_size:
        pdb.gimp_message_set_handler(ERROR_CONSOLE)
        pdb.gimp_message(
            _("Selection is {0} pixels, {1:.1f} times the maximum of {2}.  Aborting for safety.").format(
                selection_size,
                selection_size/max_selection_size,
                max_selection_size))
        return
    else:
        # source and target file names
        sfname = pdb.gimp_image_get_filename(timg)
        tfname = next_filename(sfname, rename_method, fn_sufbase,
                               (x1, y1, sx, sy))

        coord_string = "{0},{1},{2},{3}".format(x1, y1, sx, sy)
        the_command = jpegpixi_cmd(sfname, tfname, coord_string,
                                   method, direction)

        print (_('Running'), the_command)

        check_call(the_command)

        # Load the new image in a new window.
        targetimg = pdb.file_jpeg_load(tfname, RUN_INTERACTIVE)
        gimp.Display(targetimg)
        tdrawable.flush()

        # Select the interpolated part in the new window.
        pdb.gimp_image_select_rectangle(
            targetimg, CHANNEL_OP_REPLACE, x1, y1, sx, sy)

        set_grid(targetimg)

        return


def set_grid(timg):
    """Sets grid parameters for convenient selection.

    This only has effect on the specified image and does not affect the
    default values set in GIMP preferences.
    """
    pdb.gimp_image_grid_set_offset(timg, 0, 0) 
    pdb.gimp_image_grid_set_spacing(timg, REQUIRED_GRID_SPACING[0],
                                        REQUIRED_GRID_SPACING[1]) 


def next_filename(sfname, rename_method, fn_sufbase, coords):
    """Generates the target filename out of the source one and other
    data using the method specified.
    """    
    sfname_base, sfname_ext = path.splitext(sfname)

    if rename_method == 'rect_coords':
        id_from_coord_string = 'x'.join(str(x) for x in coords)
        tfname = (sfname_base + fn_sufbase + id_from_coord_string + "." +
            sfname_ext)
    elif rename_method == 'rect_coords_hex':
        # currently not exposed in the GUI
        id_from_coord_string = 'x'.join(hex(x) for x in coords)
        tfname = (sfname_base + fn_sufbase + id_from_coord_string + "." +
            sfname_ext)
    elif rename_method == 'incremental':
        tfname = next_filename_incremental(sfname_base, sfname_ext, fn_sufbase)
    else:
        # CropGUI method
        tfname = sfname_base + fn_sufbase + "." + sfname_ext

    return tfname


def next_filename_incremental(sfname_base, sfname_ext, fn_sufbase):
    """With "-pixi" as the suffix base, if the file name base ends in
    "-pixi<n>", makes it "-pixi<n+1>".  Adds "-pixi1" if there is no
    "-pixi".

    >>> next_filename_incremental("DSCN1234", "jpeg", "-pixi")
    'DSCN1234-pixi1.jpeg'

    >>> next_filename_incremental("z-pixi", "jpeg", "-pixi")
    'z-pixi1.jpeg'

    >>> next_filename_incremental("lorem.ipsum..dolor..sit.amet-pixi23", "jpg", "-pixi")
    'lorem.ipsum..dolor..sit.amet-pixi24.jpg'

    """
    (fn_presuf, fn_hopefully_sufbase,
        fn_hopefully_number) = sfname_base.rpartition(fn_sufbase)

    if fn_presuf == "":
        tfname = sfname_base + fn_sufbase + '1.' + sfname_ext
    else:
        try:
            fn_number = int(fn_hopefully_number)
        except ValueError:
            fn_number = 0
        fn_number += 1
        tfname = fn_presuf + fn_sufbase + str(fn_number) + '.' + sfname_ext

    return tfname


def rect_coords(points_coords):
    """Takes coordinates of two angles of a rectangle, and replaces
    those of the second one with the rectangle's dimensions.
    """
    x1, y1, x2, y2 = points_coords
    sx = x2 - x1
    sy = y2 - y1
    return (x1, y1, sx, sy)


def jpegpixi_cmd(sfname, tfname, coord_string, method, direction):
    """Returns the shell command ready to execute."""
    progname = "jpegpixi"
    cmdl_spec = direction + ':' + coord_string

    cmdl_total = [progname, '-m', method, sfname, tfname, cmdl_spec]
    
    return cmdl_total


if in_gimp:

    register(
        "python_pixi",
        _("Pixelize the selection using jpegpixi."),
        """Pixelize the selection using jpegpixi.  Makes GIMP serve as a
        GUI for jpegpixi. It calls jpegpixi on _the file_ by the name of
        the loaded image, not on the drawable in GIMP, so any unsaved
        changes will be ignored.
        """,
        "Aleksej",
        "Aleksej",
        "2012",
        "<Image>/Filters/Blur/_jpegpixi",
        "RGB*, GRAY*",
        [
            (PF_RADIO, 'method', 'Interpolation\n method', "li",
                (("average", "av"), ("linear", "li"), ("quadratic", "qu"),
                    ("cubic", "cu"))),
            (PF_RADIO, 'direction', 'Direction', "2", (("2d", "2"),
                ("1d vertical", "v"), ("1d horizontal", "h"))),
            # Maximum selecton size, to prevent "out-of-memory" issues.
            (PF_SLIDER, 'max_selection_size', 'Max. sel. size', 10000,
                (10000, 400000, 100)),
            (PF_RADIO, 'rename_method', 'Target file\nnaming', 'rect_coords',
                (('Coords+dims', 'rect_coords'),
                 ('add suffix (like CropGUI)', 'cropgui'),
                 ("increment number at suffix", "incremental"))),
            (PF_STRING, 'fn_sufbase', 'Filename\nsuffix base', '-pixi')
        ],
        [],
        python_pixi)

    main()
else:
    import doctest
    doctest.testmod()