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():
347 LOGGER
.debug("Cleaned zip list:\n%s", "\n".join(zip_list
))
350 def create_zip_list(global_image_list
, module_image_list
, custom_image_list
, global_path
, module_path
):
351 """ Create list of images for the zip archive.
353 :type global_image_list: dict
354 :param global_image_list: Global images.
356 :type module_image_list: dict
357 :param module_image_list: Module images.
359 :type custom_image_list: dict
360 :param custom_image_list: Custom images.
362 :type global_path: str
363 :param global_path: Global path (use when no custom path is available).
365 :type module_path: str
366 :param module_path: Module path (use when no custom path is available).
369 :return: List of images to include in zip archive.
371 LOGGER
.info("Assemble image list")
376 for key
in global_image_list
.keys():
377 if key
in module_image_list
:
378 duplicates
.append(key
)
381 if key
in custom_image_list
:
382 zip_list
[key
] = custom_image_list
[key
]
385 zip_list
[key
] = global_path
387 for key
in module_image_list
.keys():
388 if key
in custom_image_list
:
389 zip_list
[key
] = custom_image_list
[key
]
392 zip_list
[key
] = module_path
395 LOGGER
.warning("Found duplicate entries in 'global' and 'module' list")
396 LOGGER
.warning("\n".join(duplicates
))
398 LOGGER
.debug("Assembled image list:\n%s", "\n".join(zip_list
))
402 def check_links(links
):
403 """ Check that no link points to another link.
406 :param links: Links to icons
411 for link
, target
in links
.items():
412 if target
in links
.keys():
413 LOGGER
.error("Link %s -> %s -> %s", link
, target
, links
[target
])
417 LOGGER
.error("Some icons in links.txt were found to link to other linked icons")
421 def read_links(links
, filename
):
422 """ Read links from file.
425 :param links: Hash to store all links
428 :param filename: filename to read the links from
431 LOGGER
.info("Read links from file '%s'", filename
)
432 if not os
.path
.isfile(filename
):
433 LOGGER
.info("No file to read")
439 LOGGER
.error("Cannot open file %s", filename
)
445 if line
== '' or line
.startswith('#'):
448 tmp
= line
.split(' ')
450 links
[tmp
[0]] = tmp
[1]
452 LOGGER
.error("Malformed links line '%s' in file %s", line
, filename
)
455 LOGGER
.debug("Read links:\n%s", "\n".join(links
))
458 def find_custom(custom_paths
):
459 """ Find all custom images.
461 :type custom_paths: list
462 :param custom_paths: List of custom paths to search within.
465 :return: List of all custom images.
467 LOGGER
.info("Find all images in custom paths")
469 custom_image_list
= {}
470 for path
in custom_paths
:
471 # find all png files under the path including subdirs
472 custom_files
= [val
for sublist
in [
473 [os
.path
.join(i
[0], j
) for j
in i
[2]
474 if j
.endswith('.png') and os
.path
.isfile(os
.path
.join(i
[0], j
))]
475 for i
in os
.walk(path
)] for val
in sublist
]
477 for filename
in custom_files
:
478 if filename
.startswith(path
):
479 key
= filename
.replace(os
.path
.join(path
, ''), '')
480 if key
not in custom_image_list
:
481 custom_image_list
[key
] = path
483 LOGGER
.debug("Custom image list:\n%s", "\n".join(custom_image_list
.keys()))
484 return custom_image_list
487 def parse_image_list(imagelist_filenames
):
488 """ Parse and extract filename from the imagelist files.
490 :type imagelist_filenames: list
491 :param imagelist_filenames: List of imagelist files.
494 :return: Tuple with dicts containing the global or module image list.
497 global_image_list
= {}
498 module_image_list
= {}
499 for imagelist_filename
in imagelist_filenames
.keys():
500 LOGGER
.info("Parsing '%s'", imagelist_filename
)
503 fh
= open(imagelist_filename
)
505 LOGGER
.error("Cannot open imagelist file %s", imagelist_filename
)
513 if line
== '' or line
.startswith('#'):
515 # clean up backslashes and double slashes
516 line
= line
.replace('\\', '/')
517 line
= line
.replace('//', '/')
519 if line
.startswith('%GLOBALRES%'):
520 key
= "res/%s" % line
.replace('%GLOBALRES%/', '')
521 global_image_list
[key
] = True
522 if key
.endswith('.png'):
523 global_image_list
[key
[:-4] + '.svg'] = True
526 if line
.startswith('%MODULE%'):
527 key
= line
.replace('%MODULE%/', '')
528 module_image_list
[key
] = True
529 if key
.endswith('.png'):
530 module_image_list
[key
[:-4] + '.svg'] = True
533 LOGGER
.error("Cannot parse line %s:%d", imagelist_filename
, line_count
)
536 LOGGER
.debug("Global image list:\n%s", "\n".join(global_image_list
))
537 LOGGER
.debug("Module image list:\n%s", "\n".join(module_image_list
))
538 return global_image_list
, module_image_list
541 def get_imagelist_filenames(filename
):
542 """ Extract a list of imagelist filenames.
545 :param filename: Name of file from extracting the list.
548 :return: List of imagelist filenames.
550 LOGGER
.info("Extract the imagelist filenames")
552 imagelist_filenames
= {}
556 LOGGER
.error("Cannot open imagelist file %s", filename
)
562 if not line
or line
== '':
565 for line_split
in line
.split(' '):
567 imagelist_filenames
[line_split
] = ''
569 LOGGER
.debug("Extracted imagelist filenames:\n%s", "\n".join(imagelist_filenames
.keys()))
570 return imagelist_filenames
573 if __name__
== '__main__':
574 parser
= argparse
.ArgumentParser("Pack images into archives")
575 parser
.add_argument('-o', '--output-file', dest
='output_file',
576 action
='store', required
=True,
577 help='path to output archive')
578 parser
.add_argument('-l', '--imagelist-file', dest
='imagelist_file',
579 action
='store', required
=True,
580 help='file containing list of image list file')
581 parser
.add_argument('-L', '--links-file', dest
='links_file',
582 action
='store', required
=False,
583 help='file containing linked images')
584 parser
.add_argument('-s', '--sort-file', dest
='sort_file',
585 action
='store', required
=True, default
=None,
586 help='image sort order file')
587 parser
.add_argument('-c', '--custom-path', dest
='custom_path',
588 action
='append', required
=True,
589 help='path to custom path directory')
590 parser
.add_argument('-g', '--global-path', dest
='global_path',
591 action
='store', required
=True,
592 help='path to global images directory')
593 parser
.add_argument('-m', '--module-path', dest
='module_path',
594 action
='store', required
=True,
595 help='path to module images directory')
596 parser
.add_argument('-v', '--verbose', dest
='verbose',
597 action
='count', default
=0,
598 help='set the verbosity (can be used multiple times)')
600 ARGS
= parser
.parse_args()
601 LOGGER
.setLevel(logging
.ERROR
- (10 * ARGS
.verbose
if ARGS
.verbose
<= 3 else 3))
606 # vim: set shiftwidth=4 softtabstop=4 expandtab: