Jak v rohlíku řešíme nutriční hodnoty Mozektevidi.net - Mozektevidi.net.
Úkol byl jasný - vyhovět zákonům a u produků zobrazit jejich nutriční hodnoty. Na webu to bylo jednoduché, prostě se vykreslila další komponenta v šabloně. A pro naše Appky se vymyslelo, že se to tam bude přenášet jako PNG obrázek. Na první pohled nesmyslná implementace pro kterou hovoří jen několik výhod:
Nevýhody jsou takové, že vygenerování obrázku je pomalejší než vygenerování textu. A i nároky na datový přenos jsou mnohem větší. Navíc tam byly přísné požadavky na vzhled, písmo, barvy, rozměry.
Výsledek vypadá takto:
Nejprve jsem to zkoušel přes imagettfte, ale to má dost omezení a špatně se ladí. Jednodušší variantou je si vygenerovat html.
/**
* @param Grocery $product
* @return string HTMl zdroj pro generování PDF
*/
public function generateHtmlTextForCreatePDF(Grocery $product)
{
$file = file_get_contents(__DIR__ . '/SF-UI-Display-Regular.txt'); //požadované písmo
$html = "<html>
<head>
<style type='text/css'>
@font-face {
font-family: 'sfns_displayregular';
src: url(data:font/truetype;charset=utf-8;base64,$file);
}
</style>
</head>
<body>";
$html .= '<div style="padding-bottom: 10px;">Nutriční hodnoty na ...';
//...zkráceno
return $html;
}
Následně se toto html převádělo do PDF přes Wkhtmltopdf a toto PDF do PNG, což umí Imagic.
$imagic = new Imagick();
$imagic->readImage($this->staticContentPath . 'nutrient/' . $grocery->getId() . '.pdf[0]');
$imagic->setFormat('png');
Jenže výsledný obrázek neměl požadované rozměry a po resize se rozostří. Takže tento způsob jsem také zavrhnul.
Třetí varianta generuje PNG obrázky rovnou z HTML bez konverze do PDF pomocí wkhtmltoimage
.
A to funguje znamenitě.
A protože by sériové vytváření osmi tisíc obrázků by trvalo moc dlouho, napsal jsem podporu paralerního zpracování přes Symfony\Component\Process
.
V praxi to funguje takto: Hlavní command v cyklu generuje HTML soubory a spouští nové procesy, kterým jako parametr cestu k tomu html souboru.
$command = sprintf('wkhtmltoimage --crop-w 600 %s %s', $pathToHtml, $pathToPng);
$command = new Process($command);
A protože ten skript vygeneroval velké obrázky 60KB, musí se prohnat přes pngquant, který z toho udělá pouhé 6KB obrázky.
Bohužel Symfony Process neumí do jednoho procesu vložit více příkazů, něco jako wkhtmltoimage && pngquant
, takže se musí toto provést ještě třetím procesem.
A protože i toto chceme paralerně, musíme to udělat takto:
shell_exec("find $path -name '*.png' -print0 | xargs -0 -P4 -L1 pngquant --quality=65-80 --ext .png --force 256");
Hlavní proces, který generuje HTML soubory a spouští další procesy je moc rychlý, takže spustí klidně 600 podprocesů než se stihne ten první dokončit a tím se zahltí server.
V cloudu by to nebyl problém, ale na klasickém 8 jádrovém processoru to musíme trochu omezit, což o něco zesložitilo skript.
Výsledek s paralerním zpracováním obrázků vypadá takto:
protected function execute(InputInterface $input, OutputInterface $output)
{
if (!$count = $this->getCount()) {
return 0;
}
$this->progress = new ProgressBar($output, $count);
$this->progress->start();
$offset = 0;
$parameters = $this->container->getParameters();
$path = $parameters['staticContent']['path'] . '/nutrient/';
/** @var Process[] $processSet */
$processSet = [];
while ($rows = $this->getRows($offset)) {
$output->writeln(sprintf('<info>offset %s</info>', $offset));
foreach ($rows as $row) {
$html = $this->imageStorage->generateHtmlTextForCreatePDF($row);
$command = $this->imageStorage->createCommandForGeneratingNutrientImage($html, $row->id);
$processSet[$row->getId()] = $command;
$processSet[$row->getId()]->setTimeout(NULL);
$processSet[$row->getId()]->start();
$processSet = $this->checkProcessSet($output, $processSet, 4);
}
$offset += self::LIMIT_ROWS;
if (memory_get_usage(TRUE) > 167772160) {
$this->entityManager->clear();
$output->writeln('<info>gc_collect_cycles</info>');
gc_collect_cycles();
}
}
$processSet = $this->checkProcessSet($output, $processSet, 0);
//multi-core version pngquant
shell_exec("find $path -name '*.png' -print0 | xargs -0 -P4 -L1 pngquant --quality=65-80 --ext .png --force 256");
$this->progress->finish();
$output->writeln('');
return 0;
}
private function checkProcessSet(OutputInterface $output, $processSet, $maxCountOfProcess = 4)
{
$finished = [];
while (count($processSet) > $maxCountOfProcess) {
usleep(600);
foreach ($processSet as $i => $process) {
$process->checkTimeout();
if (!$process->isRunning()) {
$processSet[$i] = NULL;
unset($processSet[$i]); //remove
$finished[] = $i;
$this->progress->advance(1);
}
}
}
if ($finished) {
$output->writeln(sprintf('<info>finished: %s</info>', implode(',', $finished)));
}
return $processSet;
}
Nakonec by bylo lepší jen vygenerovat statické json soubory s nutričními hodnotami a ty by si naše aplikace stáhla a zobrazila. Měly by cca 300 bajtů.
Pamatuj na to, že tvůj příspěvek o tobě něco vypovídá. O tom, jak jsi inteligentní, jak se chováš a kolik ti je let. Tak se podle toho chovej ;-)
Kometáře s neověřenou IP adresou čekají na ruční schválení. Pokud se některý kometář nezobrazí ihned, čeká na schválení, neposílejte komentář znova.