diff --git a/plugins/avatars/avatars.js b/plugins/avatars/avatars.js
index 6a1ccd56f..cb4048361 100644
--- a/plugins/avatars/avatars.js
+++ b/plugins/avatars/avatars.js
@@ -1,14 +1,16 @@
(rl => {
+
const
queue = [],
avatars = new Map,
+ ncAvatars = new Map,
templateId = 'MailMessageView',
getAvatarUid = msg => {
let from = msg.from[0],
bimi = 'pass' == from.dkimStatus ? 1 : 0;
return `${bimi}/${from.email.toLowerCase()}`;
},
- getAvatar = msg => avatars.get(getAvatarUid(msg)),
+ getAvatar = msg => ncAvatars.get(msg.from[0].email.toLowerCase()) || avatars.get(getAvatarUid(msg)),
runQueue = (() => {
let item = queue.shift();
while (item) {
@@ -17,15 +19,18 @@
item[1](url);
item = queue.shift();
} else {
- // TODO: fetch vCard from Nextcloud contacts
-// let cfg = rl.settings.get('Nextcloud'),
-
let from = item[0].from[0];
rl.pluginRemoteRequest((iError, data) => {
if (!iError && data?.Result.type) {
url = `data:${data.Result.type};base64,${data.Result.data}`;
avatars.set(getAvatarUid(item[0]), url);
item[1](url);
+ } else if (window.identiconSvg) {
+ window.identiconSvg(from.email).then(svg => {
+ url = 'data:image/svg+xml;base64,'+btoa(svg);
+ avatars.set(getAvatarUid(item[0]), url);
+ item[1](url);
+ });
}
runQueue();
}, 'Avatar', {
@@ -37,6 +42,57 @@
}
}).debounce(1000);
+ /**
+ * Loads images from Nextcloud contacts
+ */
+ addEventListener('DOMContentLoaded', () => {
+ if (parent.OC) {
+ const OC = () => parent.OC,
+ nsDAV = 'DAV:',
+ nsNC = 'http://nextcloud.com/ns',
+ nsCard = 'urn:ietf:params:xml:ns:carddav',
+ getElementsByTagName = (parent, namespace, localName) => parent.getElementsByTagNameNS(namespace, localName),
+ getElementValue = (parent, namespace, localName) =>
+ getElementsByTagName(parent, namespace, localName)?.item(0)?.textContent,
+ generateUrl = path => OC().webroot + '/remote.php' + path;
+ if (OC().requestToken) {
+ fetch(generateUrl(`/dav/addressbooks/users/${OC().currentUser}/contacts/`), {
+ mode: 'same-origin',
+ cache: 'no-cache',
+ redirect: 'error',
+ credentials: 'same-origin',
+ method: 'REPORT',
+ headers: {
+ requesttoken: OC().requestToken,
+ 'Content-Type': 'application/xml; charset=utf-8',
+ Depth: 1
+ },
+ body: ''
+ })
+ .then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response })))
+ .then(text => {
+ const
+ xmlParser = new DOMParser(),
+ responseList = getElementsByTagName(
+ xmlParser.parseFromString(text, 'application/xml').documentElement,
+ nsDAV,
+ 'response');
+ for (let i = 0; i < responseList.length; ++i) {
+ const item = responseList.item(i);
+ if (1 == getElementValue(item, nsNC, 'has-photo')) {
+ [...getElementValue(item, nsCard, 'address-data').matchAll(/EMAIL.*?:([^@\r\n]+@[^@\r\n]+)/g)].forEach(match => {
+ ncAvatars.set(
+ match[1].toLowerCase(),
+ getElementValue(item, nsDAV, 'href') + '?photo'
+ );
+ });
+ }
+ }
+ });
+ }
+ }
+ });
+
ko.bindingHandlers.fromPic = {
init: (element, self, dummy, msg) => {
if (msg) {
@@ -46,6 +102,13 @@
fn(url);
} else if (msg.avatar) {
let bimi = 'pass' == msg.from[0].dkimStatus ? 1 : 0;
+ if (window.identiconSvg) {
+ element.onerror = () => {
+ window.identiconSvg(msg.from[0].email).then(svg =>
+ fn('data:image/svg+xml;base64,'+btoa(svg))
+ );
+ }
+ }
fn(`?Avatar/${bimi}/${msg.avatar}`);
} else {
queue.push([msg, fn]);
diff --git a/plugins/avatars/index.php b/plugins/avatars/index.php
index fcf762763..5a6f2d340 100644
--- a/plugins/avatars/index.php
+++ b/plugins/avatars/index.php
@@ -6,8 +6,8 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
NAME = 'Avatars',
AUTHOR = 'SnappyMail',
URL = 'https://snappymail.eu/',
- VERSION = '1.1',
- RELEASE = '2022-11-27',
+ VERSION = '1.2',
+ RELEASE = '2022-11-29',
REQUIRED = '2.22.4',
CATEGORY = 'Contacts',
LICENSE = 'MIT',
@@ -19,9 +19,11 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
$this->addJs('avatars.js');
$this->addJsonHook('Avatar', 'DoAvatar');
$this->addPartHook('Avatar', 'ServiceAvatar');
+// $this->addHook('filter.app-data', 'FilterAppData');
// https://github.com/the-djmaze/snappymail/issues/714
- $this->Config()->Get('plugin', 'delay', true)
- || $this->addHook('filter.json-response', 'FilterJsonResponse');
+ $this->Config()->Get('plugin', 'delay', true) || $this->addHook('filter.json-response', 'FilterJsonResponse');
+
+ $this->Config()->Get('plugin', 'identicon', false) && $this->addJs('jdenticon.js');
}
public function FilterJsonResponse(string $sAction, array &$aResponseItem)
@@ -73,24 +75,50 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
if ($sEmail && ($aResult = $this->getAvatar($sEmail, !empty($sBimi)))) {
\header('Content-Type: '.$aResult[0]);
echo $aResult[1];
- return true;
+ } else {
+ \MailSo\Base\Http::StatusHeader(404);
+ }
+ exit;
+ }
+
+ public function FilterAppData($bAdmin, &$aResult) : void
+ {
+ if (!$bAdmin && \is_array($aResult)) {
+ $aResult['Avatars'] = [
+ 'bimi' => $this->Config()->Get('plugin', 'bimi', false),
+ 'gravatar' => $this->Config()->Get('plugin', 'gravatar', false),
+ 'nextcloud' => $this->Config()->Get('plugin', 'nextcloud', false)
+ ];
}
- return false;
}
protected function configMapping() : array
{
- return array(
+ $aResult = array(
\RainLoop\Plugins\Property::NewInstance('delay')->SetLabel('Delay loading')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(true),
- \RainLoop\Plugins\Property::NewInstance('bimi')->SetLabel('Use BIMI (https://bimigroup.org/)')
- ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
- ->SetDefaultValue(false),
- \RainLoop\Plugins\Property::NewInstance('gravatar')->SetLabel('Use Gravatar')
+ \RainLoop\Plugins\Property::NewInstance('bimi')->SetLabel('Lookup BIMI')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(false)
+ ->SetDescription('https://bimigroup.org/'),
+ \RainLoop\Plugins\Property::NewInstance('gravatar')->SetLabel('Lookup Gravatar')
+ ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
+ ->SetDefaultValue(false)
+ ->SetDescription('https://wikipedia.org/wiki/Gravatar')
);
+/*
+ if (\class_exists('OC') && isset(\OC::$server)) {
+ $aResult[] = \RainLoop\Plugins\Property::NewInstance('nextcloud')->SetLabel('Lookup Nextcloud Contacts')
+ ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
+ ->SetDefaultValue(false);
+ }
+*/
+ $aResult[] = \RainLoop\Plugins\Property::NewInstance('identicon')->SetLabel('Else create Identicon')
+ ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
+ ->SetDefaultValue(false)
+ ->SetDescription('https://wikipedia.org/wiki/Identicon');
+ return $aResult;
}
private function getAvatar(string $sEmail, bool $bBimi) : ?array
@@ -182,9 +210,11 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
$aServices = [
"services/{$sDomain}",
'services/' . \preg_replace('/^.+\\.([^.]+\\.[^.]+)$/D', '$1', $sDomain),
- 'services/' . \preg_replace('/^(.+\\.)?(paypal\\.[a-z][a-z])$/D', 'paypal.com', $sDomain),
- 'empty-contact' // DATA_IMAGE_USER_DOT_PIC
+ 'services/' . \preg_replace('/^(.+\\.)?(paypal\\.[a-z][a-z])$/D', 'paypal.com', $sDomain)
];
+ if (!$this->Config()->Get('plugin', 'identicon', false)) {
+ $aServices[] = 'empty-contact'; // DATA_IMAGE_USER_DOT_PIC
+ }
foreach ($aServices as $service) {
$file = __DIR__ . "/images/{$service}.png";
if (\file_exists($file)) {
diff --git a/plugins/avatars/jdenticon.js b/plugins/avatars/jdenticon.js
new file mode 100644
index 000000000..171f85414
--- /dev/null
+++ b/plugins/avatars/jdenticon.js
@@ -0,0 +1,793 @@
+/**
+ * Jdenticon 3.2.0
+ * http://jdenticon.com
+ *
+ * Built: 2022-08-07T11:23:11.640Z
+ *
+ * MIT License
+ *
+ * Copyright (c) 2014-2021 Daniel Mester Pirttijärvi
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+(()=>{
+/**
+ * Parses a substring of the hash as a number.
+ * @param {number} startPosition
+ * @param {number=} octets
+ */
+
+class HSLColor
+{
+ constructor(h, s, l)
+ {
+ this.h = h;
+ this.s = s;
+ this.l = l;
+// this.a = a;
+ }
+
+ toString()
+ {
+ const h = this.h, s = this.s, l = this.l/*, a = this.a*/,
+ hex = v => {
+ v = Math.max(0, Math.min(255, v * 255)).toString(16);
+ return v.slice(0,2).padStart(2, "0");
+ };
+ let r=l, g=l, b=l;
+ if (0 != s) {
+ let q=l<0.5?l*(s+1):l+s-l*s,
+ p=l*2-q,
+ hue2rgb = h => {
+ h += h<0 ? 1 : (h>1 ? -1 : 0);
+ return (h*6<1) ? (p+(q-p)*h*6) : ((h*2<1) ? q : ((h*3<2) ? p+(q-p)*(2/3-h)*6 : p));
+ };
+ r = hue2rgb(h+1/3);
+ g = hue2rgb(h);
+ b = hue2rgb(h-1/3);
+ }
+ return '#' + hex(r) + hex(g) + hex(b) // isNaN(a)?100:a*100);
+ }
+}
+
+const
+ parseHex = (hash, startPosition, octets) => parseInt(hash.substr(startPosition, octets), 16),
+
+ /**
+ * Converts an HSL color to a hexadecimal RGB color. This function will correct the lightness for the "dark" hues
+ * @param {number} hue Hue in range [0, 1]
+ * @param {number} saturation Saturation in range [0, 1]
+ * @param {number} lightness Lightness in range [0, 1]
+ * @returns {string}
+ */
+ correctedHsl = (hue, saturation, lightness) => {
+ // The corrector specifies the perceived middle lightness for each hue
+ var correctors = [ 0.55, 0.5, 0.5, 0.46, 0.6, 0.55, 0.55 ],
+ corrector = correctors[(hue * 6 + 0.5) | 0];
+
+ // Adjust the input lightness relative to the corrector
+ lightness = lightness < 0.5 ? lightness * corrector * 2 : corrector + (lightness - 0.5) * (1 - corrector) * 2;
+
+ return new HSLColor(hue, saturation, lightness).toString();
+ },
+
+ /**
+ * Gets the normalized current Jdenticon color configuration. Missing fields have default values.
+ * @param {Object|number|undefined} paddingOrLocalConfig - Configuration passed to the called API method. A
+ * local configuration overrides the global configuration in it entirety. This parameter can for backward
+ * compatibility also contain a padding value. A padding value only overrides the global padding, not the
+ * entire global configuration.
+ * @param {number} defaultPadding - Padding used if no padding is specified in neither the configuration nor
+ * explicitly to the API method.
+ * @returns {ParsedConfiguration}
+ */
+ getConfiguration = (paddingOrLocalConfig, defaultPadding) => {
+ const configObject =
+ typeof paddingOrLocalConfig == "object" && paddingOrLocalConfig ||
+ { },
+
+ lightnessConfig = configObject["lightness"] || { },
+
+ // In versions < 2.1.0 there was no grayscale saturation -
+ // saturation was the color saturation.
+ saturation = configObject["saturation"] || { },
+ colorSaturation = "color" in saturation ? saturation["color"] : saturation,
+ grayscaleSaturation = saturation["grayscale"],
+
+ padding = configObject["padding"],
+
+ /**
+ * Creates a lightness range.
+ */
+ lightness = (configName, defaultRange) => {
+ var range = lightnessConfig[configName];
+
+ // Check if the lightness range is an array-like object. This way we ensure the
+ // array contain two values at the same time.
+ if (!(range && range.length > 1)) {
+ range = defaultRange;
+ }
+
+ /**
+ * Gets a lightness relative the specified value in the specified lightness range.
+ */
+ return function (value) {
+ value = range[0] + value * (range[1] - range[0]);
+ return value < 0 ? 0 : value > 1 ? 1 : value;
+ };
+ };
+
+ return {
+ /**
+ * Gets a hue allowed by the configured hue restriction,
+ * provided the originally computed hue.
+ */
+ X/*hue*/: originalHue => {
+ var hueConfig = configObject["hues"];
+ var hue;
+
+ // Check if 'hues' is an array-like object. This way we also ensure that
+ // the array is not empty, which would mean no hue restriction.
+ if (hueConfig && hueConfig.length > 0) {
+ // originalHue is in the range [0, 1]
+ // Multiply with 0.999 to change the range to [0, 1) and then truncate the index.
+ hue = hueConfig[0 | (0.999 * originalHue * hueConfig.length)];
+ }
+
+ return typeof hue == "number" ?
+
+ // A hue was specified. We need to convert the hue from
+ // degrees on any turn - e.g. 746° is a perfectly valid hue -
+ // to turns in the range [0, 1).
+ ((((hue / 360) % 1) + 1) % 1) :
+
+ // No hue configured => use original hue
+ originalHue;
+ },
+
+ p/*colorSaturation*/: typeof colorSaturation == "number" ? colorSaturation : 0.5,
+ H/*grayscaleSaturation*/: typeof grayscaleSaturation == "number" ? grayscaleSaturation : 0,
+ q/*colorLightness*/: lightness("color", [0.4, 0.8]),
+ I/*grayscaleLightness*/: lightness("grayscale", [0.3, 0.9]),
+ Y/*iconPadding*/:
+ typeof paddingOrLocalConfig == "number" ? paddingOrLocalConfig :
+ typeof padding == "number" ? padding :
+ defaultPadding
+ }
+ };
+
+/**
+ * Represents a point.
+ */
+function Point(x, y) {
+ this.x = x;
+ this.y = y;
+}
+
+/**
+ * Translates and rotates a point before being passed on to the canvas context. This was previously done by the canvas context itself,
+ * but this caused a rendering issue in Chrome on sizes > 256 where the rotation transformation of inverted paths was not done properly.
+ */
+class Transform
+{
+ constructor(x, y, size, rotation) {
+ this.u/*_x*/ = x;
+ this.v/*_y*/ = y;
+ this.K/*_size*/ = size;
+ this.Z/*_rotation*/ = rotation;
+ }
+
+ /**
+ * Transforms the specified point based on the translation and rotation specification for this Transform.
+ * @param {number} x x-coordinate
+ * @param {number} y y-coordinate
+ * @param {number=} w The width of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle.
+ * @param {number=} h The height of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle.
+ */
+ L/*transformIconPoint*/(x, y, w, h) {
+ var right = this.u/*_x*/ + this.K/*_size*/,
+ bottom = this.v/*_y*/ + this.K/*_size*/,
+ rotation = this.Z/*_rotation*/;
+ return rotation === 1 ? new Point(right - y - (h || 0), this.v/*_y*/ + x) :
+ rotation === 2 ? new Point(right - x - (w || 0), bottom - y - (h || 0)) :
+ rotation === 3 ? new Point(this.u/*_x*/ + y, bottom - x - (w || 0)) :
+ new Point(this.u/*_x*/ + x, this.v/*_y*/ + y);
+ }
+}
+
+var NO_TRANSFORM = new Transform(0, 0, 0, 0);
+
+/**
+ * Provides helper functions for rendering common basic shapes.
+ */
+class Graphics
+{
+ constructor(renderer) {
+ /**
+ * @type {Renderer}
+ * @private
+ */
+ this.M/*_renderer*/ = renderer;
+
+ /**
+ * @type {Transform}
+ */
+ this.A/*currentTransform*/ = NO_TRANSFORM;
+ }
+
+ /**
+ * Adds a polygon to the underlying renderer.
+ * @param {Array} points The points of the polygon clockwise on the format [ x0, y0, x1, y1, ..., xn, yn ]
+ * @param {boolean=} invert Specifies if the polygon will be inverted.
+ */
+ g/*addPolygon*/(points, invert) {
+ var this$1 = this;
+
+ var di = invert ? -2 : 2,
+ transformedPoints = [];
+
+ for (var i = invert ? points.length - 2 : 0; i < points.length && i >= 0; i += di) {
+ transformedPoints.push(this$1.A/*currentTransform*/.L/*transformIconPoint*/(points[i], points[i + 1]));
+ }
+
+ this.M/*_renderer*/.g/*addPolygon*/(transformedPoints);
+ }
+
+ /**
+ * Adds a polygon to the underlying renderer.
+ * Source: http://stackoverflow.com/a/2173084
+ * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the entire ellipse.
+ * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the entire ellipse.
+ * @param {number} size The size of the ellipse.
+ * @param {boolean=} invert Specifies if the ellipse will be inverted.
+ */
+ h/*addCircle*/(x, y, size, invert) {
+ var p = this.A/*currentTransform*/.L/*transformIconPoint*/(x, y, size, size);
+ this.M/*_renderer*/.h/*addCircle*/(p, size, invert);
+ }
+
+ /**
+ * Adds a rectangle to the underlying renderer.
+ * @param {number} x The x-coordinate of the upper left corner of the rectangle.
+ * @param {number} y The y-coordinate of the upper left corner of the rectangle.
+ * @param {number} w The width of the rectangle.
+ * @param {number} h The height of the rectangle.
+ * @param {boolean=} invert Specifies if the rectangle will be inverted.
+ */
+ i/*addRectangle*/(x, y, w, h, invert) {
+ this.g/*addPolygon*/([
+ x, y,
+ x + w, y,
+ x + w, y + h,
+ x, y + h
+ ], invert);
+ }
+
+ /**
+ * Adds a right triangle to the underlying renderer.
+ * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the triangle.
+ * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the triangle.
+ * @param {number} w The width of the triangle.
+ * @param {number} h The height of the triangle.
+ * @param {number} r The rotation of the triangle (clockwise). 0 = right corner of the triangle in the lower left corner of the bounding rectangle.
+ * @param {boolean=} invert Specifies if the triangle will be inverted.
+ */
+ j/*addTriangle*/(x, y, w, h, r, invert) {
+ var points = [
+ x + w, y,
+ x + w, y + h,
+ x, y + h,
+ x, y
+ ];
+ points.splice(((r || 0) % 4) * 2, 2);
+ this.g/*addPolygon*/(points, invert);
+ }
+
+ /**
+ * Adds a rhombus to the underlying renderer.
+ * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the rhombus.
+ * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the rhombus.
+ * @param {number} w The width of the rhombus.
+ * @param {number} h The height of the rhombus.
+ * @param {boolean=} invert Specifies if the rhombus will be inverted.
+ */
+ N/*addRhombus*/(x, y, w, h, invert) {
+ this.g/*addPolygon*/([
+ x + w / 2, y,
+ x + w, y + h / 2,
+ x + w / 2, y + h,
+ x, y + h / 2
+ ], invert);
+ }
+}
+
+/**
+ * @param {number} index
+ * @param {Graphics} g
+ * @param {number} cell
+ * @param {number} positionIndex
+ */
+function centerShape(index, g, cell, positionIndex) {
+ index = index % 14;
+
+ var k, m, w, h, inner, outer;
+
+ !index ? (
+ k = cell * 0.42,
+ g.g/*addPolygon*/([
+ 0, 0,
+ cell, 0,
+ cell, cell - k * 2,
+ cell - k, cell,
+ 0, cell
+ ])) :
+
+ index == 1 ? (
+ w = 0 | (cell * 0.5),
+ h = 0 | (cell * 0.8),
+
+ g.j/*addTriangle*/(cell - w, 0, w, h, 2)) :
+
+ index == 2 ? (
+ w = 0 | (cell / 3),
+ g.i/*addRectangle*/(w, w, cell - w, cell - w)) :
+
+ index == 3 ? (
+ inner = cell * 0.1,
+ // Use fixed outer border widths in small icons to ensure the border is drawn
+ outer =
+ cell < 6 ? 1 :
+ cell < 8 ? 2 :
+ (0 | (cell * 0.25)),
+
+ inner =
+ inner > 1 ? (0 | inner) : // large icon => truncate decimals
+ inner > 0.5 ? 1 : // medium size icon => fixed width
+ inner, // small icon => anti-aliased border
+
+ g.i/*addRectangle*/(outer, outer, cell - inner - outer, cell - inner - outer)) :
+
+ index == 4 ? (
+ m = 0 | (cell * 0.15),
+ w = 0 | (cell * 0.5),
+ g.h/*addCircle*/(cell - w - m, cell - w - m, w)) :
+
+ index == 5 ? (
+ inner = cell * 0.1,
+ outer = inner * 4,
+
+ // Align edge to nearest pixel in large icons
+ outer > 3 && (outer = 0 | outer),
+
+ g.i/*addRectangle*/(0, 0, cell, cell),
+ g.g/*addPolygon*/([
+ outer, outer,
+ cell - inner, outer,
+ outer + (cell - outer - inner) / 2, cell - inner
+ ], true)) :
+
+ index == 6 ?
+ g.g/*addPolygon*/([
+ 0, 0,
+ cell, 0,
+ cell, cell * 0.7,
+ cell * 0.4, cell * 0.4,
+ cell * 0.7, cell,
+ 0, cell
+ ]) :
+
+ index == 7 ?
+ g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 3) :
+
+ index == 8 ? (
+ g.i/*addRectangle*/(0, 0, cell, cell / 2),
+ g.i/*addRectangle*/(0, cell / 2, cell / 2, cell / 2),
+ g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 1)) :
+
+ index == 9 ? (
+ inner = cell * 0.14,
+ // Use fixed outer border widths in small icons to ensure the border is drawn
+ outer =
+ cell < 4 ? 1 :
+ cell < 6 ? 2 :
+ (0 | (cell * 0.35)),
+
+ inner =
+ cell < 8 ? inner : // small icon => anti-aliased border
+ (0 | inner), // large icon => truncate decimals
+
+ g.i/*addRectangle*/(0, 0, cell, cell),
+ g.i/*addRectangle*/(outer, outer, cell - outer - inner, cell - outer - inner, true)) :
+
+ index == 10 ? (
+ inner = cell * 0.12,
+ outer = inner * 3,
+
+ g.i/*addRectangle*/(0, 0, cell, cell),
+ g.h/*addCircle*/(outer, outer, cell - inner - outer, true)) :
+
+ index == 11 ?
+ g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 3) :
+
+ index == 12 ? (
+ m = cell * 0.25,
+ g.i/*addRectangle*/(0, 0, cell, cell),
+ g.N/*addRhombus*/(m, m, cell - m, cell - m, true)) :
+
+ // 13
+ (
+ !positionIndex && (
+ m = cell * 0.4, w = cell * 1.2,
+ g.h/*addCircle*/(m, m, w)
+ )
+ );
+}
+
+/**
+ * @param {number} index
+ * @param {Graphics} g
+ * @param {number} cell
+ */
+function outerShape(index, g, cell) {
+ index = index % 4;
+
+ var m;
+
+ !index ?
+ g.j/*addTriangle*/(0, 0, cell, cell, 0) :
+
+ index == 1 ?
+ g.j/*addTriangle*/(0, cell / 2, cell, cell / 2, 0) :
+
+ index == 2 ?
+ g.N/*addRhombus*/(0, 0, cell, cell) :
+
+ // 3
+ (
+ m = cell / 6,
+ g.h/*addCircle*/(m, m, cell - 2 * m)
+ );
+}
+
+/**
+ * Gets a set of identicon color candidates for a specified hue and config.
+ * @param {number} hue
+ * @param {ParsedConfiguration} config
+ */
+function colorTheme(hue, config) {
+ hue = config.X/*hue*/(hue);
+ return [
+ // Dark gray
+ correctedHsl(hue, config.H/*grayscaleSaturation*/, config.I/*grayscaleLightness*/(0)),
+ // Mid color
+ correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(0.5)),
+ // Light gray
+ correctedHsl(hue, config.H/*grayscaleSaturation*/, config.I/*grayscaleLightness*/(1)),
+ // Light color
+ correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(1)),
+ // Dark color
+ correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(0))
+ ];
+}
+
+/**
+ * Draws an identicon to a specified renderer.
+ * @param {Renderer} renderer
+ * @param {string} hash
+ * @param {Object|number=} config
+ */
+function iconGenerator(renderer, hash, config) {
+ var parsedConfig = getConfiguration(config, 0.08);
+
+ // Calculate padding and round to nearest integer
+ var size = renderer.k/*iconSize*/;
+ var padding = (0.5 + size * parsedConfig.Y/*iconPadding*/) | 0;
+ size -= padding * 2;
+
+ var graphics = new Graphics(renderer);
+
+ // Calculate cell size and ensure it is an integer
+ var cell = 0 | (size / 4);
+
+ // Since the cell size is integer based, the actual icon will be slightly smaller than specified => center icon
+ var x = 0 | (padding + size / 2 - cell * 2);
+ var y = 0 | (padding + size / 2 - cell * 2);
+
+ function renderShape(colorIndex, shapes, index, rotationIndex, positions) {
+ var shapeIndex = parseHex(hash, index, 1);
+ var r = rotationIndex ? parseHex(hash, rotationIndex, 1) : 0;
+
+ renderer.O/*beginShape*/(availableColors[selectedColorIndexes[colorIndex]]);
+
+ for (var i = 0; i < positions.length; i++) {
+ graphics.A/*currentTransform*/
+ = new Transform(x + positions[i][0] * cell, y + positions[i][1] * cell, cell, r++ % 4);
+ shapes(shapeIndex, graphics, cell, i);
+ }
+
+ renderer.P/*endShape*/();
+ }
+
+ // AVAILABLE COLORS
+ var hue = parseHex(hash, -7) / 0xfffffff,
+
+ // Available colors for this icon
+ availableColors = colorTheme(hue, parsedConfig),
+
+ // The index of the selected colors
+ selectedColorIndexes = [];
+
+ var index;
+
+ function isDuplicate(values) {
+ if (values.indexOf(index) >= 0) {
+ for (var i = 0; i < values.length; i++) {
+ if (selectedColorIndexes.indexOf(values[i]) >= 0) {
+ return true;
+ }
+ }
+ }
+ }
+
+ for (var i = 0; i < 3; i++) {
+ index = parseHex(hash, 8 + i, 1) % availableColors.length;
+ if (isDuplicate([0, 4]) || // Disallow dark gray and dark color combo
+ isDuplicate([2, 3])) { // Disallow light gray and light color combo
+ index = 1;
+ }
+ selectedColorIndexes.push(index);
+ }
+
+ // ACTUAL RENDERING
+ // Sides
+ renderShape(0, outerShape, 2, 3, [[1, 0], [2, 0], [2, 3], [1, 3], [0, 1], [3, 1], [3, 2], [0, 2]]);
+ // Corners
+ renderShape(1, outerShape, 4, 5, [[0, 0], [3, 0], [3, 3], [0, 3]]);
+ // Center
+ renderShape(2, centerShape, 1, null, [[1, 1], [2, 1], [2, 2], [1, 2]]);
+
+ renderer.finish();
+}
+
+/**
+ * Computes a SHA1 hash for any value and returns it as a hexadecimal string.
+ *
+ * This function is optimized for minimal code size and rather short messages.
+ *
+ * @param {string} message
+ */
+async function computeHash(message) {
+ const hashArray = Array.from(new Uint8Array(
+// await crypto.subtle.digest('SHA-256', (new TextEncoder()).encode(message))
+ await crypto.subtle.digest('SHA-1', (new TextEncoder()).encode(message))
+ ));
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string
+}
+
+/**
+ * Prepares a measure to be used as a measure in an SVG path, by
+ * rounding the measure to a single decimal. This reduces the file
+ * size of the generated SVG with more than 50% in some cases.
+ */
+function svgValue(value) {
+ return ((value * 10 + 0.5) | 0) / 10;
+}
+
+/**
+ * Represents an SVG path element.
+ */
+class SvgPath
+{
+ constructor() {
+ /**
+ * This property holds the data string (path.d) of the SVG path.
+ * @type {string}
+ */
+ this.B/*dataString*/ = "";
+ }
+
+ /**
+ * Adds a polygon with the current fill color to the SVG path.
+ * @param points An array of Point objects.
+ */
+ g/*addPolygon*/(points) {
+ var dataString = "";
+ for (var i = 0; i < points.length; i++) {
+ dataString += (i ? "L" : "M") + svgValue(points[i].x) + " " + svgValue(points[i].y);
+ }
+ this.B/*dataString*/ += dataString + "Z";
+ }
+
+ /**
+ * Adds a circle with the current fill color to the SVG path.
+ * @param {Point} point The upper left corner of the circle bounding box.
+ * @param {number} diameter The diameter of the circle.
+ * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path).
+ */
+ h/*addCircle*/(point, diameter, counterClockwise) {
+ var sweepFlag = counterClockwise ? 0 : 1,
+ svgRadius = svgValue(diameter / 2),
+ svgDiameter = svgValue(diameter),
+ svgArc = "a" + svgRadius + "," + svgRadius + " 0 1," + sweepFlag + " ";
+
+ this.B/*dataString*/ +=
+ "M" + svgValue(point.x) + " " + svgValue(point.y + diameter / 2) +
+ svgArc + svgDiameter + ",0" +
+ svgArc + (-svgDiameter) + ",0";
+ }
+}
+
+
+/**
+ * Renderer producing SVG output.
+ * @implements {Renderer}
+ */
+class SvgRenderer
+{
+ constructor(target) {
+ /**
+ * @type {SvgPath}
+ * @private
+ */
+ this.C/*_path*/;
+
+ /**
+ * @type {Object.}
+ * @private
+ */
+ this.D/*_pathsByColor*/ = { };
+
+ /**
+ * @type {SvgElement|SvgWriter}
+ * @private
+ */
+ this.R/*_target*/ = target;
+
+ /**
+ * @type {number}
+ */
+ this.k/*iconSize*/ = target.k/*iconSize*/;
+ }
+
+ /**
+ * Fills the background with the specified color.
+ * @param {string} fillColor Fill color on the format #rrggbb[aa].
+ */
+ m/*setBackground*/(fillColor) {
+ var match = /^(#......)(..)?/.exec(fillColor),
+ opacity = match[2] ? parseHex(match[2], 0) / 255 : 1;
+ this.R/*_target*/.m/*setBackground*/(match[1], opacity);
+ }
+
+ /**
+ * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape.
+ * @param {string} color Fill color on format #xxxxxx.
+ */
+ O/*beginShape*/(color) {
+ this.C/*_path*/ = this.D/*_pathsByColor*/[color] || (this.D/*_pathsByColor*/[color] = new SvgPath());
+ }
+
+ /**
+ * Marks the end of the currently drawn shape.
+ */
+ P/*endShape*/() { }
+
+ /**
+ * Adds a polygon with the current fill color to the SVG.
+ * @param points An array of Point objects.
+ */
+ g/*addPolygon*/(points) {
+ this.C/*_path*/.g/*addPolygon*/(points);
+ }
+
+ /**
+ * Adds a circle with the current fill color to the SVG.
+ * @param {Point} point The upper left corner of the circle bounding box.
+ * @param {number} diameter The diameter of the circle.
+ * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path).
+ */
+ h/*addCircle*/(point, diameter, counterClockwise) {
+ this.C/*_path*/.h/*addCircle*/(point, diameter, counterClockwise);
+ }
+
+ /**
+ * Called when the icon has been completely drawn.
+ */
+ finish() {
+ var pathsByColor = this.D/*_pathsByColor*/;
+ for (var color in pathsByColor) {
+ // hasOwnProperty cannot be shadowed in pathsByColor
+ // eslint-disable-next-line no-prototype-builtins
+ if (pathsByColor.hasOwnProperty(color)) {
+ this.R/*_target*/.S/*appendPath*/(color, pathsByColor[color].B/*dataString*/);
+ }
+ }
+ }
+}
+
+/**
+ * Renderer producing SVG output.
+ */
+class SvgWriter
+{
+ constructor(iconSize) {
+ /**
+ * @type {number}
+ */
+ this.k/*iconSize*/ = iconSize;
+
+ /**
+ * @type {string}
+ * @private
+ */
+ this.F =
+ '";
+ }
+}
+
+/**
+ * Draws an identicon as an SVG string.
+ * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon.
+ * @param {number} size - Icon size in pixels.
+ * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any
+ * global configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be
+ * specified in place of a configuration object.
+ * @returns {string} SVG string
+ */
+window.identiconSvg = async (hashOrValue, config) => {
+ var writer = new SvgWriter(50);
+ iconGenerator(new SvgRenderer(writer),
+ (/^[0-9a-f]{11,}$/i.test(hashOrValue) && hashOrValue)
+ || await computeHash(hashOrValue == null ? "" : "" + hashOrValue),
+ config);
+ return writer.toString();
+};
+
+})();