Browse Source
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'espaceprod
Youen
2 years ago
4 changed files with 190 additions and 0 deletions
@ -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> |
@ -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> |
@ -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…
Reference in new issue