initial commit

This commit is contained in:
Craig Raw 2023-01-05 11:53:23 +02:00
commit 0ab8d422c5
30 changed files with 2263 additions and 0 deletions

9
.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
.idea
.gradle
*iml
build
*.properties
out
*.log
build-*.sh
.DS_Store

177
LICENSE Normal file
View file

@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

47
README.md Normal file
View file

@ -0,0 +1,47 @@
# Toucan
### Java implementation of LifeHash
Toucan is a Java implementation of the [LifeHash](https://github.com/BlockchainCommons/bc-lifehash) hash visualization algorithm.
It is a direct port of the reference C++/C implementation by Wolf McNally.
Toucan requires a minimum of Java 17.
## Setup
Toucan is hosted in Maven Central and can be added as a dependency with the following:
```
implementation('com.sparrowwallet:toucan:0.9.0')
```
## Usage
A LifeHash is represented by the `LifeHash.Image` class, which can be created as follows:
```java
import com.sparrowwallet.toucan.*;
import java.awt.image.BufferedImage;
public class Main {
public static void main(String[] args) {
LifeHash.Image lifeHashImage = LifeHash.makeFromUTF8("Hello World", LifeHashVersion.VERSION2, 1, false);
BufferedImage awtImage = LifeHash.getBufferedImage(lifeHashImage);
}
}
```
## Testing
Toucan has a small testsuite ported from the C++ implementation. The tests can be run with:
```
./gradlew test
```
## License
Toucan is licensed under the Apache 2 software license.
## Dependencies
No dependencies.

69
build.gradle Normal file
View file

@ -0,0 +1,69 @@
plugins {
id 'java-library'
id 'signing'
id 'maven-publish'
id('io.github.gradle-nexus.publish-plugin') version '1.1.0'
}
group 'com.sparrowwallet'
version '0.9.0'
sourceCompatibility = 17
repositories {
mavenCentral()
}
dependencies {
testImplementation group: 'junit', name: 'junit', version: '4.13.1'
}
java {
withJavadocJar()
withSourcesJar()
}
nexusPublishing {
repositories {
sonatype()
}
}
publishing {
publications {
mavenJava(MavenPublication) {
from(components.java)
pom {
name = 'toucan'
description = 'Java implementation of Lifehash'
url = 'https://github.com/sparrowwallet/toucan'
scm {
url = 'https://github.com/sparrowwallet/toucan'
connection = 'scm:git://github.com/sparrowwallet/toucan.git'
developerConnection = 'scm:git://github.com:sparrowwallet/toucan.git'
}
licenses {
license {
name = 'The Apache Software License, Version 2.0'
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
developers {
developer {
id = 'craigraw'
name = 'Craig Raw'
email = 'mail@sparrowwallet.com'
}
}
}
}
}
}
signing {
sign publishing.publications.mavenJava
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

234
gradlew vendored Executable file
View file

@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View file

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

2
settings.gradle Normal file
View file

@ -0,0 +1,2 @@
rootProject.name = 'toucan'

View file

@ -0,0 +1,244 @@
package com.sparrowwallet.toucan;
import com.sparrowwallet.toucan.impl.*;
import java.awt.image.BufferedImage;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static com.sparrowwallet.toucan.impl.Gradients.selectGradient;
import static com.sparrowwallet.toucan.impl.Pattern.selectPattern;
import static com.sparrowwallet.toucan.impl.Utils.clamped;
import static com.sparrowwallet.toucan.impl.Utils.lerpFrom;
public class LifeHash {
/**
* Creates a LifeHash.Image object from the provided String
*
* @param s the String to extract UTF-8 bytes from as input
* @param version the version of LifeHash to use
* @param moduleSize the size of the LifeHash
* @param hasAlpha whether transparency information should be included
* @return an object representing the LifeHash
*/
public static Image makeFromUTF8(String s, LifeHashVersion version, int moduleSize, boolean hasAlpha) {
return makeFromData(s.getBytes(StandardCharsets.UTF_8), version, moduleSize, hasAlpha);
}
/**
* Creates a LifeHash.Image object from the provided bytes
*
* @param data the bytes to use as input
* @param version the version of LifeHash to use
* @param moduleSize the size of the LifeHash
* @param hasAlpha whether transparency information should be included
* @return an object representing the LifeHash
*/
public static Image makeFromData(byte[] data, LifeHashVersion version, int moduleSize, boolean hasAlpha) {
byte[] digest = Sha256Hash.hash(data);
return makeFromDigest(digest, version, moduleSize, hasAlpha);
}
/**
* Creates a LifeHash.Image object from the provided SHA256 hash
*
* @param digest a 32 byte array representing a SHA256 hash to use as input
* @param version the version of LifeHash to use
* @param moduleSize the size of the LifeHash
* @param hasAlpha whether transparency information should be included
* @return an object representing the LifeHash
*/
public static Image makeFromDigest(byte[] digest, LifeHashVersion version, int moduleSize, boolean hasAlpha) {
if(digest.length != 32) {
throw new IllegalArgumentException("Digest must be 32 bytes.");
}
int length;
int maxGenerations;
switch(version) {
case VERSION1, VERSION2 -> {
length = 16;
maxGenerations = 150;
}
case DETAILED, FIDUCIAL, GRAYSCALE_FIDUCIAL -> {
length = 32;
maxGenerations = 300;
}
default -> throw new IllegalArgumentException("Invalid version.");
}
Size size = new Size(length, length);
// These get reused from generation to generation by swapping them.
CellGrid currentCellGrid = new CellGrid(size);
CellGrid nextCellGrid = new CellGrid(size);
ChangeGrid currentChangeGrid = new ChangeGrid(size);
ChangeGrid nextChangeGrid = new ChangeGrid(size);
Set<Sha256Hash> historySet = new HashSet<>();
List<byte[]> history = new ArrayList<>();
switch(version) {
case VERSION1 -> nextCellGrid.setData(digest);
case VERSION2 ->
// Ensure that .version2 in no way resembles .version1
nextCellGrid.setData(Sha256Hash.hash(digest));
case DETAILED, FIDUCIAL, GRAYSCALE_FIDUCIAL -> {
byte[] digest1 = digest;
// Ensure that grayscale fiducials in no way resemble the regular color fiducials
if(version == LifeHashVersion.GRAYSCALE_FIDUCIAL) {
digest1 = Sha256Hash.hash(digest1);
}
byte[] digest2 = Sha256Hash.hash(digest1);
byte[] digest3 = Sha256Hash.hash(digest2);
byte[] digest4 = Sha256Hash.hash(digest3);
byte[] digestFinal = new byte[digest1.length*4];
System.arraycopy(digest1, 0, digestFinal, 0, digest1.length);
System.arraycopy(digest2, 0, digestFinal, digest1.length, digest2.length);
System.arraycopy(digest3, 0, digestFinal, digest1.length * 2, digest3.length);
System.arraycopy(digest4, 0, digestFinal, digest1.length * 3, digest4.length);
nextCellGrid.setData(digestFinal);
}
}
nextChangeGrid.setAll(true);
while (history.size() < maxGenerations) {
CellGrid tempCellGrid = currentCellGrid;
currentCellGrid = nextCellGrid;
nextCellGrid = tempCellGrid;
ChangeGrid tempChangeGrid = currentChangeGrid;
currentChangeGrid = nextChangeGrid;
nextChangeGrid = tempChangeGrid;
byte[] data = currentCellGrid.getData();
Sha256Hash hash = Sha256Hash.of(data);
if (historySet.contains(hash)) {
break;
}
historySet.add(hash);
history.add(data);
currentCellGrid.nextGeneration(currentChangeGrid, nextCellGrid, nextChangeGrid);
}
FracGrid fracGrid = new FracGrid(size);
for (int i = 0; i < history.size(); i++) {
currentCellGrid.setData(history.get(i));
double frac = clamped(lerpFrom(0, history.size(), i + 1));
fracGrid.overlay(currentCellGrid, frac);
}
// Normalizing the frac_grid to the range 0..1 was a step left out of .version1
// In some cases it can cause the full range of the gradient to go unused.
// This fixes the problem for the other versions, while remaining compatible
// with .version1.
if (version != LifeHashVersion.VERSION1) {
double minValue = Double.MAX_VALUE;
double maxValue = Double.MIN_VALUE;
for (Point point : fracGrid.getPoints()) {
double value = fracGrid.getValue(point);
minValue = Math.min(minValue, value);
maxValue = Math.max(maxValue, value);
}
for (Point point : fracGrid.getPoints()) {
double currentValue = fracGrid.getValue(point);
double value = lerpFrom(minValue, maxValue, currentValue);
fracGrid.setValue(value, point);
}
}
BitEnumerator entropy = new BitEnumerator(digest);
switch(version) {
case DETAILED ->
// Throw away a bit of entropy to ensure we generate different colors and patterns from .version1
entropy.next();
case VERSION2 ->
// Throw away two bits of entropy to ensure we generate different colors and patterns from .version1 or .detailed.
entropy.nextUint2();
default -> {
}
}
ColorFunc gradient = selectGradient(entropy, version);
Pattern pattern = selectPattern(entropy, version);
ColorGrid color_grid = new ColorGrid(fracGrid, gradient, pattern);
return makeImage(color_grid.size.width(), color_grid.size.height(), color_grid.colors(), moduleSize, hasAlpha);
}
static Image makeImage(int width, int height, List<Double> floatColors, int moduleSize, boolean hasAlpha) {
if (moduleSize == 0) {
throw new IllegalArgumentException("Invalid module size.");
}
int scaledWidth = width * moduleSize;
int scaledHeight = height * moduleSize;
int resultComponents = hasAlpha ? 4 : 3;
int scaledCapacity = scaledWidth * scaledHeight * resultComponents;
List<Byte> resultColors = new ArrayList<>(scaledCapacity);
for(int i = 0; i < scaledCapacity; i++) {
resultColors.add((byte)0);
}
for (int targetY = 0; targetY < scaledWidth; targetY++) {
for (int targetX = 0; targetX < scaledHeight; targetX++) {
int sourceX = targetX / moduleSize;
int sourceY = targetY / moduleSize;
int sourceOffset = (sourceY * width + sourceX) * 3;
int targetOffset = (targetY * scaledWidth + targetX) * resultComponents;
resultColors.set(targetOffset, (byte)(clamped(floatColors.get(sourceOffset)) * 255));
resultColors.set(targetOffset + 1, (byte)(clamped(floatColors.get(sourceOffset + 1)) * 255));
resultColors.set(targetOffset + 2, (byte)(clamped(floatColors.get(sourceOffset + 2)) * 255));
if (hasAlpha) {
resultColors.set(targetOffset + 3, (byte)255);
}
}
}
return new Image(scaledWidth, scaledHeight, resultColors, hasAlpha);
}
/**
* Creates a java.awt.image.BufferedImage from the LifeHash image
*
* @param image the LifeHash.Image to use
* @return a renderable image
*/
public static BufferedImage getBufferedImage(Image image) {
BufferedImage bufferedImage = new BufferedImage(image.width, image.height, image.hasAlpha ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB);
for (int y = 0; y < image.height; y++) {
for (int x = 0; x < image.width; x++) {
int offset = (y * image.width + x) * (image.hasAlpha ? 4 : 3);
int r = image.colors.get(offset) & 0xFF;
int g = image.colors.get(offset + 1) & 0xFF;
int b = image.colors.get(offset + 2) & 0xFF;
int color;
if(image.hasAlpha) {
int a = image.colors.get(offset + 3) & 0xFF;
color = (a << 24) | (r << 16) | (g << 8) | b;
} else {
color = (r << 16) | (g << 8) | b;
}
bufferedImage.setRGB(x, y, color);
}
}
return bufferedImage;
}
public record Image(int width, int height, List<Byte> colors, boolean hasAlpha) {
}
}

View file

@ -0,0 +1,9 @@
package com.sparrowwallet.toucan;
public enum LifeHashVersion {
VERSION1, // DEPRECATED. Uses HSB gamut. Not CMYK-friendly. Has some minor gradient bugs.
VERSION2, // CMYK-friendly gamut. Recommended for most purposes.
DETAILED, // Double resolution. CMYK-friendly gamut gamut.
FIDUCIAL, // Optimized for generating machine-vision fiducials. High-contrast. CMYK-friendly gamut.
GRAYSCALE_FIDUCIAL // Optimized for generating machine-vision fiducials. High-contrast.
}

View file

@ -0,0 +1,35 @@
package com.sparrowwallet.toucan.impl;
import java.util.ArrayList;
import java.util.List;
public class BitAggregator {
private final List<Byte> data;
private int bitMask;
public BitAggregator() {
data = new ArrayList<>();
bitMask = 0;
}
public void append(boolean bit) {
if (bitMask == 0) {
bitMask = 0x80;
data.add((byte) 0);
}
if (bit) {
data.set(data.size() - 1, (byte) (data.get(data.size() - 1) | bitMask));
}
bitMask >>= 1;
}
public byte[] getData() {
byte[] result = new byte[data.size()];
for (int i = 0; i < data.size(); i++) {
result[i] = data.get(i);
}
return result;
}
}

View file

@ -0,0 +1,63 @@
package com.sparrowwallet.toucan.impl;
public class BitEnumerator {
private final byte[] data;
private int index;
private int mask;
public BitEnumerator(byte[] data) {
this.data = data;
this.index = 0;
this.mask = 0x80;
}
public boolean hasNext() {
return mask != 0 || index != (data.length - 1);
}
public boolean next() {
if (!hasNext()) {
throw new IllegalStateException("BitEnumerator underflow.");
}
if (mask == 0) {
mask = 0x80;
index += 1;
}
boolean b = (data[index] & mask) != 0;
mask >>= 1;
return b;
}
public int nextConfigurable(int bitMask, int bits) {
int value = 0;
for (int i = 0; i < bits; i++) {
if (next()) {
value |= bitMask;
}
bitMask >>= 1;
}
return value;
}
public int nextUint2() {
int bitMask = 0x02;
return nextConfigurable(bitMask, 2);
}
public int nextUint8() {
int bitMask = 0x80;
return nextConfigurable(bitMask, 8);
}
public int nextUint16() {
int bitMask = 0x8000;
return nextConfigurable(bitMask, 16);
}
public double nextFrac() {
return (double) nextUint16() / 65535.0;
}
}

View file

@ -0,0 +1,95 @@
package com.sparrowwallet.toucan.impl;
public class CellGrid extends Grid<Boolean> {
private final BitAggregator data;
public CellGrid(Size size) {
super(size);
data = new BitAggregator();
}
public byte[] getData() {
BitAggregator aggregator = new BitAggregator();
for (Point point : getPoints()) {
aggregator.append(getValue(point));
}
return aggregator.getData();
}
public void setData(byte[] data) {
assert capacity == data.length * 8;
BitEnumerator e = new BitEnumerator(data);
int i = 0;
while (e.hasNext()) {
boolean value = e.next();
storage.set(i, value);
i += 1;
}
}
static boolean isAliveInNextGeneration(boolean currentAlive, int neighborsCount) {
if (currentAlive) {
return neighborsCount == 2 || neighborsCount == 3;
} else {
return neighborsCount == 3;
}
}
private int countNeighbors(Point point) {
int total = 0;
for (PointPair neighborhood : getNeighborhood(point)) {
Point pointO = neighborhood.o();
Point pointP = neighborhood.p();
if (pointO.equals(Point.ZERO)) {
continue;
}
if (getValue(pointP)) {
total += 1;
}
}
return total;
}
public void nextGeneration(ChangeGrid currentChangeGrid, CellGrid nextCellGrid, ChangeGrid nextChangeGrid) {
nextCellGrid.setAll(false);
nextChangeGrid.setAll(false);
for (Point p : getPoints()) {
boolean currentAlive = getValue(p);
if (currentChangeGrid.getValue(p)) {
int neighborsCount = countNeighbors(p);
boolean nextAlive = isAliveInNextGeneration(currentAlive, neighborsCount);
if (nextAlive) {
nextCellGrid.setValue(true, p);
}
if (currentAlive != nextAlive) {
nextChangeGrid.setChanged(p);
}
} else {
nextCellGrid.setValue(currentAlive, p);
}
}
}
@Override
protected Color colorForValue(Boolean colorValue) {
if (colorValue) {
return Colors.WHITE;
} else {
return Colors.BLACK;
}
}
@Override
protected Boolean getDefault() {
return Boolean.FALSE;
}
}

View file

@ -0,0 +1,23 @@
package com.sparrowwallet.toucan.impl;
public class ChangeGrid extends Grid<Boolean> {
public ChangeGrid(Size size) {
super(size);
}
public void setChanged(Point point) {
for(PointPair neighborhood : getNeighborhood(point)) {
setValue(true, neighborhood.p());
}
}
@Override
protected Color colorForValue(Boolean value) {
return value ? Colors.RED : Colors.BLUE;
}
@Override
protected Boolean getDefault() {
return Boolean.FALSE;
}
}

View file

@ -0,0 +1,48 @@
package com.sparrowwallet.toucan.impl;
import static com.sparrowwallet.toucan.impl.Utils.clamped;
public class Color {
final double r;
final double g;
final double b;
public Color(double r, double g, double b) {
this.r = r;
this.g = g;
this.b = b;
}
static Color fromUint8Values(int r, int g, int b) {
return new Color((float) r / 255, (float) g / 255, (float) b / 255);
}
Color lighten(double t) {
return this.lerpTo(Colors.WHITE, t);
}
Color darken(double t) {
return this.lerpTo(Colors.BLACK, t);
}
double luminance() {
return Math.sqrt(Math.pow(0.299 * this.r, 2) + Math.pow(0.587 * this.g, 2) + Math.pow(0.114 * this.b, 2));
}
Color burn(double t) {
double f = Math.max(1.0f - t, 1.0e-7f);
return new Color(
Math.min(1.0f - (1.0f - this.r) / f, 1.0f),
Math.min(1.0f - (1.0f - this.g) / f, 1.0f),
Math.min(1.0f - (1.0f - this.b) / f, 1.0f)
);
}
Color lerpTo(Color other, double t) {
double f = clamped(t);
double red = clamped(this.r * (1 - f) + other.r * f);
double green = clamped(this.g * (1 - f) + other.g * f);
double blue = clamped(this.b * (1 - f) + other.b * f);
return new Color(red, green, blue);
}
}

View file

@ -0,0 +1,58 @@
package com.sparrowwallet.toucan.impl;
import java.util.List;
import static com.sparrowwallet.toucan.impl.Utils.modulo;
public abstract class ColorFunc {
public static ColorFunc reverse(ColorFunc c) {
return new ColorFunc() {
@Override
Color apply(double value) {
return c.apply(1 - value);
}
};
}
public static ColorFunc blend(Color color1, Color color2) {
return new ColorFunc() {
@Override
Color apply(double value) {
return color1.lerpTo(color2, value);
}
};
}
public static ColorFunc blend(List<Color> colors) {
int count = colors.size();
switch(count) {
case 0:
return blend(Colors.BLACK, Colors.BLACK);
case 1:
return blend(colors.get(0), colors.get(0));
case 2:
return blend(colors.get(0), colors.get(1));
default:
return new ColorFunc() {
@Override
Color apply(double value) {
if (value >= 1) {
return colors.get(count - 1);
} else if (value <= 0) {
return colors.get(0);
} else {
int segments = count - 1;
double s = value * segments;
int segment = (int) s;
double segmentFrac = modulo(s, 1);
Color c1 = colors.get(segment);
Color c2 = colors.get(segment + 1);
return c1.lerpTo(c2, segmentFrac);
}
}
};
}
}
abstract Color apply(double value);
}

View file

@ -0,0 +1,85 @@
package com.sparrowwallet.toucan.impl;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class ColorGrid extends Grid<Color> {
static List<Transform> snowflakeTransforms = List.of(
new Transform(false, false, false),
new Transform(false, true, false),
new Transform(false, false, true),
new Transform(false, true, true)
);
static List<Transform> pinwheelTransforms = List.of(
new Transform(false, false, false),
new Transform(true, true, false),
new Transform(true, false, true),
new Transform(false, true, true)
);
static List<Transform> fiducialTransforms = List.of(
new Transform(false, false, false)
);
static Map<Pattern, List<Transform>> transformsMap = Map.of(
Pattern.SNOWFLAKE, snowflakeTransforms,
Pattern.PINWHEEL, pinwheelTransforms,
Pattern.FIDUCIAL, fiducialTransforms
);
public ColorGrid(FracGrid fracGrid, ColorFunc gradient, Pattern pattern) {
super(targetSize(fracGrid.getSize(), pattern));
List<Transform> transforms = transformsMap.getOrDefault(pattern, new ArrayList<>());
for(Point point : fracGrid.getPoints()) {
double value = fracGrid.getValue(point);
Color someColor = gradient.apply(value);
draw(point, someColor, transforms);
}
}
@Override
protected Color colorForValue(Color color) {
return color;
}
@Override
protected Color getDefault() {
return Colors.BLACK;
}
private static Size targetSize(Size inSize, Pattern pattern) {
int multiplier = (pattern == Pattern.FIDUCIAL) ? 1 : 2;
return new Size(inSize.width() * multiplier, inSize.height() * multiplier);
}
private Point transformPoint(Point point, Transform transform) {
int x = point.x();
int y = point.y();
if (transform.transpose) {
int temp = x;
x = y;
y = temp;
}
if (transform.reflectX) {
x = maxX - x;
}
if (transform.reflectY) {
y = maxY - y;
}
return new Point(x, y);
}
private void draw(Point p, Color color, List<Transform> transforms) {
for (Transform t : transforms) {
Point p2 = transformPoint(p, t);
setValue(color, p2);
}
}
static record Transform(boolean transpose, boolean reflectX, boolean reflectY) {
}
}

View file

@ -0,0 +1,12 @@
package com.sparrowwallet.toucan.impl;
public class Colors {
public static final Color WHITE = new Color(1, 1, 1);
public static final Color BLACK = new Color(0, 0, 0);
public static final Color RED = new Color(1, 0, 0);
public static final Color GREEN = new Color(0, 1, 0);
public static final Color BLUE = new Color(0, 0, 1);
public static final Color CYAN = new Color(0, 1, 1);
public static final Color MAGENTA = new Color(1, 0, 1);
public static final Color YELLOW = new Color(1, 1, 0);
}

View file

@ -0,0 +1,25 @@
package com.sparrowwallet.toucan.impl;
public class FracGrid extends Grid<Double> {
public FracGrid(Size size) {
super(size);
}
public void overlay(CellGrid cellGrid, double frac) {
for(Point point : getPoints()) {
if(cellGrid.getValue(point)) {
setValue(frac, point);
}
}
}
@Override
protected Color colorForValue(Double value) {
return Colors.BLACK.lerpTo(Colors.WHITE, value);
}
@Override
protected Double getDefault() {
return 0d;
}
}

View file

@ -0,0 +1,315 @@
package com.sparrowwallet.toucan.impl;
import com.sparrowwallet.toucan.LifeHashVersion;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import static com.sparrowwallet.toucan.impl.Utils.lerp;
import static com.sparrowwallet.toucan.impl.Utils.modulo;
public class Gradients {
static ColorFunc grayscale = ColorFunc.blend(Colors.BLACK, Colors.WHITE);
static ColorFunc selectGrayscale(BitEnumerator entropy) {
return entropy.next() ? grayscale : ColorFunc.reverse(grayscale);
}
static ColorFunc makeHue = new ColorFunc() {
@Override
Color apply(double value) {
return new HSBColor(value).color();
}
};
static ColorFunc spectrum = ColorFunc.blend(List.of(
Color.fromUint8Values(0, 168, 222),
Color.fromUint8Values(51, 51, 145),
Color.fromUint8Values(233, 19, 136),
Color.fromUint8Values(235, 45, 46),
Color.fromUint8Values(253, 233, 43),
Color.fromUint8Values(0, 158, 84),
Color.fromUint8Values(0, 168, 222)
));
static ColorFunc spectrumCmykSafe = ColorFunc.blend(List.of(
Color.fromUint8Values(0, 168, 222),
Color.fromUint8Values(41, 60, 130),
Color.fromUint8Values(210, 59, 130),
Color.fromUint8Values(217, 63, 53),
Color.fromUint8Values(244, 228, 81),
Color.fromUint8Values(0, 158, 84),
Color.fromUint8Values(0, 168, 222)
));
static Color adjustForLuminance(Color color, Color contrastColor) {
double lum = color.luminance();
double contrastLum = contrastColor.luminance();
double threshold = 0.6;
double offset = Math.abs(lum - contrastLum);
if (offset > threshold) {
return color;
}
double boost = 0.7;
double t = lerp(0, threshold, boost, 0, offset);
if (contrastLum > lum) {
// darken this color
return color.darken(t).burn(t * 0.6);
} else {
// lighten this color
return color.lighten(t).burn(t * 0.6);
}
}
static ColorFunc monochromatic(BitEnumerator entropy, ColorFunc hueGenerator) {
double hue = entropy.nextFrac();
boolean isTint = entropy.next();
boolean isReversed = entropy.next();
double keyAdvance = entropy.nextFrac() * 0.3 + 0.05;
double neutralAdvance = entropy.nextFrac() * 0.3 + 0.05;
Color keyColor = hueGenerator.apply(hue);
double contrastBrightness;
if (isTint) {
contrastBrightness = 1;
keyColor = keyColor.darken(0.5);
} else {
contrastBrightness = 0;
}
Color neutralColor = grayscale.apply(contrastBrightness);
Color keyColor2 = keyColor.lerpTo(neutralColor, keyAdvance);
Color neutralColor2 = neutralColor.lerpTo(keyColor, neutralAdvance);
ColorFunc gradient = ColorFunc.blend(keyColor2, neutralColor2);
return isReversed ? ColorFunc.reverse(gradient) : gradient;
}
static ColorFunc monochromaticFiducial(BitEnumerator entropy) {
double hue = entropy.nextFrac();
boolean isReversed = entropy.next();
boolean isTint = entropy.next();
Color contrastColor = isTint ? Colors.WHITE : Colors.BLACK;
Color keyColor = adjustForLuminance(spectrumCmykSafe.apply(hue), contrastColor);
ColorFunc gradient = ColorFunc.blend(List.of(keyColor, contrastColor, keyColor));
return isReversed ? ColorFunc.reverse(gradient) : gradient;
}
static ColorFunc complementary(BitEnumerator entropy, ColorFunc hueGenerator) {
double spectrum1 = entropy.nextFrac();
double spectrum2 = modulo(spectrum1 + 0.5, 1);
double lighterAdvance = entropy.nextFrac() * 0.3;
double darkerAdvance = entropy.nextFrac() * 0.3;
boolean isReversed = entropy.next();
Color color1 = hueGenerator.apply(spectrum1);
Color color2 = hueGenerator.apply(spectrum2);
double luma1 = color1.luminance();
double luma2 = color2.luminance();
Color darkerColor;
Color lighterColor;
if (luma1 > luma2) {
darkerColor = color2;
lighterColor = color1;
} else {
darkerColor = color1;
lighterColor = color2;
}
Color adjustedLighterColor = lighterColor.lighten(lighterAdvance);
Color adjustedDarkerColor = darkerColor.darken(darkerAdvance);
ColorFunc gradient = ColorFunc.blend(adjustedDarkerColor, adjustedLighterColor);
return isReversed ? ColorFunc.reverse(gradient) : gradient;
}
static ColorFunc complementaryFiducial(BitEnumerator entropy) {
double spectrum1 = entropy.nextFrac();
double spectrum2 = modulo((spectrum1 + 0.5), 1);
boolean is_tint = entropy.next();
boolean is_reversed = entropy.next();
boolean neutral_color_bias = entropy.next();
Color neutral_color = is_tint ? Colors.WHITE : Colors.BLACK;
Color color1 = spectrumCmykSafe.apply(spectrum1);
Color color2 = spectrumCmykSafe.apply(spectrum2);
Color biased_neutral_color = neutral_color.lerpTo(neutral_color_bias ? color1 : color2, 0.2).burn(0.1);
ColorFunc gradient = ColorFunc.blend(List.of(
adjustForLuminance(color1, biased_neutral_color),
biased_neutral_color,
adjustForLuminance(color2, biased_neutral_color)
));
return is_reversed ? ColorFunc.reverse(gradient) : gradient;
}
static ColorFunc triadic(BitEnumerator entropy, ColorFunc hueGenerator) {
double spectrum1 = entropy.nextFrac();
double spectrum2 = modulo(spectrum1 + 1.0 / 3, 1);
double spectrum3 = modulo((spectrum1 + 2.0 / 3), 1);
double lighter_advance = entropy.nextFrac() * 0.3;
double darker_advance = entropy.nextFrac() * 0.3;
boolean is_reversed = entropy.next();
Color color1 = hueGenerator.apply(spectrum1);
Color color2 = hueGenerator.apply(spectrum2);
Color color3 = hueGenerator.apply(spectrum3);
List<Color> colors = new ArrayList<>();
colors.add(color1);
colors.add(color2);
colors.add(color3);
colors.sort(Comparator.comparingDouble(Color::luminance));
Color darker_color = colors.get(0);
Color middle_color = colors.get(1);
Color lighter_color = colors.get(2);
Color adjusted_lighter_color = lighter_color.lighten(lighter_advance);
Color adjusted_darker_color = darker_color.darken(darker_advance);
ColorFunc gradient = ColorFunc.blend(List.of(adjusted_lighter_color, middle_color, adjusted_darker_color));
return is_reversed ? ColorFunc.reverse(gradient) : gradient;
}
static ColorFunc triadicFiducial(BitEnumerator entropy) {
double spectrum1 = entropy.nextFrac();
double spectrum2 = (spectrum1 + 1.0 / 3) % 1;
double spectrum3 = (spectrum1 + 2.0 / 3) % 1;
boolean isTint = entropy.next();
int neutralInsertIndex = entropy.nextUint8() % 2 + 1;
boolean isReversed = entropy.next();
Color neutralColor = isTint ? Colors.WHITE : Colors.BLACK;
List<Color> colors = new ArrayList<>(List.of(spectrumCmykSafe.apply(spectrum1), spectrumCmykSafe.apply(spectrum2), spectrumCmykSafe.apply(spectrum3)));
switch(neutralInsertIndex) {
case 1 -> {
colors.set(0, adjustForLuminance(colors.get(0), neutralColor));
colors.set(1, adjustForLuminance(colors.get(1), neutralColor));
colors.set(2, adjustForLuminance(colors.get(2), colors.get(1)));
}
case 2 -> {
colors.set(1, adjustForLuminance(colors.get(1), neutralColor));
colors.set(2, adjustForLuminance(colors.get(2), neutralColor));
colors.set(0, adjustForLuminance(colors.get(0), colors.get(1)));
}
default -> throw new IllegalArgumentException("Internal error.");
}
colors.add(neutralInsertIndex, neutralColor);
ColorFunc gradient = ColorFunc.blend(colors);
return isReversed ? ColorFunc.reverse(gradient) : gradient;
}
static ColorFunc analogous(BitEnumerator entropy, ColorFunc hueGenerator) {
double spectrum1 = entropy.nextFrac();
double spectrum2 = modulo(spectrum1 + 1.0 / 12, 1);
double spectrum3 = modulo(spectrum1 + 2.0 / 12, 1);
double spectrum4 = modulo(spectrum1 + 3.0 / 12, 1);
double advance = entropy.nextFrac() * 0.5 + 0.2;
boolean isReversed = entropy.next();
Color color1 = hueGenerator.apply(spectrum1);
Color color2 = hueGenerator.apply(spectrum2);
Color color3 = hueGenerator.apply(spectrum3);
Color color4 = hueGenerator.apply(spectrum4);
Color darkestColor;
Color darkColor;
Color lightColor;
Color lightestColor;
if (color1.luminance() < color4.luminance()) {
darkestColor = color1;
darkColor = color2;
lightColor = color3;
lightestColor = color4;
} else {
darkestColor = color4;
darkColor = color3;
lightColor = color2;
lightestColor = color1;
}
Color adjustedDarkestColor = darkestColor.darken(advance);
Color adjustedDarkColor = darkColor.darken(advance / 2);
Color adjustedLightColor = lightColor.lighten(advance / 2);
Color adjustedLightestColor = lightestColor.lighten(advance);
ColorFunc gradient = ColorFunc.blend(List.of(adjustedDarkestColor, adjustedDarkColor, adjustedLightColor, adjustedLightestColor));
return isReversed ? ColorFunc.reverse(gradient) : gradient;
}
static ColorFunc analogousFiducial(BitEnumerator entropy) {
double spectrum1 = entropy.nextFrac();
double spectrum2 = modulo(spectrum1 + 1.0 / 10, 1);
double spectrum3 = modulo(spectrum1 + 2.0 / 10, 1);
boolean isTint = entropy.next();
int neutralInsertIndex = entropy.nextUint8() % 2 + 1;
boolean isReversed = entropy.next();
Color neutralColor = isTint ? Colors.WHITE : Colors.BLACK;
List<Color> colors = new ArrayList<>(List.of(spectrumCmykSafe.apply(spectrum1), spectrumCmykSafe.apply(spectrum2), spectrumCmykSafe.apply(spectrum3)));
switch(neutralInsertIndex) {
case 1 -> {
colors.set(0, adjustForLuminance(colors.get(0), neutralColor));
colors.set(1, adjustForLuminance(colors.get(1), neutralColor));
colors.set(2, adjustForLuminance(colors.get(2), colors.get(1)));
}
case 2 -> {
colors.set(1, adjustForLuminance(colors.get(1), neutralColor));
colors.set(2, adjustForLuminance(colors.get(2), neutralColor));
colors.set(0, adjustForLuminance(colors.get(0), colors.get(1)));
}
default -> throw new IllegalStateException("Internal error");
}
colors.add(neutralInsertIndex, neutralColor);
ColorFunc gradient = ColorFunc.blend(colors);
return isReversed ? ColorFunc.reverse(gradient) : gradient;
}
public static ColorFunc selectGradient(BitEnumerator entropy, LifeHashVersion version) {
if(version == LifeHashVersion.GRAYSCALE_FIDUCIAL) {
return selectGrayscale(entropy);
}
int value = entropy.nextUint2();
return switch(value) {
case 0 -> switch(version) {
case VERSION1 -> monochromatic(entropy, makeHue);
case VERSION2, DETAILED -> monochromatic(entropy, spectrumCmykSafe);
case FIDUCIAL -> monochromaticFiducial(entropy);
case GRAYSCALE_FIDUCIAL -> grayscale;
};
case 1 -> switch(version) {
case VERSION1 -> complementary(entropy, spectrum);
case VERSION2, DETAILED -> complementary(entropy, spectrumCmykSafe);
case FIDUCIAL -> complementaryFiducial(entropy);
case GRAYSCALE_FIDUCIAL -> grayscale;
};
case 2 -> switch(version) {
case VERSION1 -> triadic(entropy, spectrum);
case VERSION2, DETAILED -> triadic(entropy, spectrumCmykSafe);
case FIDUCIAL -> triadicFiducial(entropy);
case GRAYSCALE_FIDUCIAL -> grayscale;
};
case 3 -> switch(version) {
case VERSION1 -> analogous(entropy, spectrum);
case VERSION2, DETAILED -> analogous(entropy, spectrumCmykSafe);
case FIDUCIAL -> analogousFiducial(entropy);
case GRAYSCALE_FIDUCIAL -> grayscale;
};
default -> grayscale;
};
}
}

View file

@ -0,0 +1,105 @@
package com.sparrowwallet.toucan.impl;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public abstract class Grid<T> {
// public
public final Size size;
// private
protected final int capacity;
protected final int maxX;
protected final int maxY;
protected final List<T> storage;
private Supplier<T> supplier;
public Grid(Size size) {
this.size = size;
this.capacity = size.width() * size.height();
this.storage = new ArrayList<>(this.capacity);
for(int i = 0; i < this.capacity; i++) {
this.storage.add(getDefault());
}
this.maxX = size.width() - 1;
this.maxY = size.height() - 1;
}
protected abstract Color colorForValue(T colorValue);
protected abstract T getDefault();
private int offset(Point point) {
return point.y() * this.size.width() + point.x();
}
static int circularIndex(int index, int modulus) {
return (index + modulus) % modulus;
}
public void setAll(T value) {
for(int i = 0; i < this.capacity; i++) {
this.storage.set(i, value);
}
}
public void setValue(T value, Point point) {
int offset = offset(point);
this.storage.set(offset, value);
}
public T getValue(Point point) {
return this.storage.get(offset(point));
}
public List<Point> getPoints() {
List<Point> points = new ArrayList<>();
for(int y = 0; y <= this.maxY; y++) {
for(int x = 0; x <= this.maxX; x++) {
points.add(new Point(x, y));
}
}
return points;
}
public List<PointPair> getNeighborhood(Point point) {
List<PointPair> pointPairs = new ArrayList<>();
for(int oy = -1; oy <= 1; oy++) {
for(int ox = -1; ox <= 1; ox++) {
Point o = new Point(ox, oy);
int px = circularIndex(ox + point.x(), this.size.width());
int py = circularIndex(oy + point.y(), this.size.height());
Point p = new Point(px, py);
pointPairs.add(new PointPair(o, p));
}
}
return pointPairs;
}
public List<Double> colors() {
List<Double> result = new ArrayList<>();
for(int idx = 0; idx < this.storage.size(); idx++) {
T colorValue = this.storage.get(idx);
Color color = this.colorForValue(colorValue);
result.add(color.r);
result.add(color.g);
result.add(color.b);
}
return result;
}
public Size getSize() {
return size;
}
protected void setChanged(Point p) {
}
}

View file

@ -0,0 +1,109 @@
package com.sparrowwallet.toucan.impl;
import static com.sparrowwallet.toucan.impl.Utils.clamped;
import static com.sparrowwallet.toucan.impl.Utils.modulo;
public class HSBColor {
public double hue;
public double saturation;
public double brightness;
public HSBColor(double hue) {
this.hue = hue;
this.saturation = 1;
this.brightness = 1;
}
public HSBColor(Color color) {
double r = color.r;
double g = color.g;
double b = color.b;
double maxValue = Math.max(r, Math.max(g, b));
double minValue = Math.min(r, Math.min(g, b));
double brightness = maxValue;
double d = maxValue - minValue;
double saturation = maxValue == 0 ? 0 : d / maxValue;
double hue;
if (maxValue == minValue) {
hue = 0; // achromatic
} else {
if (maxValue == r) {
hue = ((g - b) / d + (g < b ? 6 : 0)) / 6;
} else if (maxValue == g) {
hue = ((b - r) / d + 2) / 6;
} else if (maxValue == b) {
hue = ((r - g) / d + 4) / 6;
} else {
throw new IllegalArgumentException("Internal error.");
}
}
this.hue = hue;
this.saturation = saturation;
this.brightness = brightness;
}
public Color color() {
double v = clamped(brightness);
double s = clamped(saturation);
double red;
double green;
double blue;
if(s <= 0) {
red = v;
green = v;
blue = v;
} else {
double h = modulo(hue, 1);
if(h < 0) {
h += 1;
}
h *= 6;
int i = (int) Math.floor(h);
double f = h - i;
double p = v * (1 - s);
double q = v * (1 - s * f);
double t = v * (1 - s * (1 - f));
switch(i) {
case 0:
red = v;
green = t;
blue = p;
break;
case 1:
red = q;
green = v;
blue = p;
break;
case 2:
red = p;
green = v;
blue = t;
break;
case 3:
red = p;
green = q;
blue = v;
break;
case 4:
red = t;
green = p;
blue = v;
break;
case 5:
red = v;
green = p;
blue = q;
break;
default:
throw new IllegalArgumentException();
}
}
return new Color(red, green, blue);
}
}

View file

@ -0,0 +1,21 @@
package com.sparrowwallet.toucan.impl;
import com.sparrowwallet.toucan.LifeHashVersion;
public enum Pattern {
SNOWFLAKE, // Mirror around central axes.
PINWHEEL, // Rotate around center.
FIDUCIAL; // Identity.
public static Pattern selectPattern(BitEnumerator entropy, LifeHashVersion version) {
if (version == LifeHashVersion.FIDUCIAL || version == LifeHashVersion.GRAYSCALE_FIDUCIAL) {
return FIDUCIAL;
} else {
if (entropy.next()) {
return SNOWFLAKE;
} else {
return PINWHEEL;
}
}
}
}

View file

@ -0,0 +1,5 @@
package com.sparrowwallet.toucan.impl;
public record Point(int x, int y) {
public static Point ZERO = new Point(0, 0);
}

View file

@ -0,0 +1,4 @@
package com.sparrowwallet.toucan.impl;
public record PointPair(Point o, Point p) {
}

View file

@ -0,0 +1,259 @@
/*
* Copyright 2011 Google Inc.
* Copyright 2014 Andreas Schildbach
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.sparrowwallet.toucan.impl;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class Sha256Hash implements Comparable<Sha256Hash> {
public static final int LENGTH = 32; // bytes
public static final Sha256Hash ZERO_HASH = wrap(new byte[LENGTH]);
private final byte[] bytes;
private Sha256Hash(byte[] rawHashBytes) {
if(rawHashBytes.length != LENGTH) {
throw new IllegalArgumentException();
}
this.bytes = rawHashBytes;
}
/**
* Creates a new instance that wraps the given hash value.
*
* @param rawHashBytes the raw hash bytes to wrap
* @return a new instance
* @throws IllegalArgumentException if the given array length is not exactly 32
*/
public static Sha256Hash wrap(byte[] rawHashBytes) {
return new Sha256Hash(rawHashBytes);
}
/**
* Creates a new instance that wraps the given hash value (represented as a hex string).
*
* @param hexString a hash value represented as a hex string
* @return a new instance
* @throws IllegalArgumentException if the given string is not a valid
* hex string, or if it does not represent exactly 32 bytes
*/
public static Sha256Hash wrap(String hexString) {
return wrap(Utils.hexToBytes(hexString));
}
/**
* Creates a new instance that wraps the given hash value, but with byte order reversed.
*
* @param rawHashBytes the raw hash bytes to wrap
* @return a new instance
* @throws IllegalArgumentException if the given array length is not exactly 32
*/
public static Sha256Hash wrapReversed(byte[] rawHashBytes) {
return wrap(Utils.reverseBytes(rawHashBytes));
}
/**
* Creates a new instance containing the calculated (one-time) hash of the given bytes.
*
* @param contents the bytes on which the hash value is calculated
* @return a new instance containing the calculated (one-time) hash
*/
public static Sha256Hash of(byte[] contents) {
return wrap(hash(contents));
}
/**
* Creates a new instance containing the hash of the calculated hash of the given bytes.
*
* @param contents the bytes on which the hash value is calculated
* @return a new instance containing the calculated (two-time) hash
*/
public static Sha256Hash twiceOf(byte[] contents) {
return wrap(hashTwice(contents));
}
/**
* Creates a new instance containing the hash of the calculated hash of the given bytes.
*
* @param content1 first bytes on which the hash value is calculated
* @param content2 second bytes on which the hash value is calculated
* @return a new instance containing the calculated (two-time) hash
*/
public static Sha256Hash twiceOf(byte[] content1, byte[] content2) {
return wrap(hashTwice(content1, content2));
}
/**
* Returns a new SHA-256 MessageDigest instance.
*
* This is a convenience method which wraps the checked
* exception that can never occur with a RuntimeException.
*
* @return a new SHA-256 MessageDigest instance
*/
public static MessageDigest newDigest() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e); // Can't happen.
}
}
/**
* Calculates the SHA-256 hash of the given bytes.
*
* @param input the bytes to hash
* @return the hash (in big-endian order)
*/
public static byte[] hash(byte[] input) {
return hash(input, 0, input.length);
}
/**
* Calculates the SHA-256 hash of the given byte range.
*
* @param input the array containing the bytes to hash
* @param offset the offset within the array of the bytes to hash
* @param length the number of bytes to hash
* @return the hash (in big-endian order)
*/
public static byte[] hash(byte[] input, int offset, int length) {
MessageDigest digest = newDigest();
digest.update(input, offset, length);
return digest.digest();
}
/**
* Calculates the SHA-256 hash of the given bytes,
* and then hashes the resulting hash again.
*
* @param input the bytes to hash
* @return the double-hash (in big-endian order)
*/
public static byte[] hashTwice(byte[] input) {
return hashTwice(input, 0, input.length);
}
/**
* Calculates the hash of hash on the given chunks of bytes. This is equivalent to concatenating the two
* chunks and then passing the result to {@link #hashTwice(byte[])}.
*/
public static byte[] hashTwice(byte[] input1, byte[] input2) {
MessageDigest digest = newDigest();
digest.update(input1);
digest.update(input2);
return digest.digest(digest.digest());
}
/**
* Calculates the SHA-256 hash of the given byte range,
* and then hashes the resulting hash again.
*
* @param input the array containing the bytes to hash
* @param offset the offset within the array of the bytes to hash
* @param length the number of bytes to hash
* @return the double-hash (in big-endian order)
*/
public static byte[] hashTwice(byte[] input, int offset, int length) {
MessageDigest digest = newDigest();
digest.update(input, offset, length);
return digest.digest(digest.digest());
}
/**
* Calculates the hash of hash on the given byte ranges. This is equivalent to
* concatenating the two ranges and then passing the result to {@link #hashTwice(byte[])}.
*/
public static byte[] hashTwice(byte[] input1, int offset1, int length1,
byte[] input2, int offset2, int length2) {
MessageDigest digest = newDigest();
digest.update(input1, offset1, length1);
digest.update(input2, offset2, length2);
return digest.digest(digest.digest());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
return Arrays.equals(bytes, ((Sha256Hash)o).bytes);
}
/**
* Returns the last four bytes of the wrapped hash. This should be unique enough to be a suitable hash code even for
* blocks, where the goal is to try and get the first bytes to be zeros (i.e. the value as a big integer lower
* than the target value).
*/
@Override
public int hashCode() {
// Use the last 4 bytes, not the first 4 which are often zeros in Bitcoin.
return fromBytes(bytes[LENGTH - 4], bytes[LENGTH - 3], bytes[LENGTH - 2], bytes[LENGTH - 1]);
}
/**
* Returns the {@code int} value whose byte representation is the given 4 bytes, in big-endian
* order; equivalent to {@code Ints.fromByteArray(new byte[] {b1, b2, b3, b4})}.
*
* @since 7.0
*/
public static int fromBytes(byte b1, byte b2, byte b3, byte b4) {
return b1 << 24 | (b2 & 0xFF) << 16 | (b3 & 0xFF) << 8 | (b4 & 0xFF);
}
@Override
public String toString() {
return Utils.bytesToHex(bytes);
}
/**
* Returns the bytes interpreted as a positive integer.
*/
public BigInteger toBigInteger() {
return new BigInteger(1, bytes);
}
/**
* Returns the internal byte array, without defensively copying. Therefore do NOT modify the returned array.
*/
public byte[] getBytes() {
return bytes;
}
/**
* Returns a reversed copy of the internal byte array.
*/
public byte[] getReversedBytes() {
return Utils.reverseBytes(bytes);
}
@Override
public int compareTo(final Sha256Hash other) {
for (int i = LENGTH - 1; i >= 0; i--) {
final int thisByte = this.bytes[i] & 0xff;
final int otherByte = other.bytes[i] & 0xff;
if (thisByte > otherByte)
return 1;
if (thisByte < otherByte)
return -1;
}
return 0;
}
}

