From a3e62c5720c0d466c6998b16c055cb0e15b5bdc6 Mon Sep 17 00:00:00 2001 From: Dave M Date: Mon, 16 May 2022 01:22:47 +0000 Subject: [PATCH] - First commit of an old reworked project --- LICENSE | 14 ++ composer.json | 21 +++ src/Canvas.php | 306 ++++++++++++++++++++++++++++++++++++++++++++ src/Colors.php | 84 ++++++++++++ src/Draw.php | 32 +++++ src/Image.php | 202 +++++++++++++++++++++++++++++ src/Layer.php | 42 ++++++ src/Merge.php | 77 +++++++++++ src/Polygon.php | 113 ++++++++++++++++ src/Text.php | 20 +++ src/arrayobject.php | 189 +++++++++++++++++++++++++++ 11 files changed, 1100 insertions(+) create mode 100644 LICENSE create mode 100644 composer.json create mode 100644 src/Canvas.php create mode 100644 src/Colors.php create mode 100644 src/Draw.php create mode 100644 src/Image.php create mode 100644 src/Layer.php create mode 100644 src/Merge.php create mode 100644 src/Polygon.php create mode 100644 src/Text.php create mode 100644 src/arrayobject.php diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c0ec6bc --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ +Copyright (C) 2017 Dave Mc Nicoll + +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 3 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, see . diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..05182db --- /dev/null +++ b/composer.json @@ -0,0 +1,21 @@ +{ + "name": "mcnd/imagine", + "description": "A simple image processor using GD extension.", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Dave Mc Nicoll", + "email": "mcndave@gmail.com" + } + ], + "require": { + "php": "^7.4", + "ext-gd": "*" + }, + "autoload": { + "psr-4": { + "Imagine\\": "src/" + } + } +} diff --git a/src/Canvas.php b/src/Canvas.php new file mode 100644 index 0000000..28284c8 --- /dev/null +++ b/src/Canvas.php @@ -0,0 +1,306 @@ +create($width, $height); + } + } + + public function __destruct() { + $this->destroy(); + } + + public function destroy() : self + { + \is_resource($this->image) && \imagedestroy($this->image); + + return $this; + } + + public function create(int $width, int $height) : self + { + $this->width = $width; + $this->height = $height; + + $image = \imagecreatetruecolor($width, $height); + + $colors = new Colors($image); + $colors['fore'] = empty($this->colors['fore']) ? Colors::allocate($image, "000000") : Colors::allocate($image, $this->colors['fore']); + $colors['brush'] = empty($this->colors['brush']) ? Colors::allocate($image, "FFFFFF:0.0") : Colors::allocate($image, $this->colors['brush']); + $colors['back'] = empty($this->colors['back']) ? Colors::allocate($image, ":0.0") : Colors::allocate($image, $this->colors['back']); + $colors['text'] = empty($this->colors['text']) ? Colors::allocate($image, "000000") : Colors::allocate($image, $this->colors['text']); + + $this->colors = $colors; + + if ( empty($this->font['id']) ) { + $this->font['id'] = 1; + } + + \imagesavealpha($image, true); + \imagealphablending($image, true); + \imagefill($image, 0, 0, IMG_COLOR_TRANSPARENT); + + $this->image($image); + + return $this; + } + + /* Getters / Setters */ + public function parent($set = null) { + return $set === null ? $this->parent : $this->parent = $set; + } + + public function width($set = null) { + return $set === null ? $this->width : $this->width = $set; + } + + public function height($set = null) { + return $set === null ? $this->height : $this->height = $set; + } + + public function & image($set = null) { + if ( $set ) { + $this->image = $set; + }; + + return $this->image; + } + + public function layers($name = null, $replace = null) { + return $name ? ( $replace ? $this->layers[$name] = $replace : $this->layers[$name] ) : $this->layers; + } + + public function addLayer($name, $param = array()) : Layer + { + $param = $param + [ + 'width' => $this->width, + 'height' => $this->height, + 'parent' => $this + ]; + + return $this->layers[$name] = Layer::make($param)->onTop($this); + } + + /* + * Assign a color to the current image resource + * + * @param array $colors = [ + * 'color' => "aacc22:0.4" + * 'background' => ':0.0" # transparency + * ] + * + */ + public function colors($colors) : ? Colors + { + if ($colors && is_array($colors)) { + $colorobj = Colors::make($this->image()); + + foreach($colors as $key => $value) { + $colorobj[$key] = $value; + } + + return $colorobj; + } + + return null; + } + + public function measureText(string $text) : int + { + return Text::measure($text, $this->font['id']); + } + + public function drawText(string $text, int $x, int $y, array $colors = []) : bool + { + return Text::draw($this->image(), $colors ?: $this->colors, $this->font['id'], $text, $x, $y); + } + + public function polygonLabel($labels, $spacing, $x, $y, $size, $angle = 0) : array + { + $vertexcount = count($labels); + $r = $size / 2; + $x = $x; + $y = $y; + + $coord = []; + $points = Polygon::polygonCoord($vertexcount, $x, $y, $size, $angle); + + for ($i = 0; $i < $vertexcount; $i++) { + + $textsize = $this->measureText($labels[$i]); + + $tx = $points["x$i"]; + $ty = $points["y$i"]; + + # processing X + # we are in the center of the poly width + if ( ! \bccomp($tx, $x) ) { + $coord["x$i"] = $x - $textsize['width'] / 2; + } + else if ($tx > $x) { + $coord["x$i"] = $tx + $spacing; + } + else { + #echo $x . " = " . $tx . "
"; + $coord["x$i"] = $tx - $spacing - $textsize['width']; + } + + # processing Y + if ( ! \bccomp($ty, $y) ) { + $coord["y$i"] = $y - $textsize['height'] / 2; + } + elseif ($ty > $y) { + $coord["y$i"] = $ty + $spacing; + } + else { + $coord["y$i"] = $ty - $textsize['height'] - $spacing; + } + } + + return $coord; + } + + public function fillBorder($x, $y, $colors = null) : bool + { + return \imagefilltoborder($this->image(), $x, $y, $colors ? $colors['fore'] : $this->colors['fore'], $colors ? $colors['brush'] : $this->colors['brush']); + } + + public function merge(resource $image, int $x = 0, int $y = 0, int $srcX = 0, int $srcY = 0) : self + { + Merge::simple($this->image(), $image, $x, $y, $srcX, $srcY); + + return $this; + } + + public function grabImageResource(Canvas $src) : void + { + $this->destroy(); + $this->image($src->image()); + + unset($src->image); + } + + public function constrain(int $width, int $height, bool $immutable = true) : self + { + if ( ($this->width > $width) || ($this->height > $height) ) { + if ($this->width > $this->height) { + $height = $this->height * ( $width / $this->width ); + } + else { + $width = $this->width * ( $height / $this->height ); + } + + $new = ( new static() )->create($width, $height); + $this->copyTo( $new ); + + if ( ! $immutable ) { + $this->grabImageResource($new); + + $this->width = $width; + $this->height = $height; + } + else { + return $new; + } + } + + return $this; + } + + public function copyTo(Canvas $dest, int $dst_x = 0, int $dst_y = 0, int $src_x = 0, int $src_y = 0, int $dst_w = 0, int $dst_h = 0, int $src_w = 0, int $src_h = 0) : self + { + if ( ! \imagecopyresampled($dest->image(), $this->image(), $dst_x, $dst_y, $src_x, $src_y, $dst_w ?: $dest->width, $dst_h ?: $dest->height, $src_w ?: $this->width, $src_h ?: $this->height) ) { + trigger_error('Given image was not resampled : an error occured', \E_USER_ERROR); + } + + return $this; + } + + public function drawLine(int $x1, int $y1, int $x2, int $y2, array $colors = []) : bool + { + return Draw::line($this->image(), $x1, $y1, $x2, $y2, $colors ? : $this->colors); + } + + public function drawRectangle(int $x1, int $y1, int $width, int $height, array $colors = []) : bool + { + return Polygon::drawRectangle($this->image(), $x1, $y1, $x1 + $width, $y1 + $height, $colors ? $colors : $this->colors); + } + + public function drawIrregularPolygon(array $points, int $x, int $y, $angle = 0, array $colors = []) : bool + { + return Polygon::drawIrregular($this->image(), $colors ? : $this->colors, $points, $x, $y, $angle); + } + + public function drawPolygon(int $vertexcount, int $x, int $y, int $size, $angle = 0, $colors = []) : bool + { + return Polygon::draw($this->image(), $colors ? : $this->colors, $vertexcount, $x, $y, $size, $angle); + } + + public function drawCircle(int $x, int $y, int $size, array $colors = []) : bool + { + return Polygon::drawEllipse($this->image(), $colors ? : $this->colors, $x, $y, $size, $size); + } + + public function drawEllipse(int $x, int $y, int $width, int $height, array $colors = []) : bool + { + return Polygon::drawEllipse($this->image(), $colors ? : $this->colors, $x, $y, $width, $height); + } + + public function drawTriangle(int $posX, int $posY, int $size, $angle = 0, array $colors = []) : bool + { + return Polygon::draw($this->image(), $colors ? : $this->colors, 3, $posX, $posY, $size, $angle); + } + + public function drawQuadrilateral(int $posX, int $posY, int $size, $angle = 0, array $colors = []) : bool + { + return Polygon::draw($this->image(), $colors ? : $this->colors, 4, $posX, $posY, $size, $angle); + } + + public function drawPentagon(int $posX, int $posY, $size, $angle = 0, array $colors = []) : bool + { + return Polygon::draw($this->image(), $colors ? : $this->colors, 5, $posX, $posY, $size, $angle); + } + + public function drawHexagon(int $posX, int $posY, int $size, $angle = 0, array $colors = []) : bool + { + return Polygon::draw($this->image(), $colors ? : $this->colors, 6, $posX, $posY, $size, $angle); + } + + public function drawHeptagon(int $posX, int $posY, int $size, $angle = 0, array $colors = []) : bool + { + return Polygon::draw($this->image(), $colors ? : $this->colors, 7, $posX, $posY, $size, $angle); + } + + public function drawOctagon(int $posX, int $posY, int $size, $angle = 0, array $colors = []) : bool + { + return Polygon::draw($this->image(), $colors ? : $this->colors, 8, $posX, $posY, $size, $angle); + } + + public function drawNonagon(int $posX, int $posY, int $size, $angle = 0, array $colors = []) : bool + { + return Polygon::draw($this->image(), $colors ? : $this->colors, 9, $posX, $posY, $size, $angle); + } + + public function drawDecagon(int $posX, int $posY, int $size, $angle = 0, array $colors = []) : bool + { + return Polygon::draw($this->image(), $colors ? : $this->colors, 10, $posX, $posY, $size, $angle); + } +} diff --git a/src/Colors.php b/src/Colors.php new file mode 100644 index 0000000..3729bc4 --- /dev/null +++ b/src/Colors.php @@ -0,0 +1,84 @@ +image = $image; + } + + public static function convertHexToRGB($hex) { + return [ + 'r' => (int) base_convert(substr($hex, 0, 2), 16, 10), + 'g' => (int) base_convert(substr($hex, 2, 2), 16, 10), + 'b' => (int) base_convert(substr($hex, 4, 2), 16, 10), + ]; + } + + function compare($c1, $c2, $tolerance = 35) { + $c1 = is_array($c1) ? $c1 : [ + "r" => hexdec(substr($c1, 0, 2)), + "g" => hexdec(substr($c1, 2, 2)), + "b" => hexdec(substr($c1, 4, 2)) + ]; + $c2 = is_array($c2) ? $c2 : [ + "r" => hexdec(substr($c2, 0, 2)), + "g" => hexdec(substr($c2, 2, 2)), + "b" => hexdec(substr($c2, 4, 2)) + ]; + + return ($c1['r'] >= $c2['r'] - $tolerance && $c1['r'] <= $c2['r'] + $tolerance) && + ($c1['g'] >= $c2['g'] - $tolerance && $c1['g'] <= $c2['g'] + $tolerance) && + ($c1['b'] >= $c2['b'] - $tolerance && $c1['b'] <= $c2['b'] + $tolerance); + } + + /* * + * @param mixed $color : + array = [ int r, int g, int b, int a ], + string = "hex:alpha" ( ex: "1bd4a9:94" ) + * + */ + public static function allocate(&$img, $color = null) { + + if ( $color !== null ) { + # If we've received an Hex color , we must convert it into rgb(a). + if ( is_string($color) ) { + $hex = explode(':', $color); + + $color = self::convertHexToRGB($hex[0]); + $color['a'] = isset($hex[1]) ? (float)$hex[1] : null; + } + + $colors['a'] = isset($colors['a']) ? $colors['a'] : null; + + return $color['a'] !== null ? \imagecolorallocatealpha($img, $color['r'], $color['g'], $color['b'], (1 - $color['a']) * 127) : \imagecolorallocate($img, $color['r'], $color['g'], $color['b']); + } + + return null; + } + + public function offsetSet($offset, $value) { + if ( is_null($offset) ) { + $this->_container[] = self::allocate($this->image, $value); + } else { + if (is_string($value)) { + $this->_container[$offset] = self::allocate($this->image, $value); + } + else { + $this->_container[$offset] = $value; + } + } + } + + +} diff --git a/src/Draw.php b/src/Draw.php new file mode 100644 index 0000000..61a65ad --- /dev/null +++ b/src/Draw.php @@ -0,0 +1,32 @@ +filename = $image; + $this->fromFile($image); + } + elseif ( is_resource($image) ) { + $this->image = $image; + } + else { + parent::__construct(); + } + } + + public function type($set = null) { + return $set === null ? $this->type : $this->type = $set; + } + + public function fromFile($filename) { + switch ( \exif_imagetype($filename) ) { + case 1 : + $this->forceType('image/gif')->image(\imagecreatefromgif($filename)); + break; + case 2 : + $this->forceType('image/jpeg')->image($this->loadJpeg($filename)); + break; + case 3 : + $this->forceType('image/png')->image(\imagecreatefrompng($filename)); + break; + case 6 : + $this->forceType('image/bmp')->image(\imagecreatefrombmp($filename)); + break; + case 18: + $this->forceType('image/webp')->image(\imagecreatefromwebp($filename)); + break; + } + + $this->width = \imagesx($this->image); + $this->height = \imagesy($this->image); + + return $this; + } + + public function fromString($content) { + $this->image( \imagecreatefromstring($content) ); + + $info = \getimagesizefromstring($content); + + $this->width = $info[0]; + $this->height = $info[1]; + + $this->forceType($info['mime']); + + return $this; + } + + public function forceType(string $type) { + $this->type = $type; + + return $this; + } + + public function loadJpeg(string $filename) { + $img = \imagecreatefromjpeg($filename); + $exif = @\exif_read_data($filename); + + if ( $img && !empty($exif['Orientation']) ) { + switch($exif['Orientation']){ + case 6: + case 5: + $img = \imagerotate($img, 270, null); + break; + + case 3: + case 4: + $img = \imagerotate($img, 180, null); + break; + + case 7: + case 8: + $img = \imagerotate($img, 90, null); + break; + + /* + case 4: + case 5: + case 7: + \imageflip($img, \IMG_FLIP_HORIZONTAL); + break; + */ + } + } + + return $img; + } + + public function save(int $quality = null, $filters = null) : bool + { + return $this->render($this->filename, $quality, $filters); + } + + public function render(string $path = null, int $quality = null, $filters = null) # : bool + { + $layers = $this->layers(); + + if ($path === null) { + $path = fopen("php://memory", "w+"); + } + + usort($layers, function($e1, $e2) { + return $e1->order() <=> $e2->order(); + }); + + foreach($layers as $item) { + $this->merge( $item->image() ); + } + + switch( $this->type ) { + case "image/gif": + $retval = \imagegif($this->image(), $path); + break; + + case "image/webp": + $retval = \imagewebp($this->image(), $path, $quality ?: static::DEFAULT_QUALITY); + break; + + case "image/png": + \imagealphablending($this->image, true); + \imagesavealpha($this->image, true); + $retval = \imagepng($this->image(), $path, $quality === null ? round( static::DEFAULT_QUALITY / 10, 0) : $this->normalizeQuality($quality), $filters); + break; + + default: + case "image/jpeg": + case "image/jpg": + $retval = \imagejpeg($this->image(), $path, $quality ?: static::DEFAULT_QUALITY); + break; + + } + + if ( is_resource($path) ) { + rewind($path); + $retval = stream_get_contents($path); + } + + return $retval ?? null; + } + + public function readExif() : array + { + return \exif_read_data($this->filename, 0, true); + } + + public function remove_exif($old, $new) { + $f1 = fopen($old, 'rb'); + $f2 = fopen($new, 'wb'); + + // Find EXIF marker + while ( $s = fread($f1, 2) ) { + $word = unpack('ni', $s)['i']; + if ($word == 0xFFE1) { + // Read length (includes the word used for the length) + $s = fread($f1, 2); + $len = unpack('ni', $s)['i']; + // Skip the EXIF info + fread($f1, $len - 2); + break; + } else { + fwrite($f2, $s, 2); + } + } + + // Write the rest of the file + while ( $s = fread($f1, 4096) ) { + fwrite($f2, $s, strlen($s)); + } + + fclose($f1); + fclose($f2); + } + + protected function normalizeQuality($quality) { + switch($this->type()) { + case "image/png": + return round($quality / 100, 0); + + default: + return $quality; + } + } +} diff --git a/src/Layer.php b/src/Layer.php new file mode 100644 index 0000000..ba91d63 --- /dev/null +++ b/src/Layer.php @@ -0,0 +1,42 @@ +order = ($this->order-- === 0) ? 0 : $this->order; + + return $this; + } + + public function moveDown() { + $this->order++; + + return $this; + } + + /* Setting current layer on top, based on every other layers */ + public function onTop() { + $max = 0; + + foreach ($this->parent->layers() as $value) { + $max = max($max, $value->order()); + } + + $this->order = $max + 1; + + return $this; + } + + public function order($set = null) { + return $set ? ($this->order = $set) : $this->order; + } + +} diff --git a/src/Merge.php b/src/Merge.php new file mode 100644 index 0000000..a8fdbfc --- /dev/null +++ b/src/Merge.php @@ -0,0 +1,77 @@ += 0 && $x + $destX < $destW && $x + $srcX >= 0 && $x + $srcX < $srcW && $y + $destY >= 0 && $y + $destY < $destH && $y + $srcY >= 0 && $y + $srcY < $srcH) { + + $destPixel = \imagecolorsforindex($destImg, \imagecolorat($destImg, $x + $destX, $y + $destY)); + $srcImgColorat = \imagecolorat($srcImg, $x + $srcX, $y + $srcY); + + if ($srcImgColorat >= 0) { + $srcPixel = \imagecolorsforindex($srcImg, $srcImgColorat); + + $srcAlpha = 1 - ($srcPixel['alpha'] / 127); + $destAlpha = 1 - ($destPixel['alpha'] / 127); + $opacity = $srcAlpha * $pct / 100; + + if ($destAlpha >= $opacity) { + $alpha = $destAlpha; + } + + if ($destAlpha < $opacity) { + $alpha = $opacity; + } + + if ($alpha > 1) { + $alpha = 1; + } + + if ($opacity > 0) { + $destRed = round((($destPixel['red'] * $destAlpha * (1 - $opacity)))); + $destGreen = round((($destPixel['green'] * $destAlpha * (1 - $opacity)))); + $destBlue = round((($destPixel['blue'] * $destAlpha * (1 - $opacity)))); + $srcRed = round((($srcPixel['red'] * $opacity))); + $srcGreen = round((($srcPixel['green'] * $opacity))); + $srcBlue = round((($srcPixel['blue'] * $opacity))); + $red = round(($destRed + $srcRed ) / ($destAlpha * (1 - $opacity) + $opacity)); + $green = round(($destGreen + $srcGreen) / ($destAlpha * (1 - $opacity) + $opacity)); + $blue = round(($destBlue + $srcBlue ) / ($destAlpha * (1 - $opacity) + $opacity)); + + if ($red > 255) { + $red = 255; + } + + if ($green > 255) { + $green = 255; + } + + if ($blue > 255) { + $blue = 255; + } + + $alpha = round((1 - $alpha) * 127); + $color = \imagecolorallocatealpha($destImg, $red, $green, $blue, $alpha); + \imagesetpixel($destImg, $x + $destX, $y + $destY, $color); + } + } + } + } + } + + return true; + } +} diff --git a/src/Polygon.php b/src/Polygon.php new file mode 100644 index 0000000..fc9f3d3 --- /dev/null +++ b/src/Polygon.php @@ -0,0 +1,113 @@ + \imagepolygon($image, $data, count($data) / 2, $colors['fore']), + "points" => $data, + "coord" => $coord + ]; + } + + public static function polygonCoord($vertexcount, $x, $y, $size, $angle = 0) : array + { + $n = 0; + $r = $size / 2; + $pi2 = M_PI * 2; + + $points = []; + while ( $n < $vertexcount ) { + $points["x$n"] = $r * cos($pi2 * $n / $vertexcount + $angle) + $x; + $points["y$n"] = $r * sin($pi2 * $n / $vertexcount + $angle) + $y; + $n++; + } + + return $points; + } + + public static function rotatePoint($theta, int $x, int $y, int $px, int $py) : array + { + $cos = cos($theta); + $sin = sin($theta); + + return [ + 'x' => $cos * ($x - $px) - $sin * ($y - $py) + $px, + 'y' => $sin * ($x - $px) + $cos * ($y - $py) + $py + ]; + } + + public function dist(int $x1, int $y1, int $x2, int $y2) : float + { + return sqrt( ($x1 - $x2) * ($x1 - $x2) + ($y1 - $y2) * ($y1 - $y2) ); + } + + /* + ** Calculate the angle between 2 points, where Xc,Yc is the center of a circle + ** and x,y is a point on its circumference. All angles are relative to + ** the 3 O'Clock position. Result returned in radians + */ + public function angle($xc, $yc, $x1, $y1) : float + { + # calculate distance between two points + $d = $this->distBetween($xc, $yc, $x1, $y1); + + if ($d != 0) + if ( asin( ($y1-$yc)/$d ) >= 0 ) + $a1 = acos( ($x1-$xc)/$d ); + else + $a1 = 2 * pi() - acos( ($x1-$xc)/$d ); + else + $a1 = 0; + + return $a1; + } + + public static function drawRectangle(resource $image, int $x1, int $y1, int $x2, int $y2, array $colors) : bool + { + if ( $colors['brush'] ) { + \imagefilledrectangle($image, $x1, $y1, $x2, $y2, $colors['brush']); + } + + return \imagerectangle ( $image , $x1 , $y1 , $x2 , $y2 , $colors['fore'] ); + } + + public static function drawIrregular(resource $image, array $colors, array $points) : array + { + $data = array_values($points); + + if ( $colors['brush'] ) { + \imagefilledpolygon($image, $data, count($data) / 2, $colors['brush']); + } + + return [ + "return" => \imagepolygon($image, $data, count($data) / 2, $colors['fore']), + "points" => $data + ]; + } + + public static function drawEllipse(resource $image, array $colors, int $posX, int $posY, int $width, int $height) : bool + { + + if ( $colors['brush'] ) { + \imagefilledellipse($image, $posX, $posY, $width, $height, $colors['brush']); + } + + return \imageellipse($image, $posX, $posY, $width, $height, $colors['fore']); + } + +} diff --git a/src/Text.php b/src/Text.php new file mode 100644 index 0000000..d193866 --- /dev/null +++ b/src/Text.php @@ -0,0 +1,20 @@ + \imagefontwidth($fontid) * strlen(utf8_decode($text)), + "height" => \imagefontheight($fontid) + ]; + } + +} diff --git a/src/arrayobject.php b/src/arrayobject.php new file mode 100644 index 0000000..26a2046 --- /dev/null +++ b/src/arrayobject.php @@ -0,0 +1,189 @@ +container() ); + } + + public function contains($term, $strict = false) { + return (array_search($term, $this->container(), $strict) !== false) ; + } + + public function &arrayobjectCurrent() { + if ( !is_null($this->pointer) ) { + $var = &$this->container()[$this->pointer] ?: []; + $var || ( $var = [] ); + return $var; + } + + if ( $this->selected !== false ){ + $ret = &$this->selected ?: []; + return $ret; + } + + # Restoring integrity of container since it could be nullified + is_array($this->container()) || $this->container([]); + + return $this->container(); + } + + public function offsetSet($offset, $value, $changed = null) { + if ( $changed && (!isset($this->arrayobjectCurrent()[$offset]) || ($this->arrayobjectCurrent()[$offset] !== $value) ) ) { + $this->changed($offset, true); + } + + return is_null($offset) ? $this->arrayobject_current()[] = $value : $this->arrayobject_current()[$offset] = $value; + } + + public function arrayobjectSetPointer($pointer) { + # $pointer could nullify obj pointer + if ( $this->pointer = $pointer ) { + # Creating dataset whenever we have a new one + if ( !isset($this->container()[$this->pointer]) ) { + $this->container()[$this->pointer] = []; + $this->changed[$this->pointer] = []; + } + } + + return $this; + } + + public function arrayobjectSelect($selection, $purge = true) { + if ( is_bool($selection) ) { + return $this->selected = $selection; + } + + $purge && ( $this->selected = [] ); + + foreach($selection as $pointer) { + $this->selected[$pointer] = &$this->container[$pointer]; + } + + return true; + } + + public function arrayobjectExist($pointer) { + return isset( $this->container()[$pointer] ); + } + + public function arrayobjectFlushChanged() { + ! is_null($this->pointer) ? + $this->changed[$this->pointer] = [] + : + $this->changed = [] + ; + } + + public function changed($offset = null, $set = null) { + if ($offset) { + if ($set !== null) { + ! is_null($this->pointer) ? + ( $this->changed[$this->pointer][$offset] = $set ) + : + ( $this->changed[$offset] = $set ); + } + + return !is_null($this->pointer) ? $this->changed[$this->pointer][$offset] : $this->changed[$offset]; + } + + return array_keys( !is_null($this->pointer) + ? $this->changed[$this->pointer] ?? [] + : $this->changed) ?? []; + } + + public function arrayobjectRemove($pointer) { + if ( isset($this->container()[$pointer]) ) { + unset( $this->container()[$pointer], $this->changed[$pointer]); + } + } + + public function arrayobjectIterate($callback) { + if ( $callback && is_callable($callback) ) { + $pointer = $this->pointer; + + foreach($this->container() as $key => $value) { + $this->arrayobjectSetPointer($key); + $callback($key, $this); + } + + $this->arrayobjectSetPointer($pointer); + } + + return $this; + } + + public function offsetGet($offset) { + if ( !is_null($this->pointer) ) { + return isset($this->container()[$this->pointer][$offset]) ? $this->container()[$this->pointer][$offset] : null; + } + else { + return isset($this->container()[$offset]) ? $this->container()[$offset] : null; + } + } + + public function offsetExists($offset) { + return array_key_exists($offset, $this->arrayobjectCurrent() ); + } + + public function offsetUnset($offset) { + if ( !is_null($this->pointer)) { + unset($this->container()[$this->pointer][$offset]); + } + else { + unset($this->container()[$offset]) ; + } + } + + public function sort($field, $order = 'ASC') { + # Arrayobj::order_by($this->container(), $field); + # $order === 'DESC' && array_reverse($this->arrayobject_current()); + } + + public function rewind() { + reset( $this->container() ); + + # Rewinding will also reset the pointer + $this->arrayobjectSetPointer(key($this->container())); + + return $this; + } + + public function current() { + return $this->arrayobjectSetPointer( $this->key() ); + } + + public function key() { + $var = key( $this->container() ); + return $var; + } + + public function next() { + $var = next( $this->container() ); + return $var; + } + + public function valid() { + $key = $this->key(); + return ( $key !== NULL ) && ( $key !== FALSE ); + } + + protected function &container($set = null) { + if ( $set !== null ) { + $this->container = $set; + } + + if ( $this->selected !== false ) { + return $this->selected; + } + + return $this->container; + } +}