4

我正在使用Box/Spout库,与使用诸如 Color::BLUE 之类的预定义颜色相比,使用带有自定义十六进制颜色(例如蓝色的 0000FF)的 StyleBuilder 似乎使用了大量的内存。为什么会这样?

相关片段:

//LOW MEMORY
$row[] = WriterEntityFactory::createCell('test', (new StyleBuilder())->setFontColor(Color::BLUE)->build());

//HIGH MEMORY
$row[] = WriterEntityFactory::createCell('test', (new StyleBuilder())->setFontColor($colorHex)->build());

输出:

设置字体颜色(颜色::蓝色):Peak memory usage: 1666 KB

设置字体颜色($colorHex):Peak memory usage: 189436 KB

完整代码:

(出于演示目的,我正在加载一个 250x150 的小图像以提供多个颜色值)

<?php

    require_once 'Spout/Autoloader/autoload.php';
    use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
    use Box\Spout\Common\Entity\Style\Color;
    use Box\Spout\Writer\Common\Creator\Style\StyleBuilder;

    //load an image
    $img = imagecreatefrompng('input/test250x150.png');

    $writer = WriterEntityFactory::createXLSXWriter();
    $writer->openToFile('output/MyExcel.xlsx');

    //height of the image
    for($y=0; $y<150; $y++) {

        //create or reset array to hold this row's cells
        $row = [];

        //width of the image
        for($x=0; $x<250; $x++) {

            //gets the pixel color
            $index = imagecolorat($img, $x, $y);
            $colorRGBArr = imagecolorsforindex($img, $index);
            $colorHex = sprintf("%02x%02x%02x", $colorRGBArr['red'], $colorRGBArr['green'], $colorRGBArr['blue']);

            //LOW MEMORY
            //$row[] = WriterEntityFactory::createCell('test', (new StyleBuilder())->setFontColor(Color::BLUE)->build());
            //HIGH MEMORY
            $row[] = WriterEntityFactory::createCell('test', (new StyleBuilder())->setFontColor($colorHex)->build());

        }
        $writer->addRow(WriterEntityFactory::createRow($row));
    }

    $writer->close();

    echo 'Peak memory usage: '.round(memory_get_peak_usage() / 1024).' KB';
?>
4

1 回答 1

4

tl;博士

