"""

This script is not the starting/entry point script.  If installed with pip you
can just run `pdfcropmargins` to run the program.  When pip is not used the
starting point for the pdfCropMargins program is to import function `main` from
the `pdfCropMargins.py` script and run it.  The source directory has a
`__main__.py` file which does this automatically when Python is invoked on the
directory.  There is also standalone script in the `bin` directory which is the
preferred way to run the program when it is not installed via pip.

=====================================================================

pdfCropMargins -- a program to crop the margins of PDF files
Copyright (C) 2014 Allen Barker (Allen.L.Barker@gmail.com)

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

Source code site: https://github.com/abarker/pdfCropMargins

"""

# Might want an option to delete the XML save data.

# TODO: Resource warning on socket is raised when delay introduced in
# main_crop before handling options on file, but only when GUI also used.
# Try in a simple pymupdf thing???

# TODO: In get_image_list_mupdf in the calculate_bounding_boxes module try to pass
# the current MuPdfDocument rather than creating a new one temporarily.

# TODO: Maybe use _restored and restored_ prefix and suffix for restore ops???
# Need a new option --stringRestored.

# TODO: Maybe add option to see the MuPdf warnings, use
# fitz.TOOLS.mupdf_warnings() first to empty warnings and then to get warnings,
# see https://github.com/pymupdf/PyMuPDF/discussions/1501

# TODO: Make --evenodd option equalize the page WIDTHS after separately
# calculating the crops, just do the max over them.

# TODO: Deleting metadata on restore doesn't seem to remove key like docs say.
#       So restored document still registers as already cropped.
#       See the `check_and_set_crop_metadata` method of the pymupdf wrapper.
#       https://pymupdf.readthedocs.io/en/latest/recipes-low-level-interfaces.html#how-to-extend-pdf-metadata

# TODO: Consider the case where user saved with old pdfCropMargins but
#       chose NOT to save restore information.  If such a file has artbox
#       set then those will (check) be converted to new format.  Any way
#       to detect that case?

# Some general notes, useful for reading the code.
#
# Margins are described as left, bottom, right, and top (lbrt). Boxes in pypdf2
# (formerly used) and PDF are defined by the lower-left point's x and y values
# followed by the upper-right point's x and y values, which is equivalent
# information (since x and y are implicit in the margin names).  The origin is
# at the lower left. The pymupdf program uses the top left as origin, which
# results in ltrb ordering:
#
# From: https://github.com/pymupdf/PyMuPDF/issues/317
#    (Py-)MuPDF always uses a page's top-left point as the origin (0,0) of its
#    coordinate system - for whatever reason, presumably because it does not
#    only deal with PDF, but also other document types.  PDF uses a page's
#    bottom-left point as (0,0).
#
# This program (like the Ghostscript program and pypdf2) uses the PDF ordering
# convention (lbrt) for listing margins and defining boxes.  Note that Pillow
# uses some different conventions.  The origin in PDFs is the lower left going
# up but the origin in Pillow images is the upper left going down.  So the
# bounding box routine of Pillow returns ltrb instead of lbrt.  Keep in mind
# that the program needs to make these conversions when rendering explicitly to
# images.
#
# This program uses pymupdf, but uses the PDF and pypdf2 convention (mainly
# because it originally used pypdf2).  All values are converted by a wrapper
# around the pymupdf routines, which are in the module pymupdf_routines.

import sys
import os
import shutil
import time
from warnings import warn

try:
    import readline # Makes prompts go to stdout rather than stderr.
except ImportError: # Not available on Windows.
    pass

from . import __version__ # Get the version number from the __init__.py file.
from .manpage_data import cmd_parser, DEFAULT_THRESHOLD_VALUE
from .prettified_argparse import parse_command_line_arguments
from .pymupdf_routines import (MuPdfDocument, intersect_pdf_boxes,
                               mod_box_for_rotation, fitz)

from . import external_program_calls as ex
project_src_directory = ex.project_src_directory

from .calculate_bounding_boxes import get_bounding_box_list

##
## Some data used by the program.
##

args = None # Global set during cmd-line processing (since almost all funs use it).

##
## Begin general function definitions.
##

def generate_output_filepath(infile_path, is_cropped_file=True,
                             ignore_output_filename=False):
    """Generate the name of the output file from the name of the input file and
    any relevant options selected.

    The `is_cropped_file` boolean is used to indicate that the file has been
    (or will be) cropped, to determine which filename-modification string to
    use.

    If `ignore_output_filename` is true then only the directory path of any
    passed-in output path is used in the generated paths.

    The function assumes that `args` has been set globally by argparse."""
    outfile_dir = os.getcwd() # The default output directory is the CWD.
    if args.outfile:
        globbed_outpath = ex.glob_pathname(args.outfile[0], exact_num_args=1)[0]
        expanded_globbed_outpath = ex.get_expanded_path(globbed_outpath)
        if os.path.isdir(expanded_globbed_outpath): # Output directory was passed in.
            outfile_dir = expanded_globbed_outpath
        else: # Full output path with filename was passed in.
            outfile_dir = os.path.dirname(expanded_globbed_outpath)
            if not ignore_output_filename: # Combine and return the dir and the filename.
                # Note that globbing and expansion is only done on the directory part.
                return os.path.join(outfile_dir, os.path.basename(args.outfile[0]))

    if is_cropped_file:
        suffix = prefix = args.stringCropped
    else:
        suffix = prefix = args.stringUncropped

    # Use modified basename as output path; program writes default output to CWD.
    file_name = os.path.basename(infile_path)
    name_before_extension, extension = os.path.splitext(file_name)
    if extension not in {".pdf", ".PDF"}:
        extension += ".pdf"

    sep = args.stringSeparator
    if args.usePrefix:
        name = prefix + sep + name_before_extension + extension
    else:
        name = name_before_extension + sep + suffix + extension

    name = os.path.join(outfile_dir, name)
    return name

def parse_page_range_specifiers(spec_string, all_page_nums):
    """Parse a page range specifier argument such as "4-5,7,9".  Passed
    a specifier and the set of all page numbers, it returns the subset."""
    page_nums_to_crop = set() # Note that this set holds page num MINUS ONE, start at 0.
    for page_num_or_range in spec_string.split(","):
        split_range = page_num_or_range.split("-")
        if len(split_range) == 1:
            # Note pyPdf page nums start at 0, not 1 like usual PDF pages,
            # subtract 1.
            page_nums_to_crop.add(int(split_range[0])-1)
        else:
            left_arg = int(split_range[0])-1
            right_arg = int(split_range[1])
            if left_arg >= right_arg:
                print("Error in pdfCropMargins: Left argument of page range '{}' cannot"
                      " be less than the right one.".format(spec_string), file=sys.stderr)
                raise ValueError
            page_nums_to_crop.update(set(range(left_arg, right_arg)))
    page_nums_to_crop = page_nums_to_crop & all_page_nums # intersect chosen with actual
    if not page_nums_to_crop: # Empty set of pages.
        print("Error in pdfCropMargins: Page range selection '{}' results in empty"
                " set.".format(spec_string), file=sys.stderr)
        raise ValueError
    return page_nums_to_crop

