UNPKG

image-vectorizer

Version:

Potrace in Javascript, for NodeJS and Browser

1,303 lines (1,140 loc) 29.8 kB
import Jimp from "jimp" import Bitmap from "./types/Bitmap.js" import Curve from "./types/Curve.js" import Point from "./types/Point.js" import Path from "./types/Path.js" import Quad from "./types/Quad.js" import Sum from "./types/Sum.js" import Opti from "./types/Opti.js" import utils from "./utils.js" function isBrowser() { return typeof window !== "undefined" && typeof window.document !== "undefined" } /** * Potrace class * * @param {Potrace~Options} [options] * @constructor */ function Potrace(options) { this._luminanceData = null this._pathlist = [] this._imageLoadingIdentifier = null this._imageLoaded = false this._processed = false this._params = { turnPolicy: Potrace.TURNPOLICY_MINORITY, turdSize: 2, alphaMax: 1, optCurve: true, optTolerance: 0.2, threshold: Potrace.THRESHOLD_AUTO, blackOnWhite: true, color: Potrace.COLOR_AUTO, background: Potrace.COLOR_TRANSPARENT, width: null, height: null } if (options) { this.setParameters(options) } } Potrace.COLOR_AUTO = "auto" Potrace.COLOR_TRANSPARENT = "transparent" Potrace.THRESHOLD_AUTO = -1 Potrace.TURNPOLICY_BLACK = "black" Potrace.TURNPOLICY_WHITE = "white" Potrace.TURNPOLICY_LEFT = "left" Potrace.TURNPOLICY_RIGHT = "right" Potrace.TURNPOLICY_MINORITY = "minority" Potrace.TURNPOLICY_MAJORITY = "majority" var SUPPORTED_TURNPOLICY_VALUES = [ Potrace.TURNPOLICY_BLACK, Potrace.TURNPOLICY_WHITE, Potrace.TURNPOLICY_LEFT, Potrace.TURNPOLICY_RIGHT, Potrace.TURNPOLICY_MINORITY, Potrace.TURNPOLICY_MAJORITY ] Potrace.prototype = { /** * Creating a new {@link Path} for every group of black pixels. * @private */ _bmToPathlist: function () { var self = this, threshold = this._params.threshold, blackOnWhite = this._params.blackOnWhite, blackMap, currentPoint = new Point(0, 0), path if (threshold === Potrace.THRESHOLD_AUTO) { threshold = this._luminanceData.histogram().autoThreshold() || 128 } blackMap = this._luminanceData.copy(function (lum) { var pastTheThreshold = blackOnWhite ? lum > threshold : lum < threshold return pastTheThreshold ? 0 : 1 }) /** * finds next black pixel of the image * * @param {Point} point * @returns {boolean} * @private */ function findNext(point) { var i = blackMap.pointToIndex(point) while (i < blackMap.size && blackMap.data[i] !== 1) { i++ } return i < blackMap.size && blackMap.indexToPoint(i) } function majority(x, y) { var i, a, ct for (i = 2; i < 5; i++) { ct = 0 for (a = -i + 1; a <= i - 1; a++) { ct += blackMap.getValueAt(x + a, y + i - 1) ? 1 : -1 ct += blackMap.getValueAt(x + i - 1, y + a - 1) ? 1 : -1 ct += blackMap.getValueAt(x + a - 1, y - i) ? 1 : -1 ct += blackMap.getValueAt(x - i, y + a) ? 1 : -1 } if (ct > 0) { return 1 } else if (ct < 0) { return 0 } } return 0 } function findPath(point) { var path = new Path(), x = point.x, y = point.y, dirx = 0, diry = 1, tmp path.sign = blackMap.getValueAt(point.x, point.y) ? "+" : "-" while (1) { path.pt.push(new Point(x, y)) if (x > path.maxX) path.maxX = x if (x < path.minX) path.minX = x if (y > path.maxY) path.maxY = y if (y < path.minY) path.minY = y path.len++ x += dirx y += diry path.area -= x * diry if (x === point.x && y === point.y) break var l = blackMap.getValueAt(x + (dirx + diry - 1) / 2, y + (diry - dirx - 1) / 2) var r = blackMap.getValueAt(x + (dirx - diry - 1) / 2, y + (diry + dirx - 1) / 2) if (r && !l) { if ( self._params.turnPolicy === "right" || (self._params.turnPolicy === "black" && path.sign === "+") || (self._params.turnPolicy === "white" && path.sign === "-") || (self._params.turnPolicy === "majority" && majority(x, y)) || (self._params.turnPolicy === "minority" && !majority(x, y)) ) { tmp = dirx dirx = -diry diry = tmp } else { tmp = dirx dirx = diry diry = -tmp } } else if (r) { tmp = dirx dirx = -diry diry = tmp } else if (!l) { tmp = dirx dirx = diry diry = -tmp } } return path } function xorPath(path) { var y1 = path.pt[0].y, len = path.len, x, y, maxX, minY, i, j, indx for (i = 1; i < len; i++) { x = path.pt[i].x y = path.pt[i].y if (y !== y1) { minY = y1 < y ? y1 : y maxX = path.maxX for (j = x; j < maxX; j++) { indx = blackMap.pointToIndex(j, minY) blackMap.data[indx] = blackMap.data[indx] ? 0 : 1 } y1 = y } } } // Clear path list this._pathlist = [] while ((currentPoint = findNext(currentPoint))) { path = findPath(currentPoint) xorPath(path) if (path.area > self._params.turdSize) { this._pathlist.push(path) } } }, /** * Processes path list created by _bmToPathlist method creating and optimizing {@link Curve}'s * @private */ _processPath: function () { var self = this function calcSums(path) { var i, x, y path.x0 = path.pt[0].x path.y0 = path.pt[0].y path.sums = [] var s = path.sums s.push(new Sum(0, 0, 0, 0, 0)) for (i = 0; i < path.len; i++) { x = path.pt[i].x - path.x0 y = path.pt[i].y - path.y0 s.push(new Sum(s[i].x + x, s[i].y + y, s[i].xy + x * y, s[i].x2 + x * x, s[i].y2 + y * y)) } } function calcLon(path) { var n = path.len, pt = path.pt, dir, pivk = new Array(n), nc = new Array(n), ct = new Array(4) path.lon = new Array(n) var constraint = [new Point(), new Point()], cur = new Point(), off = new Point(), dk = new Point(), foundk var i, j, k1, a, b, c, d, k = 0 for (i = n - 1; i >= 0; i--) { if (pt[i].x != pt[k].x && pt[i].y != pt[k].y) { k = i + 1 } nc[i] = k } for (i = n - 1; i >= 0; i--) { ct[0] = ct[1] = ct[2] = ct[3] = 0 dir = (3 + 3 * (pt[utils.mod(i + 1, n)].x - pt[i].x) + (pt[utils.mod(i + 1, n)].y - pt[i].y)) / 2 ct[dir]++ constraint[0].x = 0 constraint[0].y = 0 constraint[1].x = 0 constraint[1].y = 0 k = nc[i] k1 = i while (1) { foundk = 0 dir = (3 + 3 * utils.sign(pt[k].x - pt[k1].x) + utils.sign(pt[k].y - pt[k1].y)) / 2 ct[dir]++ if (ct[0] && ct[1] && ct[2] && ct[3]) { pivk[i] = k1 foundk = 1 break } cur.x = pt[k].x - pt[i].x cur.y = pt[k].y - pt[i].y if (utils.xprod(constraint[0], cur) < 0 || utils.xprod(constraint[1], cur) > 0) { break } if (Math.abs(cur.x) <= 1 && Math.abs(cur.y) <= 1) { } else { off.x = cur.x + (cur.y >= 0 && (cur.y > 0 || cur.x < 0) ? 1 : -1) off.y = cur.y + (cur.x <= 0 && (cur.x < 0 || cur.y < 0) ? 1 : -1) if (utils.xprod(constraint[0], off) >= 0) { constraint[0].x = off.x constraint[0].y = off.y } off.x = cur.x + (cur.y <= 0 && (cur.y < 0 || cur.x < 0) ? 1 : -1) off.y = cur.y + (cur.x >= 0 && (cur.x > 0 || cur.y < 0) ? 1 : -1) if (utils.xprod(constraint[1], off) <= 0) { constraint[1].x = off.x constraint[1].y = off.y } } k1 = k k = nc[k1] if (!utils.cyclic(k, i, k1)) { break } } if (foundk === 0) { dk.x = utils.sign(pt[k].x - pt[k1].x) dk.y = utils.sign(pt[k].y - pt[k1].y) cur.x = pt[k1].x - pt[i].x cur.y = pt[k1].y - pt[i].y a = utils.xprod(constraint[0], cur) b = utils.xprod(constraint[0], dk) c = utils.xprod(constraint[1], cur) d = utils.xprod(constraint[1], dk) j = 10000000 if (b < 0) { j = Math.floor(a / -b) } if (d > 0) { j = Math.min(j, Math.floor(-c / d)) } pivk[i] = utils.mod(k1 + j, n) } } j = pivk[n - 1] path.lon[n - 1] = j for (i = n - 2; i >= 0; i--) { if (utils.cyclic(i + 1, pivk[i], j)) { j = pivk[i] } path.lon[i] = j } for (i = n - 1; utils.cyclic(utils.mod(i + 1, n), j, path.lon[i]); i--) { path.lon[i] = j } } function bestPolygon(path) { function penalty3(path, i, j) { var n = path.len, pt = path.pt, sums = path.sums var x, y, xy, x2, y2, k, a, b, c, s, px, py, ex, ey, r = 0 if (j >= n) { j -= n r = 1 } if (r === 0) { x = sums[j + 1].x - sums[i].x y = sums[j + 1].y - sums[i].y x2 = sums[j + 1].x2 - sums[i].x2 xy = sums[j + 1].xy - sums[i].xy y2 = sums[j + 1].y2 - sums[i].y2 k = j + 1 - i } else { x = sums[j + 1].x - sums[i].x + sums[n].x y = sums[j + 1].y - sums[i].y + sums[n].y x2 = sums[j + 1].x2 - sums[i].x2 + sums[n].x2 xy = sums[j + 1].xy - sums[i].xy + sums[n].xy y2 = sums[j + 1].y2 - sums[i].y2 + sums[n].y2 k = j + 1 - i + n } px = (pt[i].x + pt[j].x) / 2.0 - pt[0].x py = (pt[i].y + pt[j].y) / 2.0 - pt[0].y ey = pt[j].x - pt[i].x ex = -(pt[j].y - pt[i].y) a = (x2 - 2 * x * px) / k + px * px b = (xy - x * py - y * px) / k + px * py c = (y2 - 2 * y * py) / k + py * py s = ex * ex * a + 2 * ex * ey * b + ey * ey * c return Math.sqrt(s) } var i, j, m, k, n = path.len, pen = new Array(n + 1), prev = new Array(n + 1), clip0 = new Array(n), clip1 = new Array(n + 1), seg0 = new Array(n + 1), seg1 = new Array(n + 1), thispen, best, c for (i = 0; i < n; i++) { c = utils.mod(path.lon[utils.mod(i - 1, n)] - 1, n) if (c == i) { c = utils.mod(i + 1, n) } if (c < i) { clip0[i] = n } else { clip0[i] = c } } j = 1 for (i = 0; i < n; i++) { while (j <= clip0[i]) { clip1[j] = i j++ } } i = 0 for (j = 0; i < n; j++) { seg0[j] = i i = clip0[i] } seg0[j] = n m = j i = n for (j = m; j > 0; j--) { seg1[j] = i i = clip1[i] } seg1[0] = 0 pen[0] = 0 for (j = 1; j <= m; j++) { for (i = seg1[j]; i <= seg0[j]; i++) { best = -1 for (k = seg0[j - 1]; k >= clip1[i]; k--) { thispen = penalty3(path, k, i) + pen[k] if (best < 0 || thispen < best) { prev[i] = k best = thispen } } pen[i] = best } } path.m = m path.po = new Array(m) for (i = n, j = m - 1; i > 0; j--) { i = prev[i] path.po[j] = i } } function adjustVertices(path) { function pointslope(path, i, j, ctr, dir) { var n = path.len, sums = path.sums, x, y, x2, xy, y2, k, a, b, c, lambda2, l, r = 0 while (j >= n) { j -= n r += 1 } while (i >= n) { i -= n r -= 1 } while (j < 0) { j += n r -= 1 } while (i < 0) { i += n r += 1 } x = sums[j + 1].x - sums[i].x + r * sums[n].x y = sums[j + 1].y - sums[i].y + r * sums[n].y x2 = sums[j + 1].x2 - sums[i].x2 + r * sums[n].x2 xy = sums[j + 1].xy - sums[i].xy + r * sums[n].xy y2 = sums[j + 1].y2 - sums[i].y2 + r * sums[n].y2 k = j + 1 - i + r * n ctr.x = x / k ctr.y = y / k a = (x2 - (x * x) / k) / k b = (xy - (x * y) / k) / k c = (y2 - (y * y) / k) / k lambda2 = (a + c + Math.sqrt((a - c) * (a - c) + 4 * b * b)) / 2 a -= lambda2 c -= lambda2 if (Math.abs(a) >= Math.abs(c)) { l = Math.sqrt(a * a + b * b) if (l !== 0) { dir.x = -b / l dir.y = a / l } } else { l = Math.sqrt(c * c + b * b) if (l !== 0) { dir.x = -c / l dir.y = b / l } } if (l === 0) { dir.x = dir.y = 0 } } var m = path.m, po = path.po, n = path.len, pt = path.pt, x0 = path.x0, y0 = path.y0, ctr = new Array(m), dir = new Array(m), q = new Array(m), v = new Array(3), d, i, j, k, l, s = new Point() path.curve = new Curve(m) for (i = 0; i < m; i++) { j = po[utils.mod(i + 1, m)] j = utils.mod(j - po[i], n) + po[i] ctr[i] = new Point() dir[i] = new Point() pointslope(path, po[i], j, ctr[i], dir[i]) } for (i = 0; i < m; i++) { q[i] = new Quad() d = dir[i].x * dir[i].x + dir[i].y * dir[i].y if (d === 0.0) { for (j = 0; j < 3; j++) { for (k = 0; k < 3; k++) { q[i].data[j * 3 + k] = 0 } } } else { v[0] = dir[i].y v[1] = -dir[i].x v[2] = -v[1] * ctr[i].y - v[0] * ctr[i].x for (l = 0; l < 3; l++) { for (k = 0; k < 3; k++) { q[i].data[l * 3 + k] = (v[l] * v[k]) / d } } } } var Q, w, dx, dy, det, min, cand, xmin, ymin, z for (i = 0; i < m; i++) { Q = new Quad() w = new Point() s.x = pt[po[i]].x - x0 s.y = pt[po[i]].y - y0 j = utils.mod(i - 1, m) for (l = 0; l < 3; l++) { for (k = 0; k < 3; k++) { Q.data[l * 3 + k] = q[j].at(l, k) + q[i].at(l, k) } } while (1) { det = Q.at(0, 0) * Q.at(1, 1) - Q.at(0, 1) * Q.at(1, 0) if (det !== 0.0) { w.x = (-Q.at(0, 2) * Q.at(1, 1) + Q.at(1, 2) * Q.at(0, 1)) / det w.y = (Q.at(0, 2) * Q.at(1, 0) - Q.at(1, 2) * Q.at(0, 0)) / det break } if (Q.at(0, 0) > Q.at(1, 1)) { v[0] = -Q.at(0, 1) v[1] = Q.at(0, 0) } else if (Q.at(1, 1)) { v[0] = -Q.at(1, 1) v[1] = Q.at(1, 0) } else { v[0] = 1 v[1] = 0 } d = v[0] * v[0] + v[1] * v[1] v[2] = -v[1] * s.y - v[0] * s.x for (l = 0; l < 3; l++) { for (k = 0; k < 3; k++) { Q.data[l * 3 + k] += (v[l] * v[k]) / d } } } dx = Math.abs(w.x - s.x) dy = Math.abs(w.y - s.y) if (dx <= 0.5 && dy <= 0.5) { path.curve.vertex[i] = new Point(w.x + x0, w.y + y0) continue } min = utils.quadform(Q, s) xmin = s.x ymin = s.y if (Q.at(0, 0) !== 0.0) { for (z = 0; z < 2; z++) { w.y = s.y - 0.5 + z w.x = -(Q.at(0, 1) * w.y + Q.at(0, 2)) / Q.at(0, 0) dx = Math.abs(w.x - s.x) cand = utils.quadform(Q, w) if (dx <= 0.5 && cand < min) { min = cand xmin = w.x ymin = w.y } } } if (Q.at(1, 1) !== 0.0) { for (z = 0; z < 2; z++) { w.x = s.x - 0.5 + z w.y = -(Q.at(1, 0) * w.x + Q.at(1, 2)) / Q.at(1, 1) dy = Math.abs(w.y - s.y) cand = utils.quadform(Q, w) if (dy <= 0.5 && cand < min) { min = cand xmin = w.x ymin = w.y } } } for (l = 0; l < 2; l++) { for (k = 0; k < 2; k++) { w.x = s.x - 0.5 + l w.y = s.y - 0.5 + k cand = utils.quadform(Q, w) if (cand < min) { min = cand xmin = w.x ymin = w.y } } } path.curve.vertex[i] = new Point(xmin + x0, ymin + y0) } } function reverse(path) { var curve = path.curve, m = curve.n, v = curve.vertex, i, j, tmp for (i = 0, j = m - 1; i < j; i++, j--) { tmp = v[i] v[i] = v[j] v[j] = tmp } } function smooth(path) { var m = path.curve.n, curve = path.curve var i, j, k, dd, denom, alpha, p2, p3, p4 for (i = 0; i < m; i++) { j = utils.mod(i + 1, m) k = utils.mod(i + 2, m) p4 = utils.interval(1 / 2.0, curve.vertex[k], curve.vertex[j]) denom = utils.ddenom(curve.vertex[i], curve.vertex[k]) if (denom !== 0.0) { dd = utils.dpara(curve.vertex[i], curve.vertex[j], curve.vertex[k]) / denom dd = Math.abs(dd) alpha = dd > 1 ? 1 - 1.0 / dd : 0 alpha = alpha / 0.75 } else { alpha = 4 / 3.0 } curve.alpha0[j] = alpha if (alpha >= self._params.alphaMax) { curve.tag[j] = "CORNER" curve.c[3 * j + 1] = curve.vertex[j] curve.c[3 * j + 2] = p4 } else { if (alpha < 0.55) { alpha = 0.55 } else if (alpha > 1) { alpha = 1 } p2 = utils.interval(0.5 + 0.5 * alpha, curve.vertex[i], curve.vertex[j]) p3 = utils.interval(0.5 + 0.5 * alpha, curve.vertex[k], curve.vertex[j]) curve.tag[j] = "CURVE" curve.c[3 * j + 0] = p2 curve.c[3 * j + 1] = p3 curve.c[3 * j + 2] = p4 } curve.alpha[j] = alpha curve.beta[j] = 0.5 } curve.alphaCurve = 1 } function optiCurve(path) { function opti_penalty(path, i, j, res, opttolerance, convc, areac) { var m = path.curve.n, curve = path.curve, vertex = curve.vertex, k, k1, k2, conv, i1, area, alpha, d, d1, d2, p0, p1, p2, p3, pt, A, R, A1, A2, A3, A4, s, t if (i == j) { return 1 } k = i i1 = utils.mod(i + 1, m) k1 = utils.mod(k + 1, m) conv = convc[k1] if (conv === 0) { return 1 } d = utils.ddist(vertex[i], vertex[i1]) for (k = k1; k != j; k = k1) { k1 = utils.mod(k + 1, m) k2 = utils.mod(k + 2, m) if (convc[k1] != conv) { return 1 } if (utils.sign(utils.cprod(vertex[i], vertex[i1], vertex[k1], vertex[k2])) != conv) { return 1 } if ( utils.iprod1(vertex[i], vertex[i1], vertex[k1], vertex[k2]) < d * utils.ddist(vertex[k1], vertex[k2]) * -0.999847695156 ) { return 1 } } p0 = curve.c[utils.mod(i, m) * 3 + 2].copy() p1 = vertex[utils.mod(i + 1, m)].copy() p2 = vertex[utils.mod(j, m)].copy() p3 = curve.c[utils.mod(j, m) * 3 + 2].copy() area = areac[j] - areac[i] area -= utils.dpara(vertex[0], curve.c[i * 3 + 2], curve.c[j * 3 + 2]) / 2 if (i >= j) { area += areac[m] } A1 = utils.dpara(p0, p1, p2) A2 = utils.dpara(p0, p1, p3) A3 = utils.dpara(p0, p2, p3) A4 = A1 + A3 - A2 if (A2 == A1) { return 1 } t = A3 / (A3 - A4) s = A2 / (A2 - A1) A = (A2 * t) / 2.0 if (A === 0.0) { return 1 } R = area / A alpha = 2 - Math.sqrt(4 - R / 0.3) res.c[0] = utils.interval(t * alpha, p0, p1) res.c[1] = utils.interval(s * alpha, p3, p2) res.alpha = alpha res.t = t res.s = s p1 = res.c[0].copy() p2 = res.c[1].copy() res.pen = 0 for (k = utils.mod(i + 1, m); k != j; k = k1) { k1 = utils.mod(k + 1, m) t = utils.tangent(p0, p1, p2, p3, vertex[k], vertex[k1]) if (t < -0.5) { return 1 } pt = utils.bezier(t, p0, p1, p2, p3) d = utils.ddist(vertex[k], vertex[k1]) if (d === 0.0) { return 1 } d1 = utils.dpara(vertex[k], vertex[k1], pt) / d if (Math.abs(d1) > opttolerance) { return 1 } if (utils.iprod(vertex[k], vertex[k1], pt) < 0 || utils.iprod(vertex[k1], vertex[k], pt) < 0) { return 1 } res.pen += d1 * d1 } for (k = i; k != j; k = k1) { k1 = utils.mod(k + 1, m) t = utils.tangent(p0, p1, p2, p3, curve.c[k * 3 + 2], curve.c[k1 * 3 + 2]) if (t < -0.5) { return 1 } pt = utils.bezier(t, p0, p1, p2, p3) d = utils.ddist(curve.c[k * 3 + 2], curve.c[k1 * 3 + 2]) if (d === 0.0) { return 1 } d1 = utils.dpara(curve.c[k * 3 + 2], curve.c[k1 * 3 + 2], pt) / d d2 = utils.dpara(curve.c[k * 3 + 2], curve.c[k1 * 3 + 2], vertex[k1]) / d d2 *= 0.75 * curve.alpha[k1] if (d2 < 0) { d1 = -d1 d2 = -d2 } if (d1 < d2 - opttolerance) { return 1 } if (d1 < d2) { res.pen += (d1 - d2) * (d1 - d2) } } return 0 } var curve = path.curve, m = curve.n, vert = curve.vertex, pt = new Array(m + 1), pen = new Array(m + 1), len = new Array(m + 1), opt = new Array(m + 1), om, i, j, r, o = new Opti(), p0, i1, area, alpha, ocurve, s, t var convc = new Array(m), areac = new Array(m + 1) for (i = 0; i < m; i++) { if (curve.tag[i] == "CURVE") { convc[i] = utils.sign(utils.dpara(vert[utils.mod(i - 1, m)], vert[i], vert[utils.mod(i + 1, m)])) } else { convc[i] = 0 } } area = 0.0 areac[0] = 0.0 p0 = curve.vertex[0] for (i = 0; i < m; i++) { i1 = utils.mod(i + 1, m) if (curve.tag[i1] == "CURVE") { alpha = curve.alpha[i1] area += (0.3 * alpha * (4 - alpha) * utils.dpara(curve.c[i * 3 + 2], vert[i1], curve.c[i1 * 3 + 2])) / 2 area += utils.dpara(p0, curve.c[i * 3 + 2], curve.c[i1 * 3 + 2]) / 2 } areac[i + 1] = area } pt[0] = -1 pen[0] = 0 len[0] = 0 for (j = 1; j <= m; j++) { pt[j] = j - 1 pen[j] = pen[j - 1] len[j] = len[j - 1] + 1 for (i = j - 2; i >= 0; i--) { r = opti_penalty(path, i, utils.mod(j, m), o, self._params.optTolerance, convc, areac) if (r) { break } if (len[j] > len[i] + 1 || (len[j] == len[i] + 1 && pen[j] > pen[i] + o.pen)) { pt[j] = i pen[j] = pen[i] + o.pen len[j] = len[i] + 1 opt[j] = o o = new Opti() } } } om = len[m] ocurve = new Curve(om) s = new Array(om) t = new Array(om) j = m for (i = om - 1; i >= 0; i--) { if (pt[j] == j - 1) { ocurve.tag[i] = curve.tag[utils.mod(j, m)] ocurve.c[i * 3 + 0] = curve.c[utils.mod(j, m) * 3 + 0] ocurve.c[i * 3 + 1] = curve.c[utils.mod(j, m) * 3 + 1] ocurve.c[i * 3 + 2] = curve.c[utils.mod(j, m) * 3 + 2] ocurve.vertex[i] = curve.vertex[utils.mod(j, m)] ocurve.alpha[i] = curve.alpha[utils.mod(j, m)] ocurve.alpha0[i] = curve.alpha0[utils.mod(j, m)] ocurve.beta[i] = curve.beta[utils.mod(j, m)] s[i] = t[i] = 1.0 } else { ocurve.tag[i] = "CURVE" ocurve.c[i * 3 + 0] = opt[j].c[0] ocurve.c[i * 3 + 1] = opt[j].c[1] ocurve.c[i * 3 + 2] = curve.c[utils.mod(j, m) * 3 + 2] ocurve.vertex[i] = utils.interval(opt[j].s, curve.c[utils.mod(j, m) * 3 + 2], vert[utils.mod(j, m)]) ocurve.alpha[i] = opt[j].alpha ocurve.alpha0[i] = opt[j].alpha s[i] = opt[j].s t[i] = opt[j].t } j = pt[j] } for (i = 0; i < om; i++) { i1 = utils.mod(i + 1, om) ocurve.beta[i] = s[i] / (s[i] + t[i1]) } ocurve.alphaCurve = 1 path.curve = ocurve } for (var i = 0; i < this._pathlist.length; i++) { var path = this._pathlist[i] calcSums(path) calcLon(path) bestPolygon(path) adjustVertices(path) if (path.sign === "-") { reverse(path) } smooth(path) if (self._params.optCurve) { optiCurve(path) } } }, /** * Validates some of parameters * @param params * @private */ _validateParameters: function (params) { if (params && params.turnPolicy && SUPPORTED_TURNPOLICY_VALUES.indexOf(params.turnPolicy) === -1) { var goodVals = "'" + SUPPORTED_TURNPOLICY_VALUES.join("', '") + "'" throw new Error("Bad turnPolicy value. Allowed values are: " + goodVals) } if (params && params.threshold != null && params.threshold !== Potrace.THRESHOLD_AUTO) { if (typeof params.threshold !== "number" || !utils.between(params.threshold, 0, 255)) { throw new Error("Bad threshold value. Expected to be an integer in range 0..255") } } if (params && params.optCurve != null && typeof params.optCurve !== "boolean") { throw new Error("'optCurve' must be Boolean") } }, /** * @param {ImageData|Jimp} image */ _processLoadedImage: function (image) { if (image instanceof Jimp) { var bitmap = new Bitmap(image.bitmap.width, image.bitmap.height) var pixels = image.bitmap.data image.scan(0, 0, image.bitmap.width, image.bitmap.height, function (x, y, idx) { // We want background underneath non-opaque regions to be white var opacity = pixels[idx + 3] / 255, r = 255 + (pixels[idx + 0] - 255) * opacity, g = 255 + (pixels[idx + 1] - 255) * opacity, b = 255 + (pixels[idx + 2] - 255) * opacity bitmap.data[idx / 4] = utils.luminance(r, g, b) }) this._luminanceData = bitmap this._imageLoaded = true } else { var bitmap = new Bitmap(image.width, image.height) var pixels = image.data for (let i = 0; i < pixels.length * 4; i += 4) { const idx = i / 4 // Each pixel consists of 4 values (RGBA) let opacity = pixels[idx + 3] / 255, r = 255 + (pixels[idx + 0] - 255) * opacity, g = 255 + (pixels[idx + 1] - 255) * opacity, b = 255 + (pixels[idx + 2] - 255) * opacity bitmap.data[idx / 4] = utils.luminance(r, g, b) } this._luminanceData = bitmap this._imageLoaded = true } }, /** * Reads given image. * * @param {string|Buffer|ImageData|File} target Image source. * @param {Function} callback */ loadImage: function (target, callback) { var self = this var jobId = {} this._imageLoadingIdentifier = jobId this._imageLoaded = false if (isBrowser()) { if (target instanceof ImageData) { this._imageLoadingIdentifier = null this._imageLoaded = true self._processLoadedImage(target) callback.call(self, null) } else { utils.getImageData(target, function (err, img) { var sourceChanged = self._imageLoadingIdentifier !== jobId if (err || sourceChanged) { var error = err ? err : new Error("Another image was loaded instead") return callback.call(self, error) } self._imageLoadingIdentifier = null self._processLoadedImage(img) callback.call(self, null) }) } } else { if (target instanceof Jimp) { this._imageLoadingIdentifier = null this._imageLoaded = true self._processLoadedImage(target) callback.call(self, null) } else { Jimp.read(target, function (err, img) { var sourceChanged = self._imageLoadingIdentifier !== jobId if (err || sourceChanged) { var error = err ? err : new Error("Another image was loaded instead") return callback.call(self, error) } self._imageLoadingIdentifier = null self._processLoadedImage(img) callback.call(self, null) }) } } }, /** * Sets algorithm parameters * @param {Potrace~Options} newParams */ setParameters: function (newParams) { var key, tmpOldVal this._validateParameters(newParams) for (key in this._params) { if (this._params.hasOwnProperty(key) && newParams.hasOwnProperty(key)) { tmpOldVal = this._params[key] this._params[key] = newParams[key] if (tmpOldVal !== this._params[key] && ["color", "background"].indexOf(key) === -1) { this._processed = false } } } }, /** * Generates just <path> tag without rest of the SVG file * * @param {String} [fillColor] - overrides color from parameters * @returns {String} */ getPathTag: function (fillColor, scale) { fillColor = arguments.length === 0 ? this._params.color : fillColor if (fillColor === Potrace.COLOR_AUTO) { fillColor = this._params.blackOnWhite ? "black" : "white" } if (!this._imageLoaded) { throw new Error("Image should be loaded first") } if (!this._processed) { this._bmToPathlist() this._processPath() this._processed = true } var tag = '<path d="' tag += this._pathlist .map(function (path) { return utils.renderCurve(path.curve, scale) }) .join(" ") tag += '" stroke="none" fill="' + fillColor + '" fill-rule="evenodd"/>' return tag }, /** * Returns <symbol> tag. Always has viewBox specified and comes with no fill color, * so it could be changed with <use> tag * * @param id * @returns {string} */ getSymbol: function (id) { return ( "<symbol " + 'viewBox="0 0 ' + this._luminanceData.width + " " + this._luminanceData.height + '" ' + 'id="' + id + '">' + this.getPathTag("") + "</symbol>" ) }, /** * Generates SVG image * @returns {String} */ getSVG: function () { var width = this._params.width || this._luminanceData.width var height = this._params.height || this._luminanceData.height var scale = { x: this._params.width ? this._params.width / this._luminanceData.width : 1, y: this._params.height ? this._params.height / this._luminanceData.height : 1 } return ( '<svg xmlns="http://www.w3.org/2000/svg" ' + 'width="' + width + '" ' + 'height="' + height + '" ' + 'viewBox="0 0 ' + width + " " + height + '" ' + 'version="1.1">\n' + (this._params.background !== Potrace.COLOR_TRANSPARENT ? '\t<rect x="0" y="0" width="100%" height="100%" fill="' + this._params.background + '" />\n' : "") + "\t" + this.getPathTag(this._params.color, scale) + "\n" + "</svg>" ) } } export default Potrace /** * Potrace options * * @typedef {Object} Potrace~Options * @property {*} [turnPolicy] - how to resolve ambiguities in path decomposition (default Potrace.TURNPOLICY_MINORITY) * @property {Number} [turdSize] - suppress speckles of up to this size (default 2) * @property {Number} [alphaMax] - corner threshold parameter (default 1) * @property {Boolean} [optCurve] - curve optimization (default true) * @property {Number} [optTolerance] - curve optimization tolerance (default 0.2) * @property {Number} [threshold] - threshold below which color is considered black (0..255, default Potrace.THRESHOLD_AUTO) * @property {Boolean} [blackOnWhite] - specifies colors by which side from threshold should be traced (default true) * @property {string} [color] - foreground color (default: 'auto' (black or white)) Will be ignored when exporting as <symbol> * @property {string} [background] - background color (default: 'transparent') Will be ignored when exporting as <symbol> */