#!/usr/bin/env python3 import os import sys import json import subprocess import argparse CWEBP_PATH = "/opt/homebrew/bin/cwebp" def run_command(cmd): try: res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) return res.returncode == 0, res.stdout, res.stderr except Exception as e: return False, "", str(e) def find_images(search_dirs): extensions = {'.png', '.jpg', '.jpeg'} found = [] for s_dir in search_dirs: if not os.path.exists(s_dir): print(f"Directory not found: {s_dir}") continue for root, dirs, files in os.walk(s_dir): # Avoid node_modules, .git, .obsidian, and dist if any(p in root.split(os.sep) for p in ('.git', '.obsidian', 'node_modules', 'dist', '.trash')): continue for file in files: ext = os.path.splitext(file)[1].lower() if ext in extensions: found.append(os.path.join(root, file)) return found def convert_images(image_paths, mapping_file): mapping = {} if os.path.exists(mapping_file): try: with open(mapping_file, 'r', encoding='utf-8') as f: mapping = json.load(f) except Exception: pass success_count = 0 fail_count = 0 print(f"Starting conversion of {len(image_paths)} images...") for path in image_paths: dir_name = os.path.dirname(path) base_name = os.path.basename(path) name_no_ext, ext = os.path.splitext(base_name) webp_name = f"{name_no_ext}.webp" webp_path = os.path.join(dir_name, webp_name) print(f"Converting: {base_name} -> {webp_name}") # Run cwebp command cmd = [CWEBP_PATH, "-q", "85", path, "-o", webp_path] ok, out, err = run_command(cmd) if ok and os.path.exists(webp_path) and os.path.getsize(webp_path) > 0: print(f" [SUCCESS] Created {webp_name}") # Map the old filename to new filename mapping[base_name] = webp_name # Also keep track of relative paths or specific subfolders if needed success_count += 1 # Delete original file try: os.remove(path) print(f" [DELETED] Original {base_name}") except Exception as ex: print(f" [WARNING] Could not delete original file {base_name}: {ex}") else: print(f" [ERROR] Failed to convert {base_name}: {err.strip()}") fail_count += 1 # Save mapping JSON try: with open(mapping_file, 'w', encoding='utf-8') as f: json.dump(mapping, f, indent=2, ensure_ascii=False) print(f"\nConversion finished. Success: {success_count}, Failed: {fail_count}") print(f"Mapping saved to: {mapping_file}") except Exception as e: print(f"Error saving mapping file: {e}") def replace_references(markdown_dirs, mapping_file): if not os.path.exists(mapping_file): print(f"Mapping file not found: {mapping_file}. Run conversion phase first!") return try: with open(mapping_file, 'r', encoding='utf-8') as f: mapping = json.load(f) except Exception as e: print(f"Error loading mapping file: {e}") return if not mapping: print("No mapping entries found.") return print(f"Loaded {len(mapping)} image mappings.") modified_files = 0 total_replacements = 0 for m_dir in markdown_dirs: if not os.path.exists(m_dir): print(f"Markdown directory not found: {m_dir}") continue print(f"Scanning for markdown files in: {m_dir}") for root, dirs, files in os.walk(m_dir): if any(p in root.split(os.sep) for p in ('.git', '.obsidian', 'node_modules', 'dist', '.trash')): continue for file in files: if file.endswith(('.md', '.mdx')): filepath = os.path.join(root, file) try: with open(filepath, 'r', encoding='utf-8') as f: content = f.read() new_content = content file_replacements = 0 # Search and replace each mapping for old_name, new_name in mapping.items(): # Replace exact occurrences of old image name with new webp image name # We can just do string replace of old_name with new_name if old_name in new_content: count = new_content.count(old_name) new_content = new_content.replace(old_name, new_name) file_replacements += count total_replacements += count if file_replacements > 0: with open(filepath, 'w', encoding='utf-8') as f: f.write(new_content) print(f"Updated {file}: replaced {file_replacements} references") modified_files += 1 except Exception as e: print(f"Error processing file {filepath}: {e}") print(f"\nReplacement finished. Modified {modified_files} markdown files with {total_replacements} replacements total.") def main(): parser = argparse.ArgumentParser(description="Bulk convert images to WebP and update markdown links.") parser.add_argument("--convert", action="store_true", help="Phase 1: Convert PNG/JPG/JPEG to WebP and delete original files") parser.add_argument("--replace", action="store_true", help="Phase 2: Update all markdown links referencing converted images") parser.add_argument("--mapping-file", default="conversion_mapping.json", help="Path to JSON mapping file") args = parser.parse_args() # Define targets obsidian_vault = "/Users/keira/Documents/GitHub/Obsidian Vault Mac" blog_content = "/Users/keira/Documents/GitHub/Projects/nhi-nhi-vo-blog/src/content" if not args.convert and not args.replace: print("Please specify either --convert (Phase 1) or --replace (Phase 2).") parser.print_help() sys.exit(1) if args.convert: image_dirs = [ os.path.join(obsidian_vault, "attachments"), os.path.join(blog_content, "posts/attachments"), os.path.join(blog_content, "projects/attachments"), os.path.join(blog_content, "docs/attachments"), os.path.join(blog_content, "pages/attachments") ] image_paths = find_images(image_dirs) print(f"Found {len(image_paths)} images to convert.") if image_paths: convert_images(image_paths, args.mapping_file) else: print("No images found to convert.") if args.replace: markdown_dirs = [obsidian_vault, blog_content] replace_references(markdown_dirs, args.mapping_file) if __name__ == "__main__": main()