Software y sistemas

Sistema web para poner marca de agua con PHP y JS

Hoy te voy a presentar un software gratuito y open source para poner marcas de agua a una imagen. Lo que tienes que hacer es simplemente seleccionar la imagen, la marca de agua y listo.

Además, puedes ajustar la opacidad y la separación entre marcas de agua. Por otro lado, si no te parece algún aspecto, puedes modificar el código fuente pues es totalmente open source.

He escrito este programa en PHP y JavaScript. Básicamente el procesamiento lo hace PHP, y JS solo es el frontend.

Sé bien que esto se podría hacer con JavaScript para no cargar al servidor, pero a mí me funciona así y por eso es que lo hice de esa manera. A lo largo de este post te mostraré todos los detalles sobre este programa.

Marca de agua con PHP y JavaScript

El programa es muy simple. Seleccionamos las dos imágenes y ajustamos los parámetros. Por cierto, se ponen varias marcas de agua que cubren toda la imagen, contrario a lo que hice con WaterPy en donde una sola marca de agua se coloca en alguna posición.

Programa para colocar marca de agua con PHP y JavaScript

Después de eso la imagen se puede descargar. Y todo eso se va mostrando en tiempo real, así que siéntete libre de probar con varios estilos y marcas de agua.

Apartado técnico

En el lado del servidor se utiliza PHP con las funciones de la librería GD. Esto es una mejora a un post que ya había hecho anteriormente, solo que ahora le he colocado una interfaz además de brindar la posibilidad de cambiar la opacidad de la marca de agua.

Recuerda habilitar la librería GD en caso de que utilices este proyecto.

Funciones útiles

Yo utilizo dos funciones, la primera es para cambiar la opacidad y la segunda es para colocar la marca de agua. Solo estoy usando funciones de la biblioteca GD tales como imagepng, imagejpeg, etcétera.

Cabe mencionar que la función que coloca la marca de agua no regresa nada, en su lugar simplemente muestra la imagen, es decir, la devuelve al navegador o al cliente.

<?php
/*
 * @param resource $imageSrc Image resource. Not being modified.
 * @param float $opacity Opacity to set from 0 (fully transparent) to 1 (no change)
 * @return resource Transparent image resource
 */function imagesetopacity($imageSrc, $opacity)
{
    $width  = imagesx($imageSrc);
    $height = imagesy($imageSrc);

    // Duplicate image and convert to TrueColor
    $imageDst = imagecreatetruecolor($width, $height);
    imagealphablending($imageDst, false);
    imagefill($imageDst, 0, 0, imagecolortransparent($imageDst));
    imagecopy($imageDst, $imageSrc, 0, 0, 0, 0, $width, $height);

    // Set new opacity to each pixel
    for ($x = 0; $x < $width; ++$x)
        for ($y = 0; $y < $height; ++$y) {
            $pixelColor = imagecolorat($imageDst, $x, $y);
            $pixelOpacity = 127 - (($pixelColor >> 24) & 0xFF);
            if ($pixelOpacity > 0) {
                $pixelOpacity = $pixelOpacity * $opacity;
                $pixelColor = ($pixelColor & 0xFFFFFF) | ((int)round(127 - $pixelOpacity) << 24);
                imagesetpixel($imageDst, $x, $y, $pixelColor);
            }
        }

    return $imageDst;
}

function ponerMarcaDeAgua($rutaImagenOriginal, $rutaMarcaDeAgua, $separacionPixeles, $opacidad)
{

    $imagenEsPng = false;
    $marcaEsPng = false;
    if (mime_content_type($rutaImagenOriginal) === "image/png") {
        $imagenEsPng = true;
    }
    if (mime_content_type($rutaMarcaDeAgua) === "image/png") {
        $marcaEsPng = true;
    }
    $original = $imagenEsPng ? imagecreatefrompng($rutaImagenOriginal) : imagecreatefromjpeg($rutaImagenOriginal);
    $marcaDeAgua = $marcaEsPng ?  imagecreatefrompng($rutaMarcaDeAgua) : imagecreatefromjpeg($rutaMarcaDeAgua);
    $marcaDeAgua = imagesetopacity($marcaDeAgua, $opacidad);


    # Como vamos a centrar  necesitamos sacar antes las anchuras y alturas
    $anchuraOriginal = imagesx($original);
    $alturaOriginal = imagesy($original);
    $alturaMarcaDeAgua = imagesy($marcaDeAgua);
    $anchuraMarcaDeAgua = imagesx($marcaDeAgua);

    # Desde dónde comenzar a cortar la marca de agua (si son 0, se comienza desde el inicio)
    for ($fila = 0; $fila < $alturaOriginal; $fila += $alturaMarcaDeAgua + $separacionPixeles) {
        for ($columna = 0; $columna < $anchuraOriginal; $columna += $anchuraMarcaDeAgua + $separacionPixeles) {

            imagecopy(
                $original,
                $marcaDeAgua,

                $columna,
                $fila,
                # No modificar, creo...
                0,
                0,
                $anchuraMarcaDeAgua,
                $alturaMarcaDeAgua
            );
        }
    }

    imagealphablending($original, false);
    imagesavealpha($original, true);
    header("Content-Type: " . mime_content_type($rutaImagenOriginal));
    if ($imagenEsPng) {
        imagepng($original);
    } else {
        imagejpeg($original);
    }
    imagedestroy($original);
    imagedestroy($marcaDeAgua);
}

 

