andybeak
9/6/2012 - 10:57 PM

A convenient class for working with Data URIs in PHP

A convenient class for working with Data URIs in PHP

<?php
/* The MIT License (MIT)
 * Copyright (c) 2015 FlyingTopHat (lucas@flyingtophat.co.uk)
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

require('DataUri.php');

class DataUriTest extends PHPUnit_Framework_TestCase {

    /**
     * @var DataUri
     */
    protected $dataUri;

    protected function setUp() {
        $this->dataUri = new DataUri();
    }

    /**
     * @covers DataUri::getMediaType
     */
    public function testGetMediaType() {
        $this->assertEquals(DataUri::DEFAULT_TYPE, $this->dataUri->getMediaType());
        
        $this->dataUri->setMediaType('image/gif');
        $this->assertEquals('image/gif', $this->dataUri->getMediaType());
    }

    /**
     * @covers DataUri::setMediaType
     */
    public function testSetMediaType() {
        $this->dataUri->setMediaType('image/png');
        $this->assertEquals('image/png', $this->dataUri->getMediaType());
        
        $this->dataUri->setMediaType('');
        $this->assertEquals(DataUri::DEFAULT_TYPE, $this->dataUri->getMediaType());
    }

    /**
     * @covers DataUri::getEncoding
     */
    public function testGetEncoding() {
        $this->assertEquals(DataUri::ENCODING_URL_ENCODED_OCTETS, $this->dataUri->getEncoding());
        
        $this->dataUri->setEncodedData(DataUri::ENCODING_BASE64, '');
        $this->assertEquals(DataUri::ENCODING_BASE64, $this->dataUri->getEncoding());
    }

    /**
     * @covers DataUri::getEncodedData
     */
    public function testGetEncodedData() {
        $this->dataUri->setEncodedData(DataUri::ENCODING_BASE64, 'Example');
        $this->assertEquals('Example', $this->dataUri->getEncodedData());
    }

    /**
     * @covers DataUri::setEncodedData
     */
    public function testSetEncodedData() {
        $this->dataUri->setEncodedData(DataUri::ENCODING_BASE64, 'Example');
        $this->assertEquals('Example', $this->dataUri->getEncodedData());
    }

    /**
     * @covers DataUri::setEncodedData
     * @expectedException InvalidArgumentException
     */
    public function testSetEncodedDataException()
    {
        $this->dataUri->setEncodedData(null, 'Example');
    }
    
    /**
     * @covers DataUri::setData
     */
    public function testSetData() {
        $this->dataUri->setData('', DataUri::ENCODING_BASE64);
        $this->assertEquals('', $this->dataUri->getEncodedData());
        
        $this->dataUri->setData('ABC<>\/.?^%£');
        $this->assertEquals(rawurlencode('ABC<>\/.?^%£'), $this->dataUri->getEncodedData());
        
        $this->dataUri->setData('KFJ%&£"%*||`', DataUri::ENCODING_URL_ENCODED_OCTETS);
        $this->assertEquals(rawurlencode('KFJ%&£"%*||`'), $this->dataUri->getEncodedData());
        
        $this->dataUri->setData('~:{}[123S', DataUri::ENCODING_BASE64);
        $this->assertEquals(base64_encode('~:{}[123S'), $this->dataUri->getEncodedData());
        
        $this->dataUri->setData('', DataUri::ENCODING_URL_ENCODED_OCTETS);
        $this->assertEquals('', $this->dataUri->getEncodedData());
    }
    
    /**
     * @covers DataUri::setData
     * @expectedException InvalidArgumentException
     */
    public function testSetDataException()
    {
        $this->dataUri->setEncodedData('', null);
    }
    
    /**
     * @covers DataUri::tryDecodeData
     * @todo   Write a test where the data causes tryDecodeData() to return false
     */
    public function testTryDecodeData() {
        // Base64 with emtpy value
        $this->dataUri->setData('', DataUri::ENCODING_BASE64);
        $decodedData = null;
        $this->assertTrue($this->dataUri->tryDecodeData($decodedData));
        $this->assertEquals('', $decodedData);
        
        // Default encoding type
        $this->dataUri->setData('ABC<>\/.?^%£');
        $decodedData = null;
        $this->assertTrue($this->dataUri->tryDecodeData($decodedData));
        $this->assertEquals('ABC<>\/.?^%£', $decodedData);
        
        // URL encoded octet encoding with value
        $this->dataUri->setData('KFJ%&£"%*||`', DataUri::ENCODING_URL_ENCODED_OCTETS);
        $decodedData = null;
        $this->assertTrue($this->dataUri->tryDecodeData($decodedData));
        $this->assertEquals('KFJ%&£"%*||`', $decodedData);
        
        // Base64 with value
        $this->dataUri->setData('~:{}[123S', DataUri::ENCODING_BASE64);
        $decodedData = null;
        $this->assertTrue($this->dataUri->tryDecodeData($decodedData));
        $this->assertEquals('~:{}[123S', $decodedData);
        
        // URL encoded octet with emtpy value
        $this->dataUri->setData('', DataUri::ENCODING_URL_ENCODED_OCTETS);
        $decodedData = null;
        $this->assertTrue($this->dataUri->tryDecodeData($decodedData));
        $this->assertEquals('', $decodedData);
        
        // Encoded data set through DataUri::setEncodedData()
        $this->dataUri->setEncodedData(DataUri::ENCODING_BASE64, base64_encode('MGH4%"£4;FF'));
        $decodedData = null;
        $this->assertTrue($this->dataUri->tryDecodeData($decodedData));
        $this->assertEquals('MGH4%"£4;FF', $decodedData);
        
        //$this->dataUri->setEncodedData(DataUri::ENCODING_BASE64, null);
        //$decodedData = null;
        //$this->assertFalse($this->dataUri->tryDecodeData($decodedData));
    }

    /**
     * @covers DataUri::toString
     */
    public function testToString() {
        $this->assertEquals('data:,', $this->dataUri->toString());
        
        $this->dataUri->setMediaType('image/png');
        $this->dataUri->setData('HG2/$%&£"34A', DataUri::ENCODING_BASE64);
        
        $encoded = base64_encode('HG2/$%&£"34A');
        $this->assertEquals("data:image/png;base64,{$encoded}",
            $this->dataUri->toString());
    }
    
    /**
     * @covers DataUri::isParsable
     */
    public function testIsParsable() {
        $this->assertFalse(DataUri::isParsable(''));
        $this->assertTrue(DataUri::isParsable('data:,'));
        $this->assertTrue(DataUri::isParsable('data:text/plain;charset=US-ASCII;base64,ABC'));
    }

    /**
     * @covers DataUri::tryParse
     */
    public function testTryParse() {
        $dataUri = null;
        $this->assertFalse(DataUri::tryParse('', $dataUri));
        
        $dataUri = null;
        $this->assertTrue(DataUri::tryParse('data:,', $dataUri));
        $this->assertEquals($dataUri, new DataUri());
        
        $dataUri = null;
        $this->assertTrue(DataUri::tryParse('data:image/png;base64,', $dataUri));
        $this->assertEquals($dataUri, new DataUri('image/png', '', DataUri::ENCODING_BASE64));
    }
}
?>
<?php
/* The MIT License (MIT)
 * Copyright (c) 2015 FlyingTopHat (lucas@flyingtophat.co.uk)
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/**
 * The DataUri class provides a convenient way to access and construct 
 * data URIs, but should not be relied upon for enforcing RFC 2397 standards.
 * 
 * This class will not:
 *  - Validate the media-type provided/parsed
 *  - Validate the encoded data provided/parsed
 * 
 * @link http://www.flyingtophat.co.uk/blog/27/using-data-uris-in-php Examples
 * @author <a href="http://www.flyingtophat.co.uk/">Lucas</a>
 */
