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.')