Commit 1f7167a9 authored by VicDeo's avatar VicDeo Committed by Thomas Müller
Browse files

Refactor imagecreatefrombmp (#26849)

* Handle reading non-existing characters for any depth

* Refactor imagecreatefrombmp

* Fix undefined variable. Use class constant

* Increase coverage
parent 6f726844
<?php
/**
* @author Victor Dubiniuk <dubiniuk@owncloud.com>
*
* @copyright Copyright (c) 2016, ownCloud GmbH.
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
/**
* Class for bitmap image conversion
*/
namespace OC\Image;
class BmpToResource {
const MAGIC = 19778; // ASCII BM
const BITMAP_HEADER_SIZE_BYTES = 14;
const DIB_BITMAPINFOHEADER_SIZE_BYTES = 40;
const COMPRESSION_BI_RGB = 0;
const COMPRESSION_BI_BITFIELDS = 3;
/** @var \SplFileObject $file */
private $file;
/** @var array $header */
private $header = [];
/** @var array $palette */
private $palette = [];
/** @var string[][] $pixelArray */
private $pixelArray;
/** @var resource $resource */
private $resource;
/** @var array $bytesPerDepth */
private $bytesPerDepth = [
1 => 1,
4 => 1,
8 => 1,
16 => 2,
24 => 3,
32 => 3,
];
/**
* BmpToResource constructor.
*
* @param string $fileName
*/
public function __construct($fileName){
$this->file = new \SplFileObject($fileName, 'rb');
}
/**
* @return resource
* @throws \Exception
*/
public function toResource(){
try {
$this->header = $this->readBitmapHeader();
$this->header += $this->readDibHeader();
if ($this->header['compression'] === self::COMPRESSION_BI_BITFIELDS) {
$this->header += $this->readBitMasks();
}
// Color Table is mandatory for color depths <= 8 bits
if ($this->header['bits'] <= 8) {
$this->palette = $this->readColorTable($this->header['colors']);
}
$this->pixelArray = $this->readPixelArray();
// create gd image
$this->resource = imagecreatetruecolor($this->header['width'], $this->header['height']);
if ($this->resource === false) {
throw new \RuntimeException('imagecreatetruecolor failed for file ' . $this->getFilename() . '" with dimensions ' . $this->header['width'] . 'x' . $this->header['height']);
}
$this->pixelArrayToImage();
} catch (\Exception $e) {
$this->file = null;
throw $e;
}
$this->file = null;
return $this->resource;
}
/**
* @return array
*/
public function getHeader(){
return $this->header;
}
/**
* https://en.wikipedia.org/wiki/BMP_file_format#Bitmap_file_header
* @return array
*/
private function readBitmapHeader(){
$bitmapHeaderArray = @unpack('vtype/Vfilesize/Vreserved/Voffset', $this->readFile(self::BITMAP_HEADER_SIZE_BYTES));
if (!isset($bitmapHeaderArray['type']) || $bitmapHeaderArray['type'] !== self::MAGIC) {
throw new \DomainException('No valid bitmap signature found in ' . $this->getFilename());
}
return [
'filesize' => $bitmapHeaderArray['filesize'],
'offset' => $bitmapHeaderArray['offset'],
];
}
/**
* https://en.wikipedia.org/wiki/BMP_file_format#DIB_header_.28bitmap_information_header.29
* @return array
*/
private function readDibHeader(){
$dibHeaderSizeArray = @unpack('Vheadersize', $this->readFile(4));
if (!isset($dibHeaderSizeArray['headersize']) || $dibHeaderSizeArray['headersize'] < self::DIB_BITMAPINFOHEADER_SIZE_BYTES) {
throw new \UnexpectedValueException('Unsupported DIB header version in ' . $this->getFilename());
}
$rawDibHeader = $this->readFile($dibHeaderSizeArray['headersize'] - 4);
$dibHeader = @unpack('Vwidth/Vheight/vplanes/vbits/Vcompression/Vimagesize/Vxres/Vyres/Vcolors/Vimportant', $rawDibHeader);
// fixup colors
$dibHeader['colors'] = $dibHeader['colors'] === 0 ? pow(2, $dibHeader['bits']) : $dibHeader['colors'];
// fixup imagesize - it can be zero
if ($dibHeader['imagesize'] < 1) {
$dibHeader['imagesize'] = $this->fixImageSize($dibHeader);
}
if ($dibHeader['imagesize'] < 1) {
throw new \UnexpectedValueException('Can not obtain image size of ' . $this->getFilename());
}
$validBitDepth = array_keys($this->bytesPerDepth);
if (!in_array($dibHeader['bits'], $validBitDepth)) {
throw new \UnexpectedValueException('Bit Depth ' . $dibHeader['bits'] . ' in ' . $this->getFilename() . ' is not supported');
}
return $dibHeader;
}
private function fixImageSize($header){
// No compression - calculate it in our own
if ($header['compression'] === self::COMPRESSION_BI_RGB) {
$bytesPerRow = intval(floor(($header['bits'] * $header['width'] + 31) / 32) * 4);
$imageSize = $bytesPerRow * abs($header['height']);
} else {
$imageSize = $this->file->getSize() - $this->header['offset'];
}
return $imageSize;
}
/**
* @return array
*/
private function readBitMasks(){
return @unpack('VrMask/VgMask/VbMask', $this->readFile(12));
}
/**
* Read a color table
* http://www.dragonwins.com/domains/getteched/bmp/bmpfileformat.htm#The%20Color%20Table
* four bytes are ordered as follows:
* [ZERO][RED][GREEN][BLUE] Little Endian
* @param int $colors
* @return array
*/
private function readColorTable($colors){
$palette = @unpack('V' . $colors, $this->readFile($colors * 4));
return array_values($palette);
}
/**
* @return string[][]
*/
private function readPixelArray(){
// there is a gap possible after the header
$this->file->fseek($this->header['offset'], SEEK_SET);
$pixelString = $this->readFile($this->header['imagesize']);
$bytesPerRow = intval(floor(($this->header['bits'] * $this->header['width'] + 31) / 32) * 4);
$plainPixelArray = str_split($pixelString, $bytesPerRow);
// Positive height: Bottom row first.
// Negative height: Upper row first
$plainPixelArray = ($this->header['height']<0) ? array_reverse($plainPixelArray) : $plainPixelArray;
$bytesPerColumn = $this->bytesPerDepth[$this->header['bits']];
$pixelArray = [];
foreach ($plainPixelArray as $pixelRow){
$pixelArray[] = str_split($pixelRow, $bytesPerColumn);
}
return $pixelArray;
}
/**
* @return resource
*/
private function pixelArrayToImage(){
$x = 0;
$y = 0;
foreach ($this->pixelArray as $pixelRow){
foreach ($pixelRow as $column){
$colors = $this->getColors($column);
foreach ($colors as $color) {
imagesetpixel($this->resource, $x, $y, $color);
$x++;
if ($x>=$this->header['width']){
$x=0;
break(2);
}
}
}
$y++;
if ($y >= abs($this->header['height'])){
break;
}
}
return $this->resource;
}
/**
* Get a color(s) of the current pixel(s)
* @param string $raw
* @return array
*/
private function getColors($raw){
$extra = chr(0); // used to complement an argument to word or double word
$colors = [];
if (in_array($this->header['bits'], [32, 24])) {
$colors = @unpack('V', $raw . $extra);
} elseif ($this->header['bits'] === 16) {
$colors = @unpack('v', $raw);
if (!isset($this->header['rMask']) || $this->header['rMask'] != 0xf800) {
$colors[1] = (($colors[1] & 0x7c00) >> 7) * 65536 + (($colors[1] & 0x03e0) >> 2) * 256 + (($colors[1] & 0x001f) << 3); // 555
} else {
$colors[1] = (($colors[1] & 0xf800) >> 8) * 65536 + (($colors[1] & 0x07e0) >> 3) * 256 + (($colors[1] & 0x001f) << 3); // 565
}
} elseif (in_array($this->header['bits'], [8, 4, 1])){
$colors = array_map(
function ($i){
return $this->palette[ bindec($i) ];
},
$this->splitByteIntoArray($raw, $this->header['bits'])
);
}
$colors = array_values($colors);
return $colors;
}
/**
* Split a byte into array of its binary digits
* @param string $byte a single char
* @param int $bitsPerPart how many bits should be in one part
* @return array
*/
private function splitByteIntoArray($byte, $bitsPerPart){
$code = ord($byte);
$stringOfBits = str_pad(decbin($code), 8, "0", \STR_PAD_LEFT);
return str_split($stringOfBits, $bitsPerPart);
}
/**
* @param string $bytesToRead
* @return string
*/
protected function readFile($bytesToRead){
$data = $this->file->fread($bytesToRead);
if ($data === false) {
throw new \LengthException('Unexpected end of file. ' . $this->getFilename());
}
return $data;
}
/**
* @codeCoverageIgnore
* @return string
*/
protected function getFilename(){
return $this->file->getFilename();
}
}
......@@ -35,6 +35,8 @@
*
*/
use OC\Image\BmpToResource;
/**
* Class for basic image manipulation
*/
......@@ -633,156 +635,22 @@ class OC_Image implements \OCP\IImage {
/**
* Create a new image from file or URL
*
* @link http://www.programmierer-forum.de/function-imagecreatefrombmp-laeuft-mit-allen-bitraten-t143137.htm
* @version 1.00
* @param string $fileName <p>
* Path to the BMP image.
* </p>
* @return bool|resource an image resource identifier on success, <b>FALSE</b> on errors.
*/
private function imagecreatefrombmp($fileName) {
if (!($fh = fopen($fileName, 'rb'))) {
$this->logger->warning('imagecreatefrombmp: Can not open ' . $fileName, ['app' => 'core']);
return false;
}
// read file header
$meta = unpack('vtype/Vfilesize/Vreserved/Voffset', fread($fh, 14));
// check for bitmap
if ($meta['type'] != 19778) {
fclose($fh);
$this->logger->warning('imagecreatefrombmp: Can not open ' . $fileName . ' is not a bitmap!', ['app' => 'core']);
return false;
}
// read image header
$meta += unpack('Vheadersize/Vwidth/Vheight/vplanes/vbits/Vcompression/Vimagesize/Vxres/Vyres/Vcolors/Vimportant', fread($fh, 40));
// read additional 16bit header
if ($meta['bits'] == 16) {
$meta += unpack('VrMask/VgMask/VbMask', fread($fh, 12));
}
// set bytes and padding
$meta['bytes'] = $meta['bits'] / 8;
$this->bitDepth = $meta['bits']; //remember the bit depth for the imagebmp call
$meta['decal'] = 4 - (4 * (($meta['width'] * $meta['bytes'] / 4) - floor($meta['width'] * $meta['bytes'] / 4)));
if ($meta['decal'] == 4) {
$meta['decal'] = 0;
}
// obtain imagesize
if ($meta['imagesize'] < 1) {
$meta['imagesize'] = $meta['filesize'] - $meta['offset'];
// in rare cases filesize is equal to offset so we need to read physical size
if ($meta['imagesize'] < 1) {
$meta['imagesize'] = @filesize($fileName) - $meta['offset'];
if ($meta['imagesize'] < 1) {
fclose($fh);
$this->logger->warning('imagecreatefrombmp: Can not obtain file size of ' . $fileName . ' is not a bitmap!', ['app' => 'core']);
return false;
}
}
}
// calculate colors
$meta['colors'] = !$meta['colors'] ? pow(2, $meta['bits']) : $meta['colors'];
// read color palette
$palette = [];
if ($meta['bits'] < 16) {
$palette = unpack('l' . $meta['colors'], fread($fh, $meta['colors'] * 4));
// in rare cases the color value is signed
if ($palette[1] < 0) {
foreach ($palette as $i => $color) {
$palette[$i] = $color + 16777216;
}
}
}
// create gd image
$im = imagecreatetruecolor($meta['width'], $meta['height']);
if ($im == false) {
fclose($fh);
$this->logger->warning(
'imagecreatefrombmp: imagecreatetruecolor failed for file "' . $fileName . '" with dimensions ' . $meta['width'] . 'x' . $meta['height'],
['app' => 'core']);
try {
$bmp = new BmpToResource($fileName);
$imageHandle = $bmp->toResource();
$imageDetails = $bmp->getHeader();
$this->bitDepth = $imageDetails['bits']; //remember the bit depth for the imagebmp call
} catch (\Exception $e){
$this->logger->warning($e->getMessage(), ['app' => 'core']);
return false;
}
$data = fread($fh, $meta['imagesize']);
$p = 0;
$vide = chr(0);
$y = $meta['height'] - 1;
$error = 'imagecreatefrombmp: ' . $fileName . ' has not enough data!';
// loop through the image data beginning with the lower left corner
while ($y >= 0) {
$x = 0;
while ($x < $meta['width']) {
switch ($meta['bits']) {
case 32:
case 24:
if (!($part = substr($data, $p, 3))) {
$this->logger->warning($error, ['app' => 'core']);
return $im;
}
$color = unpack('V', $part . $vide);
break;
case 16:
if (!($part = substr($data, $p, 2))) {
fclose($fh);
$this->logger->warning($error, ['app' => 'core']);
return $im;
}
$color = unpack('v', $part);
$color[1] = (($color[1] & 0xf800) >> 8) * 65536 + (($color[1] & 0x07e0) >> 3) * 256 + (($color[1] & 0x001f) << 3);
break;
case 8:
$color = unpack('n', $vide . substr($data, $p, 1));
$color[1] = $palette[$color[1] + 1];
break;
case 4:
$color = unpack('n', $vide . substr($data, floor($p), 1));
$color[1] = ($p * 2) % 2 == 0 ? $color[1] >> 4 : $color[1] & 0x0F;
$color[1] = $palette[$color[1] + 1];
break;
case 1:
$color = unpack('n', $vide . substr($data, floor($p), 1));
switch (($p * 8) % 8) {
case 0:
$color[1] = $color[1] >> 7;
break;
case 1:
$color[1] = ($color[1] & 0x40) >> 6;
break;
case 2:
$color[1] = ($color[1] & 0x20) >> 5;
break;
case 3:
$color[1] = ($color[1] & 0x10) >> 4;
break;
case 4:
$color[1] = ($color[1] & 0x8) >> 3;
break;
case 5:
$color[1] = ($color[1] & 0x4) >> 2;
break;
case 6:
$color[1] = ($color[1] & 0x2) >> 1;
break;
case 7:
$color[1] = ($color[1] & 0x1);
break;
}
$color[1] = $palette[$color[1] + 1];
break;
default:
fclose($fh);
$this->logger->warning('imagecreatefrombmp: ' . $fileName . ' has ' . $meta['bits'] . ' bits and this is not supported!', ['app' => 'core']);
return false;
}
imagesetpixel($im, $x, $y, $color[1]);
$x++;
$p += $meta['bytes'];
}
$y--;
$p += $meta['decal'];
}
fclose($fh);
return $im;
return $imageHandle;
}
/**
......
<?php
namespace Test\Image;
use OC\Image\BmpToResource;
use Test\TestCase;
class BmpToResourceTest extends TestCase {
public function test24bitBitmap(){
$instance = new BmpToResource(__DIR__ . '/../../data/image/24bit2x2.bmp');
$instance->toResource();
$header = $instance->getHeader();
$this->assertEquals(70, $header['filesize']);
$this->assertEquals(54, $header['offset']);
$this->assertEquals(2, $header['width']);
$this->assertEquals(2, $header['height']);
$this->assertEquals(24, $header['bits']);
$this->assertEquals(16, $header['imagesize']);
}
public function test4bitBitmap(){
$instance = new BmpToResource(__DIR__ . '/../../data/image/4bit2x2.bmp');
$instance->toResource();
$header = $instance->getHeader();
$this->assertEquals(78, $header['filesize']);
$this->assertEquals(70, $header['offset']);
$this->assertEquals(2, $header['width']);
$this->assertEquals(2, $header['height']);
$this->assertEquals(4, $header['bits']);
$this->assertEquals(8, $header['imagesize']);
}
public function testReadBitmapHeader(){
$headerHex = '42 4D 9A 00 00 00 00 00 00 00 7A 00 00 00';
$headerBin = hex2bin(str_replace(' ', '', $headerHex));
$stub = $this->getReadFileStub([$headerBin]);
$bitmapHeader = self::invokePrivate($stub, 'readBitmapHeader');
$this->assertEquals(154, $bitmapHeader['filesize']);
$this->assertEquals(122, $bitmapHeader['offset']);
}
/**
* @expectedException \DomainException
*/
public function testReadWrongBitmapSignature(){
$headerHex = '99 4D 9A 00 00 00 00 00 00 00 7A 00 00 00';
$headerBin = hex2bin(str_replace(' ', '', $headerHex));
$stub = $this->getReadFileStub([$headerBin]);
self::invokePrivate($stub, 'readBitmapHeader');
}
public function testReadDibHeader(){
$headerLengthHex = '6C 00 00 00 ';
$headerHex = '20 00 00 00 12 00 00 00 01 00 20 00 03 00 00 00 20 00 00 00 14 0B 00 00 13 0B 00 00 00 00 00 00 00 00 00 00';
$headerLengthBin = hex2bin(str_replace(' ', '', $headerLengthHex));
$headerBin = hex2bin(str_replace(' ', '', $headerHex));
$stub = $this->getReadFileStub([$headerLengthBin, $headerBin]);
$dibHeader = self::invokePrivate($stub, 'readDibHeader');
$this->assertEquals(32, $dibHeader['width']);
$this->assertEquals(18, $dibHeader['height']);
$this->assertEquals(1, $dibHeader['planes']);
$this->assertEquals(32, $dibHeader['bits']);
$this->assertEquals(BmpToResource::COMPRESSION_BI_BITFIELDS, $dibHeader['compression']);
$this->assertEquals(32, $dibHeader['imagesize']);
$this->assertEquals(2836, $dibHeader['xres']);
$this->assertEquals(2835, $dibHeader['yres']);
$this->assertEquals(pow(2, $dibHeader['bits']), $dibHeader['colors']);
$this->assertEquals(0, $dibHeader['important']);
}
/**
* @expectedException \UnexpectedValueException
*/
public function testReadUnsupportedDibHeaderBitDepth(){
$headerLengthHex = '6C 00 00 00 ';
$headerHex = '20 00 00 00 12 00 00 00 01 00 07 00 03 00 00 00 20 00 00 00 14 0B 00 00 13 0B 00 00 00 00 00 00 00 00 00 00';
$headerLengthBin = hex2bin(str_replace(' ', '', $headerLengthHex));
$headerBin = hex2bin(str_replace(' ', '', $headerHex));
$stub = $this->getReadFileStub([$headerLengthBin, $headerBin]);
self::invokePrivate($stub, 'readDibHeader');
}
/**
* @expectedException \UnexpectedValueException
*/
public function testReadWrongDibHeaderLength(){
$headerHex = '10 00 00 00 00 00 00 00 00 00 7A 00 00 00';
$headerBin = hex2bin(str_replace(' ', '', $headerHex));
$stub = $this->getReadFileStub([$headerBin]);
self::invokePrivate($stub, 'readDibHeader');
}
public function testReadBitMasks(){
$bitMasksHex = '00 00 FF 00 00 FF 00 00 FF 00 00 00';
$bitMasksBin = hex2bin(str_replace(' ', '', $bitMasksHex));
$stub = $this->getReadFileStub([$bitMasksBin]);
$bitMasks = self::invokePrivate($stub, 'readBitMasks');
$this->assertEquals(0xFF0000, $bitMasks['rMask']);
$this->assertEquals(0x00FF00, $bitMasks['gMask']);
$this->assertEquals(0x0000FF, $bitMasks['bMask']);
}
public function testReadColorTable(){
$colors = 2;
// Little Endian: B G R 00 B G R 00
$colorTableHex = '0A FF 32 00 34 56 EF 00';
$colorTableBin = hex2bin(str_replace(' ', '', $colorTableHex));
$stub = $this->getReadFileStub([$colorTableBin]);
$colorTable = self::invokePrivate($stub, 'readColorTable', [$colors]);
$this->assertEquals(2, count($colorTable));
$this->assertEquals([0 => 0x32ff0a, 1 => 0xef5634], $colorTable);
}
/**
* @dataProvider bytesProvider
* @param $char
* @param $bitsPerPart
* @param $expectedArray