View file

@ -0,0 +1,4 @@
package com.sparrowwallet.toucan.impl;
public record Size(int width, int height) {
}

View file

@ -0,0 +1,85 @@
package com.sparrowwallet.toucan.impl;
public class Utils {
public static double lerpTo(double toA, double toB, double t) {
return t * (toB - toA) + toA;
}
public static double lerpFrom(double fromA, double fromB, double t) {
return (fromA - t) / (fromA - fromB);
}
public static double lerp(double fromA, double fromB, double toC, double toD, double t) {
return lerpTo(toC, toD, lerpFrom(fromA, fromB, t));
}
// Return the minimum of `a`, `b`, and `c`.
public static double min(double a, double b, double c) {
return Math.min(Math.min(a, b), c);
}
public static double max(double a, double b, double c) {
return Math.max(Math.max(a, b), c);
}
public static double clamped(double n) {
return Math.max(Math.min(n, 1), 0);
}
public static double modulo(double dividend, double divisor) {
return dividend % divisor; //Math.IEEEremainder(Math.IEEEremainder(dividend, divisor) + divisor, divisor);
}
private final static char[] hexArray = "0123456789abcdef".toCharArray();
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for ( int j = 0; j < bytes.length; j++ ) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
public static byte[] hexToBytes(final String data) {
return decodeHex(data.toCharArray());
}
public static byte[] decodeHex(final char[] data) {
final int len = data.length;
if ((len & 0x01) != 0) {
throw new IllegalArgumentException("Odd number of characters.");
}
final byte[] out = new byte[len >> 1];
// two characters form the hex value.
for (int i = 0, j = 0; j < len; i++) {
int f = toDigit(data[j], j) << 4;
j++;
f = f | toDigit(data[j], j);
j++;
out[i] = (byte) (f & 0xFF);
}
return out;
}
protected static int toDigit(final char ch, final int index) {
final int digit = Character.digit(ch, 16);
if (digit == -1) {
throw new IllegalArgumentException("Illegal hexadecimal character " + ch + " at index " + index);
}
return digit;
}
public static byte[] reverseBytes(byte[] bytes) {
byte[] buf = new byte[bytes.length];
for (int i = 0; i < bytes.length; i++)
buf[i] = bytes[bytes.length - 1 - i];
return buf;
}
}

