├── .gitignore
├── test
├── images
│ ├── fam.jpg
│ ├── fam-320.jpg
│ ├── fam-480.jpg
│ ├── fam-960.jpg
│ ├── sunset.jpg
│ ├── sunset-1280.jpg
│ ├── sunset-320.jpg
│ ├── sunset-3264.jpg
│ └── sunset-640.jpg
├── img.html
├── img-2.html
├── srcset.html
├── srcset-2.html
├── picture.html
├── picture-2.html
├── ericportis.html
└── ImageTest.js
├── package.json
├── src
├── Image.js
├── cmd.js
├── ImagingHeap.js
└── ImageMap.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
--------------------------------------------------------------------------------
/test/images/fam.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/filamentgroup/imaging-heap/HEAD/test/images/fam.jpg
--------------------------------------------------------------------------------
/test/images/fam-320.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/filamentgroup/imaging-heap/HEAD/test/images/fam-320.jpg
--------------------------------------------------------------------------------
/test/images/fam-480.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/filamentgroup/imaging-heap/HEAD/test/images/fam-480.jpg
--------------------------------------------------------------------------------
/test/images/fam-960.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/filamentgroup/imaging-heap/HEAD/test/images/fam-960.jpg
--------------------------------------------------------------------------------
/test/images/sunset.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/filamentgroup/imaging-heap/HEAD/test/images/sunset.jpg
--------------------------------------------------------------------------------
/test/images/sunset-1280.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/filamentgroup/imaging-heap/HEAD/test/images/sunset-1280.jpg
--------------------------------------------------------------------------------
/test/images/sunset-320.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/filamentgroup/imaging-heap/HEAD/test/images/sunset-320.jpg
--------------------------------------------------------------------------------
/test/images/sunset-3264.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/filamentgroup/imaging-heap/HEAD/test/images/sunset-3264.jpg
--------------------------------------------------------------------------------
/test/images/sunset-640.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/filamentgroup/imaging-heap/HEAD/test/images/sunset-640.jpg
--------------------------------------------------------------------------------
/test/img.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test/img-2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/test/srcset.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test/srcset-2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/test/picture.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/test/picture-2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/test/ericportis.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
16 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "imaging-heap",
3 | "version": "1.0.0",
4 | "description": "A command line tool to measure responsive image efficiency across viewport sizes and device pixel ratios.",
5 | "main": "src/ImagingHeap.js",
6 | "engines": {
7 | "node": ">=8.0.0"
8 | },
9 | "scripts": {
10 | "test": "ava"
11 | },
12 | "bin": {
13 | "imagingheap": "./src/cmd.js"
14 | },
15 | "keywords": [
16 | "responsive-images",
17 | "img"
18 | ],
19 | "repository": {
20 | "type": "git",
21 | "url": "git://github.com/filamentgroup/image-report.git"
22 | },
23 | "author": {
24 | "name": "Zach Leatherman",
25 | "email": "zach@filamentgroup.com",
26 | "url": "https://filamentgroup.com/"
27 | },
28 | "license": "MIT",
29 | "ava": {
30 | "files": [
31 | "test/*.js"
32 | ],
33 | "source": [
34 | "src/**/*.js"
35 | ]
36 | },
37 | "devDependencies": {
38 | "ava": "^0.25.0"
39 | },
40 | "dependencies": {
41 | "chalk": "^2.3.2",
42 | "debug": "^3.1.0",
43 | "puppeteer": "^1.2.0",
44 | "table": "^4.0.3",
45 | "yargs": "^11.0.0"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/test/ImageTest.js:
--------------------------------------------------------------------------------
1 | import test from "ava";
2 | import Image from "../src/Image";
3 |
4 | test("getStatsAtViewportWidth", t => {
5 | let img = new Image();
6 | t.deepEqual(img.getViewportSizes(), []);
7 | img.addStats( 320, [200,100], [100,50]);
8 | t.deepEqual(img.getViewportSizes(), [320]);
9 | t.truthy(img.getStatsAtViewportWidth(320));
10 | t.falsy(img.getStatsAtViewportWidth(340));
11 |
12 | img.addStats( 340, [200,100], [100,50]);
13 | t.deepEqual(img.getViewportSizes(), [320, 340]);
14 | t.truthy(img.getStatsAtViewportWidth(340));
15 | });
16 |
17 | test("getEfficiencyAtViewportWidth", t => {
18 | let img = new Image();
19 | img.addStats( 320, [100,50], [200,100]);
20 | t.is(img.getEfficiencyAtViewportWidth(320), 2);
21 |
22 | img.addStats( 340, [200,100], [100,50]);
23 | t.is(img.getEfficiencyAtViewportWidth(340), 0.5);
24 | });
25 |
26 | test("getEfficiency", t => {
27 | let img = new Image();
28 | img.addStats( 320, [100,50], [200,100]);
29 | let efficiency = img.getStats();
30 | t.is(efficiency.length, 1);
31 |
32 | t.is(efficiency[0].efficiency, 2);
33 |
34 | img.addStats( 340, [100,50], [200,100]);
35 | efficiency = img.getStats();
36 |
37 | t.is(efficiency.length, 2);
38 | t.is(efficiency[0].efficiency, 2);
39 | t.is(efficiency[1].efficiency, 2);
40 | });
--------------------------------------------------------------------------------
/src/Image.js:
--------------------------------------------------------------------------------
1 | class Image {
2 | constructor() {
3 | this.sizes = {};
4 | this.html = "";
5 | }
6 |
7 | setHTML( html ) {
8 | this.html = html;
9 | }
10 |
11 | addStatsObject( stats ) {
12 | this.sizes[ parseInt( stats.viewportWidth, 10 ) ] = stats;
13 | }
14 |
15 | addStats( viewportWidth, dimensions, fileDimensions, src ) {
16 | let obj = {
17 | src: src,
18 | viewportWidth: viewportWidth,
19 |
20 | width: Array.isArray(dimensions) ? dimensions[0] : dimensions.width,
21 | fileWidth: Array.isArray(fileDimensions) ? fileDimensions[0] : fileDimensions.width
22 | };
23 |
24 | this.addStatsObject( obj );
25 | }
26 |
27 | getSizes() {
28 | return this.sizes;
29 | }
30 |
31 | getViewportSizes() {
32 | return Object.keys(this.sizes).map(size => parseInt(size, 10));
33 | }
34 |
35 | getStatsAtViewportWidth(viewportWidth) {
36 | return this.sizes[parseInt(viewportWidth, 10)];
37 | }
38 |
39 | // images are assumed to have the same aspect ratio
40 | getEfficiencyAtViewportWidth(viewportWidth) {
41 | let stats = this.getStatsAtViewportWidth(viewportWidth);
42 | return this.getEfficiencyFromStats(stats);
43 | }
44 |
45 | getEfficiencyFromStats(stats) {
46 | return stats.fileWidth / stats.width;
47 | }
48 |
49 | getStats() {
50 | return this.getViewportSizes().map(vw => {
51 | let stats = this.getStatsAtViewportWidth(vw);
52 | let eff = this.getEfficiencyFromStats(stats);
53 |
54 | return {
55 | html: stats.html,
56 | src: stats.src,
57 | viewportWidth: vw,
58 | fileWidth: stats.fileWidth,
59 | width: stats.width,
60 | efficiency: eff
61 | };
62 | });
63 | }
64 | }
65 |
66 | module.exports = Image;
--------------------------------------------------------------------------------
/src/cmd.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | const ImagingHeap = require("./ImagingHeap");
3 | let defaults = ImagingHeap.defaultOptions;
4 |
5 | const argv = require("yargs")
6 | .usage("imagingheap http://example.com/ [options]")
7 | .demandCommand(1)
8 | .options({
9 | min: {
10 | describe: "Minimum viewport width",
11 | default: defaults.minViewportWidth
12 | },
13 | max: {
14 | describe: "Maximum viewport width",
15 | default: defaults.maxViewportWidth
16 | },
17 | by: {
18 | describe: "Increment viewport by",
19 | default: defaults.increment
20 | },
21 | dpr: {
22 | describe: "List of Device Pixel Ratios",
23 | default: defaults.dpr,
24 | type: "string"
25 | },
26 | minimagewidth: { // tracking pixels
27 | describe: "Ignore images smaller than image width",
28 | default: defaults.minImageWidth
29 | },
30 | csv: {
31 | describe: "Output CSV",
32 | default: defaults.useCsv,
33 | type: "boolean"
34 | }
35 | })
36 | .help()
37 | .argv;
38 |
39 | const ProgressBar = require("progress");
40 |
41 | (async function() {
42 | try {
43 | let report = new ImagingHeap({
44 | minViewportWidth: argv.min,
45 | maxViewportWidth: argv.max,
46 | increment: argv.by,
47 | useCsv: argv.csv,
48 | dpr: argv.dpr,
49 | minImageWidth: argv.minimagewidth
50 | });
51 |
52 | let bar;
53 | if (!process.env.DEBUG) {
54 | bar = new ProgressBar(":bar :current/:total", {
55 | incomplete: ".",
56 | clear: true,
57 | callback: async function() {
58 | await report.finish();
59 | console.log(report.getResults());
60 | },
61 | total: ((argv.max - argv.min) / argv.by + 1) * report.getDprArraySize()
62 | });
63 | }
64 |
65 | await report.start();
66 |
67 | await report.iterate(argv._.pop(), function() {
68 | if (bar) {
69 | bar.tick();
70 | }
71 | });
72 |
73 | if (!bar) {
74 | await report.finish();
75 | console.log(report.getResults());
76 | }
77 | } catch (e) {
78 | console.log("Error!", e);
79 | }
80 | })();
81 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | :warning: This project is archived and the repository is no longer maintained.
2 |
3 | # imaging-heap
4 |
5 | There’s beauty in the breakdown of bitmap image data. A command line tool to measure the efficiency of your responsive image markup across viewport sizes and device pixel ratios.
6 |
7 | Works out-of-the-box with `img` (of course), `img[srcset]`, `img[srcset][sizes]`, `picture`, `picture [srcset]`, `picture [srcset][sizes]`. Ignores `.svg` files. No support for background images (yet?).
8 |
9 | ## Installation
10 |
11 | ```sh
12 | npm install --global imaging-heap
13 | ```
14 |
15 | ## Usage
16 |
17 | ```sh
18 | imagingheap https://filamentgroup.com/
19 | ```
20 |
21 | ```sh
22 | imagingheap http://example.com/ [options]
23 |
24 | Options:
25 | --version Show version number [boolean]
26 | --min Minimum viewport width [default: 320]
27 | --max Maximum viewport width [default: 1280]
28 | --by Increment viewport by [default: 80]
29 | --dpr List of Device Pixel Ratios [string] [default: "1,2,3"]
30 | --minimagewidth Ignore images smaller than image width [default: 5]
31 | --csv Output CSV [boolean] [default: false]
32 | --help Show help [boolean]
33 | ```
34 |
35 | ### Debug Output
36 |
37 | ```sh
38 | DEBUG=ImagingHeap* imagingheap https://filamentgroup.com/
39 | ```
40 |
41 | ## Sample output
42 |
43 | ```sh
44 | ╔══════════╤══════════╤═══════╤════════════╤════════╤════════════╤════════╤════════════╗
45 | ║ │ Image │ @1x │ @1x │ @2x │ @2x │ @3x │ @3x ║
46 | ║ │ Width in │ Image │ Percentage │ Image │ Percentage │ Image │ Percentage ║
47 | ║ Viewport │ Layout │ Width │ Match │ Width │ Match │ Width │ Match ║
48 | ╟──────────┼──────────┼───────┼────────────┼────────┼────────────┼────────┼────────────╢
49 | ║ 320px │ 161px │ 301px │ 187.0% │ 301px │ 93.5% │ 601px │ 125.2% ║
50 | ║ 480px │ 241px │ 301px │ 124.9% │ 601px │ 125.2% │ 601px │ 83.5% ║
51 | ║ 640px │ 321px │ 601px │ 187.2% │ 601px │ 93.6% │ 901px │ 93.9% ║
52 | ║ 800px │ 401px │ 601px │ 149.9% │ 901px │ 112.6% │ 1201px │ 100.1% ║
53 | ║ 960px │ 480px │ 600px │ 125.0% │ 900px │ 93.8% │ 1200px │ 83.3% ║
54 | ║ 1120px │ 560px │ 600px │ 107.1% │ 1200px │ 107.1% │ 1200px │ 71.4% ║
55 | ║ 1280px │ 640px │ 900px │ 140.6% │ 1200px │ 93.8% │ 1200px │ 62.5% ║
56 | ╚══════════╧══════════╧═══════╧════════════╧════════╧════════════╧════════╧════════════╝
57 | ```
58 |
59 | ## Related Projects
60 |
61 | * Hugely inspired by the [NCC Image Checker Chrome Extension](https://github.com/nccgroup/image-checker) (which is a great visual tool). The main difference here is that imaging-heap will collate data across viewport sizes and device pixel ratios.
62 | * [RespImageLint](https://ausi.github.io/respimagelint/) a Linter Bookmarklet for Responsive Images
63 |
64 | ## Important Links
65 |
66 | * _A big big thank you_ for pre-release feedback from noted responsive images expert [Eric Portis](https://ericportis.com/) ([@eeeps](https://github.com/eeeps/)).
67 | * Idea originally documented at [@zachleat/idea-book/3](https://github.com/zachleat/idea-book/issues/3)
68 | * _“There’s beauty in the breakdown of bitmap image data.”_—[@jefflembeck](https://github.com/jefflembeck)
69 | * [Responsive Images Community Group](https://responsiveimages.org/)
70 |
--------------------------------------------------------------------------------
/src/ImagingHeap.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require("puppeteer");
2 | const ImageMap = require("./ImageMap");
3 | const debug = require("debug")("ImagingHeap");
4 |
5 | class ImagingHeap {
6 | constructor(options) {
7 | this.options = Object.assign({}, this.defaultOptions, options || {});
8 |
9 | this.dprArray = (this.options.dpr || "").split(",").map(dpr => parseInt(dpr, 10));
10 |
11 | debug("Options: %o", this.options);
12 |
13 | this.map = {};
14 | }
15 |
16 | static get defaultOptions() {
17 | return {
18 | minViewportWidth: 320,
19 | maxViewportWidth: 1280,
20 | increment: 80,
21 | useCsv: false,
22 | dpr: "1,2,3",
23 | minImageWidth: 5
24 | };
25 | }
26 |
27 | getDprArraySize() {
28 | return this.dprArray.length;
29 | }
30 |
31 | async start() {
32 | this.browser = await puppeteer.launch();
33 | }
34 |
35 | async getPage(url, dpr) {
36 | let page = await this.browser.newPage();
37 |
38 | // page.setCacheEnabled(false);
39 | let defaultPageOptions = {
40 | width: this.options.minViewportWidth,
41 | height: 768,
42 | deviceScaleFactor: dpr
43 | };
44 |
45 | debug("Setting default page options %o", defaultPageOptions);
46 | await page.setViewport(defaultPageOptions);
47 |
48 | await page.goto(url, {
49 | waitUntil: ["load", "networkidle0"]
50 | });
51 |
52 | page.on("console", function(msg) {
53 | debug("(browser console): %o", msg.text());
54 | });
55 |
56 | return page;
57 | }
58 |
59 | async iterate(url, callback) {
60 | this.map[url] = new ImageMap();
61 | this.map[url].setMinimumImageWidth(this.options.minImageWidth);
62 |
63 | for( let j = 0, k = this.dprArray.length; j < k; j++ ) {
64 | let dpr = this.dprArray[j];
65 | await this.iterateViewports(url, dpr, callback);
66 | }
67 | }
68 |
69 | async iterateViewports(url, dpr, callback) {
70 | let page = await this.getPage(url, dpr);
71 | for(let width = this.options.minViewportWidth, end = this.options.maxViewportWidth; width <= end; width += this.options.increment) {
72 | let viewportOptions = {
73 | width: width,
74 | height: 768,
75 | deviceScaleFactor: dpr
76 | };
77 | debug("Setting viewport options: %o", viewportOptions);
78 | await page.setViewport(viewportOptions);
79 |
80 | // await page.reload({
81 | // waitUntil: ["load", "networkidle0"]
82 | // });
83 |
84 | let imagesStats = await this.getImagesStats(page);
85 | for( let statsJson of imagesStats ) {
86 | let stats = JSON.parse(statsJson);
87 | this.map[url].addImage(stats.id, dpr, stats);
88 | }
89 |
90 | callback();
91 | }
92 | }
93 |
94 | async getImagesStats(page) {
95 | return page.evaluate(function() {
96 | function findNaturalWidth(src, stats, resolve) {
97 | var naturalImg = document.createElement("img");
98 | naturalImg.src = src;
99 | naturalImg.onload = function() {
100 | resolve(JSON.stringify(Object.assign(stats, {
101 | fileWidth: naturalImg.naturalWidth,
102 | src: src,
103 | })));
104 |
105 | this.parentNode.removeChild(this);
106 | };
107 |
108 | naturalImg.onerror = function() {
109 | resolve(JSON.stringify(Object.assign(stats, {
110 | fileWidth: undefined,
111 | src: src,
112 | })));
113 | };
114 |
115 | document.body.appendChild(naturalImg);
116 | }
117 |
118 | let viewportWidth = document.documentElement.clientWidth;
119 | console.log(`New viewportWidth: ${viewportWidth}`);
120 |
121 | let imgNodes = document.querySelectorAll("img");
122 | let imgArray = Array.from(imgNodes);
123 |
124 | let promises = Promise.all(
125 | imgArray.filter(function(img) {
126 | let src = img.currentSrc;
127 | if( !src ) {
128 | return true;
129 | }
130 | let split = (new URL(src)).pathname.split(".");
131 | if( !split.length ) {
132 | return true;
133 | }
134 |
135 | return split.pop().toLowerCase() !== "svg";
136 | }).map(function(img) {
137 | let key = "data-image-report-index";
138 | let id = img.getAttribute(key);
139 | if(!id) {
140 | id = img.getAttribute("src");
141 | // console.log("Creating new img id", id);
142 | img.setAttribute(key, id);
143 | } else {
144 | // console.log("Re-use existing img id", id);
145 | }
146 |
147 | let picture = img.closest("picture");
148 | img.removeAttribute(key);
149 | let html = (picture || img).outerHTML.replace(/\t/g, "");
150 | img.setAttribute(key, id);
151 |
152 |
153 | let stats = {
154 | id: id,
155 | viewportWidth: viewportWidth,
156 | html: html
157 | };
158 |
159 | return new Promise(function(resolve, reject) {
160 | function done() {
161 | stats.width = img.clientWidth;
162 | console.log(`currentSrc: ${img.currentSrc}`);
163 | console.log(`width: ${img.clientWidth}`);
164 |
165 | findNaturalWidth(img.currentSrc, stats, resolve);
166 | }
167 |
168 | if( !img.currentSrc ) {
169 | img.addEventListener("load", done, {
170 | once: true
171 | });
172 | } else {
173 | done();
174 | }
175 | });
176 | })
177 | );
178 |
179 | return promises;
180 | });
181 | }
182 |
183 | getResults() {
184 | let output = [];
185 | for( let url in this.map ) {
186 | let str = this.map[url].getOutput(this.options.useCsv);
187 | output.push(str);
188 | }
189 |
190 | return output.join( "\n" );
191 | }
192 |
193 | async finish() {
194 | if( !this.browser ) {
195 | throw new Error("this.browser doesn’t exist, did you run .start()?");
196 | }
197 | await this.browser.close();
198 | }
199 | }
200 |
201 | module.exports = ImagingHeap;
--------------------------------------------------------------------------------
/src/ImageMap.js:
--------------------------------------------------------------------------------
1 | const Image = require("./Image");
2 | const { table } = require("table");
3 | const chalk = require("chalk");
4 |
5 | class ImageMap {
6 | constructor() {
7 | this.map = {};
8 | this.showCurrentSrc = false;
9 | this.showPercentages = true;
10 | }
11 |
12 | setMinimumImageWidth(width) {
13 | this.minImageWidth = width;
14 | }
15 |
16 | setShowCurrentSrc(showCurrentSrc) {
17 | this.showCurrentSrc = !!showCurrentSrc;
18 | }
19 |
20 | setShowPercentages(showPercentages) {
21 | this.showPercentages = !!showPercentages;
22 | }
23 |
24 | addImage(identifier, dpr, stats) {
25 | if( !this.map[identifier] ) {
26 | this.map[identifier] = {};
27 | }
28 |
29 | let img = this.map[identifier][dpr];
30 |
31 | if(!img) {
32 | img = new Image();
33 | this.map[identifier][dpr] = img;
34 | }
35 |
36 | img.addStatsObject(stats);
37 | }
38 |
39 | getMap() {
40 | return this.map;
41 | }
42 |
43 | getNumberOfImages() {
44 | return Object.keys(this.map).length;
45 | }
46 |
47 | truncateUrl(url) {
48 | // let urlMaxLength = 20;
49 | // return (url.length > urlMaxLength ? "…" : "") + url.substr(-1 * urlMaxLength);
50 | return url.split("/").pop();
51 | }
52 |
53 | _getTableHeadersForIdentifier(identifier) {
54 | let tableHeaders = [
55 | ["", "Image"],
56 | ["", "Width in"],
57 | ["Viewport", "Layout"]
58 | ];
59 | let map = this.map[identifier];
60 |
61 | for( let dpr in map ) {
62 | tableHeaders[0].push(`@${dpr}x`);
63 | tableHeaders[1].push(`Image`);
64 | tableHeaders[2].push(`Width`);
65 | tableHeaders[0].push(`@${dpr}x`);
66 | if( this.showPercentages ) {
67 | tableHeaders[1].push(`Percentage`);
68 | tableHeaders[2].push(`Match`);
69 | } else {
70 | tableHeaders[1].push(`Ratio`);
71 | tableHeaders[2].push(``);
72 | }
73 |
74 | if(this.showCurrentSrc) {
75 | tableHeaders[0].push(``);
76 | tableHeaders[1].push(`@${dpr}x`);
77 | tableHeaders[2].push(`currentSrc`);
78 | }
79 | }
80 |
81 | return tableHeaders;
82 | }
83 |
84 | _convertTableHeadersToString(headers) {
85 | let ret = [];
86 | let firstRow = true;
87 | for( let row of headers ) {
88 | for( let colKey = 0, k = row.length; colKey < k; colKey++ ) {
89 | if( firstRow ) {
90 | ret.push([]);
91 | }
92 |
93 | if( row[colKey] ) {
94 | ret[colKey].push(row[colKey]);
95 | }
96 | }
97 |
98 | firstRow = false;
99 | }
100 | return ret.map(header => header.join(" "));
101 | }
102 |
103 | _getOutputObj() {
104 | let output = {};
105 | for( let identifier in this.map ) {
106 | let map = this.map[identifier];
107 |
108 | let tableRows = {};
109 | let htmlOutput = "";
110 | let includeInOutput = false;
111 |
112 | for( let dpr in map ) {
113 | let stats = map[dpr].getStats();
114 | for( let vwStats of stats ) {
115 | if( !htmlOutput ) {
116 | htmlOutput = `${vwStats.html}`;
117 | }
118 | let dprNum = parseInt(dpr);
119 | let widthRatio = vwStats.efficiency;
120 |
121 | let percentage = (widthRatio * 100 / dprNum).toFixed(1);
122 | let str = `${widthRatio.toFixed(2)}x`;
123 |
124 | if( this.showPercentages ) {
125 | str = `${percentage}%`;
126 | }
127 |
128 | let efficiencyOutput;
129 | if( widthRatio < 1 || percentage < 75 ) {
130 | efficiencyOutput = chalk.red(str);
131 | } else if( percentage < 92 || percentage > 150 ) {
132 | efficiencyOutput = chalk.yellow(str);
133 | } else {
134 | efficiencyOutput = str;
135 | }
136 |
137 | let vw = `${vwStats.viewportWidth}px`;
138 | if(!tableRows[vw]) {
139 | if(vwStats.width && (!this.minImageWidth || vwStats.width > this.minImageWidth)) {
140 | includeInOutput = true;
141 | }
142 | tableRows[vw] = [`${vwStats.width}px`];
143 | }
144 | tableRows[vw].push(`${vwStats.fileWidth}px`);
145 | tableRows[vw].push(efficiencyOutput);
146 |
147 | if( this.showCurrentSrc ) {
148 | tableRows[vw].push(vwStats.src);
149 | }
150 | }
151 | }
152 |
153 | if( includeInOutput ) {
154 | let tableContent = [];
155 | for(let row in tableRows) {
156 | tableContent.push([].concat(row, tableRows[row]));
157 | }
158 |
159 | output[htmlOutput] = {
160 | headers: this._getTableHeadersForIdentifier(identifier, this.showCurrentSrc),
161 | content: tableContent
162 | };
163 | }
164 | }
165 |
166 | return output;
167 | }
168 |
169 | getCsvOutput() {
170 | this.setShowCurrentSrc(true);
171 |
172 | let DELIMITER = ",";
173 | let obj = this._getOutputObj();
174 | let output = [];
175 | for( let html in obj ) {
176 | output.push(this._convertTableHeadersToString(obj[html].headers));
177 | for( let row of obj[html].content) {
178 | output.push(row.join(DELIMITER));
179 | }
180 | output.push("# ---"); // DELIMIT CSV FILES
181 | }
182 |
183 | return output.join("\n");
184 | }
185 |
186 | _getOutput() {
187 | let obj = this._getOutputObj();
188 | let output = [];
189 |
190 | for( let html in obj ) {
191 | output.push(html);
192 |
193 | let rows = [].concat(obj[html].headers, obj[html].content);
194 | output.push(table(rows, {
195 | drawHorizontalLine: (index, size) => {
196 | return index === 0 || index === 3 || index === size;
197 | }
198 | }) +
199 | `${chalk.underline("Legend")}: @1x ${chalk.red("<100%")} ${chalk.yellow(">150%")} Above @1x ${chalk.red("<75%")} ${chalk.yellow("75%–92%, >150%")}\n`);
200 | }
201 |
202 | let size = this.getNumberOfImages();
203 | output.push(size + " bitmap image" + (size !== 1 ? "s" : "") + " found.");
204 |
205 | return output.join("\n");
206 | }
207 |
208 | getOutput(useCsv) {
209 | if(useCsv) {
210 | chalk.enabled = false;
211 |
212 | return this.getCsvOutput();
213 | }
214 |
215 | return this._getOutput();
216 | }
217 | }
218 |
219 | module.exports = ImageMap;
--------------------------------------------------------------------------------