Skip to content

Commit 85a0f62

Browse files
committed
feat: initial release of image-optimizer-cli with parallel processing and strict typing
0 parents  commit 85a0f62

13 files changed

Lines changed: 745 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# .github/workflows/ci.yml
2+
name: CI - Type Checking
3+
4+
on:
5+
push:
6+
branches: [ main, master ]
7+
pull_request:
8+
branches: [ main, master ]
9+
10+
jobs:
11+
lint:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Install uv
17+
uses: astral-sh/setup-uv@v3
18+
with:
19+
version: "latest"
20+
enable-cache: true
21+
22+
- name: Set up Python
23+
run: uv python install 3.12
24+
25+
- name: Install dependencies
26+
run: uv sync --all-extras --dev
27+
28+
- name: Run Mypy
29+
run: uv run mypy --strict --explicit-package-bases src

.gitignore

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*.class
5+
*.so
6+
.Python
7+
env/
8+
build/
9+
develop-eggs/
10+
dist/
11+
downloads/
12+
eggs/
13+
.eggs/
14+
lib/
15+
lib64/
16+
parts/
17+
sdist/
18+
var/
19+
wheels/
20+
*.egg-info/
21+
.installed.cfg
22+
*.egg
23+
24+
# Virtual Environments (uv / venv)
25+
.venv/
26+
venv/
27+
ENV/
28+
29+
# Mypy & Linting
30+
.mypy_cache/
31+
.dmypy.json
32+
dmypy.json
33+
.ruff_cache/
34+
35+
# Environment Variables
36+
.env
37+
.venv
38+
.env.local
39+
40+
# OS Files
41+
.DS_Store
42+
Thumbs.db
43+
44+
# Project Specific
45+
assets_output/
46+
.idea/
47+
.vscode/

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12

