add script to move images

This commit is contained in:
Tom van Woudenberg 2025-03-27 09:42:42 +01:00
parent 9cb3724878
commit 9665fb5027
3 changed files with 211 additions and 0 deletions

View file

@ -118,6 +118,7 @@ parts:
- file: helper_code/intro.md
sections:
- file: helper_code/converter.md
- file: helper_code/move_images.md
- file: external/Useful_python_code/snippit.ipynb
- file: external/Useful_python_code/extensionfildownloadreturn.ipynb
- file: external/Useful_python_code/figshrinker.ipynb

View file

@ -0,0 +1,3 @@
# Move images to separate hosting
Storing big binary files like images in your git repository and book is not good practice: diffs on binary files don't make sense and blow up the size of your repository. Furthermore, the size of your book website blows up too. Therefore, you can consider hosting your binary files on a different server, as explained [here in an example for the MUDE-book](https://tudelft-mude.github.io/teacher/FTP.html). To take all the images of your existing book and update the relative links, [a Python script](./parse_images.py) has been developed. This script processes Markdown (`.md`) and Jupyter Notebook (`.ipynb`) files listed in a `_toc.yml` file. It identifies local image references (including MyST `figure` syntax) in these files, copies the images to a specified folder (`new_images`), and replaces the local image paths with a public URL (e.g., `https://files.mude.citg.tudelft.nl/<image_name>`).

View file

