Jak v rohlíku řešíme nutriční hodnoty Mozektevidi.net - Mozektevidi.net.

Jak v rohlíku řešíme nutriční hodnoty

Slepá cesta přes PNG obrázky, převod HTML do PDF přes Wkhtmltopdf a následně Imagick. Lepší cesta přes wkhtmltoimage, pngquant a to vše paralerně. Aneb jak dostat do mobilní aplikace nutriční hodnoty ve formě png obrázku

Disclaimer. Zrovna pro tento úkol by bylo lepší vygenerovat statické JSON a zpracovat v appce. Možná. A možná taky ne.

Ú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:

  • Nemusíme zvětšovat Json soubor s exportem produktů o jejich nutriční hodnoty
  • V appce se nemusí řešit grafika, grafiku si pořešíme na serveru
  • Statické obrázky se dají přesunout na CDN (ale statický json soubor taky)
  • Appka si stáhne png obrázek s nutričními hodnotami až při otevření detailu produktu

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:

nutriční hodnoty rohlík.cz

Slepá cesta

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.

zvolené řešení

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;
}

závěrem

  • Načteme z DB požadované nutriční hodnoty
  • vygenerujeme HTML soubor dle grafiky zadavatele
  • tento HTML soubor převedeme na png obrázek pomocí wkhtmltoimage
  • tento obrázek zmenšíme přes pngquant
  • A to vše paralerně.

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ů.


autor článku OS | datum publikování 11.Mar.2016 16:33 | články ze světa počítačů IT |

Kometáře

Zatím tu nejsou žádné (schválené) kometáře.

Přidej názor do diskuze

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.