def parse_page_ratio_argument(ratio_arg):
    """Parse the argument passed to setPageRatios."""
    ratio = ratio_arg.split(":")
    if len(ratio) > 2:
        print("\nError in pdfCropMargins: Bad format in aspect ratio command line"
              " argument.\nToo many colons.", file=sys.stderr)
        raise ValueError
    try:
        if len(ratio) == 2: # Colon form.
            float_ratio = float(ratio[0])/float(ratio[1])
        else: # Float form.
            float_ratio = float(ratio[0])
    except ValueError:
        print("\nError in pdfCropMargins: Bad format in argument to "
              " setPageRatios.\nCannot convert to a float.", file=sys.stderr)
        raise
    if float_ratio == 0 or float_ratio == float("inf"):
        print("\nError in pdfCropMargins: Bad format in argument to "
              " setPageRatios.\nZero or infinite aspect ratios are not allowed.",
              file=sys.stderr)
        raise ValueError
    return float_ratio

def calculate_crop_list(full_page_box_list, bounding_box_list, angle_list,
                                                               page_nums_to_crop):
    """Given a list of full-page boxes (media boxes) and a list of tight
    bounding boxes for each page, calculate and return another list giving the
    list of bounding boxes to crop down to.  The parameter `angle_list` is
    a list of rotation angles which correspond to the pages.  The pages
    selected to crop are in the set `page_nums_to_crop`."""

    # Definition: the deltas are the four differences, one for each margin,
    # between the original full page box and the final, cropped full-page box.
    # In the usual case where margin sizes decrease these are the same as the
    # four margin-reduction values (in absolute points).   The deltas are
    # usually positive but they can be negative due to either percentRetain>100
    # or a large enough absolute offset (in which case the size of the
    # corresponding margin will increase).  When percentRetain<0 the deltas are
    # always greater than the absolute difference between the full page and a
    # tight bounding box, and so part of the text within the tight bounding box
    # will also be cropped (unless absolute offsets are used to counter that).

    def combine_tuple_lists_with_mask(mask, default_list, optional_list):
        """A utility function used below.  The mask is a four-tuple of strings
        't' or 'f' for replacing elements of `default_list` with the
        corresponding elements of `optional_list`.  Used mainly for processing
        the 'uniform4' option."""
        final_list = []
        for default_tuple, optional_tuple in zip(default_list,
                                                 optional_list):
            new_default_tuple = list(default_tuple)
            for index, char in enumerate(mask):
                if char == "t":
                    new_default_tuple[index] = optional_tuple[index]
            final_list.append(tuple(new_default_tuple))

        return final_list

    num_pages = len(bounding_box_list)
    page_range = range(num_pages)
    num_pages_to_crop = len(page_nums_to_crop)

    # Handle the '--samePageSize' option.
    # Note that this is always done first, even before evenodd is handled.  It
    # is only applied to the pages in the set `page_nums_to_crop`.

    order_n = 0
    if args.samePageSizeOrderStat:
        args.samePageSize = True
        order_n = min(args.samePageSizeOrderStat[0], num_pages_to_crop - 1)
        order_n = max(order_n, 0)

    if args.samePageSize or args.setSamePageSize:
        if args.samePageSize: # Calculate the page containing all the other selected pages.
            if args.verbose:
                print("\nSetting each page size to the smallest box bounding all the pages.")
                if order_n != 0:
                    print("But ignoring the largest {} pages in calculating each edge."
                            .format(order_n))
            same_size_bounding_box = [
                  # We want the smallest of the left and bottom edges.
                  sorted(full_page_box_list[pg][0] for pg in page_nums_to_crop),
                  sorted(full_page_box_list[pg][1] for pg in page_nums_to_crop),
                  # We want the largest of the right and top edges.
                  sorted((full_page_box_list[pg][2] for pg in page_nums_to_crop), reverse=True),
                  sorted((full_page_box_list[pg][3] for pg in page_nums_to_crop), reverse=True)
                  ]
            same_size_bounding_box = [sortlist[order_n] for sortlist in same_size_bounding_box]

        else: # Set the page size to the box passed in (ignored if `--samePageSize` is set).
            same_size_bounding_box = [float(f) for f in args.setSamePageSize]
            if args.verbose:
                print("\nSetting each page size to the bounding box passed in:"
                      f"\n   {same_size_bounding_box}")

        same_size_bounding_box_list = [same_size_bounding_box] * num_pages

        if args.samePageSize4:
            same_size_bounding_box_list = combine_tuple_lists_with_mask(args.samePageSize4,
                                                                        full_page_box_list,
                                                                        same_size_bounding_box_list)

        # Set `full_page_box_list` to `same_size_bounding_box` for the pages selected.
        new_full_page_box_list = []
        for p_num, f_box in enumerate(full_page_box_list):
            if p_num not in page_nums_to_crop:
                new_full_page_box_list.append(f_box)
            else:
                new_full_page_box_list.append(same_size_bounding_box_list[p_num])
        full_page_box_list = new_full_page_box_list

    # Handle the '--evenodd' option if it was selected.
    if args.evenodd:
        even_page_nums_to_crop = {p_num for p_num in page_nums_to_crop if p_num % 2 == 0}
        odd_page_nums_to_crop = {p_num for p_num in page_nums_to_crop if p_num % 2 != 0}

        if args.uniform:
            uniform_set_with_even_odd = True
        else:
            uniform_set_with_even_odd = False

        # Recurse on even and odd pages, after resetting some options.
        if args.verbose:
            print("\nRecursively calculating crops for even and odd pages.")
        args.evenodd = False # Avoid infinite recursion.
        args.uniform = True  # --evenodd implies uniform, just on each separate group
        even_crop_list, delta_page_nums_even = calculate_crop_list(full_page_box_list, bounding_box_list,
                                                                   angle_list, even_page_nums_to_crop)
        odd_crop_list, delta_page_nums_odd = calculate_crop_list(full_page_box_list, bounding_box_list,
                                                                 angle_list, odd_page_nums_to_crop)

        # Recombine the even and odd pages.
        combine_even_odd = []
        for p_num in page_range:
            if p_num % 2 == 0:
                combine_even_odd.append(even_crop_list[p_num])
            else:
                combine_even_odd.append(odd_crop_list[p_num])

        combine_delta_crop_list = [(delta_page_nums_even[i], delta_page_nums_odd[i])
                                   for i in range(4)]

        # Handle the case where --uniform was set with --evenodd.
        if uniform_set_with_even_odd:
            min_bottom_margin = min(box[1] for p_num, box in enumerate(combine_even_odd)
                                                          if p_num in page_nums_to_crop)
            max_top_margin = max(box[3] for p_num, box in enumerate(combine_even_odd)
                                                       if p_num in page_nums_to_crop)
            combine_even_odd = [[box[0], min_bottom_margin, box[2], max_top_margin]
                              for box in combine_even_odd]
        return combine_even_odd, combine_delta_crop_list

    # Before calculating the crops we modify the percentRetain and
    # absoluteOffset values for all the pages according to any specified.
    # rotations for the pages.  This is so, for example, uniform cropping is
    # relative to what the user actually sees.
    rotated_percent_retain = [mod_box_for_rotation(args.percentRetain4, angle_list[m_val])
                                                         for m_val in range(num_pages)]
    rotated_absolute_offset = [mod_box_for_rotation(args.absoluteOffset4, angle_list[m_val])
                                                         for m_val in range(num_pages)]

    # Calculate the list of deltas to be used to modify the original page
    # sizes.  Basically, a delta is the absolute diff between the full and
    # tight-bounding boxes, scaled according to the user's percentRetain, with
    # any absolute offset then added (lb) or subtracted (tr) as appropriate.
    #
    # The deltas are all positive unless absoluteOffset changes that or
    # percent>100 or percent<0.  They are added (lb) or subtracted (tr) as
    # appropriate.

    delta_list = []
    for p_num, (b_box, f_box) in enumerate(zip(bounding_box_list, full_page_box_list)):

        # Calculate margin percentages.
        pct_fracs = [rotated_percent_retain[p_num][m_val] / 100.0 for m_val in range(4)]
        deltas = [abs(b_box[m_val] - f_box[m_val]) for m_val in range(4)]
        if not args.percentText:
            adj_deltas = [deltas[m_val] * (1.0-pct_fracs[m_val]) for m_val in range(4)]
        else:
            text_size = (b_box[2]-b_box[0], b_box[3]-b_box[1], # Text size for each margin.
                         b_box[2]-b_box[0], b_box[3]-b_box[1])
            adj_deltas = [deltas[m_val] - text_size[m_val] * pct_fracs[m_val]
                                                                 for m_val in range(4)]

        # Calculate absolute offsets.
        adj_deltas = [adj_deltas[m_val] + rotated_absolute_offset[p_num][m_val]
                                                           for m_val in range(4)]
        delta_list.append(adj_deltas)

    # Handle the '--uniform' options if one was selected.
    if args.uniformOrderPercent:
        percent_val = args.uniformOrderPercent[0]
        if percent_val < 0.0:
            percent_val = 0.0
        if percent_val > 100.0:
            percent_val = 100.0
        args.uniformOrderStat4 = [int(round(num_pages_to_crop * percent_val / 100.0))] * 4

    # Expand to tuples containing page nums, to better print verbose information.
    delta_list_paged = [(delta_list[j], j+1) for j in page_range] # Note +1 added here.

    # Only look at the deltas which correspond to pages selected for cropping.
    # The values will then be sorted for each margin and selected.
    crop_delta_list_paged = [delta_list_paged[j] for j in page_range if j in page_nums_to_crop]

    # Get a sorted list of (delta, page_num) tuples for each margin.  This is used
    # for orderstat calculations as well as for verbose information in the general case.
    # Note that we only sort over the page_nums_to_crop instead of all pages.
    sorted_left_vals = sorted([(pl[0][0], pl[1]) for pl in crop_delta_list_paged])
    sorted_lower_vals = sorted([(pl[0][1], pl[1]) for pl in crop_delta_list_paged])
    sorted_right_vals = sorted([(pl[0][2], pl[1]) for pl in crop_delta_list_paged])
    sorted_upper_vals = sorted([(pl[0][3], pl[1]) for pl in crop_delta_list_paged])

    # Save a mapping of which pages are ignored due to orderstat calculations, mapped
    # to the page that is used instead.  Used for '--centerText', to get the bounding
    # box that the delta values were calculated relative to.
    unused_orderstat_pages_left = {k:k for k in range(num_pages)}
    unused_orderstat_pages_right = {k:k for k in range(num_pages)}
    unused_orderstat_pages_bottom = {k:k for k in range(num_pages)}
    unused_orderstat_pages_top = {k:k for k in range(num_pages)}

    if args.cropSafe:
        ignored_pages_left = {p for p in range(num_pages) if p not in page_nums_to_crop}
        ignored_pages_lower = {p for p in range(num_pages) if p not in page_nums_to_crop}
        ignored_pages_right = {p for p in range(num_pages) if p not in page_nums_to_crop}
        ignored_pages_upper = {p for p in range(num_pages) if p not in page_nums_to_crop}

    if args.uniform or args.uniformOrderStat4:
        if args.verbose:
            print("\nAll the selected pages will be uniformly cropped.")

        # Handle order stats; m_values are the four index values into the sorted
        # delta lists, one per margin.
        m_values = [0, 0, 0, 0]
        if args.uniformOrderStat4:
            m_values = args.uniformOrderStat4

        bounded_m_values = []
        for m_val in m_values:
            if m_val < 0 or m_val >= num_pages_to_crop:
                print("\nWarning: The selected order statistic is out of range.",
                      "Setting to closest value.", file=sys.stderr)
                if m_val >= num_pages_to_crop:
                    m_val = num_pages_to_crop - 1
                if m_val < 0:
                    m_val = 0
            bounded_m_values.append(m_val)

        m_values = bounded_m_values

        if args.cropSafe:
            skip_pages_left = set(sorted_left_vals[:m_values[0]])
            skip_pages_lower = set(sorted_lower_vals[:m_values[1]])
            skip_pages_right = set(sorted_right_vals[:m_values[2]])
            skip_pages_upper = set(sorted_upper_vals[:m_values[3]])
            # Here we convert the +1 page nums meant for display into internal 0-based page nums.
            if skip_pages_left:
                skip_pages_left = {d[1] - 1 for d in skip_pages_left}
            if skip_pages_lower:
                skip_pages_lower = {d[1] - 1 for d in skip_pages_lower}
            if skip_pages_right:
                skip_pages_right = {d[1] - 1 for d in skip_pages_right}
            if skip_pages_upper:
                skip_pages_upper = {d[1] - 1 for d in skip_pages_upper}
            ignored_pages_left |= skip_pages_left
            ignored_pages_lower |= skip_pages_lower
            ignored_pages_right |= skip_pages_right
            ignored_pages_upper |= skip_pages_upper

        if args.verbose and (args.uniformOrderPercent or args.uniformOrderStat4):
            print("\nPer-margin, the", m_values,
                  "smallest delta values over the selected pages\nwill be ignored"
                  " when choosing common, uniform delta values.")

        # Here is where the uniform cropping is applied to make all four deltas equal.
        orig_delta_list = delta_list
        delta_list = [[sorted_left_vals[m_values[0]][0], sorted_lower_vals[m_values[1]][0],
                      sorted_right_vals[m_values[2]][0], sorted_upper_vals[m_values[3]][0]]] * num_pages

        delta_page_nums = [sorted_left_vals[m_values[0]][1], sorted_lower_vals[m_values[1]][1],
                           sorted_right_vals[m_values[2]][1], sorted_upper_vals[m_values[3]][1]]

        # Handle the --uniform4 option by replacing the margins not selected with original values.
        if args.uniform4:
            delta_list = combine_tuple_lists_with_mask(args.uniform4,
                                                       orig_delta_list,
                                                       delta_list)
            delta_page_nums_nonuniform = [sorted_left_vals[0][1], sorted_lower_vals[0][1],
                                          sorted_right_vals[0][1], sorted_upper_vals[0][1]]
            delta_page_nums = combine_tuple_lists_with_mask(args.uniform4,
                                                            [delta_page_nums_nonuniform],
                                                            [delta_page_nums])
            delta_page_nums = delta_page_nums[0]

        # Update pages not used in orderstat, to use for the centerText options below.
        for m in range(m_values[0]):
            unused_orderstat_pages_left[m] = sorted_left_vals[m_values[0]][1]-1
        for m in range(m_values[2]):
            unused_orderstat_pages_right[m] = sorted_right_vals[m_values[2]][1]-1
        for m in range(m_values[1]):
            unused_orderstat_pages_bottom[m] = sorted_lower_vals[m_values[1]][1]-1
        for m in range(m_values[3]):
            unused_orderstat_pages_top[m] = sorted_upper_vals[m_values[3]][1]-1

        if args.verbose:
            print("\nThe smallest delta values actually used to set the uniform"
                  " cropping\namounts (ignoring any '-m' skips and pages in ranges"
                  " not cropped) were\nfound on these pages, numbered from 1:\n   ",
                  delta_page_nums)
            print("\nThe uniform delta values (and page numbers) are:\n   ", delta_list_paged[0])

    else: # Use the smallest, leftmost sorted value for the non-uniform case.
        # TODO can this else safely move to above conditional????
        delta_page_nums = [sorted_left_vals[0][1], sorted_lower_vals[0][1],
                           sorted_right_vals[0][1], sorted_upper_vals[0][1]]

    if args.keepHorizCenter: # TODO: Double check this formula and placement.
        delta_list = [(min(d[0],d[2]), d[1], min(d[0],d[2]), d[3]) for d in delta_list]

    if args.keepVertCenter:
        delta_list = [(d[0], min(d[1],d[3]), d[2], min(d[1],d[3])) for d in delta_list]

    # Apply the delta modifications to the full boxes to get the final sizes.
    final_crop_list = []
    for f_box, deltas in zip(full_page_box_list, delta_list):
        final_crop_list.append((f_box[0] + deltas[0], f_box[1] + deltas[1],
                                f_box[2] - deltas[2], f_box[3] - deltas[3]))

    if args.cropSafe:
        safe_final_crop_list = []
        csm0, csm1, csm2, csm3 = args.cropSafeMin4
        for page, (final_crops, bounding_box) in enumerate(zip(final_crop_list, bounding_box_list)):
            final_crops = list(final_crops)
            if page not in ignored_pages_left and final_crops[0] > bounding_box[0]-csm0:
                final_crops[0] = bounding_box[0]-csm0
            if page not in ignored_pages_lower and final_crops[1] > bounding_box[1]-csm1:
                final_crops[1] = bounding_box[1]-csm1
            if page not in ignored_pages_right and final_crops[2] < bounding_box[2]+csm2:
                final_crops[2] = bounding_box[2]+csm2
            if page not in ignored_pages_upper and final_crops[3] < bounding_box[3]+csm3:
                final_crops[3] = bounding_box[3]+csm3
            safe_final_crop_list.append(tuple(final_crops))

        if args.uniform or args.uniformOrderStat4:
            sfcl = safe_final_crop_list # Shorter alias for below comprehensions.
            uniform_crop_list = [(min(sfcl[p][0] for p in page_nums_to_crop if p not in ignored_pages_left),
                                  min(sfcl[p][1] for p in page_nums_to_crop if p not in ignored_pages_lower),
                                  max(sfcl[p][2] for p in page_nums_to_crop if p not in ignored_pages_right),
                                  max(sfcl[p][3] for p in page_nums_to_crop if p not in ignored_pages_upper)
                               )] * num_pages

            if args.uniform4:
                safe_final_crop_list = combine_tuple_lists_with_mask(args.uniform4,
                                                                     safe_final_crop_list,
                                                                     uniform_crop_list)
            else:
                safe_final_crop_list = uniform_crop_list

        final_crop_list = safe_final_crop_list

    # Set the page ratios if user chose that option.
    if args.setPageRatios:
        ratio = args.setPageRatios
        left_weight, bottom_weight, right_weight, top_weight = args.pageRatioWeights
        if args.verbose:
            print("\nSetting all page width to height ratios to:", ratio)
            print("The weights per margin are:",
                    left_weight, bottom_weight, right_weight, top_weight)

        new_ratio_based_crop_list = []
        for pnum, (left, bottom, right, top) in enumerate(final_crop_list):
            if pnum not in page_nums_to_crop:
                new_ratio_based_crop_list.append((left, bottom, right, top))
                continue

            # Pad out left/right or top/bottom margins; padding amount is scaled.
            width = right - left
            height = top - bottom
            new_height = width / ratio
            if new_height < height: # Use new_width instead.
                new_width = height * ratio
                assert new_width >= width
                difference = new_width - width
                total_lr_weight = left_weight + right_weight
                left_weight /= total_lr_weight
                right_weight /= total_lr_weight
                new_ratio_based_crop_list.append((left - difference * left_weight, bottom,
                                                  right + difference * right_weight, top))
            else:
                difference = new_height - height
                total_tb_weight = bottom_weight + top_weight
                bottom_weight /= total_tb_weight
                top_weight /= total_tb_weight
                new_ratio_based_crop_list.append((left, bottom - difference * bottom_weight,
                                                  right, top + difference * top_weight))
        final_crop_list = new_ratio_based_crop_list

    if args.centerText:
        args.centerTextHoriz = args.centerTextVert = True

    if args.centerTextHoriz or args.centerTextVert:
        if args.verbose and args.centeringStrict:
            print(f"\nStrictly centering the bounding boxes on the pages...")
        else:
            print(f"\nCentering the bounding boxes on the pages...")

        new_centered_text_crop_list = []
        for pnum, (left, bottom, right, top) in enumerate(final_crop_list):
            if pnum not in page_nums_to_crop:
                new_centered_text_crop_list.append((left, bottom, right, top))
                continue

            if args.centeringStrict:
                b_left = bounding_box_list[pnum][0]
                b_right = bounding_box_list[pnum][2]
                b_bottom = bounding_box_list[pnum][1]
                b_top = bounding_box_list[pnum][3]
            else:
                # Use the mapping saved earlier to get the bbox the page was cropped relative
                # to.  This takes '--uniformOrderStat' into account.
                b_left = bounding_box_list[unused_orderstat_pages_left[pnum]][0]
                b_right = bounding_box_list[unused_orderstat_pages_right[pnum]][2]
                b_bottom = bounding_box_list[unused_orderstat_pages_bottom[pnum]][1]
                b_top = bounding_box_list[unused_orderstat_pages_top[pnum]][3]

            left_delta = b_left - left
            right_delta = right - b_right
            half_horiz_delta = (left_delta + right_delta) / 2.0

            bottom_delta = b_bottom - bottom
            top_delta = top - b_top
            half_vert_delta = (bottom_delta + top_delta) / 2.0

            new_centered_text_crop = [b_left - half_horiz_delta,
                                      b_bottom - half_vert_delta,
                                      b_right + half_horiz_delta,
                                      b_top + half_vert_delta]

            if not args.centerTextHoriz:
                new_centered_text_crop[0] = left
                new_centered_text_crop[2] = right

            if not args.centerTextVert:
                new_centered_text_crop[1] = bottom
                new_centered_text_crop[3] = top

            new_centered_text_crop_list.append(new_centered_text_crop)

        final_crop_list = new_centered_text_crop_list

    return final_crop_list, delta_page_nums

