outputPath = $outputPath; // Detecta o tipo da imagem e carrega $imageInfo = getimagesize($imagePath); if ($imageInfo === false) { throw new Exception('Failed to load image'); } switch ($imageInfo[2]) { case IMAGETYPE_JPEG: $this->srcImage = imagecreatefromjpeg($imagePath); break; case IMAGETYPE_PNG: $this->srcImage = imagecreatefrompng($imagePath); break; default: throw new Exception('Unsupported image type'); } if ($this->srcImage === false) { throw new Exception('Failed to create image resource'); } // Simplifica o pipeline para focar apenas na detecção e recorte $this->detectAndCropDocument() ->saveResult(); return $this->outputPath; } private function detectAndCropDocument() { $width = imagesx($this->srcImage); $height = imagesy($this->srcImage); // Cria imagem temporária em escala de cinza $gray = imagecreatetruecolor($width, $height); imagecopy($gray, $this->srcImage, 0, 0, 0, 0, $width, $height); imagefilter($gray, IMG_FILTER_GRAYSCALE); // Aplica threshold adaptativo para binarização $threshold = $this->getAdaptiveThreshold($gray, $width, $height); $binary = imagecreatetruecolor($width, $height); for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { $rgb = imagecolorat($gray, $x, $y); $colors = imagecolorsforindex($gray, $rgb); $value = ($colors['red'] + $colors['green'] + $colors['blue']) / 3; if ($value > $threshold) { imagesetpixel($binary, $x, $y, 0xFFFFFF); } else { imagesetpixel($binary, $x, $y, 0x000000); } } } // Encontra os contornos do documento $bounds = $this->findDocumentBounds($binary); if ($bounds) { // Adiciona margem de segurança $margin = 10; $cropX = max(0, $bounds['left'] - $margin); $cropY = max(0, $bounds['top'] - $margin); $cropWidth = min($width - $cropX, $bounds['right'] - $bounds['left'] + 2 * $margin); $cropHeight = min($height - $cropY, $bounds['bottom'] - $bounds['top'] + 2 * $margin); $this->processedImage = imagecreatetruecolor($cropWidth, $cropHeight); imagecopy($this->processedImage, $this->srcImage, 0, 0, $cropX, $cropY, $cropWidth, $cropHeight); } else { $this->processedImage = $this->srcImage; } return $this; } private function getAdaptiveThreshold($image, $width, $height) { $total = 0; $count = 0; for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { $rgb = imagecolorat($image, $x, $y); $colors = imagecolorsforindex($image, $rgb); $value = ($colors['red'] + $colors['green'] + $colors['blue']) / 3; $total += $value; $count++; } } return $total / $count; } private function findDocumentBounds($binary) { $width = imagesx($binary); $height = imagesy($binary); $left = $width; $right = 0; $top = $height; $bottom = 0; for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { $rgb = imagecolorat($binary, $x, $y); if ($rgb == 0x000000) { // Pixel preto if ($x < $left) $left = $x; if ($x > $right) $right = $x; if ($y < $top) $top = $y; if ($y > $bottom) $bottom = $y; } } } if ($left < $right && $top < $bottom) { return ['left' => $left, 'right' => $right, 'top' => $top, 'bottom' => $bottom]; } return null; } private function saveResult() { if (!isset($this->processedImage)) { throw new Exception('No processed image to save'); } $extension = strtolower(pathinfo($this->outputPath, PATHINFO_EXTENSION)); switch ($extension) { case 'jpg': case 'jpeg': imagejpeg($this->processedImage, $this->outputPath, 90); break; case 'png': imagepng($this->processedImage, $this->outputPath, 9); break; default: throw new Exception('Unsupported output image type'); } if (is_resource($this->srcImage)) { imagedestroy($this->srcImage); } if (is_resource($this->processedImage)) { imagedestroy($this->processedImage); } return $this; } }