1 # -*- coding: utf-8 -*-
2 # -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*-
4 # This file is part of the LibreOffice project.
6 # This Source Code Form is subject to the terms of the Mozilla Public
7 # License, v. 2.0. If a copy of the MPL was not distributed with this
8 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
10 """ Pack images into archives. """
12 from __future__
import with_statement
15 from collections
import OrderedDict
25 logging
.basicConfig(format
='%(message)s')
26 LOGGER
= logging
.getLogger()
30 """ Main function. """
32 tmp_output_file
= "%s.%d.tmp" % (args
.output_file
, os
.getpid())
35 if not os
.path
.exists(args
.imagelist_file
):
36 LOGGER
.error("imagelist_file '%s' doesn't exists", args
.imagelist_file
)
39 if args
.links_file
is not None and not os
.path
.exists(args
.links_file
):
40 LOGGER
.error("link_file '%s' doesn't exists", args
.links_file
)
43 out_path
= os
.path
.dirname(args
.output_file
)
44 for path
in (out_path
, args
.global_path
, args
.module_path
):
45 if not os
.path
.exists(path
):
46 LOGGER
.error("Path '%s' doesn't exists", path
)
48 if not os
.access(path
, os
.X_OK
):
49 LOGGER
.error("Unable to search path %s", path
)
52 if not os
.access(out_path
, os
.W_OK
):
53 LOGGER
.error("Unable to write into path %s", out_path
)
56 for path
in args
.custom_path
:
57 if not os
.path
.exists(path
):
58 LOGGER
.warning("Skipping non-existing path: %s", path
)
60 elif not os
.access(path
, os
.X_OK
):
61 LOGGER
.error("Unable to search path %s", path
)
64 custom_paths
.append(path
)
66 imagelist_filenames
= get_imagelist_filenames(args
.imagelist_file
)
67 global_image_list
, module_image_list
= parse_image_list(imagelist_filenames
)
68 custom_image_list
= find_custom(custom_paths
)
71 if args
.links_file
is not None:
72 read_links(links
, args
.links_file
)
74 read_links(links
, os
.path
.join(ARGS
.global_path
, "links.txt"))
75 for path
in custom_paths
:
76 read_links(links
, os
.path
.join(path
, "links.txt"))
79 zip_list
= create_zip_list(global_image_list
, module_image_list
, custom_image_list
,
80 args
.global_path
, args
.module_path
)
81 remove_links_from_zip_list(zip_list
, links
)
83 if check_rebuild(args
.output_file
, imagelist_filenames
, custom_paths
, zip_list
, args
.links_file
):
84 tmp_dir
= copy_images(zip_list
)
85 create_zip_archive(zip_list
, links
, tmp_dir
, tmp_output_file
, args
.sort_file
)
87 replace_zip_file(tmp_output_file
, args
.output_file
)
89 LOGGER
.info("Remove temporary directory %s", tmp_dir
)
90 shutil
.rmtree(tmp_dir
)
91 except Exception as e
:
92 LOGGER
.error("Unable to remove temporary directory %s", tmp_dir
)
95 LOGGER
.info("No rebuild needed. %s is up to date.", args
.output_file
)
98 def check_rebuild(zip_file
, imagelist_filenames
, custom_paths
, zip_list
, links_file
):
99 """ Check if a rebuild is needed.
102 :param zip_file: File to check against (use st_mtime).
104 :type imagelist_filenames: dict
105 :param imagelist_filenames: List of imagelist filename.
107 :type custom_paths: list
108 :param custom_paths: List of paths to use with links.txt files.
111 :param zip_list: List of filenames to create the zip archive.
113 :type links_file: str
114 :param links_file: filename to read the links from
117 :return: True if rebuild is needed and False if not.
120 if not os
.path
.exists(zip_file
):
123 zip_file_stat
= os
.stat(zip_file
)
125 def compare_modtime(filenames
):
126 """ Check if modification time of zip archive is older the provided
129 :type filenames: dict
130 :param filenames: List of filenames to check against.
133 :return: True if zip archive is older and False if not.
135 for filename
, path
in filenames
.items():
136 filename
= os
.path
.join(path
, filename
) if filename
else path
137 if os
.path
.exists(filename
) and zip_file_stat
.st_mtime
< os
.stat(filename
).st_mtime
:
141 if compare_modtime(imagelist_filenames
):
143 if compare_modtime(zip_list
):
145 if links_file
is not None:
146 if zip_file_stat
.st_mtime
< os
.stat(links_file
).st_mtime
:
149 for path
in custom_paths
:
150 link_file
= os
.path
.join(path
, 'links.txt')
151 if os
.path
.exists(link_file
):
152 if zip_file_stat
.st_mtime
< os
.stat(link_file
).st_mtime
:
158 def replace_zip_file(src
, dst
):
159 """ Replace the old archive file with the newly created one.
162 :param src: Source file name.
165 :param dst: Destination file name.
167 LOGGER
.info("Replace old zip archive with new archive")
168 if os
.path
.exists(dst
):
172 if os
.path
.exists(src
):
175 LOGGER
.error("Unable to unlink old archive '%s'", dst
)
180 LOGGER
.info("Copy archive '%s' to '%s'", src
, dst
)
181 shutil
.move(src
, dst
)
182 except (shutil
.SameFileError
, OSError) as e
:
184 LOGGER
.error("Cannot copy file '%s' to %s: %s", src
, dst
, str(e
))
188 def optimize_zip_layout(zip_list
, sort_file
=None):
189 """ Optimize the zip layout by ordering the list of filename alphabetically
190 or with provided sort file (can be partly).
193 :param zip_list: List of files to include in the zip archive.
196 :param sort_file: Path/Filename to file with sort order.
199 :return: Dict with right sort order.
201 if sort_file
is None:
202 LOGGER
.info("No sort file provided")
203 LOGGER
.info("Sorting entries alphabetically")
205 return OrderedDict(sorted(zip_list
.items(), key
=lambda t
: t
[0]))
207 LOGGER
.info("Sort entries from file '%s'", sort_file
)
208 sorted_zip_list
= OrderedDict()
212 LOGGER
.error("Cannot open file %s", sort_file
)
218 if line
== '' or line
.startswith('#'):
222 sorted_zip_list
[line
] = ''
224 LOGGER
.warning("Unknown file '%s'", line
)
226 for key
in sorted(zip_list
.keys()):
227 if key
not in sorted_zip_list
:
228 sorted_zip_list
[key
] = ''
230 return sorted_zip_list
233 def create_zip_archive(zip_list
, links
, tmp_dir
, tmp_zip_file
, sort_file
=None):
234 """ Create the zip archive.
237 :param zip_list: All filenames to be included in the archive.
240 :param links: All filenames to create links.txt file.
243 :param tmp_dir: Path to temporary saved files.
245 :type tmp_zip_file: str
246 :param tmp_zip_file: Filename/Path of temporary zip archive.
248 :type sort_file: str|None
249 :param sort_file: Optional filename with sort order to apply.
251 LOGGER
.info("Creating image archive")
253 old_pwd
= os
.getcwd()
256 ordered_zip_list
= optimize_zip_layout(zip_list
, sort_file
)
258 with contextlib
.closing(zipfile
.ZipFile(tmp_zip_file
, 'w')) as tmp_zip
:
260 LOGGER
.info("Add file 'links.txt' to zip archive")
261 create_links_file(tmp_dir
, links
)
262 tmp_zip
.write('links.txt', compress_type
=zipfile
.ZIP_DEFLATED
)
264 for link
in ordered_zip_list
:
265 LOGGER
.info("Add file '%s' from path '%s' to zip archive", link
, tmp_dir
)
267 if (link
.endswith(".svg")):
268 tmp_zip
.write(link
, compress_type
=zipfile
.ZIP_DEFLATED
)
272 LOGGER
.warning("Unable to add file '%s' to zip archive", link
)
277 def create_links_file(path
, links
):
278 """ Create file links.txt. Contains all links.
281 :param path: Path where to create the file.
284 :param links: Dict with links (source -> target).
286 LOGGER
.info("Create file links.txt")
289 filename
= os
.path
.join(path
, 'links.txt')
290 fh
= open(filename
, 'w')
292 LOGGER
.error("Cannot open file %s", filename
)
296 for key
in sorted(links
.keys()):
297 fh
.write("%s %s\n" % (key
, links
[key
]))
300 def copy_images(zip_list
):
301 """ Create a temporary directory and copy images to that directory.
304 :param zip_list: Dict with all files.
307 :return: Path of the temporary directory.
309 LOGGER
.info("Copy image files to temporary directory")
311 tmp_dir
= tempfile
.mkdtemp()
312 for key
, value
in zip_list
.items():
313 path
= os
.path
.join(value
, key
)
314 out_path
= os
.path
.join(tmp_dir
, key
)
316 LOGGER
.debug("Copying '%s' to '%s'", path
, out_path
)
317 if os
.path
.exists(path
):
318 dirname
= os
.path
.dirname(out_path
)
319 if not os
.path
.exists(dirname
):
323 shutil
.copyfile(path
, out_path
)
324 except (shutil
.SameFileError
, OSError) as e
:
325 LOGGER
.error("Cannot add file '%s' to image dir: %s", path
, str(e
))
328 LOGGER
.debug("Temporary directory '%s'", tmp_dir
)
332 def remove_links_from_zip_list(zip_list
, links
):
333 """ Remove any files from zip list that are linked.
336 :param zip_list: Files to include in the zip archive.
339 :param links: Images which are linked.
341 LOGGER
.info("Remove linked files from zip list")
343 for link
in links
.keys():
345 module
= zip_list
[link
]
347 # tdf#135369 if we removed something that is a link to a real file
348 # from our list of images to pack, but that real image is not in
349 # the list of images to pack, add it in instead now
350 if links
[link
] not in zip_list
:
351 zip_list
[links
[link
]] = module
353 LOGGER
.debug("Cleaned zip list:\n%s", "\n".join(zip_list
))
356 def create_zip_list(global_image_list
, module_image_list
, custom_image_list
, global_path
, module_path
):
357 """ Create list of images for the zip archive.
359 :type global_image_list: dict
360 :param global_image_list: Global images.
362 :type module_image_list: dict
363 :param module_image_list: Module images.
365 :type custom_image_list: dict
366 :param custom_image_list: Custom images.
368 :type global_path: str
369 :param global_path: Global path (use when no custom path is available).
371 :type module_path: str
372 :param module_path: Module path (use when no custom path is available).
375 :return: List of images to include in zip archive.
377 LOGGER
.info("Assemble image list")
382 for key
in global_image_list
.keys():
383 if key
in module_image_list
:
384 duplicates
.append(key
)
387 if key
in custom_image_list
:
388 zip_list
[key
] = custom_image_list
[key
]
391 zip_list
[key
] = global_path
393 for key
in module_image_list
.keys():
394 if key
in custom_image_list
:
395 zip_list
[key
] = custom_image_list
[key
]
398 zip_list
[key
] = module_path
401 LOGGER
.warning("Found duplicate entries in 'global' and 'module' list")
402 LOGGER
.warning("\n".join(duplicates
))
404 LOGGER
.debug("Assembled image list:\n%s", "\n".join(zip_list
))
408 def check_links(links
):
409 """ Check that no link points to another link.
412 :param links: Links to icons
417 for link
, target
in links
.items():
418 if target
in links
.keys():
419 LOGGER
.error("Link %s -> %s -> %s", link
, target
, links
[target
])
423 LOGGER
.error("Some icons in links.txt were found to link to other linked icons")
427 def read_links(links
, filename
):
428 """ Read links from file.
431 :param links: Hash to store all links
434 :param filename: filename to read the links from
437 LOGGER
.info("Read links from file '%s'", filename
)
438 if not os
.path
.isfile(filename
):
439 LOGGER
.info("No file to read")
445 LOGGER
.error("Cannot open file %s", filename
)
451 if line
== '' or line
.startswith('#'):
454 tmp
= line
.split(' ')
456 links
[tmp
[0]] = tmp
[1]
458 LOGGER
.error("Malformed links line '%s' in file %s", line
, filename
)
461 LOGGER
.debug("Read links:\n%s", "\n".join(links
))
464 def find_custom(custom_paths
):
465 """ Find all custom images.
467 :type custom_paths: list
468 :param custom_paths: List of custom paths to search within.
471 :return: List of all custom images.
473 LOGGER
.info("Find all images in custom paths")
475 custom_image_list
= {}
476 for path
in custom_paths
:
477 # find all png files under the path including subdirs
478 custom_files
= [val
for sublist
in [
479 [os
.path
.join(i
[0], j
) for j
in i
[2]
480 if j
.endswith('.png') and os
.path
.isfile(os
.path
.join(i
[0], j
))]
481 for i
in os
.walk(path
)] for val
in sublist
]
483 for filename
in custom_files
:
484 if filename
.startswith(path
):
485 key
= filename
.replace(os
.path
.join(path
, ''), '')
486 if key
not in custom_image_list
:
487 custom_image_list
[key
] = path
489 LOGGER
.debug("Custom image list:\n%s", "\n".join(custom_image_list
.keys()))
490 return custom_image_list
493 def parse_image_list(imagelist_filenames
):
494 """ Parse and extract filename from the imagelist files.
496 :type imagelist_filenames: list
497 :param imagelist_filenames: List of imagelist files.
500 :return: Tuple with dicts containing the global or module image list.
503 global_image_list
= {}
504 module_image_list
= {}
505 for imagelist_filename
in imagelist_filenames
.keys():
506 LOGGER
.info("Parsing '%s'", imagelist_filename
)
509 fh
= open(imagelist_filename
)
511 LOGGER
.error("Cannot open imagelist file %s", imagelist_filename
)
519 if line
== '' or line
.startswith('#'):
521 # clean up backslashes and double slashes
522 line
= line
.replace('\\', '/')
523 line
= line
.replace('//', '/')
525 if line
.startswith('%GLOBALRES%'):
526 key
= "res/%s" % line
.replace('%GLOBALRES%/', '')
527 global_image_list
[key
] = True
528 if key
.endswith('.png'):
529 global_image_list
[key
[:-4] + '.svg'] = True
532 if line
.startswith('%MODULE%'):
533 key
= line
.replace('%MODULE%/', '')
534 module_image_list
[key
] = True
535 if key
.endswith('.png'):
536 module_image_list
[key
[:-4] + '.svg'] = True
539 LOGGER
.error("Cannot parse line %s:%d", imagelist_filename
, line_count
)
542 LOGGER
.debug("Global image list:\n%s", "\n".join(global_image_list
))
543 LOGGER
.debug("Module image list:\n%s", "\n".join(module_image_list
))
544 return global_image_list
, module_image_list
547 def get_imagelist_filenames(filename
):
548 """ Extract a list of imagelist filenames.
551 :param filename: Name of file from extracting the list.
554 :return: List of imagelist filenames.
556 LOGGER
.info("Extract the imagelist filenames")
558 imagelist_filenames
= {}
562 LOGGER
.error("Cannot open imagelist file %s", filename
)
568 if not line
or line
== '':
571 for line_split
in line
.split(' '):
573 imagelist_filenames
[line_split
] = ''
575 LOGGER
.debug("Extracted imagelist filenames:\n%s", "\n".join(imagelist_filenames
.keys()))
576 return imagelist_filenames
579 if __name__
== '__main__':
580 parser
= argparse
.ArgumentParser("Pack images into archives")
581 parser
.add_argument('-o', '--output-file', dest
='output_file',
582 action
='store', required
=True,
583 help='path to output archive')
584 parser
.add_argument('-l', '--imagelist-file', dest
='imagelist_file',
585 action
='store', required
=True,
586 help='file containing list of image list file')
587 parser
.add_argument('-L', '--links-file', dest
='links_file',
588 action
='store', required
=False,
589 help='file containing linked images')
590 parser
.add_argument('-s', '--sort-file', dest
='sort_file',
591 action
='store', required
=True, default
=None,
592 help='image sort order file')
593 parser
.add_argument('-c', '--custom-path', dest
='custom_path',
594 action
='append', required
=True,
595 help='path to custom path directory')
596 parser
.add_argument('-g', '--global-path', dest
='global_path',
597 action
='store', required
=True,
598 help='path to global images directory')
599 parser
.add_argument('-m', '--module-path', dest
='module_path',
600 action
='store', required
=True,
601 help='path to module images directory')
602 parser
.add_argument('-v', '--verbose', dest
='verbose',
603 action
='count', default
=0,
604 help='set the verbosity (can be used multiple times)')
606 ARGS
= parser
.parse_args()
607 LOGGER
.setLevel(logging
.ERROR
- (10 * ARGS
.verbose
if ARGS
.verbose
<= 3 else 3))
612 # vim: set shiftwidth=4 softtabstop=4 expandtab: