How to calculate the "perfect" column widths - php

So, here's the plan: I'm using TCPDF to generate PDF documents containing a table. I'm generating an html table in PHP which I pass to TCPDF. However, TCPDF makes each column's width equal, which is a problem, as the content length in each column is quite different. The solution is to set the width attribute on the table's <td>s. But I can't quite workout the perfect way to do so. That's what I'm currently doing:
I generate an array called $maxColumnSizes in which I store the maximum number of letters per column.
I generate an array called $averageSizes in which I store the average number of letters per column.
So, below you see an example calculation. Column 0 has 8 letters average, and 26 letters at max, column 4 has 10 letters average, and 209 letters at max:
So, here's the problem: I can't think of the "right" way to combine this information to get the "perfect" column widths. If I ignore the $maxColumnSizes array and set the column widths based on the $averageSizes, the table looks quite good. Except for the one row where Column 4 has 209 characters. As Column 4 is pretty small, the row where there are 209 characters has an insane height, to fit the 209 characters in.
To sum it up: How do I calculate the "perfect" table column width (given the table data)?
Notes:
"perfect width" for me means that the whole table's height is as small as possible.
I currently do not take letter-widths into account (I do not differentiate between the width of an i and a w)
As I have access to all the data, I can also make any other calculations needed. The two arrays I mention above I only used in my first tries.
EDIT
Based on the comment I added another calculation calculating $maxColumnSize / $averageColumnSize:

This is rather subjective, but to take a stab at an algorithm:
// Following two functions taken from this answer:
// http://stackoverflow.com/a/5434698/697370
// Function to calculate square of value - mean
function sd_square($x, $mean) { return pow($x - $mean,2); }
// Function to calculate standard deviation (uses sd_square)
function sd($array) {
// square root of sum of squares devided by N-1
return sqrt(array_sum(array_map("sd_square", $array, array_fill(0,count($array), (array_sum($array) / count($array)) ) ) ) / (count($array)-1) );
}
// For any column...
$colMaxSize = /** from your table **/;
$colAvgSize = /** from your table **/;
$stdDeviation = sd(/** array of lengths for your column**/);
$coefficientVariation = $stdDeviation / $colAvgSize;
if($coefficientVariation > 0.5 && $coefficientVariation < 1.5) {
// The average width of the column is close to the standard deviation
// In this case I would just make the width of the column equal to the
// average.
} else {
// There is a large variance in your dataset (really small values and
// really large values in the same set).
// What to do here? I would base the width off of the max size, perhaps
// using (int)($colMaxSize / 2) or (int)($colMaxSize / 3) to fix long entries
// to a given number of lines.
}
There's a PECL extension that gives you the stats_standard_deviation function, but it is not bundled with PHP by default. You can also play around with the 0.5 and 1.5 values above until you get something that looks 'just right'.