API para colocar imagen

Ahora simplemente exponemos las funciones anteriores a través de PHP. Vamos a leer los archivos de $_FILES y luego mostrar la salida al cliente:

<?php
include_once "funciones.php";
$imagen = $_FILES["imagen"]["tmp_name"];
$marca = $_FILES["marca"]["tmp_name"];
$opacidad = floatval($_POST["opacidad"]);
$separacion = intval($_POST["separacion"]);
ponerMarcaDeAgua($imagen, $marca, $separacion, $opacidad);

Lado del cliente

En el lado del cliente es en donde ajustamos los parámetros como la opacidad y la separación de las marcas de agua. También verificamos el cambio de los input para generar la nueva imagen.

El código JavaScript queda así:

const $imagenSeleccionada = document.querySelector("#imagen"),
    $marcaSeleccionada = document.querySelector("#marca"),
    $imagen = document.querySelector("#imagenResultado"),
    $opacidad = document.querySelector("#opacidad"),
    $separacion = document.querySelector("#separacion"),
    $botonDescargar = document.querySelector("#botonDescargar"),
    $nombreImagen = document.querySelector("#nombreImagen"),
    $nombreMarca = document.querySelector("#nombreMarca");

let ultimoUrl, ultimoNombre; // Para cuando se descarga la imagen
// Desencadenada cuando se cambia la imagen o la marca de agua
const onCambioDeArchivo = async () => {
    if ($imagenSeleccionada.files.length != 1 || $marcaSeleccionada.files.length != 1) {
        return;
    }
    const fd = new FormData();
    fd.append("imagen", $imagenSeleccionada.files[0]);
    fd.append("marca", $marcaSeleccionada.files[0]);
    fd.append("opacidad", parseFloat($opacidad.value));
    fd.append("separacion", parseInt($separacion.value));
    const respuesta = await fetch("./api.php", {
        method: "POST",
        body: fd,
    });
    const imagenBlob = await respuesta.blob();
    const objectUrl = URL.createObjectURL(imagenBlob);
    ultimoUrl = objectUrl;
    $imagen.src = objectUrl;
}
$imagenSeleccionada.onchange = () => {
    if ($imagenSeleccionada.files.length != 1) {
        return;
    }
    const nombreArchivo = $imagenSeleccionada.files[0].name;
    ultimoNombre = nombreArchivo;
    $nombreImagen.innerHTML = nombreArchivo;
    onCambioDeArchivo();
}
$marcaSeleccionada.onchange = () => {
    if ($marcaSeleccionada.files.length != 1) {
        return;
    }
    const nombreArchivo = $marcaSeleccionada.files[0].name;
    $nombreMarca.innerHTML = nombreArchivo;
    onCambioDeArchivo();
}
[$opacidad, $separacion].forEach($elemento => $elemento.onchange = onCambioDeArchivo);


const descargar = () => {
    if (!ultimoUrl) {
        return;
    }
    const a = document.createElement("a");
    a.href = ultimoUrl;
    a.download = ultimoNombre;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
}
$botonDescargar.onclick = descargar;

Además se hacen ciertos ajustes para obtener la imagen como un BLOB usando fetch y colocarla en una etiqueta img, del mismo modo que agregamos un enlace temporal para cuando se descarga la imagen.

El código HTML completo junto con el script queda así:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Marca de agua</title>
    <link rel="stylesheet" href="./bulma.min.css">
</head>