虽然Spout可以改进,但Excel 并不是为大量样式而设计的,所以这并不是库的真正缺陷(您可能想撤销问题

故事

好吧,这里有一些东西在起作用..我用来测试的代码在我的帖子的底部 - 关键的相关颜色函数是jonEgjonEgQuant(这是一个$by可以调整的内部变量)

  • Excel 中的样式有点像 HTML+CSS。样式最初在文件中(在标题块中)定义,然后在工作表中按名称(类)引用。从根本上说,这表明 Excel 存储格式不是为过多的样式而设计的,而只是为少数在许多单元格上重复使用的样式而设计

  • Spout向用户隐藏样式定义的复杂性 - 如果样式已经定义(即确切的样式已经存在),那么它会重用该定义,否则定义新样式

  • Spout通过序列化样式对象并将其存储在数组中,构建所有样式的索引。每个样式大约 1749 字节时,该索引会占用内存(除了它在内存中用于生成文件的实际样式对象) -这可以在Spout

  • 使用此代码,我保证每个单元格都有不同的样式(使用行/列位置来定义颜色的红色和绿色分量) -这比您的图像颜色选择更极端,但可能不会太多(如果没有示例图像,我无法判断)。RGB 三元组有 1600 万种颜色可供选择。但我们的眼睛无法始终检测到几个点的差异。例如,在红色渐变中,255,254,253...128 看起来很平滑,但随机分布的单个块 255,254,253 可能看起来像单一颜色。对于除原色以外的任何颜色,这种差异现在适用于三个维度(r、g 和 b)。JPEG格式利用这一点(压缩和重建中的噪声的组合) - 所以颜色选择可能看起来像一个统一块仍然会每次返回略有不同的值

  • 当你运行我的代码时,jonEg你会得到 (100*150+1) 15001 种样式,仅索引Spout就需要 (15001*1749/1024/1024) ~25MB 内存。再次需要此索引以防止 Excel 中的每个单元格都有自己的样式(但当然,在这个人为的示例中,我确保它毫无意义 - 每个单元格都有自己的样式) - 这使用约 100MB的内存

  • 当您使用jonEgQuant(离开$by=16;)运行我的代码时,只需要 71 种样式(但这是非常极端的四舍五入,总共只允许 4096 种颜色), - 这使用约 2MB的内存

  • 当您设置$by=4(inside ColorBuilder::jonEgQuant) - 您现在拥有多达 100 万种颜色,需要 989 种样式,使用约 7MB内存(更重要的是,这看起来类似于jonEg在 Excel 中打开时)

  • Spout包括将 RGB 十进制值转换为字符串的方法Color::rgb($r,$g,$b)- 但这不会改变结果(spoutDoc方法)

  • 样式的尺寸比这多得多-我使用了背景颜色,但是有前景(您的示例),下划线,粗体,斜体,字体,字体大小-所有这些都增加了样式数量(减少了此处的颜色空间解决了您记录的问题,但可以通过更改其他样式组件来撤消)

带走

  • 问题不是使用十六进制代码、RGB 值或常量,而是您使用的样式数量

  • 减少您使用的样式数量 - Excel 不希望有大量样式,Spout同样也没有为此进行优化。压缩颜色(通过四舍五入jonEgQuant)是一种途径

  • 改进Spout样式索引机制,但请注意索引仅消耗了一些内存 - 它不小,但不是唯一的贡献者 - 每个样式对象都需要生成结果。删除此样式缓存(对于这个人为的示例没有帮助)会使内存使用量达到 40MB(并生成相同的 Excel 文件)。节省 60% 并非没有,但更多样式会占用更多内存、更大的 Excel 文件以及更慢的打开/渲染这些文件

测试笔记

  • 我在测试时放弃了图像读取/颜色采集,因为它可能会增加内存使用量,但不会增加问题的实质

  • With $colCount=250; (from your example) you exceed the 128MB default memory limit in PHP, better to set it $colCount=100; and you can run all the tests without changing that setting

  • I switched to setting background-color, it's easier to see the difference when opened in Excel

  • For each color generator (jonEg, spoutDoc, fixedEg, jonEgQuant) a different XLSX is generated (helps to see the file size and rendering differences)

  • The file sizes match the Excel complexity - jonEg is a 179KB file (and Excel struggles to open), while jonEgQuant is 43KB

Code

<?php

require_once 'Spout/Autoloader/autoload.php';
use Box\Spout\Writer\Common\Creator\WriterEntityFactory;
use Box\Spout\Common\Entity\Style\Color;
use Box\Spout\Writer\Common\Creator\Style\StyleBuilder;

// -- -- Set this to one of the method names on ColorBuilder (that isn't a helper)
$choice='jonEg';
// -- -- 

class ColorBuilder {
    static $defaultBlue=255;
    static function jonEg($x,$y) {return sprintf("%02x%02x%02x", $x, $y, static::$defaultBlue);}
    static function spoutDoc($x,$y) {return Color::rgb($x, $y, static::$defaultBlue);}
    static function fixedEg($x,$y) {return Color::BLUE;}
    static function jonEgQuant($x,$y) {$by=16;return sprintf("%02x%02x%02x", static::_quantize($x,$by),static::_quantize($y,$by), static::_quantize(static::$defaultBlue,$by));}

    //Helpers - don't use these for choice
    static function validate(string $name):bool {
        if ($name==null) return false;//Null or empty
        if (substr($name,0,1)=='_') return false;//Private by convention
        if ($name==='validate') return false;//Not the function you seek
        return method_exists('ColorBuilder',$name);
    }
    private static function _quantize(int $i,int $by=16):int {return round($i/$by)*$by;}
}

function createRow($y,$color) {
    $colCount=100;
    $row = [];

    for($x=0; $x<$colCount; $x++) {
        $row[] = WriterEntityFactory::createCell('*', (new StyleBuilder())->setBackgroundColor(ColorBuilder::$color($x,$y))->build());
    }
    return $row;
}

function buildSheet($name) {
    if (!ColorBuilder::validate($name)) {throw new Error('Invalid color provider');}

    $writer = WriterEntityFactory::createXLSXWriter();
    $writer->openToFile('output/'.$name.'.xlsx');

    for($y=0; $y<150; $y++) {
        $writer->addRow(WriterEntityFactory::createRow(createRow($y,$name)));
    }

    $writer->close();
}

buildSheet($choice);
echo 'Peak memory usage: '.round(memory_get_peak_usage() / 1024).' KB';

Tech: PHP 7.4.2 CLI, Spout: 3.1.0, Win: 7 x64 (I know), Coffee: Venti Dark

于 2020-02-07T21:49:16.440 回答