##############################################################################
#
# Functions implementing the major operations.
#
##############################################################################

def process_command_line_arguments(parsed_args, cmd_parser):
    """Perform an initial processing on the some of the command-line arguments.  This
    is called first, before any PDF processing is done."""
    global args # This is global to avoid passing it to essentially every function.
    args = parsed_args

    if args.prevCropped:
        args.gui = False # Ignore the GUI when --prevCropped option is selected.
        args.verbose = False # Wants to eval the text in Bash script.

    if args.verbose:
        print(f"\nProcessing the PDF with pdfCropMargins (version {__version__})...")
        print("Python version:", ex.python_version)
        print("System type:", ex.system_os)
        print(fitz.__doc__) # Print out PyMuPDF version info.

    if len(args.pdf_input_doc) > 1:
        print("\nError in pdfCropMargins: Only one input PDF document is allowed."
              "\nFound more than one on the command line:", file=sys.stderr)
        for f in args.pdf_input_doc:
            print("   ", f, file=sys.stderr)
        print(file=sys.stderr)
        if not args.prevCropped: # Because Bash script conditionals try to evaluate `usage` on fail.
            cmd_parser.print_usage()
        #cmd_parser.exit() # Exits whole program.
        ex.cleanup_and_exit(1)
    # Note: Below code currently handled by the argparse + option, not *, on pdf_input_doc.
    #elif len(args.pdf_input_doc) < 1:
    #    print("\nError in pdfCropMargins: No PDF document argument passed in.",
    #          file=sys.stderr)
    #    print()
    #    cmd_parser.print_usage()
    #    #cmd_parser.exit() # Exits whole program.
    #    ex.cleanup_and_exit(1)

    #
    # Process input and output filenames.
    #

    input_doc_path = args.pdf_input_doc[0]
    input_doc_path = ex.get_expanded_path(input_doc_path) # Expand vars and user.
    input_doc_path = ex.glob_pathname(input_doc_path, exact_num_args=1)[0]
    if not input_doc_path.endswith((".pdf",".PDF")):
        print("\nWarning in pdfCropMargins: The file extension is neither '.pdf'"
              "\nnor '.PDF'; continuing anyway.", file=sys.stderr)
    if args.verbose:
        print("\nThe input document's filename is:\n   ", input_doc_path)
    if not os.path.isfile(input_doc_path):
        print("\nError in pdfCropMargins: The specified input file\n   "
              + input_doc_path + "\nis not a file or does not exist.",
              file=sys.stderr)
        ex.cleanup_and_exit(1)

    if not args.outfile and args.verbose:
        print("\nUsing the default-generated output filename.")

    output_doc_path = generate_output_filepath(input_doc_path)
    if args.verbose:
        print("\nThe output document's filename will be:\n   ", output_doc_path)

    if os.path.lexists(output_doc_path) and args.noclobber:
        # Note lexists above, don't overwrite broken symbolic links, either.
        print("\nOption '--noclobber' is set, refusing to overwrite an existing"
              "\nfile with filename:\n   ", output_doc_path, file=sys.stderr)
        ex.cleanup_and_exit(1)

    if os.path.lexists(output_doc_path) and ex.samefile(input_doc_path,
                                                                output_doc_path):
        print("\nError in pdfCropMargins: The input file is the same as"
              "\nthe output file.\n", file=sys.stderr)
        ex.cleanup_and_exit(1)

    #
    # Process some args with both regular and per-page 4-param forms.  Note that
    # in all these cases the 4-param version takes precedence.
    #

    if args.uniform4:
        args.uniform = True

    if args.samePageSize4:
        args.samePageSize = True

    if args.absolutePreCrop and not args.absolutePreCrop4:
        args.absolutePreCrop4 = args.absolutePreCrop * 4 # expand to 4 offsets
    if args.verbose:
        print("\nThe absolute pre-crops to be applied to each margin, in units of bp,"
              " are:\n   ", args.absolutePreCrop4)

    if args.percentRetain and not args.percentRetain4:
        args.percentRetain4 = args.percentRetain * 4 # expand to 4 percents
    # See if all four percents are explicitly set and use those if so.
    if args.verbose:
        print("\nThe percentages of margins to retain are:\n   ",
              args.percentRetain4)

    if args.absoluteOffset and not args.absoluteOffset4:
        args.absoluteOffset4 = args.absoluteOffset * 4 # expand to 4 offsets
    if args.verbose:
        print("\nThe absolute offsets to be applied to each margin, in units of bp,"
              " are:\n   ", args.absoluteOffset4)

    # TODO: Note that these verbose messages are NOT printed when the GUI is used, since
    # the processing only calls process_pdf_file.  Similarly, range checks and repairs
    # for uniformOrderStat are not processed when entered directly into the GUI (the
    # GUI separately checks).
    if args.uniformOrderStat and not args.uniformOrderStat4:
        args.uniformOrderStat4 = args.uniformOrderStat * 4 # expand to 4 offsets
    if args.verbose:
        print("\nThe uniform order statistics to apply to each margin, in units of bp,"
              " are:\n   ", args.uniformOrderStat4)

    #
    # Process page ratios.
    #

    if args.setPageRatios and not args.gui: # GUI does its own parsing.
        # Parse the page ratio into a float if user chose that representation.
        ratio_arg = args.setPageRatios
        try:
            float_ratio = parse_page_ratio_argument(ratio_arg)
        except ValueError:
            ex.cleanup_and_exit(1) # Parse fun printed error message.
        args.setPageRatios = float_ratio

    if args.pageRatioWeights:
        for w in args.pageRatioWeights:
            if w <= 0:
                print("\nError in pdfCropMargins: Non-positive weight argument passed "
                      "to pageRatiosWeights.", file=sys.stderr)
                ex.cleanup_and_exit(1)

    #
    # Process options dealing with rendering and external programs.
    #

    if args.gsRender:
        args.calcbb = "gr" # Backward compat.
        warn("\nThe --gsRender option is deprecated and will be removed in "
                "version 3.0.  Use '-c gr' instead.", DeprecationWarning, 2)
    if args.gsBbox:
        warn("\nThe --gsBbox option is deprecated and will be removed in "
                "version 3.0.  Use '-c gb' instead.", DeprecationWarning, 2)
        args.calcbb = "gb" # Backward compat.
    if args.calcbb == "d":
        args.calcbb = "m" # Default to PyMuPDF.
    elif args.calcbb == "d": # Rendering without PyMuPDF.
        args.calcbb = "o"     # Revert to old method.

    if args.calcbb == "gb" and len(args.fullPageBox) > 1:
        print("\nWarning: only one --fullPageBox value can be used with the '--calcbb gb'"
              "\nor '--gsBbox' option. Ignoring all but the first one.", file=sys.stderr)
        args.fullPageBox = [args.fullPageBox[0]]
    elif args.calcbb == "gb" and not args.fullPageBox:
        args.fullPageBox = ["c"] # gs default
    elif not args.fullPageBox:
        args.fullPageBox = ["m", "c"] # usual default

    if args.verbose:
        print("\nFor the full page size, using values from the PDF box"
              "\nspecified by the intersection of these boxes:", args.fullPageBox)

    # Set executable paths to non-default locations if set.
    if args.pdftoppmPath:
        ex.set_pdftoppm_executable_to_string(args.pdftoppmPath)
    if args.ghostscriptPath:
        ex.set_gs_executable_to_string(args.ghostscriptPath)

    # If the option settings require pdftoppm, make sure we have a running version.
    gs_render_fallback_set = False # Set True if we switch to gs option as a fallback.
    if args.calcbb in ["p", "o"]:
        # Note that after this block, the `--calcbb o` option is converted to 'p' or 'gr'.
        found_pdftoppm = ex.init_and_test_pdftoppm_executable()
        if args.verbose:
            print("\nFound pdftoppm program at:", found_pdftoppm)
        if found_pdftoppm:
            args.calcbb = "p"
        elif args.calcbb == "p":
                print("\nError in pdfCropMargins: The '--calcbb p' option was specified "
                      "\nbut the pdftoppm executable could not be located.  Is it"
                      "\ninstalled and in the PATH for command execution?\n",
                      file=sys.stderr)
                ex.cleanup_and_exit(1)
        else:
            # Try fallback to gs.
            gs_render_fallback_set = True
            args.calcbb = "gr"
            if args.verbose:
                print("\nNo pdftoppm executable found; using Ghostscript for rendering.")

    # If any options require Ghostscript, make sure it is installed.
    if args.calcbb == "gr" or args.calcbb == "gb" or args.gsFix:
        found_gs = ex.init_and_test_gs_executable()
        if args.verbose:
            print("\nFound Ghostscript program at:", found_gs)
    if args.calcbb == "gb" and not found_gs:
        print("\nError in pdfCropMargins: The '--calcbb gb' or '--gsBbox' option was"
              "\nspecified but the Ghostscript executable could not be located.  Is it"
              "\ninstalled and in the PATH for command execution?\n", file=sys.stderr)
        ex.cleanup_and_exit(1)
    if args.gsFix and not found_gs:
        print("\nError in pdfCropMargins: The '--gsFix' option was specified but"
              "\nthe Ghostscript executable could not be located.  Is it"
              "\ninstalled and in the PATH for command execution?\n", file=sys.stderr)
        ex.cleanup_and_exit(1)
    if (args.calcbb == "gr" or gs_render_fallback_set) and not found_gs:
        if gs_render_fallback_set:
            print("\nError in pdfCropMargins: Neither Ghostscript nor pdftoppm"
                  "\nwas found in the PATH for command execution.  At least one is"
                  "\nrequired without PyMuPDF installed.\n", file=sys.stderr)
        else:
            print("\nError in pdfCropMargins: The '--calcbb gr' or the '--gsRender' option"
                  "\nwas specified but the Ghostscript executable could not be located.  Is "
                  "\nit installed and in the PATH for command execution?\n", file=sys.stderr)
        ex.cleanup_and_exit(1)

    # Give a warning message if incompatible option combinations have been selected.
    if args.threshold[0] != DEFAULT_THRESHOLD_VALUE and args.calcbb == "gb":
            print("\nWarning in pdfCropMargins: The '--threshold' option is ignored"
                "\nwhen the '--calcbb gb' or '--gsBbox' option is also selected.\n",
                file=sys.stderr)
    if args.calcbb == "gb" and args.numBlurs:
        print("\nWarning in pdfCropMargins: The '--numBlurs' option is ignored"
              "\nwhen the '--calcbb gb' or '--gsBbox' option is also selected.\n",
              file=sys.stderr)
    if args.calcbb == "gb" and args.numSmooths:
        print("\nWarning in pdfCropMargins: The '--numSmooths' option is ignored"
              "\nwhen the '--calcbb gb' or '--gsBbox' option is also selected.\n",
              file=sys.stderr)

    if args.gsFix:
        if args.verbose:
            print("\nAttempting to fix the PDF input file before reading it...")
        fixed_input_doc_pathname = ex.fix_pdf_with_ghostscript_to_tmp_file(input_doc_path)
    else:
        fixed_input_doc_pathname = input_doc_path

    return input_doc_path, fixed_input_doc_pathname, output_doc_path

