diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index fe9b9ec..909c8d8 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -3,22 +3,25 @@ name: Build and Push Docker Image on: push: branches: - - main # Déclenche le workflow sur chaque push vers la branche "main" + - main # Déclenche sur chaque push vers la branche "test" pull_request: branches: - - main # Déclenche sur un PR vers "main" (si besoin) + - main # Déclenche sur un PR vers la branche "test" jobs: build: - runs-on: ubuntu-latest # Utilise une machine virtuelle Ubuntu pour construire l'image + runs-on: ubuntu-latest # Utilise une VM Ubuntu pour la tâche steps: - - name: Checkout the repository - uses: actions/checkout@v2 # Clone le code du repo + # Étape 1 : Cloner le dépôt + - name: Checkout repository + uses: actions/checkout@v3 + # Étape 2 : Configurer Docker Buildx - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 # Configure Buildx, l'outil pour construire l'image + uses: docker/setup-buildx-action@v2 + # Étape 3 : Cacher les couches Docker pour accélérer les builds - name: Cache Docker layers uses: actions/cache@v4 with: @@ -27,18 +30,22 @@ jobs: restore-keys: | ${{ runner.os }}-buildx- + # Étape 4 : Se connecter à Docker Hub - name: Log in to Docker Hub uses: docker/login-action@v2 with: - username: ${{ secrets.DOCKER_USERNAME }} # Nom d'utilisateur Docker Hub - password: ${{ secrets.DOCKER_PASSWORD }} # Mot de passe Docker Hub (stocké dans les secrets GitHub) + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + # Étape 5 : Construire et pousser l'image Docker - name: Build and push Docker image run: | - docker build -t tiritibambix/imaguick:latest . - docker push tiritibambix/imaguick:latest - # Construire et pousser l'image vers Docker Hub + IMAGE_NAME=tiritibambix/imaguick:latest + docker buildx build --cache-from=type=local,src=/tmp/.buildx-cache \ + --cache-to=type=local,dest=/tmp/.buildx-cache \ + --push \ + -t $IMAGE_NAME . + # Étape 6 : Se déconnecter de Docker Hub - name: Logout from Docker Hub - run: docker logout # Se déconnecter de Docker Hub après avoir poussé l'image -# + run: docker logout \ No newline at end of file diff --git a/app.py b/app.py index dfda833..119b553 100644 --- a/app.py +++ b/app.py @@ -1,4 +1,5 @@ -from flask import Flask, render_template, request, redirect, url_for, send_from_directory, flash + +from flask import Flask, render_template, request, redirect, url_for, send_file, flash, Response import os import subprocess from zipfile import ZipFile @@ -10,6 +11,12 @@ # Configuration UPLOAD_FOLDER = 'uploads' OUTPUT_FOLDER = 'output' +DEFAULTS = { + "quality": "100", + "width": "", + "height": "", + "percentage": "", +} os.makedirs(UPLOAD_FOLDER, exist_ok=True) os.makedirs(OUTPUT_FOLDER, exist_ok=True) @@ -18,87 +25,100 @@ app.config['OUTPUT_FOLDER'] = OUTPUT_FOLDER app.secret_key = 'supersecretkey' - def allowed_file(filename): """Allow all file types supported by ImageMagick.""" return '.' in filename - def get_image_dimensions(filepath): """Get image dimensions as (width, height).""" try: with Image.open(filepath) as img: return img.size # Returns (width, height) except Exception as e: - print(f"Error retrieving dimensions for {filepath}: {e}") + flash_error(f"Error retrieving dimensions for {filepath}: {e}") return None +def get_available_formats(): + """Get a list of supported formats from ImageMagick.""" + try: + result = subprocess.run(["/usr/local/bin/magick", "convert", "-list", "format"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=True) + formats = [] + for line in result.stdout.splitlines(): + parts = line.split() + if len(parts) > 1 and parts[1] in {"r", "rw", "rw+", "w"}: + format_name = parts[0].lower().rstrip('*') + formats.append(format_name) + return formats + except Exception as e: + flash_error(f"Error fetching formats: {e}") + return ["jpg", "png", "webp"] + +def flash_error(message): + """Flash error message and log if needed.""" + flash(message) + print(message) + +def build_imagemagick_command(filepath, output_path, width, height, percentage, quality, keep_ratio): + """Build ImageMagick command for resizing and formatting.""" + command = ["/usr/local/bin/magick", filepath] + if width.isdigit() and height.isdigit(): + resize_value = f"{width}x{height}" if keep_ratio else f"{width}x{height}!" + command.extend(["-resize", resize_value]) + elif percentage.isdigit() and 0 < int(percentage) <= 100: + command.extend(["-resize", f"{percentage}%"]) + if quality.isdigit() and 1 <= int(quality) <= 100: + command.extend(["-quality", quality]) + command.append(output_path) + return command @app.route('/') def index(): """Homepage with upload options.""" return render_template('index.html') - @app.route('/upload', methods=['POST']) def upload_file(): """Handle file uploads.""" - if 'file' not in request.files: - flash('No file selected.') - return redirect(url_for('index')) - - files = request.files.getlist('file') # Multiple files support - if not files or files[0].filename == '': - flash('No file selected.') - return redirect(url_for('index')) + files = request.files.getlist('file') + if not files or all(file.filename == '' for file in files): + return flash_error('No file selected.'), redirect(url_for('index')) uploaded_files = [] for file in files: if file and allowed_file(file.filename): - filename = secure_filename(file.filename) - filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) + filepath = os.path.join(app.config['UPLOAD_FOLDER'], secure_filename(file.filename)) file.save(filepath) - uploaded_files.append(filename) + uploaded_files.append(filepath) else: - flash(f"Unsupported file format for {file.filename}.") + flash_error(f"Unsupported file format for {file.filename}.") - # Redirect logic if len(uploaded_files) == 1: - return redirect(url_for('resize_options', filename=uploaded_files[0])) - elif len(uploaded_files) > 1: - return redirect(url_for('resize_batch_options', filenames=','.join(uploaded_files))) - else: - flash('No valid files uploaded.') - return redirect(url_for('index')) - + return redirect(url_for('resize_options', filename=os.path.basename(uploaded_files[0]))) + if len(uploaded_files) > 1: + return redirect(url_for('resize_batch_options', filenames=','.join(map(os.path.basename, uploaded_files)))) + return redirect(url_for('index')) @app.route('/upload_url', methods=['POST']) def upload_url(): """Handle image upload from a URL.""" url = request.form.get('url') if not url: - flash("No URL provided.") - return redirect(url_for('index')) + return flash_error("No URL provided."), redirect(url_for('index')) try: - # Set headers for the request - headers = { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" - } - - # Fetch the image from the URL + headers = {"User-Agent": "Mozilla/5.0"} response = requests.get(url, stream=True, headers=headers) - response.raise_for_status() # Raise error for bad status codes + response.raise_for_status() - # Extract the filename from the URL - filename = url.split("/")[-1].split("?")[0] # Remove query parameters + filename = url.split("/")[-1].split("?")[0] if not filename: - flash("Unable to determine a valid filename from the URL.") - return redirect(url_for('index')) + return flash_error("Unable to determine a valid filename from the URL."), redirect(url_for('index')) filepath = os.path.join(app.config['UPLOAD_FOLDER'], secure_filename(filename)) - - # Write the content to a file with open(filepath, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): f.write(chunk) @@ -106,121 +126,85 @@ def upload_url(): flash(f"Image downloaded successfully: {filename}") return redirect(url_for('resize_options', filename=filename)) except requests.exceptions.RequestException as e: - flash(f"Error downloading image: {e}") - return redirect(url_for('index')) - + return flash_error(f"Error downloading image: {e}"), redirect(url_for('index')) @app.route('/resize_options/') def resize_options(filename): """Resize options page for a single image.""" filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) - dimensions = get_image_dimensions(filepath) # Get real dimensions + dimensions = get_image_dimensions(filepath) if not dimensions: - flash("Unable to get image dimensions.") return redirect(url_for('index')) - formats = ['jpg', 'png', 'webp'] # Placeholder formats + formats = get_available_formats() width, height = dimensions return render_template('resize.html', filename=filename, width=width, height=height, formats=formats) - @app.route('/resize/', methods=['POST']) def resize_image(filename): """Handle resizing or format conversion for a single image.""" - quality = request.form.get('quality', '100') # Default quality is 100 - format_conversion = request.form.get('format', None) - keep_ratio = 'keep_ratio' in request.form # Checkbox for aspect ratio - width = request.form.get('width', '') - height = request.form.get('height', '') - percentage = request.form.get('percentage', '') - filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) name, ext = os.path.splitext(filename) - output_filename = f"{name}_rsz{ext}" # Add the `_rsz` suffix before extension + output_filename = f"{name}_rsz{ext}" + format_conversion = request.form.get('format', None) if format_conversion: - output_filename = f"{name}_rsz.{format_conversion.lower()}" # Apply format conversion + output_filename = f"{name}_rsz.{format_conversion.lower()}" output_path = os.path.join(app.config['OUTPUT_FOLDER'], output_filename) + command = build_imagemagick_command( + filepath=filepath, + output_path=output_path, + width=request.form.get('width', DEFAULTS["width"]), + height=request.form.get('height', DEFAULTS["height"]), + percentage=request.form.get('percentage', DEFAULTS["percentage"]), + quality=request.form.get('quality', DEFAULTS["quality"]), + keep_ratio='keep_ratio' in request.form + ) + try: - # Build the ImageMagick command - command = ["/usr/local/bin/magick", filepath] - - if width.isdigit() and height.isdigit(): - if keep_ratio: - resize_value = f"{width}x{height}" # Keep aspect ratio - else: - resize_value = f"{width}x{height}!" # Allow deformation - command.extend(["-resize", resize_value]) - elif percentage.isdigit() and 0 < int(percentage) <= 100: - resize_value = f"{percentage}%" - command.extend(["-resize", resize_value]) - - # Add quality if specified - if quality.isdigit() and 1 <= int(quality) <= 100: - command.extend(["-quality", quality]) - - # Output path - command.append(output_path) - - # Run the ImageMagick command subprocess.run(command, check=True) - flash(f'Image processed successfully: {output_filename}') return redirect(url_for('download', filename=output_filename)) except Exception as e: - flash(f"Error processing image: {e}") - return redirect(url_for('resize_options', filename=filename)) - + return flash_error(f"Error processing image: {e}"), redirect(url_for('resize_options', filename=filename)) @app.route('/resize_batch_options/') def resize_batch_options(filenames): """Resize options page for batch processing.""" files = filenames.split(',') - formats = ['jpg', 'png', 'webp'] # Placeholder formats + formats = get_available_formats() return render_template('resize_batch.html', files=files, formats=formats) - @app.route('/resize_batch', methods=['POST']) def resize_batch(): """Resize multiple images and compress them into a ZIP.""" filenames = request.form.get('filenames').split(',') - quality = request.form.get('quality', '100') - format_conversion = request.form.get('format', None) - keep_ratio = 'keep_ratio' in request.form - width = request.form.get('width', '') - height = request.form.get('height', '') - percentage = request.form.get('percentage', '') - output_files = [] + for filename in filenames: filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) name, ext = os.path.splitext(filename) output_filename = f"{name}_rsz{ext}" + format_conversion = request.form.get('format', None) if format_conversion: output_filename = f"{name}_rsz.{format_conversion.lower()}" output_path = os.path.join(app.config['OUTPUT_FOLDER'], output_filename) + command = build_imagemagick_command( + filepath=filepath, + output_path=output_path, + width=request.form.get('width', DEFAULTS["width"]), + height=request.form.get('height', DEFAULTS["height"]), + percentage=request.form.get('percentage', DEFAULTS["percentage"]), + quality=request.form.get('quality', DEFAULTS["quality"]), + keep_ratio='keep_ratio' in request.form + ) + try: - command = ["/usr/local/bin/magick", filepath] - - if width.isdigit() and height.isdigit(): - if keep_ratio: - resize_value = f"{width}x{height}" - else: - resize_value = f"{width}x{height}!" - command.extend(["-resize", resize_value]) - elif percentage.isdigit() and 0 < int(percentage) <= 100: - resize_value = f"{percentage}%" - command.extend(["-resize", resize_value]) - - if quality.isdigit() and 1 <= int(quality) <= 100: - command.extend(["-quality", quality]) - - command.append(output_path) subprocess.run(command, check=True) output_files.append(output_path) except Exception as e: - flash(f"Error processing {filename}: {e}") + flash_error(f"Error processing {filename}: {e}") if len(output_files) > 1: zip_suffix = datetime.now().strftime("%y%m%d-%H%M") @@ -233,21 +217,22 @@ def resize_batch(): elif len(output_files) == 1: return redirect(url_for('download', filename=os.path.basename(output_files[0]))) else: - flash("No images processed.") - return redirect(url_for('index')) - + return flash_error("No images processed."), redirect(url_for('index')) @app.route('/download_batch/') def download_batch(filename): """Serve the ZIP file for download.""" - return send_from_directory(app.config['OUTPUT_FOLDER'], filename, as_attachment=True) - + zip_path = os.path.join(app.config['OUTPUT_FOLDER'], filename) + return send_file(zip_path, as_attachment=True) @app.route('/download/') def download(filename): """Serve a single file for download.""" - return send_from_directory(app.config['OUTPUT_FOLDER'], filename, as_attachment=True) - + filepath = os.path.join(app.config['OUTPUT_FOLDER'], filename) + with open(filepath, 'rb') as f: + response = Response(f.read(), mimetype='application/octet-stream') + response.headers['Content-Disposition'] = f'attachment; filename="{filename}"' + return response if __name__ == '__main__': app.run(host='0.0.0.0', port=5000) \ No newline at end of file