• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Step-by-Step Guide to building a custom bulk image watermarker block for Gutenberg using HTMX dynamic attributes

Step-by-Step Guide to building a custom bulk image watermarker block for Gutenberg using HTMX dynamic attributes

Setting Up the WordPress Plugin Environment

We’ll begin by establishing the foundational structure for our custom Gutenberg block plugin. This involves creating a standard WordPress plugin directory and the main plugin file. For this example, we’ll name our plugin bulk-image-watermarker.

Create a new directory named bulk-image-watermarker within your WordPress installation’s wp-content/plugins/ directory. Inside this new directory, create a PHP file named bulk-image-watermarker.php.

Populate bulk-image-watermarker.php with the standard WordPress plugin header information. This header is crucial for WordPress to recognize and list your plugin.

<?php
/**
 * Plugin Name: Bulk Image Watermarker
 * Plugin URI: https://example.com/plugins/bulk-image-watermarker/
 * Description: A custom Gutenberg block for bulk watermarking images with HTMX dynamic attributes.
 * Version: 1.0.0
 * Author: Your Name
 * Author URI: https://yourwebsite.com/
 * License: GPL-2.0+
 * License URI: http://www.gnu.org/licenses/gpl-2.0.txt
 * Text Domain: bulk-image-watermarker
 * Domain Path: /languages
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Enqueue Gutenberg block assets.
 */
function biw_enqueue_block_assets() {
    // Enqueue the block editor script.
    wp_enqueue_script(
        'bulk-image-watermarker-editor-script',
        plugins_url( 'build/index.js', __FILE__ ),
        array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'htmx' ), // 'htmx' dependency will be handled later
        filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
    );

    // Enqueue the block editor styles.
    wp_enqueue_style(
        'bulk-image-watermarker-editor-style',
        plugins_url( 'build/index.css', __FILE__ ),
        array( 'wp-edit-blocks' ),
        filemtime( plugin_dir_path( __FILE__ ) . 'build/index.css' )
    );

    // Enqueue the frontend styles.
    wp_enqueue_style(
        'bulk-image-watermarker-frontend-style',
        plugins_url( 'build/style-index.css', __FILE__ ),
        array(),
        filemtime( plugin_dir_path( __FILE__ ) . 'build/style-index.css' )
    );

    // Localize script for dynamic attributes and AJAX URL.
    wp_localize_script(
        'bulk-image-watermarker-editor-script',
        'biw_ajax_object',
        array(
            'ajax_url' => admin_url( 'admin-ajax.php' ),
            'nonce'    => wp_create_nonce( 'biw_watermark_nonce' ),
        )
    );
}
add_action( 'enqueue_block_editor_assets', 'biw_enqueue_block_assets' );

/**
 * Register the custom block.
 */
function biw_register_block() {
    register_block_type( 'bulk-image-watermarker/block', array(
        'editor_script' => 'bulk-image-watermarker-editor-script',
        'editor_style'  => 'bulk-image-watermarker-editor-style',
        'style'         => 'bulk-image-watermarker-frontend-style',
        'attributes'    => array(
            'watermarkImageId' => array(
                'type'    => 'number',
                'default' => 0,
            ),
            'watermarkImageUrl' => array(
                'type'    => 'string',
                'default' => '',
            ),
            'opacity' => array(
                'type'    => 'number',
                'default' => 0.7,
            ),
            'position' => array(
                'type'    => 'string',
                'default' => 'center',
            ),
            'scale' => array(
                'type'    => 'number',
                'default' => 0.2,
            ),
            'margin' => array(
                'type'    => 'number',
                'default' => 10,
            ),
            'selectedImageIds' => array(
                'type'    => 'array',
                'default' => array(),
                'items'   => array( 'type' => 'number' ),
            ),
            'watermarkedImageIds' => array(
                'type'    => 'array',
                'default' => array(),
                'items'   => array( 'type' => 'number' ),
            ),
        ),
    ) );
}
add_action( 'init', 'biw_register_block' );

