File: convert.py

package info (click to toggle)
python-fabio 0.14.0%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 10,068 kB
  • sloc: python: 20,053; ansic: 1,085; makefile: 217; sh: 215
file content (379 lines) | stat: -rw-r--r-- 14,004 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
#!/usr/bin/env python
# coding: utf-8
#
#    Project: X-ray image reader
#             https://github.com/silx-kit/fabio
#
#
#    Copyright (C) European Synchrotron Radiation Facility, Grenoble, France
#
#    Principal author:       Jérôme Kieffer (Jerome.Kieffer@ESRF.eu)
#
#  Permission is hereby granted, free of charge, to any person
#  obtaining a copy of this software and associated documentation files
#  (the "Software"), to deal in the Software without restriction,
#  including without limitation the rights to use, copy, modify, merge,
#  publish, distribute, sublicense, and/or sell copies of the Software,
#  and to permit persons to whom the Software is furnished to do so,
#  subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be
#  included in all copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
#  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
#  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
#  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
#  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
#  OTHER DEALINGS IN THE SOFTWARE.
"""Portable image converter based on FabIO library.
"""

__author__ = "Valentin Valls"
__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France"
__licence__ = "MIT"
__date__ = "23/04/2021"
__status__ = "production"

import logging
logging.basicConfig()

import sys
import os
import fabio
from fabio import fabioformats, fabioutils
from fabio.utils.cli import expand_args
import argparse

logger = logging.getLogger("fabio-convert")


def get_default_extension_from_format(format_name):
    """"
    Get a default file extension from a fabio format

    :param str format: String format like "edfimage"
    :rtype: str
    """
    class_ = fabioformats.get_class_by_name(format_name)
    if class_ is None:
        raise RuntimeError("Format '%s' unsupported" % format_name)

    extensions = class_.DEFAULT_EXTENSIONS
    if len(extensions) == 0:
        # No extensions
        return ""
    else:
        return extensions[0]


def get_output_filename(input_filename, format_name):
    """
    Returns the output filename from the input filename and the format.

    :param str input_filename: Input filename path
    :param str format_name: String format like "edfimage"
    :rtype: str
    """
    basename, _ = os.path.splitext(input_filename)
    extension = get_default_extension_from_format(format_name)
    if extension == "":
        extension = "bin"
    return basename + "." + extension


def is_user_want_to_overwrite_filename(filename):
    """
    Ask question in the shell and returns true if the user want to overwrite
    a file passed in parameter.

    :param str filename: The filename it asks for
    :rtype: bool
    """
    while True:
        question = "Do you want to overwrite the file '%s' (y/n): " % filename
        answer = input(question).strip().lower()
        if answer in ["y", "yes", "n", "no"]:
            break
    return answer in ["y", "yes"]


def is_older(filename1, filename2):
    """Returns true if the first file is older than the second one.

    :param str filename1: An existing filename
    :param str filename2: An existing filename
    :rtype: bool
    """
    time1 = os.path.getmtime(filename1)
    time2 = os.path.getmtime(filename2)
    return time1 > time2


def convert_one(input_filename, output_filename, options):
    """
    Convert a single file using options

    :param str input_filename: The input filename
    :param str output_filename: The output filename
    :param object options: List of options provided from the command line
    :rtype: bool
    :returns: True is the conversion succeeded
    """
    input_filename = os.path.abspath(input_filename)
    input_exists = os.path.exists(input_filename)
    output_filename = os.path.abspath(output_filename)
    output_exists = os.path.exists(output_filename)

    if options.verbose:
        print("Converting file '%s' to '%s'" % (input_filename, output_filename))

    if not input_exists:
        logger.error("Input file '%s' do not exists. Conversion skipped.", input_filename)
        return False

    skip_conversion = False
    remove_file = False

    if output_exists:
        if options.interactive:
            if is_user_want_to_overwrite_filename(output_filename):
                remove_file = True
            else:
                skip_conversion = True
        elif options.no_clobber:
            skip_conversion = True
        elif options.force or options.remove_destination:
            remove_file = True
        elif options.update:
            if is_older(output_filename, input_filename):
                skip_conversion = True
            else:
                remove_file = True
        elif is_user_want_to_overwrite_filename(output_filename):
                remove_file = True
        else:
            skip_conversion = True

    if remove_file:
        if options.verbose:
            print("Overwrite file %s" % output_filename)
        try:
            if not options.dry_run:
                os.remove(output_filename)
        except OSError as e:
            logger.error("Removing previous file %s failed cause: \"%s\". Conversion skipped.", e.message, output_filename)
            logger.debug("Backtrace", exc_info=True)
            return False

    if skip_conversion:
        if options.verbose:
            print("Conversion to file %s skipped" % output_filename)
        return True

    try:
        logger.debug("Load '%s'", input_filename)
        source = fabio.open(input_filename)
    except KeyboardInterrupt:
        raise
    except Exception as e:
        logger.error("Loading input file '%s' failed cause: \"%s\". Conversion skipped.", input_filename, e.message)
        logger.debug("Backtrace", exc_info=True)
        return False

    try:
        logger.debug("Convert '%s' into '%s'", input_filename, options.format)
        converted = source.convert(options.format)
    except KeyboardInterrupt:
        raise
    except Exception as e:
        logger.error("Converting input file '%s' failed cause: \"%s\". Conversion skipped.", input_filename, e.message)
        logger.debug("Backtrace", exc_info=True)
        return False

    try:
        logger.debug("Write '%s'", output_filename)
        if not options.dry_run:
            converted.write(output_filename)
    except KeyboardInterrupt:
        raise
    except Exception as e:
        logger.error("Saving output file '%s' failed cause: \"%s\". Conversion skipped.", output_filename, e.message)
        logger.debug("Backtrace", exc_info=True)
        return False

    # a success
    return True