def open_file_in_pymupdf(fixed_input_doc_pathname):
    """Open the file in a `MuPdfDocument`."""
    input_doc_mupdf_wrapper = MuPdfDocument(args)
    input_doc_num_pages = input_doc_mupdf_wrapper.open_document(fixed_input_doc_pathname)

    if args.verbose:
        print(f"\nThe input document has {input_doc_num_pages} pages.")
        if input_doc_mupdf_wrapper.document.is_repaired:
            print("\nThe document was repaired by PyMuPDF on being read.")
        else:
            print("\nThe document was not repaired by PyMuPDF on being read.")

    # Note this is only the standard metadata, not any additional metadata like
    # any restore metadata.
    metadata_info = input_doc_mupdf_wrapper.get_standard_metadata()

    if args.verbose and not metadata_info:
        print("\nNo readable metadata in the document.")
    elif args.verbose:
        try:
            print("\nThe document's metadata, if set:\n")
            print("   The Author attribute set in the input document is:\n      %s"
                  % (metadata_info["author"]))
            print("   The Creator attribute set in the input document is:\n      %s"
                  % (metadata_info["creator"]))
            print("   The Producer attribute set in the input document is:\n      %s"
                  % (metadata_info["producer"]))
            print("   The Subject attribute set in the input document is:\n      %s"
                  % (metadata_info["subject"]))
            print("   The Title attribute set in the input document is:\n      %s"
                  % (metadata_info["title"]))
        except (KeyError, UnicodeDecodeError, UnicodeEncodeError):
            print("\nWarning: Could not write all the document's metadata to the screen."
                  "\nGot a KeyError or a UnicodeEncodeError.", file=sys.stderr)

    return input_doc_mupdf_wrapper, metadata_info, input_doc_num_pages