View file

@ -0,0 +1,4 @@
module com.sparrowwallet.toucan {
requires java.desktop;
exports com.sparrowwallet.toucan;
}

View file

@ -0,0 +1,28 @@
package com.sparrowwallet.toucan;
import org.junit.Assert;
import org.junit.Test;
public class LifeHashTest {
@Test
public void testHello() {
LifeHash.Image image = LifeHash.makeFromUTF8("Hello", LifeHashVersion.VERSION2, 1, false);
Assert.assertEquals(32, image.width());
Assert.assertEquals(32, image.height());
byte[] expected = new byte[] { -110, 126, -126, -78, 104, 92, -74, 101, 87, -54, 88, 64, -57, 89, 66, -59, 90, 69, -74, 101, 87, -76, 102, 89, -97, 117, 114, -46, 82, 54 };
for(int i = 0; i < expected.length; i++) {
Assert.assertEquals(expected[i], image.colors().get(i).byteValue());
}
}
@Test
public void testHelloAlpha() {
LifeHash.Image image = LifeHash.makeFromUTF8("Hello", LifeHashVersion.VERSION2, 1, true);
Assert.assertEquals(32, image.width());
Assert.assertEquals(32, image.height());
byte[] expected = new byte[] { -110, 126, -126, -1, -78, 104, 92, -1, -74, 101, 87, -1, -54, 88, 64, -1, -57, 89, 66, -1, -59, 90, 69, -1, -74, 101, 87, -1, -76, 102, 89, -1, -97, 117, 114, -1, -46, 82, 54, -1 };
for(int i = 0; i < expected.length; i++) {
Assert.assertEquals(expected[i], image.colors().get(i).byteValue());
}
}
}