4 Commits
3 changed files with 87 additions and 43 deletions
+47 -41
View File
@@ -1,11 +1,7 @@
#!/usr/bin/env -S uv run #!/usr/bin/env -S uv run
import os import os
import shutil
import subprocess import subprocess
from pathlib import Path
from typing import List, Tuple
import click import click
import cv2 import cv2
import img2pdf import img2pdf
@@ -13,8 +9,8 @@ import numpy as np
from pdf2image import convert_from_path from pdf2image import convert_from_path
import settings import settings
from settings import OptimizationLevel
from PIL import Image from PIL import Image
import concurrent.futures
class Settings: class Settings:
TRIM_PADDING_PIXELS = 20 TRIM_PADDING_PIXELS = 20
@@ -22,8 +18,6 @@ class Settings:
OPTIPNG_OPTIMIZATION_LEVEL = 7 OPTIPNG_OPTIMIZATION_LEVEL = 7
PDF_BORDER_SIZE = 50 PDF_BORDER_SIZE = 50
settings = Settings()
def ensure_temp_dir(): def ensure_temp_dir():
"""Ensure temporary directory exists and return its path.""" """Ensure temporary directory exists and return its path."""
temp_dir = "temp_processed_images" temp_dir = "temp_processed_images"
@@ -281,7 +275,7 @@ def run_optipng(file_path: str) -> bool:
bool: True if successful, False otherwise bool: True if successful, False otherwise
""" """
try: try:
result = subprocess.run( subprocess.run(
['optipng', f'-o{settings.OPTIPNG_OPTIMIZATION_LEVEL}', file_path], ['optipng', f'-o{settings.OPTIPNG_OPTIMIZATION_LEVEL}', file_path],
capture_output=True, capture_output=True,
text=True, text=True,
@@ -306,13 +300,12 @@ def optimize(level):
2: Level 1 + convert to 1-bit monochrome 2: Level 1 + convert to 1-bit monochrome
3: Level 2 + optipng optimization 3: Level 2 + optipng optimization
""" """
temp_dir = ensure_temp_dir()
temp_files = get_temp_files() temp_files = get_temp_files()
if not temp_files: if not temp_files:
print("No pages found in temporary directory. Run 'extract' first.") print("No pages found in temporary directory. Run 'extract' first.")
return return
opt_level = OptimizationLevel(level) opt_level = settings.OptimizationLevel(level)
total_files = len(temp_files) total_files = len(temp_files)
successful = { successful = {
@@ -323,49 +316,62 @@ def optimize(level):
print(f"Processing {total_files} images at optimization level {level}...") print(f"Processing {total_files} images at optimization level {level}...")
# Step 1: Always trim whitespace # Step 1: Convert to monochrome if level >= 2
print("\nTrimming whitespace from images...") if int(opt_level) >= int(settings.OptimizationLevel.MONOCHROME):
for i, file_path in enumerate(temp_files, 1):
print(f"[{i}/{total_files}] Processing {os.path.basename(file_path)}...", end='', flush=True)
# Only optimize if this is the final step (level 1)
if trim_whitespace(file_path, is_final=(opt_level == OptimizationLevel.TRIM)):
successful['trim'] += 1
print(" ")
else:
print(" ")
# Step 2: Convert to monochrome if level >= 2
if int(opt_level) >= int(OptimizationLevel.MONOCHROME):
print("\nConverting to monochrome...") print("\nConverting to monochrome...")
for i, file_path in enumerate(temp_files, 1): with concurrent.futures.ThreadPoolExecutor() as executor:
print(f"[{i}/{total_files}] Converting {os.path.basename(file_path)}...", end='', flush=True) futures = {executor.submit(convert_to_monochrome, file_path, (opt_level == settings.OptimizationLevel.MONOCHROME)): file_path for file_path in temp_files}
# Only optimize if this is the final step (level 2) for future in concurrent.futures.as_completed(futures):
if convert_to_monochrome(file_path, is_final=(opt_level == OptimizationLevel.MONOCHROME)): file_path = futures[future]
successful['monochrome'] += 1 try:
print(" ") if future.result():
else: successful['monochrome'] += 1
print(" ") print(f"Converted {os.path.basename(file_path)}")
else:
print(f"Failed to convert {os.path.basename(file_path)}")
except Exception as exc:
print(f"{os.path.basename(file_path)} generated an exception: {exc}")
# Step 2: Always trim whitespace
print("\nTrimming whitespace from images...")
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = {executor.submit(trim_whitespace, file_path, (opt_level == settings.OptimizationLevel.TRIM)): file_path for file_path in temp_files}
for future in concurrent.futures.as_completed(futures):
file_path = futures[future]
try:
if future.result():
successful['trim'] += 1
print(f"Trimmed {os.path.basename(file_path)}")
else:
print(f"Failed to trim {os.path.basename(file_path)}")
except Exception as exc:
print(f"{os.path.basename(file_path)} generated an exception: {exc}")
# Step 3: Run optipng if level = 3 # Step 3: Run optipng if level = 3
if opt_level == OptimizationLevel.FULL: if opt_level == settings.OptimizationLevel.FULL:
if check_optipng_installed(): if check_optipng_installed():
print("\nOptimizing PNG files with optipng...") print("\nOptimizing PNG files with optipng...")
for i, file_path in enumerate(temp_files, 1): with concurrent.futures.ThreadPoolExecutor() as executor:
print(f"[{i}/{total_files}] Optimizing {os.path.basename(file_path)}...", end='', flush=True) futures = {executor.submit(run_optipng, file_path): file_path for file_path in temp_files}
if run_optipng(file_path): for future in concurrent.futures.as_completed(futures):
successful['optipng'] += 1 file_path = futures[future]
print(" ") try:
else: if future.result():
print(" ") successful['optipng'] += 1
print(f"Optimized {os.path.basename(file_path)}")
else:
print(f"Failed to optimize {os.path.basename(file_path)}")
except Exception as exc:
print(f"{os.path.basename(file_path)} generated an exception: {exc}")
else: else:
print("\nNote: optipng not found. Skipping PNG optimization.") print("\nNote: optipng not found. Skipping PNG optimization.")
# Print summary # Print summary
print("\nOptimization complete!") print("\nOptimization complete!")
print(f"Successfully trimmed: {successful['trim']}/{total_files} images") print(f"Successfully trimmed: {successful['trim']}/{total_files} images")
if int(opt_level) >= int(OptimizationLevel.MONOCHROME): if int(opt_level) >= int(settings.OptimizationLevel.MONOCHROME):
print(f"Successfully converted to monochrome: {successful['monochrome']}/{total_files} images") print(f"Successfully converted to monochrome: {successful['monochrome']}/{total_files} images")
if opt_level == OptimizationLevel.FULL and check_optipng_installed(): if opt_level == settings.OptimizationLevel.FULL and check_optipng_installed():
print(f"Successfully optimized with optipng: {successful['optipng']}/{total_files} images") print(f"Successfully optimized with optipng: {successful['optipng']}/{total_files} images")
@cli.command() @cli.command()
+6 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "notes-cleaner" name = "notes-cleaner"
version = "0.9rc2" version = "0.9rc3"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
license = "MIT" license = "MIT"
@@ -12,3 +12,8 @@ dependencies = [
"click>=8.1.7", "click>=8.1.7",
"pydantic>=2.10.3", "pydantic>=2.10.3",
] ]
[dependency-groups]
dev = [
"ruff>=0.8.1",
]
Generated
+34 -1
View File
@@ -33,19 +33,27 @@ wheels = [
[[package]] [[package]]
name = "notes-cleaner" name = "notes-cleaner"
version = "0.1.0" version = "0.9rc2"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "click" }, { name = "click" },
{ name = "pydantic" }, { name = "pydantic" },
] ]
[package.dev-dependencies]
dev = [
{ name = "ruff" },
]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "click", specifier = ">=8.1.7" }, { name = "click", specifier = ">=8.1.7" },
{ name = "pydantic", specifier = ">=2.10.3" }, { name = "pydantic", specifier = ">=2.10.3" },
] ]
[package.metadata.requires-dev]
dev = [{ name = "ruff", specifier = ">=0.8.1" }]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.10.3" version = "2.10.3"
@@ -99,6 +107,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 }, { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 },
] ]
[[package]]
name = "ruff"
version = "0.8.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/d0/8ff5b189d125f4260f2255d143bf2fa413b69c2610c405ace7a0a8ec81ec/ruff-0.8.1.tar.gz", hash = "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f", size = 3313222 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/d6/1a6314e568db88acdbb5121ed53e2c52cebf3720d3437a76f82f923bf171/ruff-0.8.1-py3-none-linux_armv6l.whl", hash = "sha256:fae0805bd514066f20309f6742f6ee7904a773eb9e6c17c45d6b1600ca65c9b5", size = 10532605 },
{ url = "https://files.pythonhosted.org/packages/89/a8/a957a8812e31facffb6a26a30be0b5b4af000a6e30c7d43a22a5232a3398/ruff-0.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8a4f7385c2285c30f34b200ca5511fcc865f17578383db154e098150ce0a087", size = 10278243 },
{ url = "https://files.pythonhosted.org/packages/a8/23/9db40fa19c453fabf94f7a35c61c58f20e8200b4734a20839515a19da790/ruff-0.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd054486da0c53e41e0086e1730eb77d1f698154f910e0cd9e0d64274979a209", size = 9917739 },
{ url = "https://files.pythonhosted.org/packages/e2/a0/6ee2d949835d5701d832fc5acd05c0bfdad5e89cfdd074a171411f5ccad5/ruff-0.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2029b8c22da147c50ae577e621a5bfbc5d1fed75d86af53643d7a7aee1d23871", size = 10779153 },
{ url = "https://files.pythonhosted.org/packages/7a/25/9c11dca9404ef1eb24833f780146236131a3c7941de394bc356912ef1041/ruff-0.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2666520828dee7dfc7e47ee4ea0d928f40de72056d929a7c5292d95071d881d1", size = 10304387 },
{ url = "https://files.pythonhosted.org/packages/c8/b9/84c323780db1b06feae603a707d82dbbd85955c8c917738571c65d7d5aff/ruff-0.8.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:333c57013ef8c97a53892aa56042831c372e0bb1785ab7026187b7abd0135ad5", size = 11360351 },
{ url = "https://files.pythonhosted.org/packages/6b/e1/9d4bbb2ace7aad14ded20e4674a48cda5b902aed7a1b14e6b028067060c4/ruff-0.8.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:288326162804f34088ac007139488dcb43de590a5ccfec3166396530b58fb89d", size = 12022879 },
{ url = "https://files.pythonhosted.org/packages/75/28/752ff6120c0e7f9981bc4bc275d540c7f36db1379ba9db9142f69c88db21/ruff-0.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b12c39b9448632284561cbf4191aa1b005882acbc81900ffa9f9f471c8ff7e26", size = 11610354 },
{ url = "https://files.pythonhosted.org/packages/ba/8c/967b61c2cc8ebd1df877607fbe462bc1e1220b4a30ae3352648aec8c24bd/ruff-0.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:364e6674450cbac8e998f7b30639040c99d81dfb5bbc6dfad69bc7a8f916b3d1", size = 12813976 },
{ url = "https://files.pythonhosted.org/packages/7f/29/e059f945d6bd2d90213387b8c360187f2fefc989ddcee6bbf3c241329b92/ruff-0.8.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c", size = 11154564 },
{ url = "https://files.pythonhosted.org/packages/55/47/cbd05e5a62f3fb4c072bc65c1e8fd709924cad1c7ec60a1000d1e4ee8307/ruff-0.8.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2f2f7a7e7648a2bfe6ead4e0a16745db956da0e3a231ad443d2a66a105c04fa", size = 10760604 },
{ url = "https://files.pythonhosted.org/packages/bb/ee/4c3981c47147c72647a198a94202633130cfda0fc95cd863a553b6f65c6a/ruff-0.8.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:adf314fc458374c25c5c4a4a9270c3e8a6a807b1bec018cfa2813d6546215540", size = 10391071 },
{ url = "https://files.pythonhosted.org/packages/6b/e6/083eb61300214590b188616a8ac6ae1ef5730a0974240fb4bec9c17de78b/ruff-0.8.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a885d68342a231b5ba4d30b8c6e1b1ee3a65cf37e3d29b3c74069cdf1ee1e3c9", size = 10896657 },
{ url = "https://files.pythonhosted.org/packages/77/bd/aacdb8285d10f1b943dbeb818968efca35459afc29f66ae3bd4596fbf954/ruff-0.8.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d2c16e3508c8cc73e96aa5127d0df8913d2290098f776416a4b157657bee44c5", size = 11228362 },
{ url = "https://files.pythonhosted.org/packages/39/72/fcb7ad41947f38b4eaa702aca0a361af0e9c2bf671d7fd964480670c297e/ruff-0.8.1-py3-none-win32.whl", hash = "sha256:93335cd7c0eaedb44882d75a7acb7df4b77cd7cd0d2255c93b28791716e81790", size = 8803476 },
{ url = "https://files.pythonhosted.org/packages/e4/ea/cae9aeb0f4822c44651c8407baacdb2e5b4dcd7b31a84e1c5df33aa2cc20/ruff-0.8.1-py3-none-win_amd64.whl", hash = "sha256:2954cdbe8dfd8ab359d4a30cd971b589d335a44d444b6ca2cb3d1da21b75e4b6", size = 9614463 },
{ url = "https://files.pythonhosted.org/packages/eb/76/fbb4bd23dfb48fa7758d35b744413b650a9fd2ddd93bca77e30376864414/ruff-0.8.1-py3-none-win_arm64.whl", hash = "sha256:55873cc1a473e5ac129d15eccb3c008c096b94809d693fc7053f588b67822737", size = 8959621 },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.12.2" version = "4.12.2"