/**
 * AJAX handler for watermarking images.
 */
function biw_ajax_watermark_images() {
    check_ajax_referer( 'biw_watermark_nonce', 'nonce' );

    if ( ! current_user_can( 'upload_files' ) ) {
        wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'bulk-image-watermarker' ) ) );
    }

    $selected_image_ids = isset( $_POST['image_ids'] ) ? array_map( 'intval', $_POST['image_ids'] ) : array();
    $watermark_image_id = isset( $_POST['watermark_image_id'] ) ? intval( $_POST['watermark_image_id'] ) : 0;
    $opacity            = isset( $_POST['opacity'] ) ? floatval( $_POST['opacity'] ) : 0.7;
    $position           = isset( $_POST['position'] ) ? sanitize_text_field( $_POST['position'] ) : 'center';
    $scale              = isset( $_POST['scale'] ) ? floatval( $_POST['scale'] ) : 0.2;
    $margin             = isset( $_POST['margin'] ) ? intval( $_POST['margin'] ) : 10;

    if ( empty( $selected_image_ids ) ) {
        wp_send_json_error( array( 'message' => __( 'No images selected for watermarking.', 'bulk-image-watermarker' ) ) );
    }

    if ( $watermark_image_id === 0 ) {
        wp_send_json_error( array( 'message' => __( 'No watermark image selected.', 'bulk-image-watermarker' ) ) );
    }

    $watermark_image_path = get_attached_file( $watermark_image_id );
    if ( ! $watermark_image_path || ! file_exists( $watermark_image_path ) ) {
        wp_send_json_error( array( 'message' => __( 'Watermark image not found.', 'bulk-image-watermarker' ) ) );
    }

    $watermarked_image_ids = array();
    $errors                = array();

    foreach ( $selected_image_ids as $image_id ) {
        $image_path = get_attached_file( $image_id );
        if ( ! $image_path || ! file_exists( $image_path ) ) {
            $errors[] = sprintf( __( 'Image ID %d not found.', 'bulk-image-watermarker' ), $image_id );
            continue;
        }

        // Attempt to watermark the image.
        $new_image_path = biw_apply_watermark( $image_path, $watermark_image_path, $opacity, $position, $scale, $margin );

        if ( is_wp_error( $new_image_path ) ) {
            $errors[] = sprintf( __( 'Error watermarking image ID %d: %s', 'bulk-image-watermarker' ), $image_id, $new_image_path->get_error_message() );
            continue;
        }

        if ( $new_image_path ) {
            // Create a new attachment for the watermarked image.
            $attachment = array(
                'guid'           => wp_upload_dir()['url'] . '/' . basename( $new_image_path ),
                'post_mime_type' => get_post_mime_type( $image_id ),
                'post_title'     => preg_replace( '/\.[^.]+$/', '', basename( $new_image_path ) ),
                'post_content'   => '',
                'post_status'    => 'inherit',
            );

            $new_attachment_id = wp_insert_attachment( $attachment, $new_image_path, $image_id ); // Parent to original image

            if ( ! is_wp_error( $new_attachment_id ) ) {
                wp_update_attachment_metadata( $new_attachment_id, wp_generate_attachment_metadata( $new_attachment_id, $new_image_path ) );
                $watermarked_image_ids[] = $new_attachment_id;
            } else {
                $errors[] = sprintf( __( 'Failed to create attachment for watermarked image ID %d: %s', 'bulk-image-watermarker' ), $image_id, $new_attachment_id->get_error_message() );
            }
        } else {
            $errors[] = sprintf( __( 'Unknown error processing image ID %d.', 'bulk-image-watermarker' ), $image_id );
        }
    }

    if ( ! empty( $errors ) ) {
        wp_send_json_error( array( 'message' => implode( '
', $errors ), 'watermarked_ids' => $watermarked_image_ids ) ); } else { wp_send_json_success( array( 'message' => __( 'Images watermarked successfully!', 'bulk-image-watermarker' ), 'watermarked_ids' => $watermarked_image_ids ) ); } } add_action( 'wp_ajax_biw_watermark_images', 'biw_ajax_watermark_images' ); add_action( 'wp_ajax_nopriv_biw_watermark_images', 'biw_ajax_watermark_images' ); // For logged-out users if needed, though unlikely for this functionality /** * Helper function to apply watermark. * This is a simplified example. For production, consider using a robust image manipulation library. * * @param string $base_image_path Path to the base image. * @param string $watermark_path Path to the watermark image. * @param float $opacity Opacity of the watermark. * @param string $position Position of the watermark (e.g., 'top-left', 'center', 'bottom-right'). * @param float $scale Scale of the watermark relative to the base image. * @param int $margin Margin in pixels. * @return string|WP_Error Path to the new watermarked image or WP_Error on failure. */ function biw_apply_watermark( $base_image_path, $watermark_path, $opacity, $position, $scale, $margin ) { if ( ! class_exists( 'Imagick' ) ) { return new WP_Error( 'imagick_missing', __( 'Imagick extension is not installed or enabled.', 'bulk-image-watermarker' ) ); } try { $base_image = new Imagick( $base_image_path ); $watermark = new Imagick( $watermark_path ); $base_width = $base_image->getImageWidth(); $base_height = $base_image->getImageHeight(); $watermark_width = $watermark->getImageWidth(); $watermark_height = $watermark->getImageHeight(); // Calculate watermark dimensions based on scale. $new_watermark_width = intval( $base_width * $scale ); $new_watermark_height = intval( $watermark_height * ( $new_watermark_width / $watermark_width ) ); // Resize watermark. $watermark->resizeImage( $new_watermark_width, $new_watermark_height, Imagick::FILTER_LANCZOS, 1 ); $watermark->setImageOpacity( $opacity ); // Calculate position. $dest_x = 0; $dest_y = 0; switch ( $position ) { case 'top-left': $dest_x = $margin; $dest_y = $margin; break; case 'top-right': $dest_x = $base_width - $new_watermark_width - $margin; $dest_y = $margin; break; case 'bottom-left': $dest_x = $margin; $dest_y = $base_height - $new_watermark_height - $margin; break; case 'bottom-right': $dest_x = $base_width - $new_watermark_width - $margin; $dest_y = $base_height - $new_watermark_height - $margin; break; case 'center': default: $dest_x = ( $base_width - $new_watermark_width ) / 2; $dest_y = ( $base_height - $new_watermark_height ) / 2; break; } // Composite the watermark onto the base image. $base_image->compositeImage( $watermark, Imagick::COMPOSITE_OVER, $dest_x, $dest_y ); // Save the watermarked image to a temporary location. $upload_dir = wp_upload_dir(); $temp_filename = uniqid( 'watermarked_' ) . '.' . $base_image->getImageFormat(); $temp_path = trailingslashit( $upload_dir['basedir'] ) . $temp_filename; $base_image->writeImage( $temp_path ); $base_image->destroy(); $watermark->destroy(); return $temp_path; } catch ( ImagickException $e ) { return new WP_Error( 'imagick_error', sprintf( __( 'Imagick error: %s', 'bulk-image-watermarker' ), $e->getMessage() ) ); } }

Note: The biw_apply_watermark function uses the Imagick PHP extension. Ensure this extension is installed and enabled on your server for image manipulation. If Imagick is not available, you would need to implement image processing using GD or another library, which would significantly increase the complexity of this function.

We’ve also registered a custom Gutenberg block named bulk-image-watermarker/block. This registration includes defining the block’s attributes, which will store the watermark settings and selected image IDs. The wp_localize_script function is used to pass the AJAX URL and a nonce to the JavaScript, essential for secure AJAX requests.

Frontend and Editor JavaScript Setup

Next, we need to set up the JavaScript for our Gutenberg block. This involves creating the block’s registration, its editor interface, and the logic for interacting with the Media Library and handling the HTMX requests.

Create a directory named build in your plugin’s root directory. Inside build, create two files: index.js and index.css. We’ll also need a package.json file to manage our JavaScript dependencies and build process.

Create a package.json file in the root of your plugin directory:

{
  "name": "bulk-image-watermarker",
  "version": "1.0.0",
  "description": "Gutenberg block for bulk image watermarking.",
  "main": "build/index.js",
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start"
  },
  "keywords": ["wordpress", "gutenberg", "block", "watermark", "htmx"],
  "author": "Your Name",
  "license": "GPL-2.0-or-later",
  "dependencies": {
    "@wordpress/blocks": "^12.0.0",
    "@wordpress/components": "^26.0.0",
    "@wordpress/element": "^5.0.0",
    "@wordpress/i18n": "^4.0.0",
    "@wordpress/editor": "^13.0.0",
    "htmx.org": "^1.9.10"
  },
  "devDependencies": {
    "@wordpress/scripts": "^26.0.0"
  }
}

Install the dependencies by running the following command in your plugin’s root directory:

npm install

Now, let’s populate build/index.js. This file will contain the core Gutenberg block registration and its editor interface.

/**
 * External dependencies
 */
import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import {
    PanelBody,
    Button,
    SelectControl,
    RangeControl,
    MediaUpload,
    MediaUploadCheck,
    IconButton,
    Notice,
    Spinner,
    ExternalLink,
} from '@wordpress/components';
import { Fragment } from '@wordpress/element';
import { InspectorControls, MediaPlaceholder } from '@wordpress/editor';
import apiFetch from '@wordpress/api-fetch';

// Import HTMX
import htmx from 'htmx.org';

/**
 * Internal dependencies
 */
import './style.scss'; // For frontend styles
import './editor.scss'; // For editor styles

const BLOCK_NAME = 'bulk-image-watermarker/block';

// Register the block
registerBlockType( BLOCK_NAME, {
    title: __( 'Bulk Image Watermarker', 'bulk-image-watermarker' ),
    icon: 'format-image', // WordPress dashicon
    category: 'media',
    description: __( 'Apply a watermark to multiple images in bulk.', 'bulk-image-watermarker' ),

    attributes: {
        watermarkImageId: {
            type: 'number',
            default: 0,
        },
        watermarkImageUrl: {
            type: 'string',
            default: '',
        },
        opacity: {
            type: 'number',
            default: 0.7,
        },
        position: {
            type: 'string',
            default: 'center',
        },
        scale: {
            type: 'number',
            default: 0.2,
        },
        margin: {
            type: 'number',
            default: 10,
        },
        selectedImageIds: {
            type: 'array',
            default: [],
            items: { type: 'number' },
        },
        watermarkedImageIds: {
            type: 'array',
            default: [],
            items: { type: 'number' },
        },
        isWatermarking: {
            type: 'boolean',
            default: false,
        },
        watermarkError: {
            type: 'string',
            default: '',
        },
        watermarkSuccess: {
            type: 'string',
            default: '',
        },
    },

    edit: function( { attributes, setAttributes } ) {
        const {
            watermarkImageId,
            watermarkImageUrl,
            opacity,
            position,
            scale,
            margin,
            selectedImageIds,
            watermarkedImageIds,
            isWatermarking,
            watermarkError,
            watermarkSuccess,
        } = attributes;

        const watermarkOptions = [
            { value: 'top-left', label: __( 'Top Left', 'bulk-image-watermarker' ) },
            { value: 'top-right', label: __( 'Top Right', 'bulk-image-watermarker' ) },
            { value: 'bottom-left', label: __( 'Bottom Left', 'bulk-image-watermarker' ) },
            { value: 'bottom-right', label: __( 'Bottom Right', 'bulk-image-watermarker' ) },
            { value: 'center', label: __( 'Center', 'bulk-image-watermarker' ) },
        ];

        const onSelectWatermarkImage = ( media ) => {
            setAttributes( {
                watermarkImageId: media.id,
                watermarkImageUrl: media.url,
            } );
        };

        const onRemoveWatermarkImage = () => {
            setAttributes( {
                watermarkImageId: 0,
                watermarkImageUrl: '',
            } );
        };

        const onSelectImages = ( media ) => {
            const newImageIds = media.map( ( img ) => img.id );
            setAttributes( { selectedImageIds: [ ...selectedImageIds, ...newImageIds ] } );
        };

        const onRemoveImage = ( imageIdToRemove ) => {
            setAttributes( {
                selectedImageIds: selectedImageIds.filter( ( id ) => id !== imageIdToRemove ),
                watermarkedImageIds: watermarkedImageIds.filter( ( id ) => id !== imageIdToRemove ), // Also remove from watermarked if it was there
            } );
        };

        const onRemoveWatermarkedImage = ( imageIdToRemove ) => {
            setAttributes( {
                watermarkedImageIds: watermarkedImageIds.filter( ( id ) => id !== imageIdToRemove ),
            } );
        };

        const handleWatermarkImages = () => {
            if ( watermarkImageId === 0 || selectedImageIds.length === 0 ) {
                setAttributes( { watermarkError: __( 'Please select a watermark image and images to watermark.', 'bulk-image-watermarker' ) } );
                return;
            }

            setAttributes( { isWatermarking: true, watermarkError: '', watermarkSuccess: '' } );

            apiFetch( {
                path: '/wp/v2/media', // This is a placeholder, we'll use admin-ajax.php
                method: 'POST',
            } ).then( ( response ) => {
                // This is where we'd typically make an AJAX call to our custom endpoint.
                // For HTMX, we'll trigger this from a button click with dynamic attributes.
                // We'll simulate the AJAX call here for demonstration purposes.

                // Simulate AJAX call to our custom handler
                const formData = new FormData();
                formData.append( 'action', 'biw_watermark_images' );
                formData.append( 'nonce', biw_ajax_object.nonce );
                formData.append( 'watermark_image_id', watermarkImageId );
                formData.append( 'image_ids', JSON.stringify( selectedImageIds ) );
                formData.append( 'opacity', opacity );
                formData.append( 'position', position );
                formData.append( 'scale', scale );
                formData.append( 'margin', margin );

                htmx.ajax( 'POST', biw_ajax_object.ajax_url, {
                    target: '#biw-watermark-results', // A dummy target, we'll handle response in events
                    swap: 'innerHTML',
                    headers: {
                        'X-Requested-With': 'XMLHttpRequest',
                    },
                    // HTMX event listeners for handling responses
                    onComplete: function( evt ) {
                        setAttributes( { isWatermarking: false } );
                        try {
                            const responseData = JSON.parse( evt.detail.xhr.responseText );
                            if ( responseData.success ) {
                                setAttributes( {
                                    watermarkSuccess: responseData.data.message,
                                    watermarkedImageIds: [ ...watermarkedImageIds, ...responseData.data.watermarked_ids ],
                                    selectedImageIds: selectedImageIds.filter(id => !responseData.data.watermarked_ids.includes(id)), // Remove successfully watermarked from selection
                                } );
                            } else {
                                setAttributes( { watermarkError: responseData.data.message } );
                            }
                        } catch ( e ) {
                            setAttributes( { watermarkError: __( 'An unexpected error occurred. Please check server logs.', 'bulk-image-watermarker' ) } );
                        }
                    },
                    onError: function( evt ) {
                        setAttributes( { isWatermarking: false, watermarkError: __( 'AJAX request failed. Please check your network connection.', 'bulk-image-watermarker' ) } );
                    }
                } );
            } ).catch( ( error ) => {
                setAttributes( { isWatermarking: false, watermarkError: error.message } );
            } );
        };

        const renderSelectedImages = () => (
            <>
                { selectedImageIds.length > 0 && (
                    <PanelBody title={ __( 'Selected Images', 'bulk-image-watermarker' ) } initialOpen={ false }>
                        <div className="biw-selected-images-list">
                            { selectedImageIds.map( ( id ) => (
                                <div key={ id } className="biw-image-item">
                                    <img src={ wp.media.attachment( id ).attributes.url } alt="" />
                                    <IconButton
                                        icon="no-alt"
                                        label={ __( 'Remove', 'bulk-image-watermarker' ) }
                                        onClick={ () => onRemoveImage( id ) }
                                        className="biw-remove-button"
                                    />
                                </div>
                            ) ) }
                        </div>
                    </PanelBody>
                ) }
            <>
        );

        const renderWatermarkedImages = () => (
            <>
                { watermarkedImageIds.length > 0 && (
                    <PanelBody title={ __( 'Watermarked Images', 'bulk-image-watermarker' ) } initialOpen={ false }>
                        <div className="biw-watermarked-images-list">
                            { watermarkedImageIds.map( ( id ) => (
                                <div key={ id } className="biw-image-item">
                                    <img src={ wp.media.attachment( id ).attributes.url } alt="" />
                                    <div className="biw-image-actions">
                                        <ExternalLink href={ wp.media.attachment( id ).attributes.url }>{ __( 'View', 'bulk-image-watermarker' ) }</ExternalLink>
                                        <IconButton
                                            icon="trash"
                                            label={ __( 'Delete', 'bulk-image-watermarker' ) }
                                            onClick={ () => onRemoveWatermarkedImage( id ) }
                                            className="biw-remove-button"
                                        />
                                    </div>
                                </div>
                            ) ) }
                        </div>
                    </PanelBody>
                ) }
            <>
        );

        return (
            <Fragment>
                <InspectorControls>
                    <PanelBody title={ __( 'Watermark Settings', 'bulk-image-watermarker' ) }>
                        <MediaUploadCheck>
                            <MediaUpload
                                onSelect={ onSelectWatermarkImage }
                                allowedTypes={ [ 'image' ] }
                                value={ watermarkImageId }
                                render={ ( { open } ) => (
                                    <Button
                                        isPrimary
                                        onClick={ open }
                                        icon="format-image"
                                        className={ watermarkImageId ? 'hidden' : '' }
                                    >
                                        { __( 'Select Watermark Image', 'bulk-image-watermarker' ) }
                                    </Button>
                                ) }
                            />
                        </MediaUploadCheck>
                        { watermarkImageUrl && (
                            <div className="biw-watermark-preview">
                                <img src={ watermarkImageUrl } alt={ __( 'Watermark Preview', 'bulk-image-watermarker' ) } />
                                <IconButton
                                    icon="no-alt"
                                    label={ __( 'Remove', 'bulk-image-watermarker' ) }
                                    onClick={ onRemoveWatermarkImage }
                                    className="biw-remove-button"
                                />
                            </div>
                        ) }

                        <RangeControl
                            label={ __( 'Opacity', 'bulk-image-watermarker' ) }
                            value={ opacity }
                            onChange={ ( value ) => setAttributes( { opacity: value } ) }
                            min={ 0 }
                            max={ 1 }
                            step={ 0.05 }
                        />
                        <SelectControl
                            label={ __( 'Position', 'bulk-image-watermarker' ) }
                            value={ position }
                            options={ watermarkOptions }
                            onChange={ ( value ) => setAttributes( { position: value } ) }
                        />
                        <RangeControl
                            label={ __( 'Scale', 'bulk-image-watermarker' ) }
                            value={ scale }
                            onChange={ ( value ) => setAttributes( { scale: value } ) }
                            min={ 0.05 }
                            max={ 1 }
                            step={ 0.05 }
                        />
                        <RangeControl
                            label={ __( 'Margin (px)', 'bulk-image-watermarker' ) }
                            value={ margin }
                            onChange={ ( value ) => setAttributes( { margin: value } ) }
                            min={ 0 }
                            max={ 100 }
                            step={ 5 }
                        />
                    </PanelBody>
                </InspectorControls>

                <div className="bulk-image-watermarker-block">
                    { watermarkError && (
                        <Notice status="error" isDismissible={ false }>
                            { watermarkError }
                        </Notice>
                    ) }
                    { watermarkSuccess && (
                        <Notice status="success" isDismissible={ false }>
                            { watermarkSuccess }
                        </Notice>
                    ) }

                    <h3>{ __( 'Watermark Configuration', 'bulk-image-watermarker' ) }</h3>

                    <div className="biw-controls">
                        <MediaUploadCheck>
                            <MediaUpload
                                title={ __( 'Select Images to Watermark', 'bulk-image-watermarker' ) }
                                onSelect={ onSelectImages }
                                allowedTypes={ [ 'image' ] }
                                multiple
                                value={ selectedImageIds }
                                render={ ( { open } ) => (
                                    <Button
                                        isPrimary
                                        onClick={ open }
                                        icon="upload"
                                        disabled={ watermarkImageId === 0 }
                                    >
                                        { __( 'Add Images', 'bulk-image-watermarker' ) }
                                    </Button>
                                ) }
                            />
                        </MediaUploadCheck>

                        <Button
                            isPrimary
                            onClick={ handleWatermarkImages }
                            disabled={ watermarkImageId === 0 || selectedImageIds.length === 0 || isWatermarking }
                            icon={ isWatermarking ? 'update' : 'format-image' }
                        >
                            { isWatermarking ? __( 'Watermarking...', 'bulk-image-watermarker' ) : __( 'Apply Watermark', 'bulk-image-watermarker' ) }
                        </Button>
                        { isWatermarking && <Spinner /> }
                    </div>

                    { renderSelectedImages() }
                    { renderWatermarkedImages() }

                    { !watermarkImageId && (
                        <Notice status="warning" isDismissible={ false }>
                            { __( 'Please select a watermark image in the block settings (Inspector Controls) to enable image selection and watermarking.', 'bulk-image-watermarker' ) }
                        </Notice>
                    ) }
                </div>
                <!-- HTMX target for results -->
                <div id="biw-watermark-results"></div>
            </Fragment>
        );
    },

    save: function( { attributes } ) {
        const {
            watermarkImageId,
            watermarkImageUrl,
            opacity,
            position,
            scale,
            margin,
            watermarkedImageIds,
        } = attributes;

        if ( watermarkedImageIds.length === 0 ) {
            return null; // Don't render anything if no images have been watermarked yet.
        }

        return (
            <div className="bulk-image-watermarker-block-frontend">

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • WordPress Development Recipe: Staggered database writes for high-volume custom form fields using Cron API (wp_schedule_event)
  • Step-by-Step Guide: Offloading high-frequency knowledge base document categories metadata writes to a Redis KV store
  • How to analyze and reduce CPU consumption of custom Singleton Registry Pattern event mediators
  • How to analyze and reduce CPU consumption of custom Factory Method design structures event mediators
  • WordPress Development Recipe: High-efficiency server-side rendering for Gutenberg blocks using Readonly classes

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (658)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (872)
  • PHP (5)
  • PHP Development (42)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (115)
  • WordPress Plugin Development (124)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • WordPress Development Recipe: Staggered database writes for high-volume custom form fields using Cron API (wp_schedule_event)
  • Step-by-Step Guide: Offloading high-frequency knowledge base document categories metadata writes to a Redis KV store
  • How to analyze and reduce CPU consumption of custom Singleton Registry Pattern event mediators

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (872)
  • Debugging & Troubleshooting (658)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala