From b5ddebed8ff864843d42f75692187accd900a675 Mon Sep 17 00:00:00 2001 From: Youen Date: Sat, 29 Apr 2023 00:39:20 +0200 Subject: [PATCH] Ajout d'un script pour optimiser les images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Conversion automatique de PNG en JPEG lorsque cela fait gagner suffisamment d'espace (le cas échéant, les fichiers markdown sont modifiés avec le nouveau nom de fichier) Réduction automatique de la résolution selon l'espace occupé sur la page Recompression des JPEG lorsque cela fait gagner de l'espace --- .gitignore | 1 + sphinx-tools/.project | 17 ++++ sphinx-tools/.pydevproject | 5 + sphinx-tools/optimize_images.py | 167 ++++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+) create mode 100644 sphinx-tools/.project create mode 100644 sphinx-tools/.pydevproject create mode 100644 sphinx-tools/optimize_images.py diff --git a/.gitignore b/.gitignore index c00f4c8..60a49da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /outils /build +/.metadata/ diff --git a/sphinx-tools/.project b/sphinx-tools/.project new file mode 100644 index 0000000..8f1c98b --- /dev/null +++ b/sphinx-tools/.project @@ -0,0 +1,17 @@ + + + sphinx-tools + + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + + diff --git a/sphinx-tools/.pydevproject b/sphinx-tools/.pydevproject new file mode 100644 index 0000000..c1abf31 --- /dev/null +++ b/sphinx-tools/.pydevproject @@ -0,0 +1,5 @@ + + + python interpreter + Default + diff --git a/sphinx-tools/optimize_images.py b/sphinx-tools/optimize_images.py new file mode 100644 index 0000000..96c0c74 --- /dev/null +++ b/sphinx-tools/optimize_images.py @@ -0,0 +1,167 @@ +import os +import glob +import re +from PIL import Image +from pathlib import Path +import shutil + +delete_unused_images = True # if true, and an image is not referenced in any markdown file, it will be deleted +max_density = 400 # maximum image resolution, in dots per inch. You can set a very big value here if you don't want to resize images + +full_width = 800 # width of the page, in pixels +page_width = 190 # in millimeters, without print margins + +source_dir = os.path.dirname(__file__) + '/../source' +images_dir = os.path.dirname(__file__) + '/../source/img' # subdirectories are explored too +target_dir = os.path.dirname(__file__) + '/../source/img' # can be the same directory as images_dir, or another one + +md_sources = list(glob.iglob(source_dir + '/**/*.md', recursive=True)) + +# Replace an image in all source files +def replace_image(original_rel_path, new_rel_path): + for src_name in md_sources: + with open(src_name) as src_file: + original_contents = src_file.read() + + new_contents = original_contents.replace(original_rel_path, new_rel_path) + if new_contents != original_contents: + with open(src_name, 'w') as new_file: + new_file.write(new_contents) + +total_saved_space = 0 + +for image_path in (Path(images_dir).glob("**/*")): + if image_path.suffix.lower() not in {".jpg", ".jpeg", ".png", ".svg"}: continue + + image_filename = image_path.name + image_display_width = 0 + + # compute target path relatively to the source folder + image_rel_path = os.path.relpath(image_path.resolve(), images_dir) + image_rel_path = os.path.relpath(target_dir + '/' + image_rel_path, source_dir) + + os.makedirs(os.path.dirname(source_dir + '/' + image_rel_path), exist_ok = True) + + if images_dir != target_dir and os.path.isfile(source_dir + '/' + image_rel_path): continue + + #print(image_rel_path) + image_search = re.escape(image_rel_path) + + image = Image.open(image_path.resolve()) if image_path.suffix.lower() != '.svg' else None + image_aspect = 1 if image is None else image.size[0] / image.size[1] + + original_size = os.path.getsize(image_path.resolve()) + + for src_name in md_sources: + with open(src_name) as src_file: + src_contents = src_file.read() + # ![alt text](image/path) + for img_code in re.finditer('!\[.*\]\('+image_search+'\)', src_contents): + #print(img_code.group(0)) + image_display_width = max(image_display_width, full_width) + + # + for img_code in re.finditer('', src_contents): + #print(img_code.group(0)) + width = full_width + width_code = re.search('width="(.*?)[px]*"', img_code.group(0)) + if width_code is None: + height_code = re.search('height="(.*?)[px]*"', img_code.group(0)) + if height_code is not None: + height = int(height_code.group(1)) + width = int(image_aspect * height + 0.5) + else: + width = int(width_code.group(1)) + image_display_width = max(image_display_width, width) + + # ```{image} img/vhelio.png :width: wpx :height: hpx``` + for img_code in re.finditer('```{image} '+image_search+'.*?```', src_contents, re.MULTILINE + re.DOTALL): + #print(img_code.group(0)) + width = full_width + width_code = re.search(':width:\s*(.*?)[px]*\s', img_code.group(0)) + if width_code is None: + height_code = re.search(':height:\s*(.*?)[px]*\s', img_code.group(0)) + if height_code is not None: + height = int(height_code.group(1)) + width = int(image_aspect * height + 0.5) + else: + width = int(width_code.group(1)) + image_display_width = max(image_display_width, width) + + if image_display_width == 0: + if delete_unused_images: + print('WARNING: removing unused image ' + image_rel_path) + os.remove(image_path.resolve()) + continue + else: + raise Exception('Image not found in source documents: ' + image_rel_path) + + if image is None: + if images_dir != target_dir: + shutil.copyfile(image_path.resolve(), source_dir + '/' + image_rel_path) + continue + + #print(image_filename + ': width=' + str(image_info.max_width)) + + image_width_inches = image_display_width / full_width * page_width / 25.4 + target_resolution_width = max(1, int(max_density * image_width_inches + 0.5)) + target_resolution_height = max(1, int(target_resolution_width/image.size[0]*image.size[1]+0.5)) + + if target_resolution_width > image.size[0]: + target_resolution_width = image.size[0] + target_resolution_height = image.size[1] + + #print('Resizing image ' + image_filename + ' from ' + str(current_image.size[0]) + ' to ' + str(target_resolution_width)) + resized = image.resize((target_resolution_width,target_resolution_height), Image.Resampling.LANCZOS) if target_resolution_width != image.size[0] else image + + target_path = source_dir + '/' + image_rel_path + if image_path.suffix.lower() == '.png': + # Try to save the file as JPEG to see if it would be significantly smaller + # This helps detecting files that should be JPEG, not PNG + + if resized.mode != 'RGB': + background = Image.new('RGBA', resized.size, (255,255,255)) + alpha_composite = Image.alpha_composite(background, resized.convert('RGBA')) + resized = alpha_composite.convert('RGB') + + png_path = target_path + jpeg_path = png_path[0:-4] + '.jpg.tmp' + png_path = png_path + '.tmp' + + resized.save(jpeg_path, format = 'JPEG', quality = 80) + resized.save(png_path, format = 'PNG') + + # Force JPEG compression if it makes the image at least twice as small (in some cases, PNG can even give a smaller file) + png_size = os.path.getsize(png_path) + jpeg_size = os.path.getsize(jpeg_path) + best_png_size = min(original_size, png_size) + if jpeg_size < best_png_size - 200*1024 or jpeg_size < best_png_size / 2: + os.remove(png_path) + os.remove(target_path) + os.rename(jpeg_path, jpeg_path[0:-4]) + print('WARNING: ' + image_rel_path + ' has been converted to JPEG format') + + replace_image(image_rel_path, os.path.relpath(jpeg_path[0:-4], source_dir)) + total_saved_space += original_size - jpeg_size + else: + os.remove(jpeg_path) + if png_size < original_size - 100*1024 or png_size < original_size * 8/10: + total_saved_space += original_size - png_size + os.remove(target_path) + os.rename(png_path, target_path) + print('Recompressed PNG ' + image_rel_path) + else: + os.remove(png_path) + else: + tmp_path = target_path + '.tmp' + resized.save(tmp_path, format = 'JPEG', quality = 80) + tmp_size = os.path.getsize(tmp_path) + if tmp_size < original_size - 100*1024 or tmp_size < original_size * 8/10: + total_saved_space += original_size - tmp_size + os.remove(target_path) + os.rename(tmp_path, target_path) + print('Recompressed JPEG ' + image_rel_path) + else: + os.remove(tmp_path) + +print('Done. Saved ' + str(int(total_saved_space/1024+0.5)) + 'kB.')