def get_set_of_page_numbers_to_crop(input_doc_num_pages):
    """Compute the set containing the page number of all the pages
    which the user has selected for cropping from the command line."""
    all_page_nums = set(range(0, input_doc_num_pages))
    if args.pages:
        try:
            page_nums_to_crop = parse_page_range_specifiers(args.pages, all_page_nums)
        except ValueError:
            print(
                "\nError in pdfCropMargins: The page range specified on the command",
                "\nline contains a non-integer value or otherwise cannot be parsed.",
                file=sys.stderr)
            ex.cleanup_and_exit(1)
    else:
        page_nums_to_crop = all_page_nums

    # In verbose mode print out information about the pages to crop.
    if args.verbose and args.pages:
        print("\nThese pages of the document will be cropped:", end="")
        p_num_list = sorted(list(page_nums_to_crop))
        num_pages_to_crop = len(p_num_list)
        for i in range(num_pages_to_crop):
            if i % 10 == 0 and i != num_pages_to_crop - 1:
                print("\n   ", end="")
            print("%5d" % (p_num_list[i]+1), " ", end="")
        print()
    elif args.verbose:
        print("\nAll the pages of the document will be cropped.")
    return page_nums_to_crop

def process_pdf_file(input_doc_pathname, fixed_input_doc_pathname, output_doc_pathname,
                     bounding_box_list=None):
    """This function does the real work.  It is called by `main()` in
    `pdfCropMargins.py`, which just handles catching exceptions and cleaning
    up.

    If a bounding box list is passed in then the bounding box calculation is
    skipped and that list is used instead (for cases with the GUI when the
    boxes do not change but the cropping does).

    Returns the bounding box list and data about the minimum cropping deltas
    for each margins."""

    input_doc_mupdf_wrapper, metadata_info, input_doc_num_pages = open_file_in_pymupdf(
                                                                   fixed_input_doc_pathname)

    # Get any necessary page boxes BEFORE the MediaBox is set.  The pyMuPDF
    # program will reset the other boxes when setting the MediaBox if they're
    # not fully contained.  For the ArtBox this is for backward compatibility
    # with the earlier PyPDF restore option.
    original_mediabox_list = input_doc_mupdf_wrapper.get_box_list("mediabox")
    original_cropbox_list = input_doc_mupdf_wrapper.get_box_list("cropbox")
    original_artbox_list = input_doc_mupdf_wrapper.get_box_list("artbox")
    #original_trimbox_list = input_doc_mupdf_wrapper.get_box_list("trimbox")
    #original_bleedbox_list = input_doc_mupdf_wrapper.get_box_list("bleedbox")

    already_cropped_by_this_program = input_doc_mupdf_wrapper.check_and_set_crop_metadata(
                                                                            metadata_info)

    if not args.noundosave:
        if already_cropped_by_this_program == "<2.0" or not already_cropped_by_this_program:
            input_doc_mupdf_wrapper.save_old_boxes_for_restore(original_mediabox_list,
                                                               original_cropbox_list,
                                                               original_artbox_list,
                                                               already_cropped_by_this_program)

    if args.prevCropped:
        input_doc_mupdf_wrapper.close_document()
        if already_cropped_by_this_program:
            #print("code 0")
            exit_code = 0
        else:
            #print("code 1")
            exit_code = 1
        ex.cleanup_and_exit(exit_code)

    # TODO: This doesn't work yet with GUI because GUI calls this fun only when cropping...
    #if args.exitPrevCropped and already_cropped_by_this_program:
    #    fixed_input_doc_file_object.close()
    #    if args.verbose:
    #        print("The file was previously cropped by pdfCropMargins, exiting.")
    #    ex.cleanup_and_exit(0)

    ##
    ## Now compute the set containing the page number of all the pages
    ## which the user has selected for cropping from the command line.  Most
    ## calculations are still carried out for all the pages in the document.
    ## (There are a few optimizations for expensive operations like finding
    ## bounding boxes; the rest is negligible).  This keeps the correspondence
    ## between page numbers and the positions of boxes in the box lists.  The
    ## function `apply_crop_list` then just ignores the cropping information
    ## for any pages which were not selected.
    ##

    page_nums_to_crop = get_set_of_page_numbers_to_crop(input_doc_num_pages)

    ##
    ## Get a list with the full-page boxes for each page: (left,bottom,right,top)
    ## This function also sets the MediaBox and CropBox of the pages to the
    ## chosen full-page size as a side-effect, saving the old boxes.  Any absolute
    ## pre-crop is also applied here (so it is rendered that way for the later
    ## bounding-box-finding operation).
    ##

    full_page_box_list, rotation_list = input_doc_mupdf_wrapper.get_full_page_box_list_assigning_media_and_crop()

    ##
    ## Write out the PDF document again, with the CropBox and MediaBox reset.
    ## This temp document version is ONLY used for calculating the bounding boxes of
    ## pages.
    ##

    if not args.restore:
        if not bounding_box_list:
            doc_with_crop_and_media_boxes_name = ex.get_temporary_filename(".pdf")
            if args.verbose:
                # TODO Consider writing this to a memory file rather than to disk.
                print("\nWriting out the PDF with the CropBox and MediaBox redefined"
                        "\n(so pre-crops are included in the bounding box calculations).")
            input_doc_mupdf_wrapper.save_document(doc_with_crop_and_media_boxes_name)

    ##
    ## Calculate the `bounding_box_list` containing tight page bounds for each page.
    ##

    if not args.restore:
        if not bounding_box_list:
            bounding_box_list = get_bounding_box_list(doc_with_crop_and_media_boxes_name,
                    input_doc_mupdf_wrapper, full_page_box_list, page_nums_to_crop, args)
            if args.verbose:
                print("\nThe bounding boxes are:")
                for pNum, b in enumerate(bounding_box_list):
                    print("\t", pNum+1, "\t", b)
            os.remove(doc_with_crop_and_media_boxes_name) # No longer needed.

        elif args.verbose:
            print("\nUsing the bounding box list passed in instead of calculating it.")

    ##
    ## Calculate the `crop_list` based on the fullpage boxes and the bounding boxes,
    ## after the precrop has been applied.
    ##

    if not args.restore:
        crop_list, delta_page_nums = calculate_crop_list(full_page_box_list, bounding_box_list,
                                        rotation_list, page_nums_to_crop)
    else:
        crop_list = None # Restore, not needed in this case.
        delta_page_nums = ("N/A","N/A","N/A","N/A")

    ##
    ## Apply the calculated crops to the pages (after restoring the original mediabox
    ## and cropbox).
    ##

    if args.restore:
        if args.verbose:
            print("\nRestoring the document to margins saved for each page.")

        if not already_cropped_by_this_program:
            print("\nWarning from pdfCropMargins: The Producer string and metadata indicate"
                  "\nthat either this document was not previously cropped by pdfCropMargins"
                  "\nor else it was modified by another program after that and cannot"
                  "\nbe restored.  Ignoring the restore operation.", file=sys.stderr)
        else:
            input_doc_mupdf_wrapper.apply_restore_operation(already_cropped_by_this_program,
                                                            original_artbox_list)

    else:
        input_doc_mupdf_wrapper.apply_crop_list(crop_list, page_nums_to_crop,
                        already_cropped_by_this_program)

    ##
    ## Write the final PDF out to a file.
    ##

    input_doc_mupdf_wrapper.save_document(output_doc_pathname)
    input_doc_mupdf_wrapper.close_document()

    return bounding_box_list, delta_page_nums