<body>
    <section class="section">
        <div class="columns">
            <div class="column has-text-centered">
                <h2 class="is-size-3">Colocar marca de agua</h2>
            </div>
        </div>
        <div class="columns">
            <div class="column is-one-fifth">
                <strong>Imagen</strong>
                <div class="file has-name is-fullwidth">
                    <label class="file-label">
                        <input class="file-input" type="file" accept="image/png,image/jpeg" id="imagen">
                        <span class="file-cta">
                            <span class="file-label">
                                Seleccione
                            </span>
                        </span>
                        <span class="file-name" id="nombreImagen">
                        </span>
                    </label>
                </div>
                <strong>Marca de agua</strong>
                <div class="file has-name is-fullwidth">
                    <label class="file-label">
                        <input class="file-input" type="file" accept="image/png,image/jpeg" id="marca">
                        <span class="file-cta">
                            <span class="file-label">
                                Seleccione
                            </span>
                        </span>
                        <span class="file-name" id="nombreMarca">

                        </span>
                    </label>
                </div>
                <div class="field">
                    <label class="label">Opacidad</label>
                    <div class="control">
                        <input id="opacidad" type="range" min="0.1" max="1.0" step="0.05">
                    </div>
                </div>
                <div class="field">
                    <label class="label">Separación entre cada marca</label>
                    <div class="control">
                        <input id="separacion" type="range" min="1" max="500">
                    </div>
                </div>
                <button class="button is-success" id="botonDescargar">Descargar</button>
            </div>
            <div class="column has-text-centered">
                <img id="imagenResultado" src="" alt="">
            </div>
        </div>
        <div class="columns">
            <div class="column has-text-centered">
                <a href="https://parzibyte.me/blog">By Parzibyte</a>
            </div>
        </div>
    </section>
    <script>
        const $imagenSeleccionada = document.querySelector("#imagen"),
            $marcaSeleccionada = document.querySelector("#marca"),
            $imagen = document.querySelector("#imagenResultado"),
            $opacidad = document.querySelector("#opacidad"),
            $separacion = document.querySelector("#separacion"),
            $botonDescargar = document.querySelector("#botonDescargar"),
            $nombreImagen = document.querySelector("#nombreImagen"),
            $nombreMarca = document.querySelector("#nombreMarca");

        let ultimoUrl, ultimoNombre; // Para cuando se descarga la imagen
        // Desencadenada cuando se cambia la imagen o la marca de agua
        const onCambioDeArchivo = async () => {
            if ($imagenSeleccionada.files.length != 1 || $marcaSeleccionada.files.length != 1) {
                return;
            }
            const fd = new FormData();
            fd.append("imagen", $imagenSeleccionada.files[0]);
            fd.append("marca", $marcaSeleccionada.files[0]);
            fd.append("opacidad", parseFloat($opacidad.value));
            fd.append("separacion", parseInt($separacion.value));
            const respuesta = await fetch("./api.php", {
                method: "POST",
                body: fd,
            });
            const imagenBlob = await respuesta.blob();
            const objectUrl = URL.createObjectURL(imagenBlob);
            ultimoUrl = objectUrl;
            $imagen.src = objectUrl;
        }
        $imagenSeleccionada.onchange = () => {
            if ($imagenSeleccionada.files.length != 1) {
                return;
            }
            const nombreArchivo = $imagenSeleccionada.files[0].name;
            ultimoNombre = nombreArchivo;
            $nombreImagen.innerHTML = nombreArchivo;
            onCambioDeArchivo();
        }
        $marcaSeleccionada.onchange = () => {
            if ($marcaSeleccionada.files.length != 1) {
                return;
            }
            const nombreArchivo = $marcaSeleccionada.files[0].name;
            $nombreMarca.innerHTML = nombreArchivo;
            onCambioDeArchivo();
        }
        [$opacidad, $separacion].forEach($elemento => $elemento.onchange = onCambioDeArchivo);


        const descargar = () => {
            if (!ultimoUrl) {
                return;
            }
            const a = document.createElement("a");
            a.href = ultimoUrl;
            a.download = ultimoNombre;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
        }
        $botonDescargar.onclick = descargar;
    </script>
</body>

</html>

Y así es como podemos agregar fácilmente marcas de agua con JavaScript y PHP. Este programa soporta imágenes JPG así como PNG, puedes probarlo y verás que funciona.

Además, al cambiar cualquier valor, la imagen se refresca de manera inmediata.

Poniendo todo junto

Te dejo el código completo en mi GitHub, solo necesitas contar con Apache, PHP y la librería GD instalada y habilitada.

Recuerda que puedes usar esto de manera local o subirlo a un servidor de internet, aunque recuerda que el trabajo con imágenes siempre es algo pesado, en especial cuando son varios clientes.

Por otro lado te invito a leer más sobre PHP y JavaScript en mi blog.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Ver comentarios

Entradas recientes

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

3 días hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

3 días hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

3 días hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

3 días hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

3 días hace

Solución: Apache – Server unable to read htaccess file

Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…

4 días hace

Esta web usa cookies.