Search code examples
phpwatermark

Add Watermark to JPG or PNG proportionally using PHP


The problem I encountered right now is that the "watermark" image/file isn't proportionally correct. The height is a bit taller than expected.

As you can see from the preview below the "watermark" word/image was stretched.

<?php
//Source folder where all images are placed
$source="source";

//Destination folder where all images with watermark will be copied
$destination="output";

$watermark =imagecreatefrompng("watermark-sample.png");

//keeps the transparency of the picture
imagealphablending( $watermark, false );
imagesavealpha( $watermark, true );

//keeps the transparency of the picture
imagealphablending( $watermark, false );
imagesavealpha( $watermark, true );

// assign image file name
$image = $source.'/large-pic.jpg';

$im = imagecreatefromjpeg($image);

// get the height/width of the stamp image
$sx = imagesx($watermark);
$sy = imagesy($watermark);
$sximg = imagesx($im);

//percentage of the size(5%)
$percent = $sximg * 0.12;

//position the stamp to the center
$posx = round(imagesx($im) / 2) - round($sx / 2);
$posy = round(imagesy($im) / 2) - round($sy / 2);

//Create the final resized watermark stamp
$dest_image = imagecreatetruecolor($percent, $percent);

//keeps the transparency of the picture
imagealphablending( $dest_image, false );
imagesavealpha( $dest_image, true );

//resizes the stamp
imagecopyresampled($dest_image, $watermark, 0, 0, 0, 0, $percent, $percent, $sx, $sy);

// Copy the resized stamp image onto the photo
imagecopy($im, $dest_image, round($posx), round($posy), 0, 0, $percent, $percent);

// Output and free memory
header('Content-type: image/jpg');
imagepng($im);
imagedestroy($im);

Output Image: enter image description here

Watermark Image: enter image description here


