开发者

Manipulating transparent PNG causes all rgb(0,0,0) in the image to become transparent

I have a PHP image upload system which allows a user to upload various images (JPG, GIF, PNG). Every image which is uploaded has an icon (20x20), thumbnail (150x150) and a postcard (500x500) generated for it--I call these "meta images". If the original image dimensions are not square like the meta image, it is scaled to best fit and a transparent canvas is generated at the proper size with the meta image laid onto it. Because of this transparent canvasing, and for other purposes, all generated meta images are themselves PNGs (regardless of original image format).

For example, if a user uploads a 800px by 600px JPG image, the following ends up in the filesystem:

  1. Original *.jpg at 800 x 600
  2. Meta icon *.png with 20 x 15 copy of original image centered horizontally and vertically on 20 x 20 tansparent canvas
  3. Meta thumbail *.png with 150 x 112 copy of original image centered horizontally and vertically on 150 x 150 tansparent canvas
  4. Meta icon *.png with 500 x 375 copy of original image centered horizontally and vertically on 500 x 500 tansparent canvas

This works perfectly for JPG and GIF files--all colors are handled properly, sizes work, etc, etc, etc.

However, if I upload a PNG which has any black in it (rgb(0,0,0), #000000), the resulting 3 meta images have all black converted to transparent. Everything else is fine--dimensions, etc. And remember, transparent GIFs seem to work fine, even if there is black within.

Can someone please explain to me how I can fix this? Below is the code I wrote for this system (note there are 3 extensions of the abstract class defined beneath it):

<?php
/*
 * TODO: There is no reason for this class to be abstract except that I have not
 *       added any of the setters/getters/etc which would make it useful on its
 *       own.  As is, an instantiation of this class would not accomplish
 *       anything so I have made it abstract to avoid instantiation.  Three
 *       simple and usable extensions of this class exist below it in this file.
 */
abstract class ObjectImageRenderer
{
  const WRITE_DIR = SITE_UPLOAD_LOCATION;

  protected $iTargetWidth = 0;
  protected $iTargetHeight = 0;
  protected $iSourceWidth = 0;
  protected $iSourceHeight = 0;
  protected $iCalculatedWidth = 0;
  protected $iCalculatedHeight = 0;

  protected $sSourceLocation = '';
  protected $sSourceExt = '';
  protected $oSourceImage = null;

  protected $sTargetLocation = '';
  protected $sTargetNamePrefix = '';
  protected $oTargetImage = null;

  protected $oTransparentCanvas = null;

  protected $bNeedsCanvas = false;
  protected $bIsRendered = false;

  public function __construct( $sSourceLocation )
  {
    if( ! is_string( $sSourceLocation ) || $sSourceLocation === '' ||
        ! is_file( $sSourceLocation ) || ! is_readable( $sSourceLocation )
      )
    {
      throw new Exception( __CLASS__ . ' must be instantiated with valid path/filename of source image as first param.' );
    }
    $this->sSourceLocation = $sSourceLocation;
    $this->resolveNames();
  }

  public static function factory( $sSourceLocation, $size )
  {
    switch( $size )
    {
      case 'icon':
        return new ObjectIconRenderer( $sSourceLocation );
        break;

      case 'thumbnail':
        return new ObjectThumbnailRenderer( $sSourceLocation );
        break;

      case 'postcard':
        return new ObjectPostcardRenderer( $sSourceLocation );
        break;
    }
  }

  public static function batchRender( $Source )
  {
    if( is_string( $Source ) )
    {
      try
      {
        ObjectImageRenderer::factory( $Source, 'icon' )->render();
        ObjectImageRenderer::factory( $Source, 'thumbnail' )->render();
        ObjectImageRenderer::factory( $Source, 'postcard' )->render();
      }
      catch( Exception $exc )
      {
        LogProcessor::submit( 500, $exc->getMessage() );
      }
    }
    else if( is_array( $Source ) && count( $Source ) > 0 )
    {
      foreach( $Source as $sSourceLocation )
      {
        if( is_string( $sSourceLocation ) )
        {
          self::batchRender( $sSourceLocation );
        }
      }
    }
  }

  /**
   * loadImageGD - read image from filesystem into GD based image resource
   *
   * @access public
   * @static
   * @param STRING $sImageFilePath
   * @param STRING $sSourceExt OPTIONAL
   * @return RESOURCE
   */
  public static function loadImageGD( $sImageFilePath, $sSourceExt = null )
  {
    $oSourceImage = null;

    if( is_string( $sImageFilePath ) && $sImageFilePath 开发者_运维百科!== '' &&
        is_file( $sImageFilePath ) && is_readable( $sImageFilePath )
      )
    {
      if( $sSourceExt === null )
      {
        $aPathInfo = pathinfo( $sImageFilePath );
        $sSourceExt = strtolower( (string) $aPathInfo['extension'] );
      }

      switch( $sSourceExt )
      {
        case 'jpg':
        case 'jpeg':
        case 'pjpeg':
          $oSourceImage = imagecreatefromjpeg( $sImageFilePath );
          break;

        case 'gif':
          $oSourceImage = imagecreatefromgif( $sImageFilePath );
          break;

        case 'png':
        case 'x-png':
          $oSourceImage = imagecreatefrompng( $sImageFilePath );
          break;

        default:
          break;
      }
    }

    return $oSourceImage;
  }

  protected function resolveNames()
  {
    $aPathInfo = pathinfo( $this->sSourceLocation );
    $this->sSourceExt = strtolower( (string) $aPathInfo['extension'] );
    $this->sTargetLocation = self::WRITE_DIR . $this->sTargetNamePrefix . $aPathInfo['basename'] . '.png';
  }

  protected function readSourceFileInfo()
  {
    $this->oSourceImage = self::loadImageGD( $this->sSourceLocation, $this->sSourceExt );

    if( ! is_resource( $this->oSourceImage ) )
    {
      throw new Exception( __METHOD__ . ': image read failed for ' . $this->sSourceLocation );
    }

    $this->iSourceWidth = imagesx( $this->oSourceImage );
    $this->iSourceHeight = imagesy( $this->oSourceImage );

    return $this;
  }

  protected function calculateNewDimensions()
  {
    if( $this->iSourceWidth === 0 || $this->iSourceHeight === 0 )
    {
      throw new Exception( __METHOD__ . ': source height or width is 0. Has ' . __CLASS__ . '::readSourceFileInfo() been called?' );
    }

    if( $this->iSourceWidth > $this->iTargetWidth || $this->iSourceHeight > $this->iTargetHeight )
    {
      $nDimensionRatio = ( $this->iSourceWidth / $this->iSourceHeight );

      if( $nDimensionRatio > 1 )
      {
        $this->iCalculatedWidth = $this->iTargetWidth;
        $this->iCalculatedHeight = (int) round( $this->iTargetWidth / $nDimensionRatio );
      }
      else
      {
        $this->iCalculatedWidth =  (int) round( $this->iTargetHeight * $nDimensionRatio );
        $this->iCalculatedHeight = $this->iTargetHeight;
      }
    }
    else
    {
      $this->iCalculatedWidth = $this->iSourceWidth;
      $this->iCalculatedHeight = $this->iSourceHeight;
    }

    if( $this->iCalculatedWidth < $this->iTargetWidth || $this->iCalculatedHeight < $this->iTargetHeight )
    {
      $this->bNeedsCanvas = true;
    }

    return $this;
  }

  protected function createTarget()
  {
    if( $this->iCalculatedWidth === 0 || $this->iCalculatedHeight === 0 )
    {
      throw new Exception( __METHOD__ . ': calculated height or width is 0. Has ' . __CLASS__ . '::calculateNewDimensions() been called?' );
    }

    $this->oTargetImage = imagecreatetruecolor( $this->iCalculatedWidth, $this->iCalculatedHeight );

    $aTransparentTypes = Array( 'gif', 'png', 'x-png' );
    if( in_array( $this->sSourceExt, $aTransparentTypes ) )
    {
      $oTransparentColor = imagecolorallocate( $this->oTargetImage, 0, 0, 0 );
      imagecolortransparent( $this->oTargetImage, $oTransparentColor);
      imagealphablending( $this->oTargetImage, false );
    }

    return $this;
  }

  protected function fitToMaxDimensions()
  {
    $iTargetX = (int) round( ( $this->iTargetWidth - $this->iCalculatedWidth ) / 2 );
    $iTargetY = (int) round( ( $this->iTargetHeight - $this->iCalculatedHeight ) / 2 );

    $this->oTransparentCanvas = imagecreatetruecolor( $this->iTargetWidth, $this->iTargetHeight );
    imagealphablending( $this->oTransparentCanvas, false );
    imagesavealpha( $this->oTransparentCanvas, true );
    $oTransparentColor = imagecolorallocatealpha( $this->oTransparentCanvas, 0, 0, 0, 127 );
    imagefill($this->oTransparentCanvas, 0, 0, $oTransparentColor );

    $bReturnValue = imagecopyresampled( $this->oTransparentCanvas, $this->oTargetImage, $iTargetX, $iTargetY, 0, 0, $this->iCalculatedWidth, $this->iCalculatedHeight, $this->iCalculatedWidth, $this->iCalculatedHeight );

    $this->oTargetImage = $this->oTransparentCanvas;

    return $bReturnValue;
  }

  public function render()
  {
    /*
     * TODO: If this base class is ever made instantiable, some re-working is
     *       needed such that the developer harnessing it can choose whether to
     *       write to the filesystem on render, he can ask for the
     *       image resources, determine whether cleanup needs to happen, etc.
     */
    $this
      ->readSourceFileInfo()
      ->calculateNewDimensions()
      ->createTarget();

    $this->bIsRendered = imagecopyresampled( $this->oTargetImage, $this->oSourceImage, 0, 0, 0, 0, $this->iCalculatedWidth, $this->iCalculatedHeight, $this->iSourceWidth, $this->iSourceHeight );

    if( $this->bIsRendered && $this->bNeedsCanvas )
    {
      $this->bIsRendered = $this->fitToMaxDimensions();
    }

    if( $this->bIsRendered )
    {
      imagepng( $this->oTargetImage, $this->sTargetLocation );
      @chmod( $this->sTargetLocation, 0644 );
    }

    if( ! $this->bIsRendered )
    {
      throw new Exception( __METHOD__ . ': failed to copy image' );
    }

    $this->cleanUp();
  }

  public function cleanUp()
  {
    if( is_resource( $this->oSourceImage ) )
    {
      imagedestroy( $this->oSourceImage );
    }

    if( is_resource( $this->oTargetImage ) )
    {
      imagedestroy( $this->oTargetImage );
    }

    if( is_resource( $this->oTransparentCanvas ) )
    {
      imagedestroy( $this->oTransparentCanvas );
    }
  }
}




class ObjectIconRenderer extends ObjectImageRenderer
{
  public function __construct( $sSourceLocation )
  {
    /* These Height/Width values are also coded in
     * src/php/reference/display/IconUploadHandler.inc
     * so if you edit them here, do it there too
     */
    $this->iTargetWidth = 20;
    $this->iTargetHeight = 20;
    $this->sTargetNamePrefix = 'icon_';

    parent::__construct( $sSourceLocation );
  }
}

class ObjectThumbnailRenderer extends ObjectImageRenderer
{
  public function __construct( $sSourceLocation )
  {
    /* These Height/Width values are also coded in
     * src/php/reference/display/IconUploadHandler.inc
     * so if you edit them here, do it there too
     */
    $this->iTargetWidth = 150;
    $this->iTargetHeight = 150;
    $this->sTargetNamePrefix = 'thumbnail_';

    parent::__construct( $sSourceLocation );
  }
}

class ObjectPostcardRenderer extends ObjectImageRenderer
{
  public function __construct( $sSourceLocation )
  {
    /* These Height/Width values are also coded in
     * src/php/reference/display/IconUploadHandler.inc
     * so if you edit them here, do it there too
     */
    $this->iTargetWidth = 500;
    $this->iTargetHeight = 500;
    $this->sTargetNamePrefix = 'postcard_';

    parent::__construct( $sSourceLocation );
  }
}

The code I use to run it is:

<?php
ObjectImageRenderer::batchRender( $sSourceFile );

The main issues seem to be in the createTarget() and fitToMaxDimensions() methods. In createTarget(), if I comment out the following lines:

$aTransparentTypes = Array( 'gif', 'png', 'x-png' );
if( in_array( $this->sSourceExt, $aTransparentTypes ) )
{
  $oTransparentColor = imagecolorallocate( $this->oTargetImage, 0, 0, 0 );
  imagecolortransparent( $this->oTargetImage, $oTransparentColor);
  imagealphablending( $this->oTargetImage, false );
}

I no longer lose my black, but all existing transparency becomes black.

I assume the issue is that I am using black as the transparency channel. But how do I avoid using a color which is in the image as the transparency channel?

Thanks to anyone who can help me understand the mysteries behind transparency!

Jim


You're explictly specifying that 0,0,0 is transparent:

$oTransparentColor = imagecolorallocatealpha( $this->oTransparentCanvas, 0, 0, 0, 127 );

This will apply to ANY pixel in the image where the color triplet is 0,0,0 - in other words, all blacks, as you're finding.

If you want the original images to come through, you'd convert to using an alpha channel. It'd be a seperate "layer" in the image that exclusively specifies transparency/opaqueness for each pixel. That or scan the images for a color that's not used in the original, and then specify that as the new transparent value instead of defaulting to 0,0,0.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