class DataUri {
    
    /** @var Regular expression used for decomposition of data URI scheme */
    private static $REGEX_URI = '/^data:(.+?){0,1}(?:(?:;(base64)\,){1}|\,)(.+){0,1}$/';
    
    const DEFAULT_TYPE = 'text/plain;charset=US-ASCII';
    
    const ENCODING_URL_ENCODED_OCTETS = 0;
    const ENCODING_BASE64 = 1;
    
    /** @var Keyword used in the data URI to signify base64 encoding */
    const BASE64_KEYWORD = 'base64';
    
    private $mediaType;
    private $encoding;
    private $encodedData;
    
    /**
     * Instantiates an instance of the DataURI class, initialised with the 
     * default values defined in RFC 2397. That is the media-type of 
     * text/plain;charset=US-ASCII and encoding type of URL encoded octets.
     * 
     * @param string $mediaType
     * @param string $data  Unencoded data
     * @param integer $encoding Class constant of either
     * {@link DataUri::ENCODING_URL_ENCODED_OCTETS} or
     * {@link DataUri::ENCODING_BASE64}
     * 
     * @throws InvalidArgumentException
     */
    public function __construct($mediaType = DataUri::DEFAULT_TYPE,
            $data = '',
            $encoding = DataUri::ENCODING_URL_ENCODED_OCTETS
            ) {
        try {
            $this->setMediaType($mediaType);
            $this->setData($data, $encoding);
        } catch (InvalidArgumentException $e) {
            throw $e;
        }
    }

