From 49ef4b59655f575daf3dba70dbaa708fe11fb110 Mon Sep 17 00:00:00 2001 From: ful1e5 <24286590+ful1e5@users.noreply.github.com> Date: Sat, 17 Apr 2021 16:29:05 +0530 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=96=20bitmapper=20"core"=20module=20in?= =?UTF-8?q?it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bitmapper/src/core/BitmapsGenerator.ts | 155 ++++++++++++++++++ .../src/core/SVGHandler/SvgDirectoryParser.ts | 77 +++++++++ bitmapper/src/core/SVGHandler/colorSvg.ts | 52 ++++++ bitmapper/src/core/SVGHandler/index.ts | 4 + bitmapper/src/core/index.ts | 4 + bitmapper/src/core/types.ts | 20 +++ bitmapper/src/core/util/frameNumber.ts | 7 + bitmapper/src/core/util/matchImages.ts | 11 ++ bitmapper/src/core/util/toHTML.ts | 19 +++ 9 files changed, 349 insertions(+) create mode 100644 bitmapper/src/core/BitmapsGenerator.ts create mode 100644 bitmapper/src/core/SVGHandler/SvgDirectoryParser.ts create mode 100644 bitmapper/src/core/SVGHandler/colorSvg.ts create mode 100644 bitmapper/src/core/SVGHandler/index.ts create mode 100644 bitmapper/src/core/index.ts create mode 100644 bitmapper/src/core/types.ts create mode 100644 bitmapper/src/core/util/frameNumber.ts create mode 100644 bitmapper/src/core/util/matchImages.ts create mode 100644 bitmapper/src/core/util/toHTML.ts diff --git a/bitmapper/src/core/BitmapsGenerator.ts b/bitmapper/src/core/BitmapsGenerator.ts new file mode 100644 index 0000000..c9017a0 --- /dev/null +++ b/bitmapper/src/core/BitmapsGenerator.ts @@ -0,0 +1,155 @@ +import fs from "fs"; +import path from "path"; + +import puppeteer, { Browser, ElementHandle, Page } from "puppeteer"; + +import { frameNumber } from "./util/frameNumber"; +import { matchImages } from "./util/matchImages"; +import { toHTML } from "./util/toHTML"; + +class BitmapsGenerator { + /** + * Generate Png files from svg code. + * @param themeName Give name, So all bitmaps files are organized in one directory. + * @param bitmapsDir `absolute` or `relative` path, Where `.png` files will store. + */ + constructor(private bitmapsDir: string) { + this.bitmapsDir = path.resolve(bitmapsDir); + this.createDir(this.bitmapsDir); + } + + /** + * Create directory if it doesn't exists. + * @param dirPath directory `absolute` path. + */ + private createDir(dirPath: string) { + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + } + + /** + * Prepare headless browser. + */ + public async getBrowser(): Promise { + return await puppeteer.launch({ + ignoreDefaultArgs: ["--no-sandbox"], + headless: true, + }); + } + + private async getSvgElement( + page: Page, + content: string + ): Promise> { + if (!content) { + throw new Error(`${content} File Read error`); + } + + const html = toHTML(content); + await page.setContent(html, { timeout: 0 }); + + const svg = await page.$("#container svg"); + + if (!svg) { + throw new Error("svg element not found!"); + } + return svg; + } + + public async generateStatic(browser: Browser, content: string, key: string) { + const page = await browser.newPage(); + const svg = await this.getSvgElement(page, content); + + const out = path.resolve(this.bitmapsDir, `${key}.png`); + + await svg.screenshot({ omitBackground: true, path: out }); + await page.close(); + } + + private async screenshot( + element: ElementHandle + ): Promise { + const buffer = await element.screenshot({ + encoding: "binary", + omitBackground: true, + }); + + if (!buffer) { + throw new Error("SVG element screenshot not working"); + } + return buffer; + } + + private async stopAnimation(page: Page) { + const client = await page.target().createCDPSession(); + await client.send("Animation.setPlaybackRate", { + playbackRate: 0, + }); + } + + private async resumeAnimation(page: Page, playbackRate: number) { + const client = await page.target().createCDPSession(); + await client.send("Animation.setPlaybackRate", { + playbackRate, + }); + } + + private async saveFrameImage(key: string, frame: Buffer | string) { + const out_path = path.resolve(this.bitmapsDir, key); + fs.writeFileSync(out_path, frame); + } + + public async generateAnimated( + browser: Browser, + content: string, + key: string, + options?: { + playbackRate?: number; + diff?: number; + frameLimit?: number; + framePadding?: number; + } + ) { + const opt = Object.assign( + { playbackRate: 0.1, diff: 0, frameLimit: 300, framePadding: 4 }, + options + ); + + const page = await browser.newPage(); + const svg = await this.getSvgElement(page, content); + await this.stopAnimation(page); + + let index = 1; + let breakRendering = false; + let prevImg: Buffer | string; + + // Rendering frames till `imgN` matched to `imgN-1` (When Animation is done) + while (!breakRendering) { + if (index > opt.frameLimit) { + throw new Error("Reached the frame limit."); + } + + await this.resumeAnimation(page, opt.playbackRate); + const img: string | Buffer = await this.screenshot(svg); + await this.stopAnimation(page); + + if (index > 1) { + // @ts-ignore + const diff = matchImages(prevImg, img); + if (diff <= opt.diff) { + breakRendering = !breakRendering; + } + } + const number = frameNumber(index, opt.framePadding); + const frame = `${key}-${number}.png`; + + this.saveFrameImage(frame, img); + + prevImg = img; + ++index; + } + await page.close(); + } +} +export { BitmapsGenerator }; diff --git a/bitmapper/src/core/SVGHandler/SvgDirectoryParser.ts b/bitmapper/src/core/SVGHandler/SvgDirectoryParser.ts new file mode 100644 index 0000000..516c69b --- /dev/null +++ b/bitmapper/src/core/SVGHandler/SvgDirectoryParser.ts @@ -0,0 +1,77 @@ +import fs from "fs"; +import path from "path"; + +interface Svg { + key: string; + content: string; +} + +class SvgDirectoryParser { + /** + * Manage and Parse SVG file path in `absolute` fashion. + * This Parser look svg files as below fashion: + * ` + * <@svgDir>/static + * <@svgDir>/animated + * ` + * @param svgDir is relative/absolute path, Where `SVG` files are stored. + */ + semiAnimated: boolean = false; + constructor(private svgDir: string) { + if (!fs.existsSync(this.svgDir)) { + throw new Error(`SVG files not found in ${this.svgDir}`); + } + } + + private readData(f: string): Svg { + const content = fs.readFileSync(f, "utf-8"); + const key = path.basename(f, ".svg"); + return { content, key }; + } + + /** + * Return absolute paths array of SVG files data located inside '@svgDir/static' + */ + public getStatic(): Svg[] { + const staticDir = path.resolve(this.svgDir, "static"); + + if (!fs.existsSync(staticDir)) { + console.log(`${this.svgDir} contains semi-animated .svg files`); + this.semiAnimated = true; + return []; + } else { + const svgs = fs + .readdirSync(staticDir) + .map((f) => this.readData(path.resolve(staticDir, f))); + + if (svgs.length == 0) { + throw new Error("Static Cursors directory is empty"); + } + return svgs; + } + } + /** + * Return absolute paths array of SVG files data located inside '@svgDir/animated' + */ + public getAnimated(): Svg[] { + const animatedDir = path.resolve(this.svgDir, "animated"); + + if (!fs.existsSync(animatedDir)) { + throw new Error("Animated Cursors directory not found"); + } + + const svgs = fs + .readdirSync(animatedDir) + .map((f) => this.readData(path.resolve(animatedDir, f))); + + if (svgs.length == 0 && this.semiAnimated) { + throw new Error( + `Can't parse svg directory ${this.svgDir} as semi-animated theme` + ); + } + + return svgs; + } +} + +export { SvgDirectoryParser }; diff --git a/bitmapper/src/core/SVGHandler/colorSvg.ts b/bitmapper/src/core/SVGHandler/colorSvg.ts new file mode 100644 index 0000000..49a0b50 --- /dev/null +++ b/bitmapper/src/core/SVGHandler/colorSvg.ts @@ -0,0 +1,52 @@ +import { Colors } from "../types"; + +/** + * Default Key Colors for generating colored svg. + * base="#00FF00" (Green) + * outline="#0000FF" (Blue) + * watch.background="#FF0000" (Red) + * */ +const defaultKeyColors: Colors = { + base: "#00FF00", + outline: "#0000FF", + watch: { + background: "#FF0000", + }, +}; + +/** + * Customize colors of svg code. + * @param {string} content SVG code. + * @param {Colors} colors Customize colors. + * @param {Colors} [keys] Colors Key, That was written SVG code. + * @returns {string} SVG code with colors. + */ +const colorSvg = ( + content: string, + colors: Colors, + keys: Colors = defaultKeyColors +): string => { + content = content + .replace(new RegExp(keys.base, "ig"), colors.base) + .replace(new RegExp(keys.outline, "ig"), colors.outline); + + try { + // === trying to replace `watch` color === + + if (!colors.watch?.background) { + throw new Error(""); + } + const { background: b } = colors.watch; + content = content.replace(new RegExp(keys.watch!.background, "ig"), b); // Watch Background + } catch (error) { + // === on error => replace `watch` color as `base` === + + content = content.replace( + new RegExp(keys.watch!.background, "ig"), + colors.base + ); + } + return content; +}; + +export { colorSvg }; diff --git a/bitmapper/src/core/SVGHandler/index.ts b/bitmapper/src/core/SVGHandler/index.ts new file mode 100644 index 0000000..308a433 --- /dev/null +++ b/bitmapper/src/core/SVGHandler/index.ts @@ -0,0 +1,4 @@ +import { colorSvg } from "./colorSvg"; +import { SvgDirectoryParser } from "./SvgDirectoryParser"; + +export { colorSvg, SvgDirectoryParser }; diff --git a/bitmapper/src/core/index.ts b/bitmapper/src/core/index.ts new file mode 100644 index 0000000..2fe958b --- /dev/null +++ b/bitmapper/src/core/index.ts @@ -0,0 +1,4 @@ +import { BitmapsGenerator } from "./BitmapsGenerator"; +import * as SVGHandler from "./SVGHandler"; + +export { BitmapsGenerator, SVGHandler }; diff --git a/bitmapper/src/core/types.ts b/bitmapper/src/core/types.ts new file mode 100644 index 0000000..da9fb1c --- /dev/null +++ b/bitmapper/src/core/types.ts @@ -0,0 +1,20 @@ +/** + * Hex Colors in string Format. + * + * `Example: `"#FFFFFF" + */ +type HexColor = string; + +/** + * @Colors expect `base`, `outline` & `watch-background` colors in **HexColor** Format. + * @default background is `base` color. + */ +type Colors = { + base: HexColor; + outline: HexColor; + watch?: { + background: HexColor; + }; +}; + +export { Colors }; diff --git a/bitmapper/src/core/util/frameNumber.ts b/bitmapper/src/core/util/frameNumber.ts new file mode 100644 index 0000000..76d1165 --- /dev/null +++ b/bitmapper/src/core/util/frameNumber.ts @@ -0,0 +1,7 @@ +export const frameNumber = (index: number, padding: number) => { + let result = "" + index; + while (result.length < padding) { + result = "0" + result; + } + return result; +}; diff --git a/bitmapper/src/core/util/matchImages.ts b/bitmapper/src/core/util/matchImages.ts new file mode 100644 index 0000000..6d09a95 --- /dev/null +++ b/bitmapper/src/core/util/matchImages.ts @@ -0,0 +1,11 @@ +import Pixelmatch from "pixelmatch"; +import { PNG } from "pngjs"; + +export const matchImages = (img1: Buffer, img2: Buffer): number => { + const { data: img1Data, width, height } = PNG.sync.read(img1); + const { data: imgNData } = PNG.sync.read(img2); + + return Pixelmatch(img1Data, imgNData, null, width, height, { + threshold: 0.1, + }); +}; diff --git a/bitmapper/src/core/util/toHTML.ts b/bitmapper/src/core/util/toHTML.ts new file mode 100644 index 0000000..36fbbf4 --- /dev/null +++ b/bitmapper/src/core/util/toHTML.ts @@ -0,0 +1,19 @@ +export const template = ` + + + + + + Render Template + + + +
+ +
+ + +`; + +export const toHTML = (svgData: string): string => + template.replace("", svgData);