Based on #watcher's answer, I came up with the following code. It works great in my test cases. I also made a GitHub repository with my code, as it is far better readable than here on StackOverflow.
<?php
/**
* A simple class to auto-calculate the "perfect" column widths of a table.
* Copyright (C) 2014 Christian Flach
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This is based on my question at StackOverflow:
* http://stackoverflow.com/questions/24394787/how-to-calculate-the-perfect-column-widths
*
* Thank you "watcher" (http://stackoverflow.com/users/697370/watcher) for the initial idea!
*/
namespace Cmfcmf;
class ColumnWidthCalculator
{
/**
* #var array
*/
private $rows;
/**
* #var bool
*/
private $html;
/**
* #var bool
*/
private $stripTags;
/**
* #var int
*/
private $minPercentage;
/**
* #var Callable|null
*/
private $customColumnFunction;
/**
* #param array $rows An array of rows, where each row is an array of cells containing the cell content.
* #param bool $html Whether or not the rows contain html content. This will call html_entity_decode.
* #param bool $stripTags Whether or not to strip tags (only if $html is true).
* #param int $minPercentage The minimum percentage each row must be wide.
* #param null $customColumnFunction A custom function to transform a cell's value before it's length is measured.
*/
public function __construct(array $rows, $html = false, $stripTags = false, $minPercentage = 3, $customColumnFunction = null)
{
$this->rows = $rows;
$this->html = $html;
$this->stripTags = $stripTags;
$this->minPercentage = $minPercentage;
$this->customColumnFunction = $customColumnFunction;
}
/**
* Calculate the column widths.
*
* #return array
*
* Explanation of return array:
* - $columnSizes[$colNumber]['percentage'] The calculated column width in percents.
* - $columnSizes[$colNumber]['calc'] The calculated column width in letters.
*
* - $columnSizes[$colNumber]['max'] The maximum column width in letters.
* - $columnSizes[$colNumber]['avg'] The average column width in letters.
* - $columnSizes[$colNumber]['raw'] An array of all the column widths of this column in letters.
* - $columnSizes[$colNumber]['stdd'] The calculated standard deviation in letters.
*
* INTERNAL
* - $columnSizes[$colNumber]['cv'] The calculated standard deviation / the average column width in letters.
* - $columnSizes[$colNumber]['stdd/max'] The calculated standard deviation / the maximum column width in letters.
*/
public function calculateWidths()
{
$columnSizes = array();
foreach ($this->rows as $row) {
foreach ($row as $key => $column) {
if (isset($this->customColumnFunction)) {
$column = call_user_func_array($this->customColumnFunction, array($column));
}
$length = $this->strWidth($this->html ? html_entity_decode($this->stripTags ? strip_tags($column) : $column) : $column);
$columnSizes[$key]['max'] = !isset($columnSizes[$key]['max']) ? $length : ($columnSizes[$key]['max'] < $length ? $length : $columnSizes[$key]['max']);
// Sum up the lengths in `avg` for now. See below where it is converted to the actual average.
$columnSizes[$key]['avg'] = !isset($columnSizes[$key]['avg']) ? $length : $columnSizes[$key]['avg'] + $length;
$columnSizes[$key]['raw'][] = $length;
}
}
// Calculate the actual averages.
$columnSizes = array_map(function ($columnSize) {
$columnSize['avg'] = $columnSize['avg'] / count ($columnSize['raw']);
return $columnSize;
}, $columnSizes);
foreach ($columnSizes as $key => $columnSize) {
$colMaxSize = $columnSize['max'];
$colAvgSize = $columnSize['avg'];
$stdDeviation = $this->sd($columnSize['raw']);
$coefficientVariation = $stdDeviation / $colAvgSize;
$columnSizes[$key]['cv'] = $coefficientVariation;
$columnSizes[$key]['stdd'] = $stdDeviation;
$columnSizes[$key]['stdd/max'] = $stdDeviation / $colMaxSize;
// $columnSizes[$key]['stdd/max'] < 0.3 is here for no mathematical reason, it's been found by trying stuff
if(($columnSizes[$key]['stdd/max'] < 0.3 || $coefficientVariation == 1) && ($coefficientVariation == 0 || ($coefficientVariation > 0.6 && $coefficientVariation < 1.5))) {
// The average width of the column is close to the standard deviation
// In this case I would just make the width of the column equal to the
// average.
$columnSizes[$key]['calc'] = $colAvgSize;
} else {
// There is a large variance in the dataset (really small values and
// really large values in the same set).
// Do some magic! (There is no mathematical rule behind that line, it's been created by trying different combinations.)
if ($coefficientVariation > 1 && $columnSizes[$key]['stdd'] > 4.5 && $columnSizes[$key]['stdd/max'] > 0.2) {
$tmp = ($colMaxSize - $colAvgSize) / 2;
} else {
$tmp = 0;
}
$columnSizes[$key]['calc'] = $colAvgSize + ($colMaxSize / $colAvgSize) * 2 / abs(1 - $coefficientVariation);
$columnSizes[$key]['calc'] = $columnSizes[$key]['calc'] > $colMaxSize ? $colMaxSize - $tmp : $columnSizes[$key]['calc'];
}
}
$totalCalculatedSize = 0;
foreach ($columnSizes as $columnSize) {
$totalCalculatedSize += $columnSize['calc'];
}
// Convert calculated sizes to percentages.
foreach ($columnSizes as $key => $columnSize) {
$columnSizes[$key]['percentage'] = 100 / ($totalCalculatedSize / $columnSize['calc']);
}
// Make sure everything is at least 3 percent wide.
if ($this->minPercentage > 0) {
foreach ($columnSizes as $key => $columnSize) {
if ($columnSize['percentage'] < $this->minPercentage) {
// That's how many percent we need to steal.
$neededPercents = ($this->minPercentage - $columnSize['percentage']);
// Steal some percents from the column with the $coefficientVariation nearest to one and being big enough.
$lowestDistance = 9999999;
$stealKey = null;
foreach ($columnSizes as $k => $val) {
// This is the distance from the actual $coefficientVariation to 1.
$distance = abs(1 - $val['cv']);
if ($distance < $lowestDistance
&& $val['calc'] - $neededPercents > $val['avg'] /* This line is here due to whatever reason :/ */
&& $val['percentage'] - $this->minPercentage >= $neededPercents /* Make sure the column we steal from would still be wider than $this->minPercentage percent after stealing. */
) {
$stealKey = $k;
$lowestDistance = $distance;
}
}
if (!isset($stealKey)) {
// Dang it! We could not get something reliable here. Fallback to stealing from the largest column.
$max = -1;
foreach ($columnSizes as $k => $val) {
if ($val['percentage'] > $max) {
$stealKey = $k;
$max = $val['percentage'];
}
}
}
$columnSizes[$stealKey]['percentage'] = $columnSizes[$stealKey]['percentage'] - $neededPercents;
$columnSizes[$key]['percentage'] = $this->minPercentage;
}
}
}
return $columnSizes;
}
/**
* Function to calculate standard deviation.
* http://stackoverflow.com/a/5434698/697370
*
* #param $array
*
* #return float
*/
protected function sd($array)
{
if (count($array) == 1) {
// Return 1 if we only have one value.
return 1.0;
}
// Function to calculate square of value - mean
$sd_square = function ($x, $mean) { return pow($x - $mean,2); };
// square root of sum of squares devided by N-1
return sqrt(array_sum(array_map($sd_square, $array, array_fill(0,count($array), (array_sum($array) / count($array)) ) ) ) / (count($array)-1) );
}
/**
* Helper function to get the (approximate) width of a string. A normal character counts as 1, short characters
* count as 0.4 and long characters count as 1.3.
* The minimum width returned is 1.
*
* #param $text
*
* #return float
*/
protected function strWidth($text)
{
$smallCharacters = array('!', 'i', 'f', 'j', 'l', ',', ';', '.', ':', '-', '|',
' ', /* normal whitespace */
"\xC2", /* non breaking whitespace */
"\xA0", /* non breaking whitespace */
"\n",
"\r",
"\t",
"\0",
"\x0B" /* vertical tab */
);
$bigCharacters = array('w', 'm', '—', 'G', 'ß', '#');
$width = strlen($text);
foreach (count_chars($text, 1) as $i => $val) {
if (in_array(chr($i), $smallCharacters)) {
$width -= (0.6 * $val);
}
if (in_array(chr($i), $bigCharacters)) {
$width += (0.3 * $val);
}
}
if ($width < 1) {
$width = 1;
}
return (float)$width;
}
}
That's it! $columnSizes[$colNumber]['percentage'] now includes a well fitting ("perfect") width for each column.