    /**
     * Returns the data URI's media-type. If none was provided then in 
     * accordance to RFC 2397 it will default to text/plain;charset=US-ASCII
     * 
     * @return string Media-type
     */
    public function getMediaType() {
        return empty($this->mediaType) === false
            ? $this->mediaType
            : DataUri::DEFAULT_TYPE;
    }
    
    /**
     * Sets the media-type.
     * 
     * @param string $mediaType Media-type
     */
    public function setMediaType($mediaType) {
        $this->mediaType = $mediaType;
    }
    
    /**
     * Returns the method of encoding used for the data.
     * 
     * @return int Class constant of either 
     * {@link DataUri::ENCODING_URL_ENCODED_OCTETS} or
     * {@link DataUri::ENCODING_BASE64}
     */
    public function getEncoding() {
        return $this->encoding;
    }
    
    /**
     * Returns the data in its encoded form.
     * 
     * @return string Encoded data
     */
    public function getEncodedData() {
        return $this->encodedData;
    }
    
    /**
     * Sets the encoded data and the encoding scheme used to encode/decode it.
     * Be aware that the data is not validated, so ensure that the correct
     * encoding scheme is provided otherwise the method 
     * {@link DataUri::tryDecodeData($decodedData)} will fail.

     * @param int $encoding Class constant of either 
     * {@link DataUri::ENCODING_URL_ENCODED_OCTETS} or
     * {@link DataUri::ENCODING_BASE64}
     * @param string $data Data encoded with the encoding scheme provided
     * @throws InvalidArgumentException
     */
    public function setEncodedData($encoding, $data) {
        if(($encoding !== DataUri::ENCODING_URL_ENCODED_OCTETS) &&
            ($encoding !== DataUri::ENCODING_BASE64)) {
            throw new InvalidArgumentException('Unsupported encoding scheme');
        }
        
        $this->encoding = $encoding;
        $this->encodedData = $data;
    }
    
    
    /**
     * Sets the data for the data URI, which it stores in encoded form using
     * the encoding scheme provided.
     * 
     * @param string $data Data to encode then store
     * @param int $encoding Class constant of either 
     * {@link DataUri::ENCODING_URL_ENCODED_OCTETS} or
     * {@link DataUri::ENCODING_BASE64}
     * @throws InvalidArgumentException
     */
    public function setData($data, $encoding = DataUri::ENCODING_URL_ENCODED_OCTETS) {
        switch($encoding) {
            case DataUri::ENCODING_URL_ENCODED_OCTETS:
		$this->encoding = DataUri::ENCODING_URL_ENCODED_OCTETS;
                $this->encodedData = rawurlencode($data);
                break;
            case DataUri::ENCODING_BASE64:
		$this->encoding = DataUri::ENCODING_BASE64;
                $this->encodedData = base64_encode($data);
                break;
            default:
                throw new InvalidArgumentException('Unsupported encoding scheme');
                break;
        }
    }
    
