Related
I would like to draw in a PDF file.
Example: Open the PDF file and get such drawing tools like circle, square, text etc... Using these tools will draw shapes on the PDF file.
I searched on google and found such options like pdf.js. But it's not implemented in core PHP or normal MVC structure. It's implemented in JavaScript.
Any alternative for pdf.js to draw the shape in a PDF file?
I am looking for the same as the example shown here.
Last year when I came upon the same problem I researched and after some tweaking and adjustments I managed to make it work. So here's a detailed explanation on how to set up and use my method.
I'm using a combination of two libraries:
FPDF: (Free Portable Document Format) which allows to generate PDF files with PHP
FPDI: (Free Portable Document Importer) which uses existing PDF and converts them to templates to used by FPDF
First, you'll need to download the two libraries: FPDF is found here in the download section and FPDI is on this page. You will be given two folders. Go ahead and add them to your project.
Here's my directory structure:
Let's go in index.php (or any other file for that matter) and edit a PDF file that we'll name sample.pdf. I had found some code from the official documentation but actually did some modifications to simplify it.
You will see I have added the method nextPage() to the PDF class to make the navigation between pages easier.
<?php
require_once('FPDF/fpdf.php');
require_once('FPDI/fpdi.php');
// path of PDF file
$fullPathToFile = "sample.pdf";
class PDF extends FPDI {
var $fileIndex;
var $currentPage = 1;
function Header() {
global $fullPathToFile;
if (is_null($this->fileIndex)) {
$this->numPages = $this->setSourceFile($fullPathToFile);
$this->fileIndex = $this->importPage(1);
} $this->useTemplate($this->fileIndex, 0, 0,200);
}
function nextPage() {
if($this->currentPage != 1) {
$this->fileIndex = $this->importPage($this->currentPage);
}
$this->addPage();
return ++$this->currentPage;
}
}
// initiate PDF
$pdf = new PDF();
// go to first page
$pdf->nextPage();
// add content to current page
$pdf->SetFont("helvetica", "", 20);
$pdf->SetTextColor(220, 20, 60);
$pdf->Text(50, 20, "I should not be here!");
// move to next page and add content
$pdf->nextPage();
$pdf->SetFont("arial", "", 15);
$pdf->SetTextColor(65, 105, 225);
$pdf->Text(50, 20, "Me neither!!!");
//show the PDF in page
$pdf->Output();
The Output() method can receive different arguments: you can simply output the PDF file in a frame or you can force the download of the PDF file to the user's computer. Read here for more information on that.
DEMO!
FPDF's community has written several scripts one of which might interest you:
it's the geometric figures FPDF plugin (id script69.php). It allows you to draw lines, rectangles, curves, ellipses, circles, polygon among others.
Here's a bonus for you:
Create a new file called draw.php and put the source code provided on here. I have provided the source code below (the first three lines are different than the original source code to make it work).
<?php
require_once('FPDF/fpdf.php');
require_once('FPDI/fpdi.php');
class PDF_Draw extends FPDI {
// Sets line style
// Parameters:
// - style: Line style. Array with keys among the following:
// . width: Width of the line in user units
// . cap: Type of cap to put on the line (butt, round, square). The difference between 'square' and 'butt' is that 'square' projects a flat end past the end of the line.
// . join: miter, round or bevel
// . dash: Dash pattern. Is 0 (without dash) or array with series of length values, which are the lengths of the on and off dashes.
// For example: (2) represents 2 on, 2 off, 2 on , 2 off ...
// (2,1) is 2 on, 1 off, 2 on, 1 off.. etc
// . phase: Modifier of the dash pattern which is used to shift the point at which the pattern starts
// . color: Draw color. Array with components (red, green, blue)
function SetLineStyle($style) {
extract($style);
if (isset($width)) {
$width_prev = $this->LineWidth;
$this->SetLineWidth($width);
$this->LineWidth = $width_prev;
}
if (isset($cap)) {
$ca = array('butt' => 0, 'round'=> 1, 'square' => 2);
if (isset($ca[$cap]))
$this->_out($ca[$cap] . ' J');
}
if (isset($join)) {
$ja = array('miter' => 0, 'round' => 1, 'bevel' => 2);
if (isset($ja[$join]))
$this->_out($ja[$join] . ' j');
}
if (isset($dash)) {
$dash_string = '';
if ($dash) {
$tab = explode(',', $dash);
$dash_string = '';
foreach ($tab as $i => $v) {
if ($i > 0)
$dash_string .= ' ';
$dash_string .= sprintf('%.2F', $v);
}
}
if (!isset($phase) || !$dash)
$phase = 0;
$this->_out(sprintf('[%s] %.2F d', $dash_string, $phase));
}
if (isset($color)) {
list($r, $g, $b) = $color;
$this->SetDrawColor($r, $g, $b);
}
}
// Draws a line
// Parameters:
// - x1, y1: Start point
// - x2, y2: End point
// - style: Line style. Array like for SetLineStyle
function Line($x1, $y1, $x2, $y2, $style = null) {
if ($style)
$this->SetLineStyle($style);
parent::Line($x1, $y1, $x2, $y2);
}
// Draws a rectangle
// Parameters:
// - x, y: Top left corner
// - w, h: Width and height
// - style: Style of rectangle (draw and/or fill: D, F, DF, FD)
// - border_style: Border style of rectangle. Array with some of this index
// . all: Line style of all borders. Array like for SetLineStyle
// . L: Line style of left border. null (no border) or array like for SetLineStyle
// . T: Line style of top border. null (no border) or array like for SetLineStyle
// . R: Line style of right border. null (no border) or array like for SetLineStyle
// . B: Line style of bottom border. null (no border) or array like for SetLineStyle
// - fill_color: Fill color. Array with components (red, green, blue)
function Rect($x, $y, $w, $h, $style = '', $border_style = null, $fill_color = null) {
if (!(false === strpos($style, 'F')) && $fill_color) {
list($r, $g, $b) = $fill_color;
$this->SetFillColor($r, $g, $b);
}
switch ($style) {
case 'F':
$border_style = null;
parent::Rect($x, $y, $w, $h, $style);
break;
case 'DF': case 'FD':
if (!$border_style || isset($border_style['all'])) {
if (isset($border_style['all'])) {
$this->SetLineStyle($border_style['all']);
$border_style = null;
}
} else
$style = 'F';
parent::Rect($x, $y, $w, $h, $style);
break;
default:
if (!$border_style || isset($border_style['all'])) {
if (isset($border_style['all']) && $border_style['all']) {
$this->SetLineStyle($border_style['all']);
$border_style = null;
}
parent::Rect($x, $y, $w, $h, $style);
}
break;
}
if ($border_style) {
if (isset($border_style['L']) && $border_style['L'])
$this->Line($x, $y, $x, $y + $h, $border_style['L']);
if (isset($border_style['T']) && $border_style['T'])
$this->Line($x, $y, $x + $w, $y, $border_style['T']);
if (isset($border_style['R']) && $border_style['R'])
$this->Line($x + $w, $y, $x + $w, $y + $h, $border_style['R']);
if (isset($border_style['B']) && $border_style['B'])
$this->Line($x, $y + $h, $x + $w, $y + $h, $border_style['B']);
}
}
// Draws a Bézier curve (the Bézier curve is tangent to the line between the control points at either end of the curve)
// Parameters:
// - x0, y0: Start point
// - x1, y1: Control point 1
// - x2, y2: Control point 2
// - x3, y3: End point
// - style: Style of rectangule (draw and/or fill: D, F, DF, FD)
// - line_style: Line style for curve. Array like for SetLineStyle
// - fill_color: Fill color. Array with components (red, green, blue)
function Curve($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3, $style = '', $line_style = null, $fill_color = null) {
if (!(false === strpos($style, 'F')) && $fill_color) {
list($r, $g, $b) = $fill_color;
$this->SetFillColor($r, $g, $b);
}
switch ($style) {
case 'F':
$op = 'f';
$line_style = null;
break;
case 'FD': case 'DF':
$op = 'B';
break;
default:
$op = 'S';
break;
}
if ($line_style)
$this->SetLineStyle($line_style);
$this->_Point($x0, $y0);
$this->_Curve($x1, $y1, $x2, $y2, $x3, $y3);
$this->_out($op);
}
// Draws an ellipse
// Parameters:
// - x0, y0: Center point
// - rx, ry: Horizontal and vertical radius (if ry = 0, draws a circle)
// - angle: Orientation angle (anti-clockwise)
// - astart: Start angle
// - afinish: Finish angle
// - style: Style of ellipse (draw and/or fill: D, F, DF, FD, C (D + close))
// - line_style: Line style for ellipse. Array like for SetLineStyle
// - fill_color: Fill color. Array with components (red, green, blue)
// - nSeg: Ellipse is made up of nSeg Bézier curves
function Ellipse($x0, $y0, $rx, $ry = 0, $angle = 0, $astart = 0, $afinish = 360, $style = '', $line_style = null, $fill_color = null, $nSeg = 8) {
if ($rx) {
if (!(false === strpos($style, 'F')) && $fill_color) {
list($r, $g, $b) = $fill_color;
$this->SetFillColor($r, $g, $b);
}
switch ($style) {
case 'F':
$op = 'f';
$line_style = null;
break;
case 'FD': case 'DF':
$op = 'B';
break;
case 'C':
$op = 's'; // small 's' means closing the path as well
break;
default:
$op = 'S';
break;
}
if ($line_style)
$this->SetLineStyle($line_style);
if (!$ry)
$ry = $rx;
$rx *= $this->k;
$ry *= $this->k;
if ($nSeg < 2)
$nSeg = 2;
$astart = deg2rad((float) $astart);
$afinish = deg2rad((float) $afinish);
$totalAngle = $afinish - $astart;
$dt = $totalAngle/$nSeg;
$dtm = $dt/3;
$x0 *= $this->k;
$y0 = ($this->h - $y0) * $this->k;
if ($angle != 0) {
$a = -deg2rad((float) $angle);
$this->_out(sprintf('q %.2F %.2F %.2F %.2F %.2F %.2F cm', cos($a), -1 * sin($a), sin($a), cos($a), $x0, $y0));
$x0 = 0;
$y0 = 0;
}
$t1 = $astart;
$a0 = $x0 + ($rx * cos($t1));
$b0 = $y0 + ($ry * sin($t1));
$c0 = -$rx * sin($t1);
$d0 = $ry * cos($t1);
$this->_Point($a0 / $this->k, $this->h - ($b0 / $this->k));
for ($i = 1; $i <= $nSeg; $i++) {
// Draw this bit of the total curve
$t1 = ($i * $dt) + $astart;
$a1 = $x0 + ($rx * cos($t1));
$b1 = $y0 + ($ry * sin($t1));
$c1 = -$rx * sin($t1);
$d1 = $ry * cos($t1);
$this->_Curve(($a0 + ($c0 * $dtm)) / $this->k,
$this->h - (($b0 + ($d0 * $dtm)) / $this->k),
($a1 - ($c1 * $dtm)) / $this->k,
$this->h - (($b1 - ($d1 * $dtm)) / $this->k),
$a1 / $this->k,
$this->h - ($b1 / $this->k));
$a0 = $a1;
$b0 = $b1;
$c0 = $c1;
$d0 = $d1;
}
$this->_out($op);
if ($angle !=0)
$this->_out('Q');
}
}
// Draws a circle
// Parameters:
// - x0, y0: Center point
// - r: Radius
// - astart: Start angle
// - afinish: Finish angle
// - style: Style of circle (draw and/or fill) (D, F, DF, FD, C (D + close))
// - line_style: Line style for circle. Array like for SetLineStyle
// - fill_color: Fill color. Array with components (red, green, blue)
// - nSeg: Ellipse is made up of nSeg Bézier curves
function Circle($x0, $y0, $r, $astart = 0, $afinish = 360, $style = '', $line_style = null, $fill_color = null, $nSeg = 8) {
$this->Ellipse($x0, $y0, $r, 0, 0, $astart, $afinish, $style, $line_style, $fill_color, $nSeg);
}
// Draws a polygon
// Parameters:
// - p: Points. Array with values x0, y0, x1, y1,..., x(np-1), y(np - 1)
// - style: Style of polygon (draw and/or fill) (D, F, DF, FD)
// - line_style: Line style. Array with one of this index
// . all: Line style of all lines. Array like for SetLineStyle
// . 0..np-1: Line style of each line. Item is 0 (not line) or like for SetLineStyle
// - fill_color: Fill color. Array with components (red, green, blue)
function Polygon($p, $style = '', $line_style = null, $fill_color = null) {
$np = count($p) / 2;
if (!(false === strpos($style, 'F')) && $fill_color) {
list($r, $g, $b) = $fill_color;
$this->SetFillColor($r, $g, $b);
}
switch ($style) {
case 'F':
$line_style = null;
$op = 'f';
break;
case 'FD': case 'DF':
$op = 'B';
break;
default:
$op = 'S';
break;
}
$draw = true;
if ($line_style)
if (isset($line_style['all']))
$this->SetLineStyle($line_style['all']);
else { // 0 .. (np - 1), op = {B, S}
$draw = false;
if ('B' == $op) {
$op = 'f';
$this->_Point($p[0], $p[1]);
for ($i = 2; $i < ($np * 2); $i = $i + 2)
$this->_Line($p[$i], $p[$i + 1]);
$this->_Line($p[0], $p[1]);
$this->_out($op);
}
$p[$np * 2] = $p[0];
$p[($np * 2) + 1] = $p[1];
for ($i = 0; $i < $np; $i++)
if (!empty($line_style[$i]))
$this->Line($p[$i * 2], $p[($i * 2) + 1], $p[($i * 2) + 2], $p[($i * 2) + 3], $line_style[$i]);
}
if ($draw) {
$this->_Point($p[0], $p[1]);
for ($i = 2; $i < ($np * 2); $i = $i + 2)
$this->_Line($p[$i], $p[$i + 1]);
$this->_Line($p[0], $p[1]);
$this->_out($op);
}
}
// Draws a regular polygon
// Parameters:
// - x0, y0: Center point
// - r: Radius of circumscribed circle
// - ns: Number of sides
// - angle: Orientation angle (anti-clockwise)
// - circle: Draw circumscribed circle or not
// - style: Style of polygon (draw and/or fill) (D, F, DF, FD)
// - line_style: Line style. Array with one of this index
// . all: Line style of all lines. Array like for SetLineStyle
// . 0..ns-1: Line style of each line. Item is 0 (not line) or like for SetLineStyle
// - fill_color: Fill color. Array with components (red, green, blue)
// - circle_style: Style of circumscribed circle (draw and/or fill) (D, F, DF, FD) (if draw)
// - circle_line_style: Line style for circumscribed circle. Array like for SetLineStyle (if draw)
// - circle_fill_color: Fill color for circumscribed circle. Array with components (red, green, blue) (if draw fill circle)
function RegularPolygon($x0, $y0, $r, $ns, $angle = 0, $circle = false, $style = '', $line_style = null, $fill_color = null, $circle_style = '', $circle_line_style = null, $circle_fill_color = null) {
if ($ns < 3)
$ns = 3;
if ($circle)
$this->Circle($x0, $y0, $r, 0, 360, $circle_style, $circle_line_style, $circle_fill_color);
$p = null;
for ($i = 0; $i < $ns; $i++) {
$a = $angle + ($i * 360 / $ns);
$a_rad = deg2rad((float) $a);
$p[] = $x0 + ($r * sin($a_rad));
$p[] = $y0 + ($r * cos($a_rad));
}
$this->Polygon($p, $style, $line_style, $fill_color);
}
// Draws a star polygon
// Parameters:
// - x0, y0: Center point
// - r: Radius of circumscribed circle
// - nv: Number of vertices
// - ng: Number of gaps (ng % nv = 1 => regular polygon)
// - angle: Orientation angle (anti-clockwise)
// - circle: Draw circumscribed circle or not
// - style: Style of polygon (draw and/or fill) (D, F, DF, FD)
// - line_style: Line style. Array with one of this index
// . all: Line style of all lines. Array like for SetLineStyle
// . 0..n-1: Line style of each line. Item is 0 (not line) or like for SetLineStyle
// - fill_color: Fill color. Array with components (red, green, blue)
// - circle_style: Style of circumscribed circle (draw and/or fill) (D, F, DF, FD) (if draw)
// - circle_line_style: Line style for circumscribed circle. Array like for SetLineStyle (if draw)
// - circle_fill_color: Fill color for circumscribed circle. Array with components (red, green, blue) (if draw fill circle)
function StarPolygon($x0, $y0, $r, $nv, $ng, $angle = 0, $circle = false, $style = '', $line_style = null, $fill_color = null, $circle_style = '', $circle_line_style = null, $circle_fill_color = null) {
if ($nv < 2)
$nv = 2;
if ($circle)
$this->Circle($x0, $y0, $r, 0, 360, $circle_style, $circle_line_style, $circle_fill_color);
$p2 = null;
$visited = null;
for ($i = 0; $i < $nv; $i++) {
$a = $angle + ($i * 360 / $nv);
$a_rad = deg2rad((float) $a);
$p2[] = $x0 + ($r * sin($a_rad));
$p2[] = $y0 + ($r * cos($a_rad));
$visited[] = false;
}
$p = null;
$i = 0;
do {
$p[] = $p2[$i * 2];
$p[] = $p2[($i * 2) + 1];
$visited[$i] = true;
$i += $ng;
$i %= $nv;
} while (!$visited[$i]);
$this->Polygon($p, $style, $line_style, $fill_color);
}
// Draws a rounded rectangle
// Parameters:
// - x, y: Top left corner
// - w, h: Width and height
// - r: Radius of the rounded corners
// - round_corner: Draws rounded corner or not. String with a 0 (not rounded i-corner) or 1 (rounded i-corner) in i-position. Positions are, in order and begin to 0: top left, top right, bottom right and bottom left
// - style: Style of rectangle (draw and/or fill) (D, F, DF, FD)
// - border_style: Border style of rectangle. Array like for SetLineStyle
// - fill_color: Fill color. Array with components (red, green, blue)
function RoundedRect($x, $y, $w, $h, $r, $round_corner = '1111', $style = '', $border_style = null, $fill_color = null) {
if ('0000' == $round_corner) // Not rounded
$this->Rect($x, $y, $w, $h, $style, $border_style, $fill_color);
else { // Rounded
if (!(false === strpos($style, 'F')) && $fill_color) {
list($red, $g, $b) = $fill_color;
$this->SetFillColor($red, $g, $b);
}
switch ($style) {
case 'F':
$border_style = null;
$op = 'f';
break;
case 'FD': case 'DF':
$op = 'B';
break;
default:
$op = 'S';
break;
}
if ($border_style)
$this->SetLineStyle($border_style);
$MyArc = 4 / 3 * (sqrt(2) - 1);
$this->_Point($x + $r, $y);
$xc = $x + $w - $r;
$yc = $y + $r;
$this->_Line($xc, $y);
if ($round_corner[0])
$this->_Curve($xc + ($r * $MyArc), $yc - $r, $xc + $r, $yc - ($r * $MyArc), $xc + $r, $yc);
else
$this->_Line($x + $w, $y);
$xc = $x + $w - $r ;
$yc = $y + $h - $r;
$this->_Line($x + $w, $yc);
if ($round_corner[1])
$this->_Curve($xc + $r, $yc + ($r * $MyArc), $xc + ($r * $MyArc), $yc + $r, $xc, $yc + $r);
else
$this->_Line($x + $w, $y + $h);
$xc = $x + $r;
$yc = $y + $h - $r;
$this->_Line($xc, $y + $h);
if ($round_corner[2])
$this->_Curve($xc - ($r * $MyArc), $yc + $r, $xc - $r, $yc + ($r * $MyArc), $xc - $r, $yc);
else
$this->_Line($x, $y + $h);
$xc = $x + $r;
$yc = $y + $r;
$this->_Line($x, $yc);
if ($round_corner[3])
$this->_Curve($xc - $r, $yc - ($r * $MyArc), $xc - ($r * $MyArc), $yc - $r, $xc, $yc - $r);
else {
$this->_Line($x, $y);
$this->_Line($x + $r, $y);
}
$this->_out($op);
}
}
/* PRIVATE METHODS */
// Sets a draw point
// Parameters:
// - x, y: Point
function _Point($x, $y) {
$this->_out(sprintf('%.2F %.2F m', $x * $this->k, ($this->h - $y) * $this->k));
}
// Draws a line from last draw point
// Parameters:
// - x, y: End point
function _Line($x, $y) {
$this->_out(sprintf('%.2F %.2F l', $x * $this->k, ($this->h - $y) * $this->k));
}
// Draws a Bézier curve from last draw point
// Parameters:
// - x1, y1: Control point 1
// - x2, y2: Control point 2
// - x3, y3: End point
function _Curve($x1, $y1, $x2, $y2, $x3, $y3) {
$this->_out(sprintf('%.2F %.2F %.2F %.2F %.2F %.2F c', $x1 * $this->k, ($this->h - $y1) * $this->k, $x2 * $this->k, ($this->h - $y2) * $this->k, $x3 * $this->k, ($this->h - $y3) * $this->k));
}
}
?>
Don't forget to add require_once('FPDF/fpdf.php'); and require_once('FPDI/fpdi.php'); at the top of the page assuming you have this directory structure:
Then with code in index.php instead of extending the class FPDI we can directly extend PDF_Draw. This way the code we add earlier still work but now we can use new methods like Line(), Curve(), Rect() etc...
Here is the full index.php code:
<?php
require_once('draw.php');
// path of PDF file
$fullPathToFile = "sample.pdf";
class PDF extends PDF_Draw {
.
.
.
}
// initiate PDF
$pdf = new PDF();
// go to first page
$pdf->nextPage();
// add content to current page
$pdf->SetFont("helvetica", "", 20);
$pdf->SetTextColor(220, 20, 60);
$pdf->Text(50, 20, "I should not be here!");
// move to next page and add content
$pdf->nextPage();
$pdf->SetFont("arial", "", 15);
$style = array('width' => 0.5, 'cap' => 'butt', 'join' => 'miter', 'dash' => '10,20,5,10', 'phase' => 10, 'color' => array(255, 0, 0));
$style2 = array('width' => 0.5, 'cap' => 'butt', 'join' => 'miter', 'dash' => 0, 'color' => array(255, 0, 0));
$style3 = array('width' => 1, 'cap' => 'round', 'join' => 'round', 'dash' => '2,10', 'color' => array(255, 0, 0));
$style4 = array('L' => 0,
'T' => array('width' => 0.25, 'cap' => 'butt', 'join' => 'miter', 'dash' => '20,10', 'phase' => 10, 'color' => array(100, 100, 255)),
'R' => array('width' => 0.50, 'cap' => 'round', 'join' => 'miter', 'dash' => 0, 'color' => array(50, 50, 127)),
'B' => array('width' => 0.75, 'cap' => 'square', 'join' => 'miter', 'dash' => '30,10,5,10'));
$style5 = array('width' => 0.25, 'cap' => 'butt', 'join' => 'miter', 'dash' => 0, 'color' => array(0, 0, 0));
$style6 = array('width' => 0.5, 'cap' => 'butt', 'join' => 'miter', 'dash' => '10,10', 'color' => array(0, 255, 0));
$style7 = array('width' => 0.5, 'cap' => 'butt', 'join' => 'miter', 'dash' => 0, 'color' => array(200, 200, 0));
// Line
$pdf->Text(5, 7, 'Line');
$pdf->Line(5, 10, 80, 30, $style);
// Rect
$pdf->Text(100, 7, 'Rectangle');
$pdf->Rect(100, 10, 40, 20, 'DF', $style4, array(220, 220, 200));
// Curve
$pdf->Text(5, 37, 'Curve');
$pdf->Curve(5, 40, 30, 55, 70, 45, 60, 75, null, $style6);
// Circle and ellipse
$pdf->Text(5, 82, 'Circle and ellipse');
$pdf->SetLineStyle($style5);
$pdf->Circle(25,105,20);
// Polygon
$pdf->Text(5, 132, 'Polygon');
$pdf->SetLineStyle(array('width' => 0.5, 'cap' => 'butt', 'join' => 'miter', 'dash' => 0, 'color' => array(0, 0, 0)));
$pdf->Polygon(array(5,135,45,135,15,165));
// Regular polygon
$pdf->Text(5, 172, 'Regular polygon');
$pdf->SetLineStyle($style5);
$pdf->RegularPolygon(20, 190, 15, 6, 0, 1, 'F');
// Star polygon
$pdf->Text(5, 212, 'Star polygon');
$pdf->SetLineStyle($style5);
$pdf->StarPolygon(20, 230, 15, 20, 3, 0, 1, 'F');
// Rounded rectangle
$pdf->Text(5, 252, 'Rounded rectangle');
$pdf->SetLineStyle(array('width' => 0.5, 'cap' => 'butt', 'join' => 'miter', 'dash' => 0, 'color' => array(0, 0, 0)));
$pdf->RoundedRect(5, 255, 40, 30, 3.50, '1111', 'DF');
$pdf->SetTextColor(65, 105, 225);
$pdf->Text(50, 20, "Me neither!!!");
//show the PDF in page
$pdf->Output();
DEMO!
TCPDF (tcpdf.org) seems to handle PDF graphics methods.
Cf. examples/example_012.php:
// create new PDF document
$pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
// Line
$pdf->Text(5, 4, 'Line examples');
$pdf->Line(5, 10, 80, 30, $style);
$pdf->Line(5, 10, 5, 30, $style2);
$pdf->Line(5, 10, 80, 10, $style3);
// Rect
$pdf->Text(100, 4, 'Rectangle examples');
$pdf->Rect(100, 10, 40, 20, 'DF', $style4, array(220, 220, 200));
$pdf->Rect(145, 10, 40, 20, 'D', array('all' => $style3));
And its GitHub project tecnickcom/tcpdf indicated being 100% PHP.
However, this search shows it can create and modify a new PDF document. It might not be able to open and modify an existing one.
http://www.fpdf.org
If You want to draw using x, y coordinates
fpdf as well as tcpdf can be useful for you.
global $title;
// Calculate width
$w = $this->GetStringWidth($title)+6;
$this->SetX((210-$w)/2);
// Colors of frame, background and text
$this->SetDrawColor(0,80,180);
$this->SetFillColor(230,230,0);
$this->SetTextColor(220,50,50);
// Thickness of frame (1 mm)
$this->SetLineWidth(1);
// Title
$this->Cell($w,9,$title,1,1,'C',true);
// Line break
$this->Ln(10);
}
Background: I am working on a site for small shopping center, which has multiple rectangular "units" to rent. When a "shop" comes, it can rent one or multiple "units", and I'd like to generate a map consisting of shops (sans unrented units)
Problem:
I have a list of rectangles (units), defined by pairs of points – [[lefttop_x;lefttop_y];[rightbottom_x;rightbottom_y]] – and I want to merge them into polygons, so I can properly style them (which I will then render via Canvas/SVG/VML/Raphael.js).
Units are always rectangles
Units have different sizes
Units are always adjacent (there is no space between them)
As a result of this (preferably PHP, but I can deal with pseudocode) operation, I'd like to have an array of polygons points.
Thank you.
P.S.: I've been looking into this, and I found multiple 'close to what I want' questions+answers, but I am either too tired or I've been out of touch with maths for too long :)
O'Rourke has studied a problem that is related to this one (along many others that relate to Computational Geometry) and as a consequence, produced a very beautiful method to efficiently solve it. His method is described in the paper Uniqueness of orthogonal connect-the-dots and is also clearly illustrated at http://www.cs.mcgill.ca/~cs644/Godfried/2005/Fall/sdroui9/p0_introduction.html. Note that it says that the polygon shouldn't share vertices in order to apply this method, but this happens very often in the problem we are discussing here. Thus, all we need to do is to eliminate vertices that are shared. Note that this still produces a polygon, and it is the polygon that is wanted as result. Also observe that the list of rectangles must not contain duplicates (I will assume that is the case, otherwise preprocess it to make the list unique).
I've used Python to code it, and if there is any doubt about its meaning, feel free to ask.
Here is an overview of the implementation. We start with a list of rectangles described according to OP's notation. Then we obtain the four vertices of each rectangle, discarding shared vertices along the way. This is efficiently achieved using a set. Now we simply apply the algorithm mentioned. Note that I use two hash tables, edges_h (for horizontal edges) and edges_v (for vertical edges), to store the polygon edges. These hash tables effectively work as an undirected graph. Thus, after all the edges are obtained it is easy and fast to obtain the ordered vertices of the polygon. Pick any (key, value) from the hash table edges_h, for example. Now, the next ordered vertex is the one given by edges_v[value] = next_value, and the next one by edges_h[next_value] and so on. Repeat this process till we hit the first chosen vertex, and it is done.
A quick view into the mentioned algorithm is:
Sort points by lowest x, lowest y
Go through each column and create edges between the vertices 2i and 2i + 1 in that column
Sort points by lowest y, lowest x
Go through each row and create edges between the vertices 2i and 2i + 1 in that row.
# These rectangles resemble the OP's illustration.
rect = ([[0, 10], [10, 0]],
[[10, 13], [19, 0]],
[[19, 10], [23, 0]])
points = set()
for (x1, y1), (x2, y2) in rect:
for pt in ((x1, y1), (x2, y1), (x2, y2), (x1, y2)):
if pt in points: # Shared vertice, remove it.
points.remove(pt)
else:
points.add(pt)
points = list(points)
def y_then_x(a, b):
if a[1] < b[1] or (a[1] == b[1] and a[0] < b[0]):
return -1
elif a == b:
return 0
else:
return 1
sort_x = sorted(points)
sort_y = sorted(points, cmp=y_then_x)
edges_h = {}
edges_v = {}
i = 0
while i < len(points):
curr_y = sort_y[i][1]
while i < len(points) and sort_y[i][1] == curr_y: //6chars comments, remove it
edges_h[sort_y[i]] = sort_y[i + 1]
edges_h[sort_y[i + 1]] = sort_y[i]
i += 2
i = 0
while i < len(points):
curr_x = sort_x[i][0]
while i < len(points) and sort_x[i][0] == curr_x:
edges_v[sort_x[i]] = sort_x[i + 1]
edges_v[sort_x[i + 1]] = sort_x[i]
i += 2
# Get all the polygons.
p = []
while edges_h:
# We can start with any point.
polygon = [(edges_h.popitem()[0], 0)]
while True:
curr, e = polygon[-1]
if e == 0:
next_vertex = edges_v.pop(curr)
polygon.append((next_vertex, 1))
else:
next_vertex = edges_h.pop(curr)
polygon.append((next_vertex, 0))
if polygon[-1] == polygon[0]:
# Closed polygon
polygon.pop()
break
# Remove implementation-markers from the polygon.
poly = [point for point, _ in polygon]
for vertex in poly:
if vertex in edges_h: edges_h.pop(vertex)
if vertex in edges_v: edges_v.pop(vertex)
p.append(poly)
for poly in p:
print poly
the result is a list of ordered vertices for the polygon:
[(0, 0), (0, 10), (10, 10), (10, 13), (19, 13), (19, 10), (23, 10), (23, 0)]
We can also experiment with a more complicated layout:
rect = ([[1, 2], [3, 1]], [[1, 4], [2, 2]], [[1, 6], [2, 4]], [[2, 6], [3, 5]],
[[3, 8], [4, 4]], [[2, 8], [3, 7]], [[3, 10], [5, 8]], [[3, 4], [9, 3]],
[[4, 5], [7, 4]], [[6, 8], [7, 5]], [[6, 9], [8, 8]], [[8, 9], [10, 6]],
[[9, 6], [10, 3]])
which is represented as the following set of rectangles:
and the method produces the following lists:
[(6, 9), (6, 5), (4, 5), (4, 8), (5, 8), (5, 10), (3, 10), (3, 8),
(2, 8), (2, 7), (3, 7), (3, 6), (1, 6), (1, 1), (3, 1), (3, 2),
(2, 2), (2, 5), (3, 5), (3, 3), (10, 3), (10, 9)]
[(9, 4), (9, 6), (8, 6), (8, 8), (7, 8), (7, 4)]
which, if drawn, represents the polygons in blue and red, respectively, as in:
As simple benchmarks go:
1000 rectangles: ~ 0.04 seconds
10000 rectangles: ~ 0.62 seconds
100000 rectangles: ~ 8.68 seconds
These timings are simply the average of 10 runs on a busy outdated machine. Rectangles were randomly generated.
EDIT:
Implementation in PHP if needed.
Here is my solution:
RectUnion.php
<?php
class RectUnion {
private $x, $y;
private $sides;
private $points;
function __construct() {
$this->x = array();
$this->y = array();
$this->sides = array();
$this->points = array();
}
function addRect($r) {
extract($r);
$this->x[] = $x1;
$this->x[] = $x2;
$this->y[] = $y1;
$this->y[] = $y2;
if ($x1 > $x2) { $tmp = $x1; $x1 = $x2; $x2 = $tmp; }
if ($y1 > $y2) { $tmp = $y1; $y1 = $y2; $y2 = $tmp; }
$this->sides[] = array($x1, $y1, $x2, $y1);
$this->sides[] = array($x2, $y1, $x2, $y2);
$this->sides[] = array($x1, $y2, $x2, $y2);
$this->sides[] = array($x1, $y1, $x1, $y2);
}
function splitSides() {
$result = array();
$this->x = array_unique($this->x);
$this->y = array_unique($this->y);
sort($this->x);
sort($this->y);
foreach ($this->sides as $i => $s) {
if ($s[0] - $s[2]) { // Horizontal
foreach ($this->x as $xx) {
if (($xx > $s[0]) && ($xx < $s[2])) {
$result[] = array($s[0], $s[1], $xx, $s[3]);
$s[0] = $xx;
}
}
} else { // Vertical
foreach ($this->y as $yy) {
if (($yy > $s[1]) && ($yy < $s[3])) {
$result[] = array($s[0], $s[1], $s[2], $yy);
$s[1] = $yy;
}
}
}
$result[] = $s;
}
return($result);
}
function removeDuplicates($sides) {
$x = array();
foreach ($sides as $i => $s) {
#$x[$s[0].','.$s[1].','.$s[2].','.$s[3]]++;
}
foreach ($x as $s => $n) {
if ($n > 1) {
unset($x[$s]);
} else {
$this->points[] = explode(",", $s);
}
}
return($x);
}
function drawPoints($points, $outfile = null) {
$xs = $this->x[count($this->x) - 1] + $this->x[0];
$ys = $this->y[count($this->y) - 1] + $this->y[0];
$img = imagecreate($xs, $ys);
if ($img !== FALSE) {
$wht = imagecolorallocate($img, 255, 255, 255);
$blk = imagecolorallocate($img, 0, 0, 0);
$red = imagecolorallocate($img, 255, 0, 0);
imagerectangle($img, 0, 0, $xs - 1, $ys - 1, $red);
$oldp = $points[0];
for ($i = 1; $i < count($points); $i++) {
$p = $points[$i];
imageline($img, $oldp['x'], $oldp['y'], $p['x'], $p['y'], $blk);
$oldp = $p;
}
imageline($img, $oldp['x'], $oldp['y'], $points[0]['x'], $points[0]['y'], $blk);
if ($outfile == null) header("content-type: image/png");
imagepng($img, $outfile);
imagedestroy($img);
}
}
function drawSides($sides, $outfile = null) {
$xs = $this->x[count($this->x) - 1] + $this->x[0];
$ys = $this->y[count($this->y) - 1] + $this->y[0];
$img = imagecreate($xs, $ys);
if ($img !== FALSE) {
$wht = imagecolorallocate($img, 255, 255, 255);
$blk = imagecolorallocate($img, 0, 0, 0);
$red = imagecolorallocate($img, 255, 0, 0);
imagerectangle($img, 0, 0, $xs - 1, $ys - 1, $red);
foreach ($sides as $s => $n) {
if (is_array($n)) {
$r = $n;
} else {
$r = explode(",", $s);
}
imageline($img, $r['x1'], $r['y1'], $r['x2'], $r['y2'], $blk);
}
if ($outfile == null) header("content-type: image/png");
imagepng($img, $outfile);
imagedestroy($img);
}
}
function getSides($sides = FALSE) {
if ($sides === FALSE) {
foreach ($this->sides as $r) {
$result[] = array('x1' => $r[0], 'y1' => $r[1], 'x2' => $r[2], 'y2' => $r[3]);
}
} else {
$result = array();
foreach ($sides as $s => $n) {
$r = explode(",", $s);
$result[] = array('x1' => $r[0], 'y1' => $r[1], 'x2' => $r[2], 'y2' => $r[3]);
}
}
return($result);
}
private function _nextPoint(&$points, $lastpt) {
#extract($lastpt);
foreach ($points as $i => $p) {
if (($p[0] == $x) && ($p[1] == $y)) {
unset($points[$i]);
return(array('x' => $p[2], 'y' => $p[3]));
} else if (($p[2] == $x) && ($p[3] == $y)) {
unset($points[$i]);
return(array('x' => $p[0], 'y' => $p[1]));
}
}
return false;
}
function getPoints($points = FALSE) {
if ($points === FALSE) $points = $this->points;
$result = array(
array('x' => $points[0][0], 'y' => $points[0][1])
);
$lastpt = array('x' => $points[0][2], 'y' => $points[0][3]);
unset($points[0]);
do {
$result[] = $lastpt;
} while ($lastpt = $this->_nextPoint($points, $lastpt));
return($result);
}
}
?>
merge.php
<?php
require_once("RectUnion.php");
function generateRect($prev, $step) {
$rect = array(
'x1' => $prev['x2'],
'x2' => $prev['x2'] + rand($step, $step * 10),
'y1' => rand($prev['y1'] + 2, $prev['y2'] - 2),
'y2' => rand($step * 2, $step * 10)
);
return($rect);
}
$x0 = 50; // Pixels
$y0 = 50; // Pixels
$step = 20; // Pixels
$nrect = 10; // Number of rectangles
$rects = array(
array("x1" => 50, "y1" => 50, "x2" => 100, "y2" => 100)
);
for ($i = 1; $i < $nrect - 1; $i++) {
$rects[$i] = generateRect($rects[$i - 1], $step);
}
$start_tm = microtime(true);
$ru = new RectUnion();
foreach ($rects as $r) {
$ru->addRect($r);
}
$union = $ru->removeDuplicates($ru->splitSides());
$stop_tm = microtime(true);
$ru->drawSides($ru->getSides(), "before.png");
if (FALSE) { // Lines
$sides = $ru->getSides($union);
$ru->drawSides($sides, "after.png");
} else { // Points
$points = $ru->getPoints();
$ru->drawPoints($points, "after.png");
}
?>
<!DOCTYPE html>
<html>
<body>
<fieldset>
<legend>Before Union</legend>
<img src='before.png'>
</fieldset>
<fieldset>
<legend>After Union</legend>
<img src='after.png'>
</fieldset>
<h4>Elapsed Time: <?= round($stop_tm - $start_tm, 4) ?> seconds</h4>
<?php if (isset($sides)): ?>
<h4>Sides:</h4>
<pre><?= print_r($sides, true) ?></pre>
<?php elseif (isset($points)): ?>
<h4>Points:</h4>
<pre><?= print_r($points, true) ?></pre>
<?php endif ?>
</body>
</html>
How does it work?
The script identifies and removes all "overlapping" segments. For example:
First, the sides of each rectangle are split at the intersections with the sides of the adiacent rectangle.
For example, consider the B2-B3 side of the B rectangle: the "splitSides" method splits it into the B2-D1, D1-D4 and D4-B3 segments.
Then the "removeDuplicates" method removes all the overlapping (duplicate) segments.
For example, the D1-D4 segment is a duplicate, since it appears either in the B rectangle and in the D rectangle.
Finally the "getSides" method returns the list of the remaining segments, while the "getPoints" method returns the list of the polygon points.
The "draw" methods are only for the graphical representation of the result, and require the GD extension to work:
About performance
Here are some execution times:
10 rectangles: 0,003 seconds
100 rectangles: 0,220 seconds
1000 rectangles: 4,407 seconds
2000 rectangles: 13,448 seconds
By profiling the execution with XDebug, I've got the following results:
If any one else comes by this, I've also implemented this algorithm based off of the answer from mmgp in JavaScript. If anyone else is trying to do this in JS, you're probably better off using mine than having to figure out the little issues with porting this yourself from the other examples.
I have it open sourced as part of a larger project here - https://github.com/Crabcyborg/crabcyborg/blob/e282aff3a033fba76bba1a7c924f04e6df7b3dc4/shapeup/svg-helper.js#L199
Since it's JavaScript, I can demo it live. It's used in the "polygon per color" example on https://crabcyb.org/experiment/shapeup-svg/
const pointsToPolygons = (points, size) => {
let edges_v = {}, edges_h = {};
const setEdges = (edges, cmp, e) => {
points.sort(cmp);
let edge_index = 0;
const length = points.length;
while(edge_index < length) {
const curr = points[edge_index][e];
do {
edges[points[edge_index]] = points[edge_index+1];
edges[points[edge_index+1]] = points[edge_index];
edge_index += 2
} while(edge_index < length && points[edge_index][e] == curr);
}
};
setEdges(edges_v, xThenY, 0);
setEdges(edges_h, yThenX, 1);
let polygon = [], keys;
while((keys = Object.keys(edges_h)).length) {
const [ key ] = keys;
delete edges_h[key];
const first_vertex = new V2(key);
let previous = [first_vertex.toArray(), 0];
let vertices = [first_vertex];
while(1) {
const [edge_index, edge] = previous;
const edges = [edges_v, edges_h][edge];
const next_vertex = new V2(edges[edge_index]);
const next = [next_vertex.toArray(), 1-edge];
delete edges[edge_index];
if(first_vertex.compare(next_vertex)) {
break;
}
vertices.push(next_vertex);
previous = next;
}
let scaled_vertices = [];
for(let vertex of vertices) {
scaled_vertices.push(vertex.scale(size).toArray());
const edge_index = vertex.toArray();
delete edges_v[edge_index];
delete edges_h[edge_index];
}
polygon.push(scaled_vertices);
}
return polygon;
};
function V2(x,y) {
if(Array.isArray(x)) {
y = x[1];
x = x[0];
} else {
switch(typeof x) {
case 'object':
y = x.y;
x = x.x;
break;
case 'string':
const split = x.split(',');
x = parseInt(split[0]);
y = parseInt(split[1]);
break;
}
}
this.x = x;
this.y = y;
}
V2.prototype = {
scale: function(scale) {
return new V2(this.x * scale, this.y * scale);
},
compare: function(v) {
return this.x == v.x && this.y == v.y;
},
toArray: function() {
return [this.x, this.y];
}
};
const xThenY = (a,b) => a[0]<b[0] || (a[0]==b[0] && a[1]<b[1]) ? -1 : 1;
const yThenX = (a,b) => a[1]<b[1] || (a[1]==b[1] && a[0]<b[0]) ? -1 : 1;
Just some thoughts:
Iterate over all corners to find one which is incident with only one unit, and therefore a corner of your union.
From there choose one direction to iterate, e.g. always counter-clockwise.
Check whether the edge in this direction is incident with a corner of another unit, or whether the next corner along this edge is incident with an edge of another unit. Either of these would form the next corner of the union. Otherwise the endpoint of this edge is the next corner.
Continue in this fashion, moving from one unit to the next, until you reach your starting point.
I will not use mathematics to solve this problem, but only analysis.
Consider the following image :
Here, we have 2 examples at once, to be sure we will cover every cases.
in the first image, we have a special case : the rectangles no 3, 4, 5, 11, 12, 13 creates an empty area, this may be a smoke space in your case.
in the second image, we have a corner between rectangles no 16, 17, 18, 19... this will have its importance later.
How I solved the problem uses the following things :
A corner is a point that have been written from 2 to 8 times : at least 2 because if we imagine a rectangle ABCD, the corner B will be shared with AB and BC (so the pixel has been put 2 times). It can be written 8 times in the case of the rectangles 16, 17, 18, 19, where one point is shared with 4 rectangles, so 8 sides.
A side is a set of points that can be written 1 or 2 times (without considering corners) : 1 time if the side is alone, not close to another side, and 2 times if the side close to another one. And a side who is not close to another one is close to the outside : it should take part of the final polygon.
So here is the logic :
We create a virtual space of the same size as the whole image, filled of zeros ( 0 ).
We write all rectangles, but instead of writting pixels, we increment the value of the virtual pixel
21111111112
1 1
1 1
1 1
1 1
1 1
1 1
1 1
1 1
1 1
2111111111622222222261111111112
1 2 2 1
1 2 2 1
1 2 2 1
1 2 2 1
1 2 2 1
1 2 2 1
1 2 2 1
1 2 2 1
1 2 2 1
21111111116222222222611111111141111111112
1 2 1
1 2 1
1 2 1
1 2 1
1 2 1
1 2 1
1 2 1
1 2 1
1 2 1
(...)
(Sorry, it looks like my indentation has problems with the SO's formatting tool)
We remove all virtual points that have a value greater than 2, except corners that we set to 1
At this point, we have polygons, and points alone (where there is a corner at the middle of several other rectangles).
11111111111
1 1
1 1
1 1
1 1
1 1
1 1
1 1
1 1
1 1
11111111111 11111111111
1 1
1 1
1 1
1 1
1 1
1 1
1 1
1 1
1 1
11111111111 111111111111111111111
1 1
1 1
1 1
1 1
1 1
1 1
1 1
1 1
1 1
11111111111 1 11111111111
1 1
1 1
1 1
1 1
1 1
1 1
1 1
1 1
1 1
11111111111111111111111111111111111111111
Now we need to look for one or several polygons (we may have several polygons when we're in the 11 12 13 14 3 4 5 rectangles's case). This mean, search a point into our virtual image.
If the point is alone (see above), it has no point at its top, left, bottom or right side, this is a corner (we saved our corner earlier) in the middle of several other rectangles. This is quite tricky, but works if all your rectangles are greater than 4 pixels.
When we find a point, we store it, try to iterate one direction (top/left/right/bottom) and go ahead while removing points to this direction until there is no more point : this is one corner of the polygon. We continue this way until it is not possible to move to any direction : this means we are at the end of the polygon.
Now, you get a 2-dimention array : the first dimention is the list of polygons (in the case of the first example), and the second dimention is the list of the points that describe your polygon. For each polygons, you just have to iterate those points and join the current one to the following one to get your polygon.
What about the result now ?
Implementation :
class PolygonMaker
{
private $image;
private $width;
private $height;
private $vImage;
public function __construct($width, $height)
{
// create a GD image to display results and debug
$this->width = $width;
$this->height = $height;
$this->image = imagecreatetruecolor($width, $height);
$white = imagecolorallocate($this->image, 0xFF, 0xFF, 0xFF);
imagefill($this->image, 0, 0, $white);
imagesetthickness($this->image, 3);
}
public function __destruct()
{
imagedestroy($this->image);
}
public function display()
{
// Display gd image as png
header("Content-type: image/png");
imagepng($this->image);
}
public function drawRectangles(array $rectangles, $r, $g, $b)
{
// Draw rectangles as they are inside the gd image
foreach ($rectangles as $rectangle)
{
list($tx, $ty) = $rectangle[0];
list($bx, $by) = $rectangle[1];
$color = imagecolorallocate($this->image, $r, $g, $b);
imagerectangle($this->image, $tx, $ty, $bx, $by, $color);
}
}
public function findPolygonsPoints(array $rectangles)
{
// Create a virtual image where rectangles will be "drawn"
$this->_createVirtualImage($rectangles);
$polygons = array ();
// Searches for all polygons inside the virtual image
while (!is_null($beginPoint = $this->_findPolygon()))
{
$polygon = array ();
// Push the first point
$polygon[] = $this->_cleanAndReturnPolygonPoint($beginPoint);
$point = $beginPoint;
// Try to go up, down, left, right until there is no more point
while ($point = $this->_getNextPolygonPoint($point))
{
// Push the found point
$polygon[] = $this->_cleanAndReturnPolygonPoint($point);
}
// Push the first point at the end to close polygon
$polygon[] = $beginPoint;
// Add the polygon to the list, in case of several polygons in the image
$polygons[] = $polygon;
}
$this->vImage = null;
return $polygons;
}
private function _createVirtualImage(array $rectangles)
{
// Create a 0-filled grid where will be stored rectangles
$this->vImage = array_fill(0, $this->height, array_fill(0, $this->width, 0));
// Draw each rectangle to that grid (each pixel increments the corresponding value of the grid of 1)
foreach ($rectangles as $rectangle)
{
list($x1, $y1, $x2, $y2) = array ($rectangle[0][0], $rectangle[0][1], $rectangle[1][0], $rectangle[1][1]);
$this->_drawVirtualLine($x1, $y1, $x1, $y2); // top-left, bottom-left
$this->_drawVirtualLine($x2, $y1, $x2, $y2); // top-right, bottom-right
$this->_drawVirtualLine($x1, $y1, $x2, $y1); // top-left, top-right
$this->_drawVirtualLine($x1, $y2, $x2, $y2); // bottom-left, bottom-right
}
// Remove all pixels that are scored > 1 (that's our logic!)
for ($y = 0; ($y < $this->height); $y++)
{
for ($x = 0; ($x < $this->width); $x++)
{
$value = &$this->vImage[$y][$x];
$value = $value > 1 ? 0 : $value;
}
}
}
private function _drawVirtualLine($x1, $y1, $x2, $y2)
{
// Draw a vertial line in the virtual image
if ($x1 == $x2)
{
if ($y1 > $y2)
{
list($x1, $y1, $x2, $y2) = array ($x2, $y2, $x1, $y1);
}
for ($y = $y1; ($y <= $y2); $y++)
{
$this->vImage[$y][$x1]++;
}
}
// Draw an horizontal line in the virtual image
if ($y1 == $y2)
{
if ($x1 > $x2)
{
list($x1, $y1, $x2, $y2) = array ($x2, $y2, $x1, $y1);
}
for ($x = $x1; ($x <= $x2); $x++)
{
$this->vImage[$y1][$x]++;
}
}
// Force corners to be 1 (because one corner is at least used 2 times but we don't want to remove them)
$this->vImage[$y1][$x1] = 1;
$this->vImage[$y1][$x2] = 1;
$this->vImage[$y2][$x1] = 1;
$this->vImage[$y2][$x2] = 1;
}
private function _findPolygon()
{
// We're looking for the first point in the virtual image
foreach ($this->vImage as $y => $row)
{
foreach ($row as $x => $value)
{
if ($value == 1)
{
// Removes alone points ( every corner have been set to 1, but some corners are alone (eg: middle of 4 rectangles)
if ((!$this->_hasPixelAtBottom($x, $y)) && (!$this->_hasPixelAtTop($x, $y))
&& (!$this->_hasPixelAtRight($x, $y)) && (!$this->_hasPixelAtLeft($x, $y)))
{
$this->vImage[$y][$x] = 0;
continue;
}
return array ($x, $y);
}
}
}
return null;
}
private function _hasPixelAtBottom($x, $y)
{
// The closest bottom point is a point positionned at (x, y + 1)
return $this->_hasPixelAt($x, $y + 1);
}
private function _hasPixelAtTop($x, $y)
{
// The closest top point is a point positionned at (x, y - 1)
return $this->_hasPixelAt($x, $y - 1);
}
private function _hasPixelAtLeft($x, $y)
{
// The closest left point is a point positionned at (x - 1, y)
return $this->_hasPixelAt($x - 1, $y);
}
private function _hasPixelAtRight($x, $y)
{
// The closest right point is a point positionned at (x + 1, y)
return $this->_hasPixelAt($x + 1, $y);
}
private function _hasPixelAt($x, $y)
{
// Check if the pixel (x, y) exists
return ((isset($this->vImage[$y])) && (isset($this->vImage[$y][$x])) && ($this->vImage[$y][$x] > 0));
}
private function _cleanAndReturnPolygonPoint(array $point)
{
// Remove a point from the virtual image
list($x, $y) = $point;
$this->vImage[$y][$x] = 0;
return $point;
}
private function _getNextPolygonPoint(array $point)
{
list($x, $y) = $point;
// Initialize modifiers, to move to the right, bottom, left or top.
$directions = array(
array(1, 0), // right
array(0, 1), // bottom
array(-1, 0), // left
array(0, -1), // top
);
// Try to get to one direction, if we can go ahead, there is a following corner
$return = null;
foreach ($directions as $direction)
{
list($xModifier, $yModifier) = $direction;
if (($return = $this->_iterateDirection($x, $y, $xModifier, $yModifier)) !== null)
{
return $return;
}
}
// the point is alone : we are at the end of the polygon
return $return;
}
private function _iterateDirection($x, $y, $xModifier, $yModifier)
{
// This method follows points in a direction until the last point
$return = null;
while ($this->_hasPixelAt($x + $xModifier, $y + $yModifier))
{
$x = $x + $xModifier;
$y = $y + $yModifier;
// Important : we remove the point so we'll not get back when moving
$return = $this->_cleanAndReturnPolygonPoint(array ($x, $y));
}
// The last point is a corner of the polygon because if it has no following point, we change direction
return $return;
}
/**
* This method draws a polygon with the given points. That's to check if
* our calculations are valid.
*
* #param array $points An array of points that define the polygon
*/
public function drawPolygon(array $points, $r, $g, $b)
{
$count = count($points);
for ($i = 0; ($i < $count); $i++)
{
// Draws a line between the current and the next point until the last point is reached
if (array_key_exists($i + 1, $points))
{
list($x1, $y1) = $points[$i];
list($x2, $y2) = $points[$i + 1];
$black = imagecolorallocate($this->image, $r, $g, $b);
imageline($this->image, $x1, $y1, $x2, $y2, $black);
}
}
}
}
Usage example :
$rectanglesA = array (
array ( // 1
array (50, 50), // tx, ty
array (75, 75), // bx, by
),
array ( // 2
array (75, 50), // tx, ty
array (125, 75), // bx, by
),
array ( // 3
array (125, 50), // tx, ty
array (175, 75), // bx, by
),
array ( // 4
array (175, 50), // tx, ty
array (225, 75), // bx, by
),
array ( // 5
array (225, 50), // tx, ty
array (275, 75), // bx, by
),
array ( // 6
array (275, 50), // tx, ty
array (325, 75), // bx, by
),
array ( // 7
array (325, 50), // tx, ty
array (375, 75), // bx, by
),
array ( // 8
array (375, 50), // tx, ty
array (425, 75), // bx, by
),
array ( // 9
array (320, 42), // tx, ty
array (330, 50), // bx, by
),
array ( // 10
array (425, 60), // tx, ty
array (430, 65), // bx, by
),
array ( // 11
array (100, 75), // tx, ty
array (150, 250), // bx, by
),
array ( // 12
array (150, 125), // tx, ty
array (250, 150), // bx, by
),
array ( // 13
array (225, 75), // tx, ty
array (250, 125), // bx, by
),
array ( // 14
array (150, 92), // tx, ty
array (180, 107), // bx, by
),
);
$rectanglesB = array (
array ( // 15
array (200, 300), // tx, ty
array (250, 350), // bx, by
),
array ( // 16
array (250, 250), // tx, ty
array (300, 300), // bx, by
),
array ( // 17
array (250, 300), // tx, ty
array (300, 350), // bx, by
),
array ( // 18
array (300, 250), // tx, ty
array (350, 300), // bx, by
),
array ( // 19
array (300, 300), // tx, ty
array (350, 350), // bx, by
),
array ( // 20
array (300, 200), // tx, ty
array (350, 250), // bx, by
),
array ( // 21
array (350, 300), // tx, ty
array (400, 350), // bx, by
),
array ( // 22
array (350, 200), // tx, ty
array (400, 250), // bx, by
),
array ( // 23
array (350, 150), // tx, ty
array (400, 200), // bx, by
),
array ( // 24
array (400, 200), // tx, ty
array (450, 250), // bx, by
),
);
$polygonMaker = new PolygonMaker(500, 400);
// Just to get started and see what's happens
//$polygonMaker->drawRectangles($rectanglesA, 0xFF, 0x00, 0x00);
//$polygonMaker->drawRectangles($rectanglesB, 0xFF, 0x00, 0x00);
$polygonsA = $polygonMaker->findPolygonsPoints($rectanglesA);
foreach ($polygonsA as $polygon)
{
$polygonMaker->drawPolygon($polygon, 0x00, 0x00, 0x00);
}
$polygonsB = $polygonMaker->findPolygonsPoints($rectanglesB);
foreach ($polygonsB as $polygon)
{
$polygonMaker->drawPolygon($polygon, 0x00, 0x00, 0x00);
}
// Display image to see if everything is correct
$polygonMaker->display();
In PHP, what is the most straightforward way to convert a RGB triplet to HSV values?
Here is a simple, straightforward method that returns HSV values as degrees and percentages, which is what Photoshop's color picker uses.
Note that the return values are not rounded, you can do that yourself if required. Keep in mind that H(360) == H(0), so H values of 359.5 and greater should round to 0
Heavily documented for learning purposes.
/**
* Licensed under the terms of the BSD License.
* (Basically, this means you can do whatever you like with it,
* but if you just copy and paste my code into your app, you
* should give me a shout-out/credit :)
*/
<?php
function RGBtoHSV($R, $G, $B) // RGB values: 0-255, 0-255, 0-255
{ // HSV values: 0-360, 0-100, 0-100
// Convert the RGB byte-values to percentages
$R = ($R / 255);
$G = ($G / 255);
$B = ($B / 255);
// Calculate a few basic values, the maximum value of R,G,B, the
// minimum value, and the difference of the two (chroma).
$maxRGB = max($R, $G, $B);
$minRGB = min($R, $G, $B);
$chroma = $maxRGB - $minRGB;
// Value (also called Brightness) is the easiest component to calculate,
// and is simply the highest value among the R,G,B components.
// We multiply by 100 to turn the decimal into a readable percent value.
$computedV = 100 * $maxRGB;
// Special case if hueless (equal parts RGB make black, white, or grays)
// Note that Hue is technically undefined when chroma is zero, as
// attempting to calculate it would cause division by zero (see
// below), so most applications simply substitute a Hue of zero.
// Saturation will always be zero in this case, see below for details.
if ($chroma == 0)
return array(0, 0, $computedV);
// Saturation is also simple to compute, and is simply the chroma
// over the Value (or Brightness)
// Again, multiplied by 100 to get a percentage.
$computedS = 100 * ($chroma / $maxRGB);
// Calculate Hue component
// Hue is calculated on the "chromacity plane", which is represented
// as a 2D hexagon, divided into six 60-degree sectors. We calculate
// the bisecting angle as a value 0 <= x < 6, that represents which
// portion of which sector the line falls on.
if ($R == $minRGB)
$h = 3 - (($G - $B) / $chroma);
elseif ($B == $minRGB)
$h = 1 - (($R - $G) / $chroma);
else // $G == $minRGB
$h = 5 - (($B - $R) / $chroma);
// After we have the sector position, we multiply it by the size of
// each sector's arc (60 degrees) to obtain the angle in degrees.
$computedH = 60 * $h;
return array($computedH, $computedS, $computedV);
}
?>
<?php
function RGB_TO_HSV ($R, $G, $B) // RGB Values:Number 0-255
{ // HSV Results:Number 0-1
$HSL = array();
$var_R = ($R / 255);
$var_G = ($G / 255);
$var_B = ($B / 255);
$var_Min = min($var_R, $var_G, $var_B);
$var_Max = max($var_R, $var_G, $var_B);
$del_Max = $var_Max - $var_Min;
$V = $var_Max;
if ($del_Max == 0)
{
$H = 0;
$S = 0;
}
else
{
$S = $del_Max / $var_Max;
$del_R = ( ( ( $var_Max - $var_R ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max;
$del_G = ( ( ( $var_Max - $var_G ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max;
$del_B = ( ( ( $var_Max - $var_B ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max;
if ($var_R == $var_Max) $H = $del_B - $del_G;
else if ($var_G == $var_Max) $H = ( 1 / 3 ) + $del_R - $del_B;
else if ($var_B == $var_Max) $H = ( 2 / 3 ) + $del_G - $del_R;
if ($H<0) $H++;
if ($H>1) $H--;
}
$HSL['H'] = $H;
$HSL['S'] = $S;
$HSL['V'] = $V;
return $HSL;
}
Thoroughly tested and compressed, this is the function I'm going to stick with for converting RGB to HSV:
function RGBtoHSV($r,$g,$b) {
$r=($r/255); $g=($g/255); $b=($b/255);
$maxRGB=max($r,$g,$b); $minRGB=min($r,$g,$b); $chroma=$maxRGB-$minRGB;
if($chroma==0) return array('h'=>0,'s'=>0,'v'=>$maxRGB);
if($r==$minRGB)$h=3-(($g-$b)/$chroma);
elseif($b==$minRGB)$h=1-(($r-$g)/$chroma); else $h=5-(($b-$r)/$chroma);
return array('h'=>60*$h,'s'=>$chroma/$maxRGB,'v'=>$maxRGB);
}
Example:
Example using color "DarkSalmon":
echo '<pre><code>'. print_r( RGBtoHSV(233,150,122), true ) .'</code></pre>';
...returns:
Array
(
[h] => 15.135135135135
[s] => 0.47639484978541
[v] => 0.91372549019608
)
I did it like this
function convertRgbToHsv($rgb)
{
$r = (int)substr($rgb, 0, 3) / 255;
$g = (int)substr($rgb, 3, 3) / 255;
$b = (int)substr($rgb, 6, 3) / 255;
$max = max($r, $g, $b);
$min = min($r, $g, $b);
$delta = $max - $min;
if (!$delta) {
$h = 0;
} else if ($r === $max) {
$h = 60 * ((($g - $b) / $delta) % 6);
} else if ($g === $max) {
$h = 60 * ((($b - $r) / $delta) + 2);
} else {
$h = 60 * ((($r - $g) / $delta) + 4);
}
$s = !!$max ? $delta / $max : 0;
$v = $max;
$hsv = array("h" => $h, "s" => $s, "v" => $v);
return $hsv;
}
Link to reference material here
Here's my spin on it, along with a unit test. Since the S and V values are percentages, this code returns them as integers (0, 100) as opposed to (0, 1) - Example, 75 instead of 0.75.
final class MathService
{
/**
* Converts an RGB point into HSV
*
* #param int $r
* #param int $g
* #param int $b
* #return array
*/
public function rgbToHsv(int $r, int $g, int $b): array
{
$rPrime = $r / 255;
$gPrime = $g / 255;
$bPrime = $b / 255;
$max = max([$rPrime, $gPrime, $bPrime]);
$min = min([$rPrime, $gPrime, $bPrime]);
$delta = $max - $min;
// Calculate H
if ($delta == 0) {
$h = 0;
} else {
if ($max === $rPrime) {
$h = 60 * ((($gPrime - $bPrime) / $delta) % 6);
}
if ($max === $gPrime) {
$h = 60 * ((($bPrime - $rPrime) / $delta) + 2);
}
if ($max === $bPrime) {
$h = 60 * ((($rPrime - $gPrime) / $delta) + 4);
}
}
// Calculate S
if ($max == 0) {
$s = 0;
} else {
$s = $delta / $max;
}
// Calculate V
$v = $max;
return [$h, (int)($s * 100), (int)($v * 100)];
}
}
PHPUnit test case with PHP 7.2
/**
* #test
*/
public function rgbToHsv_ComputesCorrectValues(): void
{
$service = new MathService();
$samples = [
// [R, G, B, H, S, V]
[0, 0, 0, 0, 0, 0],
[255, 255, 255, 0, 0, 100],
[255, 0, 0, 0, 100, 100],
[0, 255, 0, 120, 100, 100],
[0, 0, 255, 240, 100, 100],
[255, 255, 0, 60, 100, 100],
[0, 255, 255, 180, 100, 100],
[255, 0, 255, 300, 100, 100],
[192, 192, 192, 0, 0, 75],
[128, 128, 128, 0, 0, 50],
[128, 0, 0, 0, 100, 50],
[128, 128, 0, 60, 100, 50],
[0, 128, 0, 120, 100, 50],
[128, 0, 128, 300, 100, 50],
[0, 128, 128, 180, 100, 50],
[0, 0, 128, 240, 100, 50],
];
foreach ($samples as $sample) {
list($r, $g, $b) = array_slice($sample, 0, 3);
$expected = array_slice($sample, 3);
$hsv = $service->rgbToHsv($r, $g, $b);
list($h, $s, $v) = $hsv;
self::assertEquals($expected, $hsv, "Error converting ({$r}, ${g}, ${b}). Got ({$h}, {$s}, {$v})");
}
}
Have a look here: http://tyilo.jbusers.com/PNG/progress.php?l=100&p=20
I want to remove the white-thingy at the end of the blue part, but I have tried many different things that didn't work.
If needed the pngs can be found in http://tyilo.jbusers.com/PNG/ folder (http://tyilo.jbusers.com/PNG/Empty.png)
header('Content-type: image/png');
echo imagepng(progressbar($_GET['l'], $_GET['p']));
function progressbar($length, $percentage)
{
$length = round($length / 2) * 2;
$percentage = min(100, max(0, $percentage));
if($length > 0)
{
$bar = imagecreate($length, 14);
$empty = imagecreatefrompng('Empty.png');
$fill = imagecreatefrompng('Fill.png');
$lempty = imagecreatefrompng('LeftEmpty.png');
$lfill = imagecreatefrompng('LeftFill.png');
$rempty = imagecreatefrompng('RightEmpty.png');
$rfill = imagecreatefrompng('RightFill.png');
$emptycaplength = min(7, $length / 2); //5
imagecopy($bar, $lempty, 0, 0, 0, 0, $emptycaplength, 14);
imagecopy($bar, $rempty, $length - $emptycaplength, 0, 7 - $emptycaplength, 0, $emptycaplength, 14);
if($length > 14)
{
imagecopyresized($bar, $empty, 7, 0, 0, 0, $length - 14, 14, 1, 14);
}
$filllength = round(($length * ($percentage / 100)) / 2) * 2;
$fillcaplength = min(7, $filllength / 2);
imagecopy($bar, $lfill, 0, 0, 0, 0, $fillcaplength, 14);
imagecopy($bar, $rfill, $filllength - $fillcaplength, 0, 7 - $fillcaplength, 0, $fillcaplength, 14);
if($filllength > 14)
{
imagecopyresized($bar, $fill, 7, 0, 0, 0, $filllength - 14, 14, 1, 14);
}
return $bar;
}
else
{
return false;
}
}
try using imagecreatetruecolor() to create your image.
http://www.php.net/manual/en/function.imagecreatetruecolor.php
I think you need to set the Alpha blend of the image...
imagealphablending($cropimg, false);
imagesavealpha($cropimg, true);
I've also found that it helps to set the Color Allocation.
imageColorAllocate ($cropimg, 0, 0, 0);
I think you will need to call those three functions on all your filled images. Sorry for the incomplete answer, I'm in a rush but thought I'd throw you a bone.
In PHP, what is the most straightforward way to convert a RGB triplet to HSV values?
Here is a simple, straightforward method that returns HSV values as degrees and percentages, which is what Photoshop's color picker uses.
Note that the return values are not rounded, you can do that yourself if required. Keep in mind that H(360) == H(0), so H values of 359.5 and greater should round to 0
Heavily documented for learning purposes.
/**
* Licensed under the terms of the BSD License.
* (Basically, this means you can do whatever you like with it,
* but if you just copy and paste my code into your app, you
* should give me a shout-out/credit :)
*/
<?php
function RGBtoHSV($R, $G, $B) // RGB values: 0-255, 0-255, 0-255
{ // HSV values: 0-360, 0-100, 0-100
// Convert the RGB byte-values to percentages
$R = ($R / 255);
$G = ($G / 255);
$B = ($B / 255);
// Calculate a few basic values, the maximum value of R,G,B, the
// minimum value, and the difference of the two (chroma).
$maxRGB = max($R, $G, $B);
$minRGB = min($R, $G, $B);
$chroma = $maxRGB - $minRGB;
// Value (also called Brightness) is the easiest component to calculate,
// and is simply the highest value among the R,G,B components.
// We multiply by 100 to turn the decimal into a readable percent value.
$computedV = 100 * $maxRGB;
// Special case if hueless (equal parts RGB make black, white, or grays)
// Note that Hue is technically undefined when chroma is zero, as
// attempting to calculate it would cause division by zero (see
// below), so most applications simply substitute a Hue of zero.
// Saturation will always be zero in this case, see below for details.
if ($chroma == 0)
return array(0, 0, $computedV);
// Saturation is also simple to compute, and is simply the chroma
// over the Value (or Brightness)
// Again, multiplied by 100 to get a percentage.
$computedS = 100 * ($chroma / $maxRGB);
// Calculate Hue component
// Hue is calculated on the "chromacity plane", which is represented
// as a 2D hexagon, divided into six 60-degree sectors. We calculate
// the bisecting angle as a value 0 <= x < 6, that represents which
// portion of which sector the line falls on.
if ($R == $minRGB)
$h = 3 - (($G - $B) / $chroma);
elseif ($B == $minRGB)
$h = 1 - (($R - $G) / $chroma);
else // $G == $minRGB
$h = 5 - (($B - $R) / $chroma);
// After we have the sector position, we multiply it by the size of
// each sector's arc (60 degrees) to obtain the angle in degrees.
$computedH = 60 * $h;
return array($computedH, $computedS, $computedV);
}
?>
<?php
function RGB_TO_HSV ($R, $G, $B) // RGB Values:Number 0-255
{ // HSV Results:Number 0-1
$HSL = array();
$var_R = ($R / 255);
$var_G = ($G / 255);
$var_B = ($B / 255);
$var_Min = min($var_R, $var_G, $var_B);
$var_Max = max($var_R, $var_G, $var_B);
$del_Max = $var_Max - $var_Min;
$V = $var_Max;
if ($del_Max == 0)
{
$H = 0;
$S = 0;
}
else
{
$S = $del_Max / $var_Max;
$del_R = ( ( ( $var_Max - $var_R ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max;
$del_G = ( ( ( $var_Max - $var_G ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max;
$del_B = ( ( ( $var_Max - $var_B ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max;
if ($var_R == $var_Max) $H = $del_B - $del_G;
else if ($var_G == $var_Max) $H = ( 1 / 3 ) + $del_R - $del_B;
else if ($var_B == $var_Max) $H = ( 2 / 3 ) + $del_G - $del_R;
if ($H<0) $H++;
if ($H>1) $H--;
}
$HSL['H'] = $H;
$HSL['S'] = $S;
$HSL['V'] = $V;
return $HSL;
}
Thoroughly tested and compressed, this is the function I'm going to stick with for converting RGB to HSV:
function RGBtoHSV($r,$g,$b) {
$r=($r/255); $g=($g/255); $b=($b/255);
$maxRGB=max($r,$g,$b); $minRGB=min($r,$g,$b); $chroma=$maxRGB-$minRGB;
if($chroma==0) return array('h'=>0,'s'=>0,'v'=>$maxRGB);
if($r==$minRGB)$h=3-(($g-$b)/$chroma);
elseif($b==$minRGB)$h=1-(($r-$g)/$chroma); else $h=5-(($b-$r)/$chroma);
return array('h'=>60*$h,'s'=>$chroma/$maxRGB,'v'=>$maxRGB);
}
Example:
Example using color "DarkSalmon":
echo '<pre><code>'. print_r( RGBtoHSV(233,150,122), true ) .'</code></pre>';
...returns:
Array
(
[h] => 15.135135135135
[s] => 0.47639484978541
[v] => 0.91372549019608
)
I did it like this
function convertRgbToHsv($rgb)
{
$r = (int)substr($rgb, 0, 3) / 255;
$g = (int)substr($rgb, 3, 3) / 255;
$b = (int)substr($rgb, 6, 3) / 255;
$max = max($r, $g, $b);
$min = min($r, $g, $b);
$delta = $max - $min;
if (!$delta) {
$h = 0;
} else if ($r === $max) {
$h = 60 * ((($g - $b) / $delta) % 6);
} else if ($g === $max) {
$h = 60 * ((($b - $r) / $delta) + 2);
} else {
$h = 60 * ((($r - $g) / $delta) + 4);
}
$s = !!$max ? $delta / $max : 0;
$v = $max;
$hsv = array("h" => $h, "s" => $s, "v" => $v);
return $hsv;
}
Link to reference material here
Here's my spin on it, along with a unit test. Since the S and V values are percentages, this code returns them as integers (0, 100) as opposed to (0, 1) - Example, 75 instead of 0.75.
final class MathService
{
/**
* Converts an RGB point into HSV
*
* #param int $r
* #param int $g
* #param int $b
* #return array
*/
public function rgbToHsv(int $r, int $g, int $b): array
{
$rPrime = $r / 255;
$gPrime = $g / 255;
$bPrime = $b / 255;
$max = max([$rPrime, $gPrime, $bPrime]);
$min = min([$rPrime, $gPrime, $bPrime]);
$delta = $max - $min;
// Calculate H
if ($delta == 0) {
$h = 0;
} else {
if ($max === $rPrime) {
$h = 60 * ((($gPrime - $bPrime) / $delta) % 6);
}
if ($max === $gPrime) {
$h = 60 * ((($bPrime - $rPrime) / $delta) + 2);
}
if ($max === $bPrime) {
$h = 60 * ((($rPrime - $gPrime) / $delta) + 4);
}
}
// Calculate S
if ($max == 0) {
$s = 0;
} else {
$s = $delta / $max;
}
// Calculate V
$v = $max;
return [$h, (int)($s * 100), (int)($v * 100)];
}
}
PHPUnit test case with PHP 7.2
/**
* #test
*/
public function rgbToHsv_ComputesCorrectValues(): void
{
$service = new MathService();
$samples = [
// [R, G, B, H, S, V]
[0, 0, 0, 0, 0, 0],
[255, 255, 255, 0, 0, 100],
[255, 0, 0, 0, 100, 100],
[0, 255, 0, 120, 100, 100],
[0, 0, 255, 240, 100, 100],
[255, 255, 0, 60, 100, 100],
[0, 255, 255, 180, 100, 100],
[255, 0, 255, 300, 100, 100],
[192, 192, 192, 0, 0, 75],
[128, 128, 128, 0, 0, 50],
[128, 0, 0, 0, 100, 50],
[128, 128, 0, 60, 100, 50],
[0, 128, 0, 120, 100, 50],
[128, 0, 128, 300, 100, 50],
[0, 128, 128, 180, 100, 50],
[0, 0, 128, 240, 100, 50],
];
foreach ($samples as $sample) {
list($r, $g, $b) = array_slice($sample, 0, 3);
$expected = array_slice($sample, 3);
$hsv = $service->rgbToHsv($r, $g, $b);
list($h, $s, $v) = $hsv;
self::assertEquals($expected, $hsv, "Error converting ({$r}, ${g}, ${b}). Got ({$h}, {$s}, {$v})");
}
}