From 5a59e784e58e14a8c3634969674369839c5ee26e Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Wed, 31 Oct 2018 01:59:56 -0700 Subject: Initial commit --- .gitignore | 1 + build.sbt | 24 ++++++ js/src/main/scala/identicon.scala | 12 +++ jvm/src/main/scala/identicon.scala | 1 + native/src/main/scala/identicon.scala | 1 + project/build.properties | 1 + project/plugins.sbt | 6 ++ project/publish.sbt | 2 + publish.sbt | 26 ++++++ shared/src/main/scala/Identicon.scala | 149 ++++++++++++++++++++++++++++++++++ 10 files changed, 223 insertions(+) create mode 100644 .gitignore create mode 100644 build.sbt create mode 100644 js/src/main/scala/identicon.scala create mode 100644 jvm/src/main/scala/identicon.scala create mode 100644 native/src/main/scala/identicon.scala create mode 100644 project/build.properties create mode 100644 project/plugins.sbt create mode 100644 project/publish.sbt create mode 100644 publish.sbt create mode 100644 shared/src/main/scala/Identicon.scala diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..74d5822 --- /dev/null +++ b/build.sbt @@ -0,0 +1,24 @@ +// shadow sbt-scalajs' crossProject and CrossType from Scala.js 0.6.x +import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType} + +version in ThisBuild := { + import sys.process._ + ("git describe --always --dirty=-SNAPSHOT --match v[0-9].*" !!).tail.trim +} + +lazy val identicon = crossProject(JSPlatform, JVMPlatform, NativePlatform) + .crossType(CrossType.Full) + .in(file(".")) + .settings( + scalacOptions in Compile += "-deprecation", + scalaVersion := crossScalaVersions.value.head + ) + .jvmSettings( + crossScalaVersions := "2.13.0-M5" :: "2.12.7" :: "2.11.12" :: Nil + ) + .jsSettings( + crossScalaVersions := "2.12.6" :: "2.11.12" :: Nil + ) + .nativeSettings( + crossScalaVersions := "2.11.12" :: Nil + ) diff --git a/js/src/main/scala/identicon.scala b/js/src/main/scala/identicon.scala new file mode 100644 index 0000000..b5a618c --- /dev/null +++ b/js/src/main/scala/identicon.scala @@ -0,0 +1,12 @@ +import scala.scalajs.js.annotation._ + +@JSExportTopLevel("identicon") +object identicon extends _root_.identicon.Identicon { + + @JSExport + override def svg(name: String): String = super.svg(name) + + @JSExport + override def url(name: String): String = super.url(name) + +} diff --git a/jvm/src/main/scala/identicon.scala b/jvm/src/main/scala/identicon.scala new file mode 100644 index 0000000..91e936f --- /dev/null +++ b/jvm/src/main/scala/identicon.scala @@ -0,0 +1 @@ +package object identicon extends _root_.identicon.Identicon diff --git a/native/src/main/scala/identicon.scala b/native/src/main/scala/identicon.scala new file mode 100644 index 0000000..fc7c4fc --- /dev/null +++ b/native/src/main/scala/identicon.scala @@ -0,0 +1 @@ +package object identicon extends identicon.Identicon diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..7c58a83 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.2.6 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..1b68035 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,6 @@ +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.0") +addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "0.6.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.23") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.3.7") + +addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.5.1") diff --git a/project/publish.sbt b/project/publish.sbt new file mode 100644 index 0000000..b39e15a --- /dev/null +++ b/project/publish.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3") +addSbtPlugin("io.crashbox" % "sbt-gpg" % "0.2.0") diff --git a/publish.sbt b/publish.sbt new file mode 100644 index 0000000..154209c --- /dev/null +++ b/publish.sbt @@ -0,0 +1,26 @@ +organization in ThisBuild := "io.crashbox" +licenses in ThisBuild := Seq( + ("Apache 2.0", url("https://www.apache.org/licenses/LICENSE-2.0"))) +homepage in ThisBuild := Some( + url("https://github.com/jodersky/scala-identicon")) +publishMavenStyle in ThisBuild := true +publishTo in ThisBuild := Some( + if (isSnapshot.value) + Opts.resolver.sonatypeSnapshots + else + Opts.resolver.sonatypeStaging +) +scmInfo in ThisBuild := Some( + ScmInfo( + url("https://github.com/jodersky/scala-identicon"), + "scm:git@github.com:jodersky/scala-identicon.git" + ) +) +developers in ThisBuild := List( + Developer( + id = "jodersky", + name = "Jakob Odersky", + email = "jakob@odersky.com", + url = url("https://crashbox.io") + ) +) diff --git a/shared/src/main/scala/Identicon.scala b/shared/src/main/scala/Identicon.scala new file mode 100644 index 0000000..0242d92 --- /dev/null +++ b/shared/src/main/scala/Identicon.scala @@ -0,0 +1,149 @@ +package identicon + +import java.lang.Math +import java.util.Base64 + +trait Identicon { + + private def lrot(x: Int, c: Int) = (x << c) | (x >>> (32 - c)) + + // NB: this implementation has a bug and does NOT correspond to the actual MD5 + // specification. It does however produce a hash that appears to be good + // enough for generating identicons. + private def md5(in: IndexedSeq[Byte]): Array[Byte] = { + var s = Array[Int]( + 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, + 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, + 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, + 10, 15, 21 + ) + var K = Array[Int]( + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, + 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, + 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, + 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, + 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, + 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, + 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, + 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, + 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391 + ) + + var a0 = 0x67452301 + var b0 = 0xefcdab89 + var c0 = 0x98badcfe + var d0 = 0x10325476 + + // 1 = extra "0b1000000" byte + val zeroBytes = Math.floorMod(61 - (in.size + 1), 64) + val data = new Array[Byte](in.size + 1 + zeroBytes + 4) // 4 = length + for ((byte, i) <- in.zipWithIndex) { + data(i) = byte + } + data(in.size) = (1 << 7).toByte + data(in.size + 1 + zeroBytes) = ((in.size >>> 24) & 0xff).toByte + data(in.size + 1 + zeroBytes + 1) = ((in.size >>> 16) & 0xff).toByte + data(in.size + 1 + zeroBytes + 2) = ((in.size >>> 8) & 0xff).toByte + data(in.size + 1 + zeroBytes + 3) = ((in.size) & 0xff).toByte + + for (chunk <- 0 until data.size / 64) { + val M = for (i <- 0 until 16) yield { + (data(chunk * 64 + i * 4) << 24) | + (data(chunk * 64 + i * 4 + 1) << 16) | + (data(chunk * 64 + i * 4 + 2) << 8) | + data(chunk * 64 + i * 4 + 3) + } + + var A = a0 + var B = b0 + var C = c0 + var D = d0 + + for (i <- 0 until 64) { + var F = 0 + var g = 0 + if (0 <= i && i <= 15) { + F = (B & C) | ((~B) & D) + g = i + } else if (16 <= i && i <= 31) { + F = (D & B) | ((~D) & C) + g = (5 * i + 1) % 16 + } else if (32 <= i && i <= 47) { + F = B ^ C ^ D + g = (3 * i + 5) % 16 + } else if (48 <= i && i <= 63) { + F = C ^ (B | (~D)) + g = (7 * i) % 16 + } + F = F + A + K(i) + M(g) + A = D + D = C + C = B + B = B + lrot(F, s(i)) + } + a0 = a0 + A + b0 = b0 + B + c0 = c0 + C + d0 = d0 + D + } + + val digest = new Array[Byte](16) + digest(0) = ((a0 >>> 24) & 0xff).toByte + digest(1) = ((a0 >>> 16) & 0xff).toByte + digest(2) = ((a0 >>> 8) & 0xff).toByte + digest(3) = ((a0 & 0xff)).toByte + digest(4) = ((b0 >>> 24) & 0xff).toByte + digest(5) = ((b0 >>> 16) & 0xff).toByte + digest(6) = ((b0 >>> 8) & 0xff).toByte + digest(7) = ((b0 & 0xff)).toByte + digest(8) = ((c0 >>> 24) & 0xff).toByte + digest(9) = ((c0 >>> 16) & 0xff).toByte + digest(10) = ((c0 >>> 8) & 0xff).toByte + digest(11) = ((c0 & 0xff)).toByte + digest(12) = ((d0 >>> 24) & 0xff).toByte + digest(13) = ((d0 >>> 16) & 0xff).toByte + digest(14) = ((d0 >>> 8) & 0xff).toByte + digest(15) = ((d0 & 0xff)).toByte + digest + } + + def svg(name: String): String = { + val hash = md5(name.getBytes("UTF-8")) + val builder = new StringBuilder + val color = { + val r = hash(0) & 0xff + val g = hash(1) & 0xff + val b = hash(2) & 0xff + f"#$r%02x$g%02x$b%02x" + } + val style = s"fill:$color;stroke:$color;stroke-width:0.05" + builder ++= s"""""" + for (x <- 0 until 2) { + for (y <- 0 until 5) { + if (((hash(x) >>> y) & 0x01) != 0) { + builder ++= s"""""" + builder ++= s"""""" + } + } + } + for (y <- 0 until 5) { + if (((hash(2) >>> y) & 0x01) != 0) { + builder ++= s"""""" + } + } + builder ++= "" + builder.result() + } + + def url(name: String): String = { + val b64 = Base64.getEncoder().encodeToString(svg(name).getBytes("UTF-8")) + s"data:image/svg+xml;base64,$b64" + } + + // def url(name: String): String = { + // s"data:image/svg+xml;utf8,${svg(name)}" + // } + +} -- cgit v1.2.3