Solution

  • In order to maintain the aspect ratio of a resized watermark at 5% of the background image size you need to know the ratio of width to height and use that when generating the resized image. As per the comment by @CBroe, using a single $percent value for both will yield the wrong aspect ratio! The positioning of the watermark also requires the new watermark sizes to be able to calculate the appropriate image centre.

    The two initial uses of:

    imagealphablending( $watermark, false );
    imagesavealpha( $watermark, true );
    

    were not required and can be removed.

    The header call at the end should be set to image/png as you then use imagepng to output the composite image to the browser.


    <?php
    
    # Source folder where all images are placed
    $source=__DIR__ . '/source';
    
    # As the $destination folder/variable was unused it is gone.
    
    
    $watermark = imagecreatefrompng( $source . '/watermark-sample.png' );
    $im = imagecreatefromjpeg( $source . '/large-pic.jpg' );
    
    
    
    # get the height/width of the watermark image
    $sx = imagesx( $watermark );
    $sy = imagesy( $watermark );
    $sximg = imagesx( $im );
    
    
    # very important - aspect ratio of the watermark!!!
    $ratio=$sx / $sy;
    
    # percentage of the size( 5% )
    $percent = 5 / 100; # 5% as per question
    
    
    
    # Determine new sizes for the watermark.
    # The width will be 5% of the background width
    # but the height will be the respective proportion
    # of the background height based upon the aspect
    # ratio of the watermark!
    
    $width = $sximg * $percent;
    $height = $width / $ratio;
    
    
    
    # Determine the position the watermark on the background image and
    # use the new sizes of watermark as part of the centre calculations!
    $posx = round( imagesx( $im ) / 2 ) - round( $width / 2 );
    $posy = round( imagesy( $im ) / 2 ) - round( $height / 2 );
    
    
    
    
    
    # Create the final resized watermark stamp
    $dest_image = imagecreatetruecolor( $width, $height );
    
    # keeps the transparency of the picture
    imagealphablending( $dest_image, false );
    imagesavealpha( $dest_image, true );
    
    
    # resizes the stamp
    imagecopyresampled( $dest_image, $watermark, 0, 0, 0, 0, $width, $height, $sx, $sy );
    
    # Copy the resized stamp image onto the photo
    imagecopy( $im, $dest_image, round( $posx ), round( $posy ), 0, 0, $width, $height );
    
    
    
    # Output and free memory - use correct header however!
    header('Content-type: image/png');
    imagepng( $im );
    
    imagedestroy( $im );
    imagedestroy( $watermark );
    ?>
    

    What I ended up with whilst playing about:

    <?php
    
        error_reporting( 0 );
    
        $config=json_decode(
            json_encode(
                ( array(
                        'percentage'    =>  10,
                        'save'          =>  true,                                   # save composite image to disk.
                        'both'          =>  true,                                   # both save & output in same operation.
                        'quality'       =>  0,                                      # Compression level: from 0 (no compression) to 9. default: 6
                        'filters'       =>  PNG_NO_FILTER,                          # Bitmask of filter constants to achieve particular effects.
                        'download'      =>  true,                                   # Download source images from internet if needed
                        'force'         =>  false,                                  # Force overwrite of source images if new background is used etc
                        'urls'          =>  array(
                            'watermark'         =>  'https://i.imgur.com/YoiJKXC.png',
                            'background'        =>  'https://res.cloudinary.com/dk-find-out/image/upload/q_80,w_1920,f_auto/MA_00089970_icgvox.jpg'
                        ),
                        'folders'       =>  array(
                            'source'            =>  __DIR__ . '/images/uploads',    # Directory used for the two source images
                            'destination'       =>  __DIR__ . '/images/composites'  # Directory to save composite image into if being saved.
                        ),
                        'align'         =>  array(
                            'horizontal'    =>  'center',                           # center, left, right
                            'vertical'      =>  'middle'                            # middle, top, bottom
                        )
                    )
                )
            )
        );
    
    
    
    
    
        #------------------------------------------------------------------------------------
        # Create the directory structure defined in the config if it does not exist.
        # If this happens logically the images cannot exist so the script will terminate below.
        # Override this by allowing download of source images.
        foreach( $config->folders as $folder ){
            if( !file_exists( $folder ) ){
                mkdir( $folder, 0777, true );
                clearstatcache();
            }
        }
    
        # download source images from interweb if needed and save to the source directory defined above.
        if( $config->download && count( array_keys( get_object_vars( $config->urls ) ) ) > 0 ){
            foreach( $config->urls as $folder => $url ){
                $src=sprintf( '%s/%s', $config->folders->source, basename( $url ) );
                if( !file_exists( $src ) or $config->force ){
                    file_put_contents( $src, file_get_contents( $url ) );
                }
                clearstatcache();
            }
        }
        
        
        switch( pathinfo( basename( $config->urls->background ), PATHINFO_EXTENSION ) ){
            case 'jpg':
            case 'jpeg': 
            case 'pjpeg': $func='imagecreatefromjpeg'; break;
            case 'png':   $func='imagecreatefrompng';  break;
            case 'gif':   $func='imagecreatefromgif';  break;
            case 'webp':  $func='imagecreatefromwebp'; break;
            case 'bmp':   $func='imagecreatefrombmp';  break;
        }
    
    
        # create the two base images upon which the composite will be based.
        $watermark = imagecreatefrompng( sprintf( '%s/%s', $config->folders->source, basename( $config->urls->watermark ) ) );
        $srcimage  = call_user_func( $func, sprintf( '%s/%s', $config->folders->source, basename( $config->urls->background ) ) );
    
        # if the images do not exist, terminate the script.
        if( !$watermark or !$srcimage ) exit('Bad Foo! One or other source images could not be created.');
    
    
        # get the width / height of the watermark image & the ratio of with to height.
        $wm_img_x = imagesx( $watermark );
        $wm_img_y = imagesy( $watermark );
        $ratio = $wm_img_x / $wm_img_y;
    
    
        # get width / height of background image
        $src_img_x = imagesx( $srcimage );
        $src_img_y = imagesy( $srcimage );
    
    
    
    
    
        #   The watermark is to be X% of the size of the background image
        #   but to retain the proportionality of the watermark - for this
        #   we need to know the ratio of width to height in order to
        #   determine new width & height of watermark image.
        $wm_width = $src_img_x * ( $config->percentage / 100 );
        $wm_height = round( $wm_width / $ratio, 2 );
    
    
    
        # identify centre of background image - this is where the watermark will be placed.
        $posx = round( $src_img_x / 2 ) - round( $wm_width / 2 );
        $posy = round( $src_img_y / 2 ) - round( $wm_height / 2 );
    
        # set the location of watermark if not at default center/middle point.
        if( strtolower( $config->align->horizontal ) == 'left' )$posx=10;
        if( strtolower( $config->align->horizontal ) == 'right' )$posx=$src_img_x - ( $wm_width + 10 );
    
        if( strtolower( $config->align->vertical ) == 'top' )$posy=10;
        if( strtolower( $config->align->vertical ) == 'bottom' )$posy=$src_img_y - ( $wm_height + 10 );
    
    
    
    
        # create the composite output image that will be populated next.
        $output = imagecreatetruecolor( $src_img_x, $src_img_y );
        imagealphablending( $output, true );
        imagesavealpha( $output, true );
    
    
    
        # copy the background to the output image
        imagecopy( $output, $srcimage, 0, 0, 0, 0, $src_img_x, $src_img_y );
        # add the new resized watermark with it's transparency to the output
        imagecopyresampled( $output, $watermark, $posx, $posy, 0, 0, $wm_width, $wm_height, $wm_img_x, $wm_img_y );
    
    
    
        # direct output or save to disk?
        $filepath=$config->save ? sprintf('%s/composite.png', $config->folders->destination ) : null;
    
    
    
        // Save/Output and free memory.
        header('Content-Type: image/png');
        imagepng( $output, $filepath, $config->quality, $config->filters );
        if( $config->both ) imagepng( $output );
    
    
        imagedestroy( $watermark );
        imagedestroy( $srcimage );
        imagedestroy( $output );
    ?>