def convert_all(options):
    """Convert all the files from the command line.

    :param object options: List of options provided from the command line
    :rtype: bool
    :returns: True is the conversion succeeded
    """
    succeeded = True
    for filename in options.images:

        if options.output is None:
            output_filename = get_output_filename(filename, options.format)
        elif os.path.isdir(options.output):
            output_filename = get_output_filename(filename, options.format)
            output_filename = os.path.basename(output_filename)
            directory = os.path.abspath(options.output)
            output_filename = os.path.join(directory, output_filename)
        else:
            output_filename = options.output

        succeeded = succeeded and convert_one(filename, output_filename, options)

    return succeeded


def print_supported_formats():
    """List supported format to the output"""
    classes = fabioformats.get_classes(writer=True)
    classes.sort(key=lambda c: c.__module__.lower())

    indentation = "    "

    print(f"List of writable file formats supported by FabIO version {fabio.version}")
    print()

    for class_ in classes:
        if len(class_.DEFAULT_EXTENSIONS) > 0:
            extensions = ", ".join(["*." + x for x in class_.DEFAULT_EXTENSIONS])
            extensions = "(%s)" % extensions
        else:
            extensions = ""

        print("- %s %s" % (class_.codec_name(), extensions))
        print("%s%s" % (indentation, class_.DESCRIPTION))


def is_format_supported(format_name):
    """
    Returns true if the file format is supported.

    :param str format_name: Name of the format (for example edfimage)
    :rtype: bool
    """
    try:
        fabio.factory(format_name)
        return True
    except RuntimeError:
        logger.debug("Backtrace", exc_info=True)
        return False


EXIT_SUCCESS = 0
EXIT_FAILURE = 1
EXIT_ARGUMENT_FAILURE = 2


def main():

    epilog = """return codes: 0 means a success. 1 means the conversion
                contains a failure, 2 means there was an error in the
                arguments"""

    parser = argparse.ArgumentParser(prog="fabio-convert",
                                     description=__doc__,
                                     epilog=epilog)
    parser.add_argument("IMAGE", nargs="*",
                        help="Input file images")
    parser.add_argument("-V", "--version", action='version', version=fabio.version,
                        help="output version and exit")
    parser.add_argument("-v", "--verbose", action='store_true', dest="verbose", default=False,
                        help="show information for each conversions")
    parser.add_argument("--debug", action='store_true', dest="debug", default=False,
                        help="show debug information")

    group = parser.add_argument_group("main arguments")
    group.add_argument("-l", "--list", action="store_true", dest="list", default=None,
                       help="show the list of available formats and exit")
    group.add_argument("-o", "--output", dest='output', type=str,
                       help="output file or directory")
    group.add_argument("-F", "--output-format", dest="format", type=str, default=None,
                       help="output format")

    group = parser.add_argument_group("optional behaviour arguments")
    group.add_argument("-f", "--force", dest="force", action="store_true", default=False,
                       help="if an existing destination file cannot be" +
                       " opened, remove it and try again (this option" +
                       " is ignored when the -n option is also used)")
    group.add_argument("-n", "--no-clobber", dest="no_clobber", action="store_true", default=False,
                       help="do not overwrite an existing file (this option" +
                       " is ignored when the -i option is also used)")
    group.add_argument("--remove-destination", dest="remove_destination", action="store_true", default=False,
                       help="remove each existing destination file before" +
                       " attempting to open it (contrast with --force)")
    group.add_argument("-u", "--update", dest="update", action="store_true", default=False,
                       help="copy only when the SOURCE file is newer" +
                       " than the destination file or when the" +
                       " destination file is missing")
    group.add_argument("-i", "--interactive", dest="interactive", action="store_true", default=False,
                       help="prompt before overwrite (overrides a previous -n" +
                       " option)")
    group.add_argument("--dry-run", dest="dry_run", action="store_true", default=False,
                       help="do everything except modifying the file system")

    try:
        args = parser.parse_args()

        if args.debug:
            logger.setLevel(logging.DEBUG)

        if args.list:
            print_supported_formats()
            return

        if len(args.IMAGE) == 0:
            raise argparse.ArgumentError(None, "No input file specified.")

        # the upper case IMAGE is used for the --help auto-documentation
        args.images = expand_args(args.IMAGE)
        args.images.sort()

        if args.format is None or not args.format.endswith("image"):

            if args.format is None:
                if args.output is None:
                    raise argparse.ArgumentError(None, "No format specified. Use -F or -o.")
                dummy_filename = args.output
            else:
                # format looks to be an extension
                dummy_filename = "foo." + args.format

            # extract file format from file name
            filename = fabioutils.FilenameObject(filename=dummy_filename)

            if filename.format is None or len(filename.format) == 0:
                raise argparse.ArgumentError(None, "This file extension is unknown. You have also to specify a format using -F.")
            elif filename.format is None or len(filename.format) > 1:
                formats = [i + "image" for i in filename.format]
                formats = ', '.join(formats)
                raise argparse.ArgumentError(None, "This file extension correspond to different file formats: '%s'. You have to specify it using -F." % formats)
            args.format = filename.format[0] + "image"

        if not is_format_supported(args.format):
            raise argparse.ArgumentError(None, "Format '%s' is unknown. Use -l to list all available formats." % args.format)
    except argparse.ArgumentError as e:
        logger.error(e.message)
        logger.debug("Backtrace", exc_info=True)
        return EXIT_ARGUMENT_FAILURE

    succeeded = convert_all(args)
    if not succeeded:
        print("Conversion or part of it failed. You can try with --debug to have more output information.")
        return EXIT_FAILURE

    return EXIT_SUCCESS


if __name__ == "__main__":
    result = main()
    sys.exit(result)