svg-inplace-rasterize
Version:
Rasterize parts of an SVG to keep it animatable while reducing size and improving render time.
144 lines (143 loc) • 6.92 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const sharp_1 = __importDefault(require("sharp")); // Load this before librsvg to avoid zlib loading order error
const librsvg_prebuilt_1 = require("librsvg-prebuilt");
const stream_1 = require("stream");
const cheerio_1 = __importDefault(require("cheerio"));
const image_data_uri_1 = __importDefault(require("image-data-uri"));
const cheerio_get_css_selector_1 = __importDefault(require("cheerio-get-css-selector"));
class InplaceRasterizer {
rasterize(readable, options) {
return __awaiter(this, void 0, void 0, function* () {
console.log('Starting rasterize');
const originalSvg = yield new Promise((res, rej) => {
const chunks = [];
readable.on("data", function (chunk) {
chunks.push(chunk);
});
readable.on("end", function () {
res(Buffer.concat(chunks).toString());
});
});
console.log('Cheerio loading original svg');
const $ = cheerio_1.default.load(originalSvg, { xmlMode: true, xml: true, withDomLvl1: false, decodeEntities: false });
cheerio_get_css_selector_1.default.init($);
const viewbox = $('svg').attr('viewBox').split(' ');
function isCheerio(x) {
return !!x.toArray;
}
console.log('Filtering elements');
let filtered = options.filter($);
let elements = isCheerio(filtered) ? filtered.toArray() : filtered;
console.log(`Getting roots and selectors for ${elements.length} elements`);
const rootsAndSelectors = elements.map((element, i) => {
if (i % 25 == 0)
console.log(`Creating unique selector for element ${i}`);
const selector = $(element).getUniqueSelector();
return { selector, element };
});
console.log('Creating exclusive svgs');
function asyncForEach(array, callback) {
return __awaiter(this, void 0, void 0, function* () {
for (let index = 0; index < array.length; index++) {
yield callback(array[index], index, array);
}
});
}
const stuff = yield asyncForEach(rootsAndSelectors, ({ selector, element }, i) => __awaiter(this, void 0, void 0, function* () {
if (i % 25 == 0)
console.log(`Creating exclusive svgs for element ${i}`);
const els = [];
console.log(selector);
let el = $(selector);
els.push(el.clone());
while ((el = el.parent()) && el.length) {
const clone = el.clone();
clone.children(':not(use):not(defs):not(clipPath)').remove();
els.push(clone);
}
const $sub = cheerio_1.default.load('', { xmlMode: true, xml: true, withDomLvl1: false, decodeEntities: false });
let p = $sub.root();
let lastEl;
els.forEach((el, i) => {
if (i === 0) {
lastEl = el;
}
else {
lastEl.appendTo(el);
lastEl = el;
}
});
lastEl.appendTo(p);
let data;
const exclusiveSvg = $sub.html();
try {
console.log(`Converted to html: ${i} size ${exclusiveSvg.length}`);
data = yield this.rsvgConvert(exclusiveSvg, { multiplier: options.multiplier });
console.log(`SVG ${i} converted`);
}
catch (e) {
console.error(`Error in partial ${i} with size ${exclusiveSvg.length}`);
throw e;
}
let uri;
if (!options.format || options.format.toLowerCase() === 'png') {
uri = image_data_uri_1.default.encode(data, 'PNG');
}
else if (options.format.toLowerCase() === 'webp') {
uri = image_data_uri_1.default.encode(yield sharp_1.default(data).webp().toBuffer(), 'WEBP');
console.log(`PNG ${i} converted to WEBP (uri length ${uri.length})`);
}
const totalElement = $(element);
const group = $('<g></g>');
const image = $('<image></image>');
const attrs = totalElement.attr();
for (const key in attrs) {
group.attr(key, attrs[key]);
}
image
.attr('href', uri)
// .attr('href', '' + i)
.attr('x', viewbox[0])
.attr('y', viewbox[1])
.attr('width', viewbox[2])
.attr('height', viewbox[3]);
$(group).append(image);
totalElement.before(group);
totalElement.remove();
}));
return $.html();
});
}
rsvgConvert(orig, options) {
return new Promise((res, rej) => {
// const svg = new Rsvg(, {keepImageData: true, unlimited: true}, {keepImageData: true, unlimited: true});
const svg = new librsvg_prebuilt_1.Rsvg();
// const svg = new Rsvg({keepImageData: true, unlimited: true});
const readable = new stream_1.Readable();
svg.on('finish', () => {
res(svg.render({
format: 'png',
width: svg.width * (options.multiplier || 1),
height: svg.height * (options.multiplier || 1)
}).data);
});
readable._read = () => { };
readable.pipe(svg);
readable.push(orig);
readable.push(null);
});
}
}
exports.InplaceRasterizer = InplaceRasterizer;