    /**
     * Tries to decode the URI's data using the encoding scheme set.
     * 
     * @param null $decodedData Stores the decoded data
     * @return boolean <code>true</code> if data was output, 
     * else <code>false</code>
     */
    public function tryDecodeData(&$decodedData) {
        $hasOutput = false;
        
        switch($this->getEncoding()) {
            case DataUri::ENCODING_URL_ENCODED_OCTETS:
                $decodedData = rawurldecode($this->getEncodedData());
                $hasOutput = true;
                break;
            case DataUri::ENCODING_BASE64:
                $b64Decoded = base64_decode($this->getEncodedData(), true);

                if($b64Decoded !== false) {
                    $decodedData = $b64Decoded;
                    $hasOutput = true;
                }
                break;
            default:
                // NOP
                break;
        }

        return $hasOutput;
    }
    
    /**
     * Generates a data URI string representation of the object.
     * 
     * @return string
     */
    public function toString() {
        $output = 'data:';
        
        if(($this->getMediaType() !== DataUri::DEFAULT_TYPE) || 
            ($this->getEncoding() !== DataUri::ENCODING_URL_ENCODED_OCTETS)) {
            $output .= $this->getMediaType();

            if($this->getEncoding() === DataUri::ENCODING_BASE64) {
                $output .= ';'.DataUri::BASE64_KEYWORD;
            }
        }

        $output .= ','.$this->getEncodedData();
        return $output; 
    }
    
    public function __toString()
    {
        return $this->toString();
    }
    
    /**
     * Determines whether a string is data URI with the components necessary for
     * it to be parsed by the {@link DataUri::tryParse($s, &$out)} method.
     * 
     * @param string $string Data URI
     * @return boolean <code>true</code> if possible to parse,
     * else <code>false</code>
     */
    public static function isParsable ($dataUriString) {
        return (preg_match(DataUri::$REGEX_URI, $dataUriString) === 1);
    }
    
    /**
     * Parses a string data URI into an instance of a DataUri object.
     * 
     * @param string $dataUriString Data URI to be parsed
     * @param DataUri $out Output DataUri of the method
     * @return boolean <code>true</code> if successful, else <code>false</code>
     */
    public static function tryParse($dataUriString, &$out) {
        $hasOutput = false;
        
        if(DataUri::isParsable($dataUriString)) {
            $matches = null;
            if(preg_match_all(DataUri::$REGEX_URI,
                $dataUriString,
                $matches,
                PREG_SET_ORDER) !== false) {

                $mediatype = isset($matches[0][1])
                    ? $matches[0][1]
                    : DataUri::DEFAULT_TYPE;

                $matchedEncoding = isset($matches[0][2]) ? $matches[0][2] : '';
                $encoding = (strtolower($matchedEncoding) === DataUri::BASE64_KEYWORD)
                    ? DataUri::ENCODING_BASE64
                    : DataUri::ENCODING_URL_ENCODED_OCTETS;

                $data = isset($matches[0][3]) 
                    ? $matches[0][3] 
                    : '';

                $dataUri = new DataUri();
                $dataUri->setMediaType($mediatype);
                $dataUri->setEncodedData($encoding, $data);

                $out = $dataUri;
                $hasOutput = true;
            }
        }
        
        return $hasOutput;
    }
}
?>