Related

PHP - Return an array key based on a hash

Given a N-bit hash (e.g. output of md5()), I have 2 situations I need solutions for:
Based on the hash, return an integer value in a given range.
Based on the hash, return an array value from a given array.
Same hash, should always return same number or array key within that range or from that same input array. If the input array changes but hash remains the same, then i would get a different selection.
So for example i would have code like this:
echo intFromHash(1, 100, 'abcd'); // 15
echo intFromHash(1, 100, 'defg'); // 90
echo arrayValueFromHash(['moe', 'joe', 'pike'], 'abcd'); // 'joe'
echo arrayValueFromHash(['pike', 'dolly']); // pike
You can write intFromHash() in 1 line of code using the crc32() PHP function:
function intFromHash($min, $max, $hash {
return $min + crc32($hash) % ($max - $min + 1);
}
Use abs(crc32($hash)) if you are running it on a 32-bit system (read the documentation for details).
Then you can use it to implement arrayValueFromHash() (in another line of code):
function arrayValueFromHash(array $array, $hash) {
return $array[intFromHash(0, count($array) - 1, $hash)];
}
Use return $array[array_keys($array)[intFromHash(...)]]; if $array is an associative array (the expression presented in the code works only for numerically indexed arrays, as those listed in the question.)
Figured it out I think. Here's the code for whoever needs it:
/**
* Return a key from an array based on a given 4-bit hash.
*
* #param array $array Array to return a key from.
* #param string $hash 4-bit hash. If hash is longer than 4-bit only first 4 bits will be used.
* #return mixed
*/
function getArrayValueByHash($array, $hash)
{
$arrayKeys = array_keys($array);
$index = getIntFromHash(0, sizeof($arrayKeys)-1, $hash);
return $array[$arrayKeys[$index]];
}
/**
* Return an integer in range, based on a hash.
*
* #param int $start
* #param int $end
* #param string $hash 4-bit hash. If hash is longer than 4-bit only first 4 bits will be used.
* #return int
*/
function getIntFromHash($start, $end, $hash)
{
$size = $end-$start;
$hash = str_split($hash);
$intHash = ord($hash[0]) * 16777216 + ord($hash[1]) * 65536 + ord($hash[2]) * 256 + ord($hash[3]);
$fits = $intHash / $size;
$decimals = $fits - floor($fits);
$index = floor($decimals * $size);
return $start+$index;
}

Finding nearest match RGB color from array of colors

I know I need to use a loop to look in the $palette Array, but I need help making the color comparison.
GOAL is to find the nearest value of $rgbcolor to $palette and show the color that matches from $palette.
<?php
//input color
$rgbcolor = array(110,84,43);
//listed color
$palette = array(
array(238,216,152),
array(252,216,113),
array(253,217,0),
array(255,208,62),
array(255,182,20),
array(206,137,0),
array(235,169,0),
array(170,137,0),
array(173,132,28),
array(183,131,0),
array(139,120,37),
array(108,86,26)
);
?>
There are many different ways to determine color "distance."
There's absolute distance, i.e. the sum of the differences between each channel value:
/**
* Find the "naive" difference between two colors.
* #param int[] $color_a Three-element array with R,G,B color values 0-255.
* #param int[] $color_b Three-element array with R,G,B color values 0-255.
* #return int
*/
function absoluteColorDistance(array $color_a, array $color_b): int {
return
abs($color_a[0] - $color_b[0]) +
abs($color_a[1] - $color_b[1]) +
abs($color_a[2] - $color_b[2]);
}
There's also difference in luminosity, which will give more of a color-independent comparison:
/**
* Find the difference between two colors' luminance values.
* #param int[] $color_a Three-element array with R,G,B color values 0-255.
* #param int[] $color_b Three-element array with R,G,B color values 0-255.
* #return int
*/
function luminanceDistance(int $color_a, int $color_b): int {
$luminance_f = function ($red, $green, $blue): int {
// Source: https://en.wikipedia.org/wiki/Relative_luminance
$luminance = (int) (0.2126 * $red + 0.7152 * $green + 0.0722 * $blue);
return $luminance;
};
return abs(
$luminance_f($color_a[0], $color_a[1], $color_a[2]) -
$luminance_f($color_b[0], $color_b[1], $color_b[2])
);
}
Once you figure out how to compare colors, the next problem you need to solve is finding the color with the least distance from your target color:
$nearest_distance = null;
$nearest_color = null;
foreach ($palate as $test_color) {
$test_distance = luminanceDistance($test_color, $rgbcolor);
if (isset($nearest_distance)) {
if ($nearest_distance > $test_distance) {
// found a closer color
$nearest_distance = $test_distance;
$nearest_color = $test_color;
}
} else {
$nearest_color = $test_color;
$nearest_distance = $test_distance;
}
}

What is the best way to validate a credit card in codeigniter

hi i am using codeigniter . i want to validate my credit card details . i saw there are classes in php to validate credit card numbers . i saw a helper in codeigniter to validate credit cards
http://codeigniter.com/wiki/Credit_Card_Helper
/**
* Truncates a card number retaining only the first 4 and the last 4 digits. It then returns the truncated form.
*
* #param string The card number to truncate.
* #return string The truncated card number.
*/
function truncate_card($card_num) {
$padsize = (strlen($card_num) < 7 ? 0 : strlen($card_num) - 7);
return substr($card_num, 0, 4) . str_repeat('X', $padsize). substr($card_num, -3);
}
/**
* Validates a card expiry date. Finds the midnight on first day of the following
* month and ensures that is greater than the current time (cards expire at the
* end of the printed month). Assumes basic sanity checks have already been performed
* on month/year (i.e. length, numeric, etc).
*
* #param integer The expiry month shown on the card.
* #param integer The expiry year printed on the card.
* #return boolean Returns true if the card is still valid, false if it has expired.
*/
function card_expiry_valid($month, $year) {
$expiry_date = mktime(0, 0, 0, ($month + 1), 1, $year);
return ($expiry_date > time());
}
/**
* Strips all non-numerics from the card number.
*
* #param string The card number to clean up.
* #return string The stripped down card number.
*/
function card_number_clean($number) {
return ereg_replace("[^0-9]", "", $number);
}
/**
* Uses the Luhn algorithm (aka Mod10) <http://en.wikipedia.org/wiki/Luhn_algorithm>
* to perform basic validation of a credit card number.
*
* #param string The card number to validate.
* #return boolean True if valid according to the Luhn algorith, false otherwise.
*/
function card_number_valid ($card_number) {
$card_number = strrev(card_number_clean($card_number));
$sum = 0;
for ($i = 0; $i < strlen($card_number); $i++) {
$digit = substr($card_number, $i, 1);
// Double every second digit
if ($i % 2 == 1) {
$digit *= 2;
}
// Add digits of 2-digit numbers together
if ($digit > 9) {
$digit = ($digit % 10) + floor($digit / 10);
}
$sum += $digit;
}
// If the total has no remainder it's OK
return ($sum % 10 == 0);
}
?>
it uses a common validation . but i want a validation according to card type like this
http://www.braemoor.co.uk/software/creditcard.php
is there any libraries or helpers in codeigniter . please help.....................
As people already told you, CodeIgniter is a php framework, coded using php, works in a php environment and makes use of..,php classes and functions :).
What's more, the file you linked to is a simple function. One function. You know what you can do? Take the file as it is, name it creditcard_helper.php, put it inside the helpers folder, open it and place the whole code inside this snippet (ugly but necessary, as whenever you'll load the helper a second time it would give you error otherwise):
if(!function_exists('checkCreditCard')
{
//the whole content goes here untouched;
}
And you're set. Just use:
$this->load->helper('creditcard');
if(checkCreditCard($cardnumber, $cardname, &$errornumber, &$errortext))
{
echo 'card OK';
}
else
{
echo 'wrong card type/number';
}
I found this helper inside the CodeIgniter Github wiki page. When dropped inside your helpers folder, you can then use the functions from the file in a controller or model you load it in.

Bell Curve Algorithm With PHP

I am working on a personal project in which IQ ranges will be randomly assignes to fake characters. This asignment will be random, yet realistic, so IQ ranges must be distributed along a bell curve. There are 3 range categories: low, normal, and high. The half of the fake characters will fall within normal, but about 25% will either fall into the low or high range.
How can I code this?
It might look long and complicated (and was written procedural for PHP4) but I used to use the following for generating non-linear random distributions:
function random_0_1()
{
// returns random number using mt_rand() with a flat distribution from 0 to 1 inclusive
//
return (float) mt_rand() / (float) mt_getrandmax() ;
}
function random_PN()
{
// returns random number using mt_rand() with a flat distribution from -1 to 1 inclusive
//
return (2.0 * random_0_1()) - 1.0 ;
}
function gauss()
{
static $useExists = false ;
static $useValue ;
if ($useExists) {
// Use value from a previous call to this function
//
$useExists = false ;
return $useValue ;
} else {
// Polar form of the Box-Muller transformation
//
$w = 2.0 ;
while (($w >= 1.0) || ($w == 0.0)) {
$x = random_PN() ;
$y = random_PN() ;
$w = ($x * $x) + ($y * $y) ;
}
$w = sqrt((-2.0 * log($w)) / $w) ;
// Set value for next call to this function
//
$useValue = $y * $w ;
$useExists = true ;
return $x * $w ;
}
}
function gauss_ms( $mean,
$stddev )
{
// Adjust our gaussian random to fit the mean and standard deviation
// The division by 4 is an arbitrary value to help fit the distribution
// within our required range, and gives a best fit for $stddev = 1.0
//
return gauss() * ($stddev/4) + $mean;
}
function gaussianWeightedRnd( $LowValue,
$maxRand,
$mean=0.0,
$stddev=2.0 )
{
// Adjust a gaussian random value to fit within our specified range
// by 'trimming' the extreme values as the distribution curve
// approaches +/- infinity
$rand_val = $LowValue + $maxRand ;
while (($rand_val < $LowValue) || ($rand_val >= ($LowValue + $maxRand))) {
$rand_val = floor(gauss_ms($mean,$stddev) * $maxRand) + $LowValue ;
$rand_val = ($rand_val + $maxRand) / 2 ;
}
return $rand_val ;
}
function bellWeightedRnd( $LowValue,
$maxRand )
{
return gaussianWeightedRnd( $LowValue, $maxRand, 0.0, 1.0 ) ;
}
For the simple bell distribution, just call bellWeightedRnd() with the min and max values; for a more sophisticated distribution, gaussianWeightedRnd() allows you to specify the mean and stdev for your distribution as well.
The gaussian bell curve is well suited to IQ distribution, although I also have similar routines for alternative distribution curves such as poisson, gamma, logarithmic, &c.
first assume you have 3 function to provide high medium and low IQs, then simply
function randomIQ(){
$dice = rand(1,100);
if($dice <= 25) $iq = low_iq();
elseif($dice <= 75) $iq = medium_iq();
else $iq = high_iq();
return $iq;
}
You could randomize multiple 'dice', random number from each adding up to the highest point. This will generate a normal distribution (approximately).
Using the link that ithcy posted I created the following function:
function RandomIQ()
{
return round((rand(-1000,1000) + rand(-1000,1000) + rand(-1000,1000))/100,0) * 2 + 100;
}
It's a little messy but some quick checking gives it a mean of approximately 100 and a roughly Normal Distribution. It should fall in line with the information that I got from this site.

Algorithm to add Color in Bezier curves

I'm playing with GD library for a while and more particuraly with Bezier curves atm.
I used some existant class which I modified a little (seriously eval()...). I found out it was a generic algorithm used in and convert for GD.
Now I want to take it to another level: I want some colors.
No problem for line color but with fill color it's harder.
My question is:
Is there any existant algorithm for that? I mean mathematical algorithm or any language doing it already so that I could transfer it to PHP + GD?
EDIT2
So, I tried #MizardX solution with a harder curve :
1st position : 50 - 50
final position : 50 - 200
1st control point : 300 - 225
2nd control point : 300 - 25
Which should show this :
And gives this :
EDIT
I already read about #MizardX solution. Using imagefilledpolygon to make it works.
But it doesn't work as expected. See the image below to see the problem.
Top graph is what I expect (w/o the blackline for now, only the red part).
Coordinates used:
first point is 100 - 100
final point is 300 - 100
first control point is 100 - 0
final control point is 300 - 200
Bottom part is what I get with that kind of algorithm...
Convert the Bezier curve to a polyline/polygon, and fill that. If you evaluate the Bezier polynomial at close enough intervals (~1 pixel) it will be identical to an ideal Bezier curve.
I don't know how familiar you are with Bezier curves, but here is a crash course:
<?php
// Calculate the coordinate of the Bezier curve at $t = 0..1
function Bezier_eval($p1,$p2,$p3,$p4,$t) {
// lines between successive pairs of points (degree 1)
$q1 = array((1-$t) * $p1[0] + $t * $p2[0],(1-$t) * $p1[1] + $t * $p2[1]);
$q2 = array((1-$t) * $p2[0] + $t * $p3[0],(1-$t) * $p2[1] + $t * $p3[1]);
$q3 = array((1-$t) * $p3[0] + $t * $p4[0],(1-$t) * $p3[1] + $t * $p4[1]);
// curves between successive pairs of lines. (degree 2)
$r1 = array((1-$t) * $q1[0] + $t * $q2[0],(1-$t) * $q1[1] + $t * $q2[1]);
$r2 = array((1-$t) * $q2[0] + $t * $q3[0],(1-$t) * $q2[1] + $t * $q3[1]);
// final curve between the two 2-degree curves. (degree 3)
return array((1-$t) * $r1[0] + $t * $r2[0],(1-$t) * $r1[1] + $t * $r2[1]);
}
// Calculate the squared distance between two points
function Point_distance2($p1,$p2) {
$dx = $p2[0] - $p1[0];
$dy = $p2[1] - $p1[1];
return $dx * $dx + $dy * $dy;
}
// Convert the curve to a polyline
function Bezier_convert($p1,$p2,$p3,$p4,$tolerance) {
$t1 = 0.0;
$prev = $p1;
$t2 = 0.1;
$tol2 = $tolerance * $tolerance;
$result []= $prev[0];
$result []= $prev[1];
while ($t1 < 1.0) {
if ($t2 > 1.0) {
$t2 = 1.0;
}
$next = Bezier_eval($p1,$p2,$p3,$p4,$t2);
$dist = Point_distance2($prev,$next);
while ($dist > $tol2) {
// Halve the distance until small enough
$t2 = $t1 + ($t2 - $t1) * 0.5;
$next = Bezier_eval($p1,$p2,$p3,$p4,$t2);
$dist = Point_distance2($prev,$next);
}
// the image*polygon functions expect a flattened array of coordiantes
$result []= $next[0];
$result []= $next[1];
$t1 = $t2;
$prev = $next;
$t2 = $t1 + 0.1;
}
return $result;
}
// Draw a Bezier curve on an image
function Bezier_drawfilled($image,$p1,$p2,$p3,$p4,$color) {
$polygon = Bezier_convert($p1,$p2,$p3,$p4,1.0);
imagefilledpolygon($image,$polygon,count($polygon)/2,$color);
}
?>
Edit:
I forgot to test the routine. It is indeed as you said; It doesn't give a correct result. Now I have fixed two bugs:
I unintentionally re-used the variable names $p1 and $p2. I renamed them $prev and $next.
Wrong sign in the while-loop. Now it loops until the distance is small enough, instead of big enough.
I checked the algorithm for generating a Polygon ensuring a bounded distance between successive parameter-generated points, and seems to work well for all the curves I tested.
Code in Mathematica:
pts={{50,50},{300,225},{300,25},{50,200}};
f=BezierFunction[pts];
step=.1; (*initial step*)
While[ (*get the final step - Points no more than .01 appart*)
Max[
EuclideanDistance ###
Partition[Table[f[t],{t,0,1,step}],2,1]] > .01,
step=step/2]
(*plot it*)
Graphics#Polygon#Table[f[t],{t,0,1,step}]
.
.
The algorithm could be optimized (ie. generate less points) if you don't require the same parameter increment between points, meaning you can chose a parameter increment at each point that ensures a bounded distance to the next.
Random examples:
Generate a list of successive points which lie along the curve (p_list)).
You create a line between the two end points of the curve (l1).
Then you are going to find the normal of the line (n1). Using this normal find the distance between the two furthest points (p_max1, and p_max2) along this normal (d1). Divide this distance into n discrete units (delta).
Now shift l1 along n1 by delta, and solve for the points of intersection (start with brute force and check for a solution between all the line segments in p_list). You should be able to get two points of intersection for each shift of l1, excepting boundaries and self intersection where you may have only have a single point. Hopefully the quad routine can have two points of the quad be at the same location (a triangle) and fill without complaint otherwise you'll need triangles in this case.
Sorry I didn't provide pseudo code but the idea is pretty simple. It's just like taking the two end points and joining them with a ruler and then keeping that ruler parallel to the original line start at one end and with successive very close pencil marks fill in the whole figure. You'll see that when you create your little pencil mark (a fine rectangle) that the rectangle it highly unlikely to use the points on the curve. Even if you force it to use a point on one side of the curve it would be quite the coincidence for it to exactly match a point on the other side, for this reason it is better to just calculate new points. At the time of calculating new points it would probably be a good idea to regenerate the curves p_list in terms of these points so you can fill it more quickly (if the curve is to stay static of course otherwise it wouldn't make any sense).
This answer is very similar to #MizardX's, but uses a different method to find suitable points along the Bezier for a polygonal approximation.
function split_cubic($p, $t)
{
$a_x = $p[0] + ($t * ($p[2] - $p[0]));
$a_y = $p[1] + ($t * ($p[3] - $p[1]));
$b_x = $p[2] + ($t * ($p[4] - $p[2]));
$b_y = $p[3] + ($t * ($p[5] - $p[3]));
$c_x = $p[4] + ($t * ($p[6] - $p[4]));
$c_y = $p[5] + ($t * ($p[7] - $p[5]));
$d_x = $a_x + ($t * ($b_x - $a_x));
$d_y = $a_y + ($t * ($b_y - $a_y));
$e_x = $b_x + ($t * ($c_x - $b_x));
$e_y = $b_y + ($t * ($c_y - $b_y));
$f_x = $d_x + ($t * ($e_x - $d_x));
$f_y = $d_y + ($t * ($e_y - $d_y));
return array(
array($p[0], $p[1], $a_x, $a_y, $d_x, $d_y, $f_x, $f_y),
array($f_x, $f_y, $e_x, $e_y, $c_x, $c_y, $p[6], $p[7]));
}
$flatness_sq = 0.25; /* flatness = 0.5 */
function cubic_ok($p)
{
global $flatness_sq;
/* test is essentially:
* perpendicular distance of control points from line < flatness */
$a_x = $p[6] - $p[0]; $a_y = $p[7] - $p[1];
$b_x = $p[2] - $p[0]; $b_y = $p[3] - $p[1];
$c_x = $p[4] - $p[6]; $c_y = $p[5] - $p[7];
$a_cross_b = ($a_x * $b_y) - ($a_y * $b_x);
$a_cross_c = ($a_x * $c_y) - ($a_y * $c_x);
$d_sq = ($a_x * $a_x) + ($a_y * $a_y);
return max($a_cross_b * $a_cross_b, $a_cross_c * $a_cross_c) < ($flatness_sq * $d_sq);
}
$max_level = 8;
function subdivide_cubic($p, $level)
{
global $max_level;
if (($level == $max_level) || cubic_ok($p)) {
return array();
}
list($q, $r) = split_cubic($p, 0.5);
$v = subdivide_cubic($q, $level + 1);
$v[] = $r[0]; /* add a point where we split the cubic */
$v[] = $r[1];
$v = array_merge($v, subdivide_cubic($r, $level + 1));
return $v;
}
function get_cubic_points($p)
{
$v[] = $p[0];
$v[] = $p[1];
$v = array_merge($v, subdivide_cubic($p, 0));
$v[] = $p[6];
$v[] = $p[7];
return $v;
}
function imagefilledcubic($img, $p, $color)
{
$v = get_cubic_points($p);
imagefilledpolygon($img, $v, count($v) / 2, $color);
}
The basic idea is to recursively split the cubic in half until the bits we're left with are almost flat. Everywhere we split the cubic, we stick a polygon point.
split_cubic splits the cubic in two at parameter $t. cubic_ok is the "are we flat enough?" test. subdivide_cubic is the recursive function. Note that we stick a limit on the recursion depth to avoid nasty cases really screwing us up.
Your self-intersecting test case:
$img = imagecreatetruecolor(256, 256);
imagefilledcubic($img, array(
50.0, 50.0, /* first point */
300.0, 225.0, /* first control point */
300.0, 25.0, /* second control point */
50.0, 200.0), /* last point */
imagecolorallocate($img, 255, 255, 255));
imagepng($img, 'out.png');
imagedestroy($img);
Gives this output:
I can't figure out how to make PHP nicely anti-alias this; imageantialias($img, TRUE); didn't seem to work.

Categories