@ -0,0 +1,207 @@
"""
Script: parse_images.py
Description:
This script processes Markdown (.md) and Jupyter Notebook (.ipynb) files listed in a `_toc.yml` file.
It identifies local image references (including MyST `figure` syntax) in these files, copies the images
to a specified folder (`new_images`), and replaces the local image paths with a public URL
(e.g., `https://files.mude.citg.tudelft.nl/<image_name>`).
Key Features:
- Parses `_toc.yml` to extract all Markdown and Notebook files.
- Detects image references in Markdown files (e.g., `![alt text](image.png "title")`).
- Detects MyST `figure` syntax (e.g., `{figure} /path/to/image.png`).
- Resolves image paths relative to the file or project root.
- Handles image paths with optional titles (e.g., `"title"`) in Markdown and Notebook files.
- Skips processing images that are already converted to the public URL.
- Copies images to the `new_images` folder.
- Updates the files to replace local image paths with public URLs.
How to Use:
1. Place this script in the root directory of your project (where `_toc.yml` is located).
2. Ensure `_toc.yml` contains the list of files to process.
3. Run the script using Python: python parse_images.py
4. The script will:
- Copy referenced images to the `new_images` folder.
- Update the Markdown and Notebook files with the new image URLs.
Dependencies:
- Python 3.x
- PyYAML (install using `pip install pyyaml`)
Notes:
- Ensure the script is run from the project root directory.
- The `base_url` variable should be updated to match your desired public URL prefix.
- The script handles image paths in the following formats:
- Relative paths (e.g., `image.png`, `./image.png`).
- Absolute paths (e.g., `/path/to/image.png`).
- Paths with optional titles (e.g., `![alt text](image.png "title")`).
"""
import os
import re
import shutil
import yaml
import json
# Define the folder where images will be copied
output_folder = "new_images"
base_url = "https://files.mude.citg.tudelft.nl/"
# Ensure the output folder exists
os.makedirs(output_folder, exist_ok=True)
# Function to parse the _toc.yml file and extract all Markdown and Notebook files
def get_files_from_toc(toc_file):
files = []
with open(toc_file, "r", encoding="utf-8") as f:
toc = yaml.safe_load(f)
# Recursive function to extract file paths
def extract_files(item):
if isinstance(item, dict):
if "file" in item:
# Add the file with the appropriate extension
file_path = item["file"]
if not file_path.endswith((".md", ".ipynb")):
if os.path.isfile(file_path + ".md"):
file_path += ".md"
elif os.path.isfile(file_path + ".ipynb"):
file_path += ".ipynb"
files.append(file_path)
if "sections" in item:
for section in item["sections"]:
extract_files(section)
if "chapters" in item:
for chapter in item["chapters"]:
extract_files(chapter)
elif isinstance(item, list):
for sub_item in item:
extract_files(sub_item)
# Start extracting files from the "parts" section
extract_files(toc.get("parts", []))
return files
# Function to normalize image paths
def normalize_image_path(file_dir, image_path):
# Check if the path starts with a `/` (absolute path relative to the project root)
if image_path.startswith("/"):
# Resolve the path relative to the project root
project_root = os.getcwd() # Assuming the script is run from the project root
resolved_path = os.path.join(project_root, image_path.lstrip("/"))
else:
# Resolve the path relative to the file's directory
resolved_path = os.path.normpath(os.path.join(file_dir, image_path))
return resolved_path
# Function to find and replace local image references
def process_file(file_path):
if file_path.endswith(".md"):
process_markdown(file_path)
elif file_path.endswith(".ipynb"):
process_notebook(file_path)
# Process Markdown files
def process_markdown(file_path):
file_dir = os.path.dirname(file_path) # Get the directory of the file
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
# Find all standard Markdown image references (e.g., ![alt text](path "title"))
matches = re.findall(r"!\[.*?\]\((.*?)(?:\s+\".*?\")?\)", content)
# Find all MyST figure syntax references (e.g., {figure} path)
# Handles cases where the `---` block is missing or caption follows directly
myst_matches = re.findall(r"\{figure\}\s+(.*?)\s*(?:\n|$)", content)
matches.extend(myst_matches)
for match in matches:
# Skip if the image path is already converted to the public URL
if match.startswith(base_url):
continue
# Normalize and resolve the image path
image_path = normalize_image_path(file_dir, match)
if os.path.isfile(image_path):
print(f"Image found: {image_path}") # Debugging output
# Copy the image to the output folder
image_name = os.path.basename(image_path)
new_path = os.path.join(output_folder, image_name)
shutil.copy(image_path, new_path)
# Replace the local URL with the new URL
new_url = base_url + image_name
content = content.replace(match, new_url)
else:
print(f"Warning: Image not found: {image_path}") # Debugging output
# Write the updated content back to the file
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
# Process Jupyter Notebook files
def process_notebook(file_path):
file_dir = os.path.dirname(file_path) # Get the directory of the file
with open(file_path, "r", encoding="utf-8") as f:
notebook = json.load(f)
updated = False
for cell in notebook.get("cells", []):
if cell.get("cell_type") == "markdown":
# Join all lines in the cell into a single string
cell_content = "".join(cell.get("source", []))
# Find all standard Markdown image references (e.g., ![alt text](path "title"))
matches = re.findall(r"!\[.*?\]\((.*?)(?:\s+\".*?\")?\)", cell_content)
# Find all MyST figure syntax references (e.g., {figure} path)
myst_matches = re.findall(r"\{figure\}\s+(.*?)\s*(?:\n|$)", cell_content)
matches.extend(myst_matches)
for match in matches:
# Skip if the image path is already converted to the public URL
if match.startswith(base_url):
continue
# Normalize and resolve the image path
image_path = normalize_image_path(file_dir, match)
if os.path.isfile(image_path):
print(f"Image found: {image_path}") # Debugging output
# Copy the image to the output folder
image_name = os.path.basename(image_path)
new_path = os.path.join(output_folder, image_name)
shutil.copy(image_path, new_path)
# Replace the local URL with the new URL
new_url = base_url + image_name
cell_content = cell_content.replace(match, new_url)
updated = True
else:
print(f"Warning: Image not found: {image_path}") # Debugging output
# Split the updated content back into lines and update the cell
cell["source"] = cell_content.splitlines(keepends=True)
# Write the updated notebook back to the file if changes were made
if updated:
with open(file_path, "w", encoding="utf-8") as f:
json.dump(notebook, f, indent=2)
# Main function
def main():
toc_file = "_toc.yml" # Replace with the path to your _toc.yml file
files = get_files_from_toc(toc_file)
print("Files extracted from TOC:", files) # Debugging output
for file_path in files:
# Ensure the file has the correct extension and exists
if os.path.isfile(file_path):
print(f"Processing file: {file_path}") # Debugging output
process_file(file_path)
else:
print(f"Warning: File {file_path} not found or unsupported.")
if __name__ == "__main__":
main()