Browse Source

Ajout d'un script pour optimiser les images

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
prod
Youen 2 years ago
parent
commit
b5ddebed8f
  1. 1
      .gitignore
  2. 17
      sphinx-tools/.project
  3. 5
      sphinx-tools/.pydevproject
  4. 167
      sphinx-tools/optimize_images.py

1
.gitignore vendored

@ -1,3 +1,4 @@
/outils
/build
/.metadata/

17
sphinx-tools/.project

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>sphinx-tools</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.python.pydev.PyDevBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.python.pydev.pythonNature</nature>
</natures>
</projectDescription>

5
sphinx-tools/.pydevproject

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python interpreter</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
</pydev_project>

167
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)
# <img src="image/path" width="w" height="h">
for img_code in re.finditer('<img.*?src="'+image_search+'".*?>', 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.')
Loading…
Cancel
Save