box/spout - 使用十六进制颜色与预定义颜色时会占用大量内存

box/spout - Massive memory usage when using hex color vs pre-defined colors

我正在使用 Box/Spout 库,与使用预定义颜色(例如 [=36)相比,使用具有自定义十六进制颜色(例如 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());

输出:

setFontColor(Color::BLUE): Peak memory usage: 1666 KB

setFontColor($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';
?>

tl;博士

虽然 Spout 可以改进,Excel isn't designed for large quantities of styles, so this isn't really a flaw of the library (you might want to rescind the issue)

故事

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

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

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

  • Spout建立所有样式的索引,通过serializing样式object存储在数组中。在每个样式大约 1749 字节的情况下,该索引会占用内存(除了它在内存中生成文件的实际样式 objects 之外)- 这可以在 Spout[ 中得到改进=124=]

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

    [=121] 略有不同的值=]
  • 当你 运行 我的代码 jonEg 你得到 (100*150+1) 15001 种样式,需要 (15001*1749/1024/1024) ~25MB仅 Spout 中索引的内存。同样需要这个索引来防止 Excel 中的每个单元格都有自己的样式(当然,在这个人为的例子中我已经确保它没有意义 - 每个单元格都有自己的样式) - 这使用 ~100MB 内存

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

  • 当你设置$by=4时(在ColorBuilder::jonEgQuant内)-你现在有四分之一百万种颜色,需要989种样式,使用~ 7MB 内存(更重要的是,在 Excel 中打开时这看起来类似于 jonEg

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

  • 一种样式的尺寸比这多得多 - 我使用了背景颜色,但有前景(你的例子)、下划线、粗体、斜体、字体,font-size - 全部这些乘以样式数量(减少颜色 space 此处解决了您记录的问题,但可以通过更改其他样式组件来撤消)

外卖

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

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

  • 改进Spout样式索引机制,但注意索引只消耗了一些内存——它不小,但不是唯一的贡献者——每个样式object 需要来生成结果。删除此样式缓存(对于这个人为的示例没有帮助)会使内存使用量达到 40MB(并生成相同的 Excel 文件)。 60% 的节省并不是什么,但更多的样式会导致更多的内存使用、更大的 Excel 文件和更慢的 opening/rendering 这些文件

测试笔记

  • 我在测试时放弃了图像 reading/color 收获,因为它可能会增加内存使用量,但不会增加问题的实质

  • 使用$colCount=250;(从你的例子)你超过了PHP中的128MB默认内存限制,最好设置它$colCount=100;并且你可以运行 不改变设置的所有测试

  • 我切换到设置background-color,在Excel

  • 中打开更容易看出区别
  • 对于每个颜色生成器(jonEgspoutDocfixedEgjonEgQuant)一个不同的 XLSX 是基因ate(有助于查看文件大小和渲染差异)

  • 文件大小与 Excel 复杂度匹配 - jonEg 是一个 179KB 的文件(并且 Excel 难以打开),而 jonEgQuant 是43KB

代码

<?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';

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