README.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
## 1. Automação de CI (GitHub Actions)
2+
3+
Este arquivo garantirá que qualquer contribuição futura ou alteração sua mantenha esse padrão de "Zero Erros" de tipagem.
4+
5+
6+
## 🧑‍💻 Stack de desenvolvimento
7+
8+
![Python](https://img.shields.io/badge/python-3.12+-3776AB?style=for-the-badge&logo=python&logoColor=white)
9+
![uv](https://img.shields.io/badge/managed%20by-uv-de5fe9?style=for-the-badge&logo=astral&logoColor=white)
10+
![Mypy](https://img.shields.io/badge/types-strict-blue?style=for-the-badge&logo=python&logoColor=white)
11+
12+
![Github Repo Size](https://img.shields.io/github/repo-size/LeonardoFirme/image-optimizer-cli?style=for-the-badge&logo=github&color=000000)
13+
![Github License](https://img.shields.io/github/license/LeonardoFirme/image-optimizer-cli?style=for-the-badge&logo=github&color=000000)
14+
![Mypy Checked](https://img.shields.io/badge/mypy-checked-2ca447?style=for-the-badge&logo=python&logoColor=white)
15+
16+
> Ferramenta profissional de otimização de assets em paralelo, desenvolvida com tipagem estrita para máxima confiabilidade.
17+
18+
---
19+
20+
```yaml
21+
# .github/workflows/ci.yml
22+
name: CI - Type Checking
23+
24+
on:
25+
push:
26+
branches: [ main, master ]
27+
pull_request:
28+
branches: [ main, master ]
29+
30+
jobs:
31+
lint:
32+
runs-on: ubuntu-latest
33+
steps:
34+
- uses: actions/checkout@v4
35+
36+
- name: Install uv
37+
uses: astral-sh/setup-uv@v3
38+
with:
39+
version: "latest"
40+
enable-cache: true
41+
42+
- name: Set up Python
43+
run: uv python install 3.12
44+
45+
- name: Install dependencies
46+
run: uv sync --all-extras --dev
47+
48+
- name: Run Mypy
49+
run: uv run mypy --strict --explicit-package-bases src
50+
51+
```
52+
53+
---
54+
55+
## 2. README.md Profissional
56+
57+
Um repositório útil para outros devs precisa "vender" o problema que ele resolve. Como você foca em **Next.js/React**, o foco aqui é a performance de carregamento (LCP).
58+
59+
60+
# ⚡ v0 Image Optimizer CLI
61+
62+
Uma ferramenta de linha de comando de alta performance para otimização em lote de assets visuais, projetada especificamente para fluxos de trabalho Web (Next.js, React, Laravel).
63+
64+
## 🚀 Por que usar?
65+
66+
Em projetos modernos, o **LCP (Largest Contentful Paint)** é crucial. Esta ferramenta automatiza a conversão e compressão de imagens utilizando **processamento paralelo**, garantindo que seus assets pesem o mínimo possível sem perda de qualidade perceptível.
67+
68+
## 🛠️ Diferenciais Técnicos
69+
70+
* **Multithreading Real:** Utiliza `ProcessPoolExecutor` para contornar o GIL do Python e usar todos os núcleos do seu processador (X99/Xeon/M1/M2).
71+
* **Tipagem Estrita:** 100% validado com `mypy --strict` para garantir robustez e previsibilidade.
72+
* **Modern Stack:** Gerenciado via `uv` (Rust-based python manager) para instalações instantâneas.
73+
* **Relatórios Ricos:** Interface visual via terminal com `rich` e `typer`.
74+
75+
## 📦 Instalação
76+
77+
```bash
78+
# Clone o projeto
79+
git clone [https://github.com/LeonardoFirme/image-optimizer-cli.git](https://github.com/LeonardoFirme/image-optimizer-cli.git)
80+
cd image-optimizer-cli
81+
82+
```
83+
84+
```bash
85+
# Instale as dependências (requer uv instalado)
86+
uv sync
87+
88+
```
89+
90+
## 💻 Como usar
91+
92+
```bash
93+
uv run src/main.py --input ./public/assets/raw --output ./public/assets/optimized --format WEBP
94+
```
95+
96+
97+
### Argumentos:
98+
99+
* `-i, --input`: Diretório contendo JPG/PNG originais.
100+
* `-o, --output`: Diretório de destino para os assets otimizados.
101+
* `-f, --format`: Formato de saída (`WEBP`, `AVIF`, `PNG`, `JPEG`). Padrão: `WEBP`.
102+
103+
## 📊 Performance
104+
105+
Em nossos benchmarks, conseguimos reduções de até **80% no tamanho do arquivo** original, processando centenas de imagens em segundos graças à arquitetura paralela.
106+
107+
---
108+
109+
Desenvolvido por **Leonardo Firme** | [LeonardoFirme](https://github.com/LeonardoFirme)
110+
111+
---

assets_input/teste.png

7.17 KB
Loading

main.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# src/main.py
2+
import typer
3+
from pathlib import Path
4+
from rich.console import Console
5+
from rich.table import Table
6+
from src.core.processor import ImageProcessor
7+
from src.core.schemas import SupportedFormat # Importação vital para o Mypy
8+
9+
app = typer.Typer(help="CLI Profissional para Otimização de Assets")
10+
console = Console()
11+
12+
@app.command()
13+
def optimize(
14+
input_dir: Path = typer.Option(..., "--input", "-i", help="Diretório de origem"),
15+
output_dir: Path = typer.Option(..., "--output", "-o", help="Diretório de destino"),
16+
# Alteramos de 'str' para 'SupportedFormat' para satisfazer o Mypy e o Typer
17+
format: SupportedFormat = typer.Option("WEBP", "--format", "-f", help="Formato de saída")
18+
) -> None:
19+
"""
20+
Escaneia o diretório e otimiza assets em paralelo usando múltiplos cores.
21+
"""
22+
if not input_dir.exists():
23+
console.print(f"[bold red]Erro:[/] Diretório '{input_dir}' não encontrado.")
24+
raise typer.Exit(code=1)
25+
26+
if not output_dir.exists():
27+
output_dir.mkdir(parents=True)
28+
29+
valid_extensions: list[str] = [".jpg", ".jpeg", ".png"]
30+
files_to_process = [
31+
f for f in input_dir.iterdir()
32+
if f.suffix.lower() in valid_extensions
33+
]
34+
35+
if not files_to_process:
36+
console.print("[yellow]Nenhum asset compatível encontrado.[/]")
37+
return
38+
39+
table = Table(title="Relatório de Otimização", border_style="bright_blue")
40+
table.add_column("Arquivo", style="cyan")
41+
table.add_column("Original", style="magenta")
42+
table.add_column("Otimizado", style="green")
43+
table.add_column("Economia", style="bold green")
44+
45+
with console.status(f"[bold green]Processando {len(files_to_process)} imagens...") as status:
46+
# Agora o Mypy aceita pois 'format' é do tipo SupportedFormat (Literal)
47+
results = ImageProcessor.process_all(files_to_process, output_dir, target_format=format)
48+
49+
for res in results:
50+
table.add_row(
51+
res["filename"],
52+
f"{res['original_size'] / 1024:.1f} KB",
53+
f"{res['optimized_size'] / 1024:.1f} KB",
54+
f"{res['ratio']:.2f}%"
55+
)
56+
57+
console.print(table)
58+
console.print(f"\n[bold green]✔[/] Sucesso! Processo finalizado.")
59+
60+
if __name__ == "__main__":
61+
app()

pyproject.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# pyproject.toml
2+
[project]
3+
name = "v0-image-optimizer"
4+
version = "1.0.0"
5+
description = "High-performance CLI for WebP conversion and image optimization"
6+
authors = [{name = "Leonardo Firme", email = "seu-email@exemplo.com"}]
7+
requires-python = ">=3.12"
8+
dependencies = [
9+
"Pillow>=10.2.0",
10+
"typer>=0.9.0",
11+
"rich>=13.7.0",
12+
]
13+
14+
[build-system]
15+
requires = ["hatchling"]
16+
build-backend = "hatchling.build"
17+
18+
[tool.hatch.build.targets.wheel]
19+
packages = ["src"]
20+
21+
[dependency-groups]
22+
dev = [
23+
"mypy>=1.19.1",
24+
"types-pillow>=10.2.0.20240822",
25+
]

src/core/processor.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# src/core/processor.py
2+
import os
3+
from pathlib import Path
4+
from concurrent.futures import ProcessPoolExecutor
5+
from typing import List
6+
from PIL import Image
7+
from src.core.schemas import SupportedFormat, OptimizationResult, DEFAULT_CONFIG
8+
9+
class ImageProcessor:
10+
@staticmethod
11+
def process_single_image(
12+
input_path: Path,
13+
output_dir: Path,
14+
target_format: SupportedFormat = "WEBP"
15+
) -> OptimizationResult:
16+
"""
17+
Lógica de processamento isolada para execução em paralelo.
18+
"""
19+
original_size: int = input_path.stat().st_size
20+
output_path: Path = output_dir / f"{input_path.stem}.{target_format.lower()}"
21+
22+
with Image.open(input_path) as img:
23+
# Garante compatibilidade de cores (RGB) para formatos que não suportam transparência
24+
if img.mode in ("RGBA", "P") and target_format not in ["WEBP", "PNG"]:
25+
img = img.convert("RGB")
26+
27+
img.save(
28+
output_path,
29+
format=target_format,
30+
**DEFAULT_CONFIG
31+
)
32+
33+
optimized_size: int = output_path.stat().st_size
34+
saved: int = original_size - optimized_size
35+
36+
return {
37+
"filename": input_path.name,
38+
"original_size": original_size,
39+
"optimized_size": optimized_size,
40+
"saved_bytes": saved,
41+
"ratio": (saved / original_size) * 100 if original_size > 0 else 0
42+
}
43+
44+
@classmethod
45+
def process_all(
46+
cls,
47+
files: List[Path],
48+
output_dir: Path,
49+
target_format: SupportedFormat = "WEBP"
50+
) -> List[OptimizationResult]:
51+
"""
52+
Gerencia o pool de processos para alta performance aproveitando múltiplos cores.
53+
"""
54+
with ProcessPoolExecutor() as executor:
55+
futures = [
56+
executor.submit(cls.process_single_image, f, output_dir, target_format)
57+
for f in files
58+
]
59+
return [future.result() for future in futures]

src/core/schemas.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# src/core/types.py
2+
from typing import TypedDict, Literal, Final
3+
4+
# Formatos de saída suportados para garantir profissionalismo exato
5+
SupportedFormat = Literal["WEBP", "AVIF", "PNG", "JPEG"]
6+
7+
class OptimizationResult(TypedDict):
8+
filename: str
9+
original_size: int
10+
optimized_size: int
11+
saved_bytes: int
12+
ratio: float
13+
14+
# Configurações padrão de compressão (Pillow)
15+
class CompressionConfig(TypedDict):
16+
quality: int
17+
optimize: bool
18+
lossless: bool
19+
20+
DEFAULT_CONFIG: Final[CompressionConfig] = {
21+
"quality": 80,
22+
"optimize": True,
23+
"lossless": False
24+
}

0 commit comments

Comments
 (0)