def handle_options_on_cropped_file(input_doc_pathname, output_doc_pathname):
    """Handle the options which apply after the file is written such as previewing
    and renaming."""

    def do_preview(output_doc_pathname):
        viewer = args.preview
        if args.verbose:
            print("\nPreviewing the output document with viewer:\n   ", viewer)
        ex.show_preview(viewer, output_doc_pathname)
        return

    if args.replaceOriginal:
        args.modifyOriginal = True

    # Handle the '--queryModifyOriginal' option.
    if args.queryModifyOriginal:
        if args.preview:
            print("\nRunning the preview viewer on the file, will query whether or not"
                  "\nto modify the original file after the viewer is launched in the"
                  "\nbackground...\n")
            do_preview(output_doc_pathname)
            # Give preview time to start; it may write startup garbage to the terminal...
            query_wait_time = 2 # seconds
            time.sleep(query_wait_time)
            print()

        while True: # Loop until we get an allowed response.
            query_string = "\nModify the original file to the cropped file " \
                           "(saving the original)? [yn] "
            query_result = input(query_string).strip()
            if query_result in ["y", "Y"]:
                args.modifyOriginal = True
                print("\nModifying the original file.")
                break
            elif query_result in ["n", "N", "q", "Q"]:
                print("\nNot modifying the original file.  The cropped file is saved"
                      " as:\n   {}".format(output_doc_pathname))
                args.modifyOriginal = False
                break
            else:
                print("Response must be in the set {y,Y,n,N,q,Q}, none recognized.")
                continue

    # Handle the '--modifyOriginal' option.
    final_output_document_name = output_doc_pathname
    if args.modifyOriginal:
        # Generate the backup filename for the original, uncropped file.
        generated_uncropped_filepath = generate_output_filepath(input_doc_pathname,
                                                                is_cropped_file=False,
                                                                ignore_output_filename=True)

        # Remove any existing file with the name `generated_uncropped_filename` unless
        # the relevant noclobber option is set, or it isn't a file.
        if os.path.exists(generated_uncropped_filepath):
            if (os.path.isfile(generated_uncropped_filepath)
                    and not args.noclobberOriginal and not args.noclobber):
                if args.verbose:
                    print("\nRemoving the file\n   ", generated_uncropped_filepath)
                try:
                    os.remove(generated_uncropped_filepath)
                except OSError:
                    print("Removing the file {} failed.  Maybe a permission error?"
                          "\nFiles are as if option '--modifyOriginal' were not set."
                          .format(generated_uncropped_filepath))
                    args.modifyOriginal = False # Failed.
            else:
                print("\nA noclobber option is set or else not a file; refusing to"
                    " overwrite:\n   ", generated_uncropped_filepath,
                    "\nFiles are as if option '--modifyOriginal' were not set.",
                    file=sys.stderr)
                args.modifyOriginal = False # Failed.

        if args.modifyOriginal: # Set false above if blocked by an existing file.
            # Move the original file to the name for uncropped files.
            if args.replaceOriginal:
                if args.verbose:
                    print("\nRemoving the original file at:\n   ", input_doc_pathname)
                os.remove(input_doc_pathname)
            else:
                if args.verbose:
                    print("\nDoing a file move:\n   ", input_doc_pathname,
                          "\nis moving to:\n   ", generated_uncropped_filepath)
                shutil.move(input_doc_pathname, generated_uncropped_filepath)

            # Move the cropped file to the original file's name.
            if not os.path.exists(input_doc_pathname):
                if args.verbose:
                    print("\nDoing a file move:\n   ", output_doc_pathname,
                          "\nis moving to:\n   ", input_doc_pathname)
                shutil.move(output_doc_pathname, input_doc_pathname)
                final_output_document_name = input_doc_pathname
            else:
                print("\nWarning: Failed to remove the original file or move it to the"
                      " uncropped filename.", file=sys.stderr)

    # Handle any previewing which still needs to be done.
    if args.preview and not args.queryModifyOriginal: # queryModifyOriginal does its own.
        do_preview(final_output_document_name)

    if args.verbose:
        print("\nFinished this run of pdfCropMargins.\n")

