blanks, papers, pages, fit system, render 100% precision

This commit is contained in:
2026-05-30 16:06:12 +00:00
parent 99daad5074
commit 51e8214cd2
7 changed files with 260 additions and 109 deletions

View File

@@ -8,7 +8,9 @@ namespace svoboda\pechatalka\controllers;
use svoboda\pechatalka\controllers\core, use svoboda\pechatalka\controllers\core,
svoboda\pechatalka\models\paper, svoboda\pechatalka\models\paper,
svoboda\pechatalka\models\product, svoboda\pechatalka\models\product,
svoboda\pechatalka\models\enumerations\products\type as product_type; /* svoboda\pechatalka\models\enumerations\paper\type as paper_type, */
svoboda\pechatalka\models\enumerations\paper\format as paper_format,
svoboda\pechatalka\models\enumerations\product\type as product_type;
// Framework for PHP // Framework for PHP
use mirzaev\minimal\http\enumerations\content, use mirzaev\minimal\http\enumerations\content,
@@ -194,7 +196,14 @@ final class generator extends core
if ($type === product_type::pin_37) { if ($type === product_type::pin_37) {
// Pin 37mm // Pin 37mm
$biba = paper::a5(type: $type, products: $products, canvas: $canvas); $biba = paper::generate(
format: paper_format::a6,
type: $type,
products: $products,
canvas: $canvas,
dpi: 80,
fit: true
);
} }
if (str_contains($this->request->headers['accept'], content::any->value)) { if (str_contains($this->request->headers['accept'], content::any->value)) {

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace svoboda\pechatalka\models\enumerations\paper;
// Files of the project
use svoboda\pechatalka\models\enumerations\product\type as product_type;
/**
* Types of products
*
* @package svoboda\pechatalka\models\enumerations\paper
*
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
*/
enum format
{
case a6;
/**
* Dimensions
*
* @return array The paper format dimensions in centimeters (cm) ['width', 'height']
*/
public function dimensions(): array
{
return match ($this) {
format::a6 => [
'width' => 10.5,
'height' => 14.8
]
};
}
/**
* Places
*
* @param product_type $type The product type
* @param int|float $padding The paper padding (print borders)
*
* @return array The paper places by the product type in centimeters (cm)
*/
public function places(product_type $type, int|float $padding = 0): array|false
{
if ($this === format::a6) {
// A6
// Initializing the paper dimensions
$paper = $this->dimensions();
if ($type === product_type::pin_37) {
// Pin 37mm
// Initializing the product dimensions
$blank = $type->areas()['blank'];
// Calculating the general X coordinate
$x = $paper['width'] / 2 - $blank['width'] / 2 - $padding;
// Initializing the amount of products
$amount = 2;
// Calculating the summary products height
$height = $blank['height'] * $amount;
// Calculating the free space on the page
$free = $paper['height'] - $padding * 2 - $height;
// Calculating the offset between products and from products to the paper borders
$offset = $free / ($amount + 1);
// Exit (success)
return [
[
'x' => $x,
'y' => $offset
],
[
'x' => $x,
'y' => $offset + $blank['height'] + $offset
],
];
}
}
// Exit (fail)
return false;
}
}

View File

@@ -2,12 +2,12 @@
declare(strict_types=1); declare(strict_types=1);
namespace svoboda\pechatalka\models\enumerations\products; namespace svoboda\pechatalka\models\enumerations\product;
/** /**
* Types of products * Types of products
* *
* @package svoboda\pechatalka\models\enumerations\products * @package svoboda\pechatalka\models\enumerations\product
* *
* @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License * @license http://www.wtfpl.net/ Do What The Fuck You Want To Public License
* @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy> * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
@@ -19,19 +19,19 @@ enum type
/** /**
* Areas * Areas
* *
* @return array ['visible' => ['width', 'height'], 'blank' => ['width', 'height']] * @return array The product type areas in centimeters (cm) ['visible' => ['width', 'height'], 'blank' => ['width', 'height']]
*/ */
public function areas(): array public function areas(): array
{ {
return match ($this) { return match ($this) {
type::pin_37 => [ type::pin_37 => [
'visible' => [ 'visible' => [
'width' => 37, 'width' => 3.7,
'height' => 37 'height' => 3.7
], ],
'blank' => [ 'blank' => [
'width' => 48.5, 'width' => 4.85,
'height' => 48.5 'height' => 4.85
] ]
] ]
}; };

View File

@@ -6,7 +6,9 @@ namespace svoboda\pechatalka\models;
// Files of the project // Files of the project
use svoboda\pechatalka\models\core, use svoboda\pechatalka\models\core,
svoboda\pechatalka\models\enumerations\products\type as product_type; /* svoboda\pechatalka\models\enumerations\paper\type as paper_type, */
svoboda\pechatalka\models\enumerations\paper\format as paper_format,
svoboda\pechatalka\models\enumerations\product\type as product_type;
// Svoboda time // Svoboda time
use svoboda\time\statement as svoboda; use svoboda\time\statement as svoboda;
@@ -23,6 +25,7 @@ use Zanzara\Telegram\Type\User as telegram;
// Built-in libraries // Built-in libraries
use Imagick as imagick, use Imagick as imagick,
ImagickKernel as imagick_kernel,
ImagickPixel as imagick_pixel, ImagickPixel as imagick_pixel,
ImagickDraw as imagick_draw, ImagickDraw as imagick_draw,
Exception as exception, Exception as exception,
@@ -39,120 +42,167 @@ use Imagick as imagick,
final class paper extends core final class paper extends core
{ {
/** /**
* A5 * Generate
*
* *
* @param paper_format $format
* @param product_type $type
* @param array $products
* @param array $canvas
* @param int|float $dpi Dots (pixels) per Inch (used with centimeters)
* @param bool $fit Fit the maximum number of blanks on paper? (duplication)
* *
* @return record The account record from the database * @return record The account record from the database
*/ */
public static function a5(product_type $type, array $products, array $canvas = []): mixed public static function generate(paper_format $format, product_type $type, array $products, array $canvas = [], int|float $dpi = 300, bool $fit = true): mixed
{ {
// Initializing the print DPI // Initializing the paper dimensions
define('dpi', 80); $dimensions = $format->dimensions();
/* define('dpi', 300); */ $width = (int) round($dimensions['width'] * $dpi);
$height = (int) round($dimensions['height'] * $dpi);
// Initializing the A5 paper dimensions unset($dimensions);
$a5 = [
'width' => (int) round(10.5 * dpi),
'height' => (int) round(14.8 * dpi)
];
// Initializing the paper
$paper = new imagick();
$paper->setResolution(dpi, dpi);
$paper->newImage($a5['width'], $a5['height'], new imagick_pixel("white"));
$paper->setImageUnits(imagick::RESOLUTION_PIXELSPERCENTIMETER);
$paper->setImageFormat("jpg");
foreach ($products as $product) {
// Iterating over products
foreach ($product as $layer) {
// Iterating over the product layers
// Filtering
empty($layer['scale']) || $layer['scale'] == 0 and $layer['scale'] = 1;
// Initializing the product areas // Initializing the product areas
$areas = $type->areas(); $areas = $type->areas();
// Calculating the product blank dimensions by the print DPI // Calculating the product blank dimensions by the print $dpi
$blank = [ $blank = [
'width' => (int) round($areas['blank']['width'] / 10 * dpi), 'width' => (int) round($areas['blank']['width'] * $dpi),
'height' => (int) round($areas['blank']['height'] / 10 * dpi) 'height' => (int) round($areas['blank']['height'] * $dpi)
]; ];
// Calculating the DPI ratio of the pechatalka interface to the blank dimensions // Calculating the $dpi ratio of the pechatalka interface to the blank dimensions
$ratio = [ $ratio = [
'width' => $blank['width'] / $canvas['width'], 'width' => $blank['width'] / $canvas['width'],
'height' => $blank['height'] / $canvas['height'] 'height' => $blank['height'] / $canvas['height']
]; ];
// Initializing the circle mask // Initializing the circle mask
$circle = new imagick(); $mask = new imagick();
$circle->setResolution(dpi, dpi); $mask->setResolution($dpi, $dpi);
$circle->newImage($blank['width'], $blank['height'], '#0008'); $mask->newImage($blank['width'], $blank['height'], '#0000');
$circle->setImageUnits(imagick::RESOLUTION_PIXELSPERCENTIMETER); $mask->setImageUnits(imagick::RESOLUTION_PIXELSPERCENTIMETER);
$circle->setimageformat('png'); $mask->setimageformat('png');
$circle->setimagematte(true); $mask->setimagematte(true);
$draw = new imagick_draw(); $draw = new imagick_draw();
$draw->setfillcolor('#fff'); $draw->setfillcolor('#fff');
$draw->circle($blank['width'] / 2, $blank['height'] / 2, $blank['width'] / 2, $blank['height']); $draw->circle($blank['width'] / 2, $blank['height'] / 2, $blank['width'] / 2, $blank['height']);
$circle->drawimage($draw); $mask->drawimage($draw);
// Initializing the paper places
$places = $format->places(type: $type);
// Calculating the amount of products
$have = count($products);
// Calculating the amount of places per paper
$limit = count($places);
// Calculating the amount of missing products on the paper
$need = $limit - $have;
if ($fit && $have < $limit) {
// Received less products then can be fit on the page
for (; $need++ < $limit;) {
// Iterating over free places
// Dublicating the product
$products[] = $products[rand(0, $have - 1)];
}
}
// Calculating the amount of papers
$amount = $have % $limit;
for ($page = 0; $page < $amount; $page++) {
// Iterating over pages
// Initializing the paper
$paper = new imagick();
$paper->setResolution($dpi, $dpi);
$paper->newImage($width, $height, new imagick_pixel("#fff"));
$paper->setImageUnits(imagick::RESOLUTION_PIXELSPERCENTIMETER);
$paper->setImageFormat("jpg");
$paper->setimagematte(true);
// Calculating the products pages offset
$offset = $page === 0 ? 0 : $page * $amount;
foreach ($places as $index => $place) {
// Iterating over places
// Recalculating the place coordinates with DPI
$place['x'] *= $dpi;
$place['y'] *= $dpi;
// Initializing the product
$product = $products[$offset + $index];
foreach ($product as $layer) {
// Iterating over the product layers
// Filtering and normalizing the layer parameters
empty($layer['scale']) || $layer['scale'] == 0 and $layer['scale'] = 1;
// Initializing the layer image // Initializing the layer image
$image = new imagick(); $image = new imagick();
$image->setResolution(dpi, dpi); $image->setResolution($dpi, $dpi);
$image->readImage($layer['image']); $image->readImage($layer['image']);
$image->setImageUnits(imagick::RESOLUTION_PIXELSPERCENTIMETER); $image->setImageUnits(imagick::RESOLUTION_PIXELSPERCENTIMETER);
$image->setimagematte(true); $image->setimagematte(true);
$width = (int) round($blank['width'] * $layer['scale']); // Initializing the layer image height before resizing
$image->adaptiveResizeImage($width, 0); $before = $image->getImageHeight();
/* $image->roundCornersImage(50, 50); */
$offset = [ // Resizing the layer image
'x' => ($blank['width'] - $image->getImageWidth()) / 2, $image->adaptiveResizeImage((int) round($blank['width'] * $layer['scale']), 0);
'y' => ($blank['height'] - $image->getImageHeight()) / 2 $image->roundCornersImage($layer['corners'], $layer['corners']);
];
$vertical = $blank['height'] - $image->getImageHeight(); // Calculating the layer image coordinates by the layer image mask
$vertical = $blank['height'] - $before;
$layer['x'] = (int) round($layer['x'] * $ratio['width'] + ($blank['width'] - $image->getImageWidth()) / 2);
$layer['y'] = (int) round($layer['y'] * $ratio['height'] + ($blank['height'] - $image->getImageHeight() + ($vertical > 0 ? $vertical : 0)) / 2);
// Добавить нормальные комментарии после глубокго тестирования unset($before, $vertical);
// Reinitializing the product layer offsets
$layer['x'] = (int) round($layer['x'] * $ratio['width']);
$layer['y'] = (int) round($layer['y'] * $ratio['height']);
// Compositing the layer image mask with the layer image
$image->compositeImage( $image->compositeImage(
$circle, $mask,
imagick::COMPOSITE_DSTIN, imagick::COMPOSITE_DSTIN,
(int) round(-$layer['x'] - $offset['x']), (int) round(-$layer['x']),
/* (int) round(-$layer['y'] - $offset['y'] - $vertical / 2) */ (int) round(-$layer['y'])
(int) round(-$layer['y'] - $offset['y'])
); );
// Compositing the layer with the paper // Drawing the cutting line
$paper->setImageVirtualPixelMethod(imagick::VIRTUALPIXELMETHOD_TRANSPARENT); $draw = new imagick_draw();
$paper->setImageArtifact('compose:args', "1,0,-0.5,0.5"); $draw->setfillcolor($canvas['background'] ?? '#fff');
$stroke = 1;
$draw->setStrokeOpacity($stroke);
$draw->setStrokeColor('#000');
$draw->setStrokeWidth(2);
$draw->circle(
$place['x'] + $blank['width'] / 2,
$place['y'] + $blank['height'] / 2,
round($place['x'] + $blank['width'] / 2 - $stroke),
round($place['y'] + $blank['height'])
);
$paper->drawimage($draw);
// Compositing the layer image with the paper
$paper->compositeImage( $paper->compositeImage(
$image, $image,
imagick::COMPOSITE_MATHEMATICS, $image->getImageCompose(),
(int) round($a5['width'] / 2 - $blank['width'] / 2 + $layer['x'] + $offset['x']), (int) round($place['x'] + $layer['x']),
/* (int) round($a5['height'] / 2 - $blank['height'] / 2 + $layer['y'] + $offset['y'] + $vertical / 2) */ (int) round($place['y'] + $layer['y'])
(int) round($a5['height'] / 2 - $blank['height'] / 2 + $layer['y'] + $offset['y'])
); );
}
}
// Writing the paper file // Writing the paper file
$paper->writeImage(INDEX . DIRECTORY_SEPARATOR . 'test.jpg'); $paper->writeImage(INDEX . DIRECTORY_SEPARATOR . 'test.jpg');
} }
}
return true;
// Exit (success)
return [];
} }
} }

View File

@@ -6,7 +6,7 @@ namespace svoboda\pechatalka\models;
// Files of the project // Files of the project
use svoboda\pechatalka\models\core, use svoboda\pechatalka\models\core,
svoboda\pechatalka\models\enumerations\products\type as product_type; svoboda\pechatalka\models\enumerations\product\type as product_type;
// Svoboda time // Svoboda time
use svoboda\time\statement as svoboda; use svoboda\time\statement as svoboda;

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -155,7 +155,8 @@ div#pechatalka {
width: calc(var(--diameter-cut, var(--width, 100%)) + var(--width-zoom, 0px)); width: calc(var(--diameter-cut, var(--width, 100%)) + var(--width-zoom, 0px));
height: calc(var(--diameter-cut, var(--width, 100%)) + var(--width-zoom, 0px)); height: calc(var(--diameter-cut, var(--width, 100%)) + var(--width-zoom, 0px));
cursor: grab; cursor: grab;
align-content: center; display: flex;
place-items: center;
&:active { &:active {
cursor: grabbing; cursor: grabbing;