def main_crop(argv_list=None):
    """Process command-line arguments, do the PDF processing, and then perform final
    processing on the filenames.  If `argv_list` is set then it is used instead of
    `sys.argv`.  Returns the pathname of the output document."""
    parsed_args = parse_command_line_arguments(cmd_parser, argv_list=argv_list)

    # Process some of the command-line arguments (also sets `args` globally).
    input_doc_pathname, fixed_input_doc_pathname, output_doc_pathname = (
                                           process_command_line_arguments(parsed_args,
                                                                          cmd_parser))

    if args.gui:
        from .gui import create_gui # Import here; tkinter might not be installed.
        if args.verbose:
            print("\nWaiting for the GUI...")

        did_crop, bounding_box_list, delta_page_nums = create_gui(input_doc_pathname,
                              fixed_input_doc_pathname, output_doc_pathname,
                              cmd_parser, parsed_args)
        if did_crop:
            #time.sleep(8)  # TODO: This alone causes resource bug!! queryModifyOriginal wait does the same.
            #               # But only in combination with the gui...  May be fixed now?
            handle_options_on_cropped_file(input_doc_pathname, output_doc_pathname)
    else:
        bounding_box_list, delta_page_nums = process_pdf_file(input_doc_pathname,
                                                              fixed_input_doc_pathname,
                                                              output_doc_pathname)
        #time.sleep(8)  # TODO: This doesn't cause the error...
        handle_options_on_cropped_file(input_doc_pathname, output_doc_pathname)

    return output_doc_pathname

