├── snap-server ├── data │ ├── .gitkeep │ └── tessdata │ │ └── .gitkeep ├── src │ ├── test │ │ ├── resources │ │ │ ├── pit.pdf │ │ │ ├── digit.png │ │ │ ├── dummy.pdf │ │ │ ├── hugs.pdf │ │ │ ├── hugs.png │ │ │ ├── mats.png │ │ │ ├── zsasz.png │ │ │ ├── blood_bowl.png │ │ │ ├── crosswerd.png │ │ │ ├── meatballs.pdf │ │ │ ├── role_call.png │ │ │ └── kitchen_rush.pdf │ │ └── java │ │ │ └── com │ │ │ └── kyc │ │ │ └── snap │ │ │ ├── words │ │ │ ├── DatamuseUtilTest.java │ │ │ ├── WordSearchSolverTest.java │ │ │ ├── PhoneticsUtilTest.java │ │ │ └── StringUtilTest.java │ │ │ ├── image │ │ │ └── ImageUtilsTests.java │ │ │ ├── opencv │ │ │ └── OpenCvManagerTest.java │ │ │ ├── document │ │ │ └── PdfTest.java │ │ │ ├── store │ │ │ └── FileStoreTest.java │ │ │ ├── util │ │ │ └── MathUtilTest.java │ │ │ ├── SnapTest.java │ │ │ ├── scraper │ │ │ └── ScraperTest.java │ │ │ ├── grid │ │ │ └── GridParserTests.java │ │ │ ├── crossword │ │ │ └── CrosswordParserTest.java │ │ │ └── solver │ │ │ ├── GenericSolverTest.java │ │ │ └── PregexSolverTest.java │ └── main │ │ ├── java │ │ └── com │ │ │ └── kyc │ │ │ └── snap │ │ │ ├── document │ │ │ ├── Section.java │ │ │ ├── Rectangle.java │ │ │ ├── Document.java │ │ │ └── Pdf.java │ │ │ ├── crossword │ │ │ ├── ClueDirection.java │ │ │ ├── CrosswordFormula.java │ │ │ ├── Crossword.java │ │ │ ├── CrosswordClues.java │ │ │ └── CrosswordSpreadsheetWrapper.java │ │ │ ├── grid │ │ │ ├── GridLines.java │ │ │ ├── GridPosition.java │ │ │ ├── Border.java │ │ │ ├── Grid.java │ │ │ └── GridSpreadsheetWrapper.java │ │ │ ├── store │ │ │ ├── Store.java │ │ │ └── FileStore.java │ │ │ ├── image │ │ │ ├── ImageBlob.java │ │ │ └── ImageAnnotater.java │ │ │ ├── server │ │ │ ├── FileResource.java │ │ │ ├── ServerProperties.java │ │ │ ├── HostingClient.java │ │ │ ├── SnapServer.java │ │ │ ├── WordsResource.java │ │ │ └── DocumentResource.java │ │ │ ├── api │ │ │ ├── FileService.java │ │ │ ├── WordsService.java │ │ │ └── DocumentService.java │ │ │ ├── words │ │ │ ├── CustomDictionary.java │ │ │ ├── Dictionary.java │ │ │ ├── WikipediaTitlesDictionary.java │ │ │ ├── DatamuseUtil.java │ │ │ ├── EnglishDictionary.java │ │ │ ├── EnglishTrie.java │ │ │ ├── PhoneticsUtil.java │ │ │ ├── StringUtil.java │ │ │ └── WordSearchSolver.java │ │ │ ├── solver │ │ │ ├── EnglishTokens.java │ │ │ ├── GenericSolver.java │ │ │ ├── EnglishModel.java │ │ │ └── GenericSolverImpl.java │ │ │ ├── util │ │ │ ├── MathUtil.java │ │ │ └── Utils.java │ │ │ ├── google │ │ │ ├── GoogleAPIManager.java │ │ │ └── PresentationManager.java │ │ │ ├── scraper │ │ │ └── Scraper.java │ │ │ └── opencv │ │ │ └── OpenCvManager.java │ │ └── antlr │ │ └── Pregex.g4 ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties ├── WebApp.gs ├── README.md ├── build.gradle └── gradlew ├── snap-python ├── .gitignore ├── bootstrap.py ├── README.md └── util.py ├── snap-app ├── .gitignore ├── src │ ├── parser │ │ ├── index.js │ │ ├── cluesArea.css │ │ ├── output.css │ │ ├── documentImage.css │ │ ├── popup.css │ │ ├── parser.css │ │ ├── popup.js │ │ ├── cluesArea.js │ │ ├── advancedSettingsPopup.js │ │ ├── output.js │ │ └── exportPopup.js │ ├── solver │ │ ├── index.js │ │ ├── solver.css │ │ └── solver.js │ ├── findwords │ │ ├── index.js │ │ └── findwords.js │ ├── wordsearch │ │ ├── index.js │ │ └── wordsearch.css │ ├── index.css │ ├── fetch.js │ └── index.js ├── public │ ├── wrench.png │ └── index.html └── package.json ├── docs ├── blobs.gif ├── anagram.gif ├── crossword.gif ├── wordsearch.gif ├── bordered-grid.gif └── parse_crossword.gif ├── LICENSE └── README.md /snap-server/data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /snap-python/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | -------------------------------------------------------------------------------- /snap-server/data/tessdata/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /snap-app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /node_modules -------------------------------------------------------------------------------- /snap-python/bootstrap.py: -------------------------------------------------------------------------------- 1 | from util import * 2 | 3 | -------------------------------------------------------------------------------- /snap-app/src/parser/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './parser.js'; -------------------------------------------------------------------------------- /snap-app/src/solver/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './solver.js'; 2 | -------------------------------------------------------------------------------- /snap-app/src/findwords/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './findwords.js'; 2 | -------------------------------------------------------------------------------- /docs/blobs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/docs/blobs.gif -------------------------------------------------------------------------------- /snap-app/src/solver/solver.css: -------------------------------------------------------------------------------- 1 | .parsed { 2 | font-family: monospace; 3 | } 4 | -------------------------------------------------------------------------------- /snap-app/src/wordsearch/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './wordsearch.js'; 2 | -------------------------------------------------------------------------------- /docs/anagram.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/docs/anagram.gif -------------------------------------------------------------------------------- /docs/crossword.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/docs/crossword.gif -------------------------------------------------------------------------------- /docs/wordsearch.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/docs/wordsearch.gif -------------------------------------------------------------------------------- /docs/bordered-grid.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/docs/bordered-grid.gif -------------------------------------------------------------------------------- /docs/parse_crossword.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/docs/parse_crossword.gif -------------------------------------------------------------------------------- /snap-app/public/wrench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/snap-app/public/wrench.png -------------------------------------------------------------------------------- /snap-server/src/test/resources/pit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/snap-server/src/test/resources/pit.pdf -------------------------------------------------------------------------------- /snap-server/src/test/resources/digit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/snap-server/src/test/resources/digit.png -------------------------------------------------------------------------------- /snap-server/src/test/resources/dummy.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/snap-server/src/test/resources/dummy.pdf -------------------------------------------------------------------------------- /snap-server/src/test/resources/hugs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/snap-server/src/test/resources/hugs.pdf -------------------------------------------------------------------------------- /snap-server/src/test/resources/hugs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/snap-server/src/test/resources/hugs.png -------------------------------------------------------------------------------- /snap-server/src/test/resources/mats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/snap-server/src/test/resources/mats.png -------------------------------------------------------------------------------- /snap-server/src/test/resources/zsasz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/snap-server/src/test/resources/zsasz.png -------------------------------------------------------------------------------- /snap-server/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/snap-server/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /snap-server/src/test/resources/blood_bowl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/snap-server/src/test/resources/blood_bowl.png -------------------------------------------------------------------------------- /snap-server/src/test/resources/crosswerd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/snap-server/src/test/resources/crosswerd.png -------------------------------------------------------------------------------- /snap-server/src/test/resources/meatballs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/snap-server/src/test/resources/meatballs.pdf -------------------------------------------------------------------------------- /snap-server/src/test/resources/role_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/snap-server/src/test/resources/role_call.png -------------------------------------------------------------------------------- /snap-server/src/test/resources/kitchen_rush.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinychen/snap2/HEAD/snap-server/src/test/resources/kitchen_rush.pdf -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/document/Section.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.document; 2 | 3 | public record Section(int page, Rectangle rectangle) {} 4 | -------------------------------------------------------------------------------- /snap-app/src/parser/cluesArea.css: -------------------------------------------------------------------------------- 1 | .clues-area { 2 | display: inline-block; 3 | } 4 | 5 | .clues-area textarea { 6 | width: 100%; 7 | height: 250px; 8 | } 9 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/document/Rectangle.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.document; 2 | 3 | public record Rectangle(double x, double y, double width, double height) {} 4 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/crossword/ClueDirection.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.crossword; 2 | 3 | public enum ClueDirection { 4 | ACROSS, 5 | DOWN, 6 | UNKNOWN, 7 | } 8 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/grid/GridLines.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.grid; 2 | 3 | import java.util.TreeSet; 4 | 5 | public record GridLines(TreeSet horizontalLines, TreeSet verticalLines) {} 6 | -------------------------------------------------------------------------------- /snap-app/src/parser/output.css: -------------------------------------------------------------------------------- 1 | table.parser-output { 2 | border: 1px solid black; 3 | border-spacing: 0; 4 | display: inline-table; 5 | margin-right: 40px; 6 | } 7 | 8 | .parser-output tr, td { 9 | padding: 0; 10 | } 11 | -------------------------------------------------------------------------------- /snap-server/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/crossword/CrosswordFormula.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.crossword; 2 | 3 | public record CrosswordFormula( 4 | int row, 5 | int col, 6 | boolean formula, 7 | String value, 8 | Integer clueNumber) {} 9 | -------------------------------------------------------------------------------- /snap-server/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | *.ipr 4 | *.iws 5 | .DS_Store 6 | .classpath 7 | .gradle 8 | .project 9 | .settings 10 | /bin/ 11 | /build/ 12 | data 13 | google-api-credentials.json 14 | out 15 | src/main/java/com/kyc/snap/antlr 16 | src/main/resources/assets 17 | 18 | -------------------------------------------------------------------------------- /snap-server/gradle.properties: -------------------------------------------------------------------------------- 1 | draftSpreadsheetId=1i-C1fbfZiW5DpBDLiJuzT4tzBa_xfSB675IY3EM04Uw 2 | googleServerScriptUrl=https://script.google.com/macros/s/AKfycbx5K87z-a91q8-uLS6htQPEWjjMBmrvMrPtdENcF7QkECKQJvkw2IAdDQVwmc3-CAmybg/exec 3 | hostingServerOrigin = https://util.in 4 | platform=macosx-arm64 5 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/crossword/Crossword.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.crossword; 2 | 3 | import java.util.List; 4 | 5 | public record Crossword(int numRows, int numCols, List entries) { 6 | 7 | public record Entry(int startRow, int startCol, int numSquares, ClueDirection direction, int clueNumber) {} 8 | } 9 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/crossword/CrosswordClues.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.crossword; 2 | 3 | import java.util.List; 4 | 5 | public record CrosswordClues(List sections) { 6 | 7 | public record ClueSection(ClueDirection direction, List clues) {} 8 | 9 | public record NumberedClue(int clueNumber, String clue) {} 10 | } 11 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/store/Store.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.store; 2 | 3 | public interface Store { 4 | 5 | String storeBlob(byte[] blob); 6 | 7 | byte[] getBlob(String id); 8 | 9 | String storeObject(Object object); 10 | 11 | void updateObject(String id, Object newObject); 12 | 13 | T getObject(String id, Class clazz); 14 | } 15 | -------------------------------------------------------------------------------- /snap-app/src/parser/documentImage.css: -------------------------------------------------------------------------------- 1 | .image-container { 2 | position: relative; 3 | width: 97%; 4 | user-select: none; 5 | -webkit-user-select: none; 6 | } 7 | 8 | .image { 9 | max-height: 100%; 10 | width: 100%; 11 | border: 1px black solid; 12 | } 13 | 14 | .overlay-canvas { 15 | position: absolute; 16 | top: 0px; 17 | left: 0px; 18 | width: 100%; 19 | height: calc(100% - 5px); 20 | } 21 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/image/ImageBlob.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.image; 2 | 3 | import java.awt.Point; 4 | import java.util.Set; 5 | 6 | /** 7 | * @param fencePoints Points right outside the boundary of this blob, but not part of the blob. 8 | * @param innerPoint A point that when flood-filled inside the fence points, gives the entire blob. 9 | */ 10 | public record ImageBlob(int x, int y, int width, int height, Set fencePoints, Point innerPoint) {} 11 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/document/Document.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.document; 2 | 3 | import java.util.List; 4 | 5 | public record Document(String id, List pages) { 6 | 7 | /** 8 | * @param compressedImageId A compressed image to send over the network 9 | */ 10 | public record DocumentPage(String imageId, String compressedImageId, double scale, List texts) {} 11 | 12 | public record DocumentText(String text, Rectangle bounds) {} 13 | } 14 | -------------------------------------------------------------------------------- /snap-server/src/test/java/com/kyc/snap/words/DatamuseUtilTest.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.words; 2 | 3 | import org.junit.Test; 4 | 5 | import com.kyc.snap.words.DatamuseUtil.WordResult; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | public class DatamuseUtilTest { 10 | 11 | @Test 12 | public void testGetCommonWordsAfter() { 13 | assertThat(DatamuseUtil.getCommonWordsAfter("santa")) 14 | .extracting(WordResult::word) 15 | .contains("claus"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/server/FileResource.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.server; 2 | 3 | import com.kyc.snap.api.FileService; 4 | import com.kyc.snap.store.Store; 5 | 6 | public record FileResource(Store store) implements FileService { 7 | 8 | @Override 9 | public StringJson uploadFile(byte[] data) { 10 | String id = store.storeBlob(data); 11 | return new StringJson(id); 12 | } 13 | 14 | @Override 15 | public byte[] getFile(String fileId) { 16 | return store.getBlob(fileId); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/grid/GridPosition.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.grid; 2 | 3 | import java.util.List; 4 | 5 | import com.fasterxml.jackson.annotation.JsonIgnore; 6 | 7 | public record GridPosition(List rows, List cols) { 8 | 9 | @JsonIgnore 10 | public int getNumRows() { 11 | return rows.size(); 12 | } 13 | 14 | @JsonIgnore 15 | public int getNumCols() { 16 | return cols.size(); 17 | } 18 | 19 | public record Row(int startY, int height) {} 20 | 21 | public record Col(int startX, int width) {} 22 | } 23 | -------------------------------------------------------------------------------- /snap-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | Snap 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /snap-python/README.md: -------------------------------------------------------------------------------- 1 | Python 2 | ====== 3 | 4 | To use Snap's Python functions, add the following to your `.bashrc`: 5 | 6 | export PYTHONPATH=/path/to/snap2/snap-python 7 | alias snap='PYTHONSTARTUP=/path/to/snap2/snap-python/bootstrap.py python' 8 | 9 | You can then access the functions anywhere: 10 | 11 | $ python 12 | >>> from util import anagram 13 | >>> anagram('aaagmnr') 14 | ['anagram', 'mangara'] 15 | 16 | Or, start a Python console with the functions already boostrapped: 17 | 18 | $ snap 19 | >>> anagram('aaagmnr') 20 | ['anagram', 'mangara'] 21 | 22 | -------------------------------------------------------------------------------- /snap-server/src/test/java/com/kyc/snap/image/ImageUtilsTests.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.image; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.io.File; 5 | import java.io.IOException; 6 | 7 | import org.junit.Test; 8 | 9 | import javax.imageio.ImageIO; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | public class ImageUtilsTests { 14 | 15 | @Test 16 | public void testFindBlobs() throws IOException { 17 | BufferedImage image = ImageIO.read(new File("./src/test/resources/mats.png")); 18 | assertThat(ImageUtils.findBlobs(image, true)).hasSize(19); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/grid/Border.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.grid; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class Border { 6 | 7 | public static final Border NONE = new Border(-1, 0); 8 | 9 | public int rgb; 10 | public int width; 11 | 12 | /** 13 | * Styles are relative (e.g. thin/thick), so this field is null until it is filled by comparing 14 | * with other borders. 15 | */ 16 | public Style style = Style.NONE; 17 | 18 | public Border(@JsonProperty("rgb") int rgb, @JsonProperty("width") int width) { 19 | this.rgb = rgb; 20 | this.width = width; 21 | } 22 | 23 | public enum Style { 24 | NONE, 25 | THIN, 26 | MEDIUM, 27 | THICK, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /snap-server/src/test/java/com/kyc/snap/opencv/OpenCvManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.opencv; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.util.List; 5 | 6 | import org.junit.Test; 7 | 8 | import com.kyc.snap.opencv.OpenCvManager.Line; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | public class OpenCvManagerTest { 13 | 14 | final OpenCvManager openCv = new OpenCvManager(); 15 | 16 | @Test 17 | public void testFindLines() { 18 | BufferedImage image = new BufferedImage(100, 100, BufferedImage.TYPE_3BYTE_BGR); 19 | image.getGraphics().fillRect(50, 0, 50, 100); 20 | List lines = openCv.findLines(image, Math.PI / 2, 99); 21 | assertThat(lines).containsExactly(new Line(49., 99., 49., 0.)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/server/ServerProperties.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.server; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | 6 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 7 | import com.fasterxml.jackson.dataformat.javaprop.JavaPropsMapper; 8 | 9 | @JsonIgnoreProperties(ignoreUnknown = true) 10 | public record ServerProperties( 11 | String draftSpreadsheetId, 12 | String googleServerScriptUrl, 13 | String hostingServerOrigin) { 14 | 15 | public static ServerProperties get() { 16 | try { 17 | return new JavaPropsMapper().readValue(new File("gradle.properties"), ServerProperties.class); 18 | } catch (IOException e) { 19 | throw new RuntimeException(e); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/api/FileService.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.api; 2 | 3 | import com.fasterxml.jackson.annotation.JsonValue; 4 | 5 | import javax.ws.rs.Consumes; 6 | import javax.ws.rs.GET; 7 | import javax.ws.rs.POST; 8 | import javax.ws.rs.Path; 9 | import javax.ws.rs.PathParam; 10 | import javax.ws.rs.Produces; 11 | import javax.ws.rs.core.MediaType; 12 | 13 | @Path("/files") 14 | public interface FileService { 15 | 16 | @POST 17 | @Path("/") 18 | @Consumes(MediaType.WILDCARD) 19 | @Produces(MediaType.APPLICATION_JSON) 20 | StringJson uploadFile(byte[] data); 21 | 22 | record StringJson(@JsonValue String value) {} 23 | 24 | @GET 25 | @Path("/{fileId}") 26 | @Produces(MediaType.WILDCARD) 27 | byte[] getFile(@PathParam("fileId") String fileId); 28 | } 29 | -------------------------------------------------------------------------------- /snap-server/src/main/antlr/Pregex.g4: -------------------------------------------------------------------------------- 1 | grammar Pregex; 2 | 3 | @header{ 4 | package com.kyc.snap.antlr; 5 | } 6 | 7 | // '*' and '.' are the same and both mean "any letter" 8 | fragment CHAR : [*.A-Za-z]; 9 | fragment DIGIT : [0-9]; 10 | 11 | SYMBOL : CHAR; 12 | COUNT : DIGIT+; 13 | 14 | term 15 | : SYMBOL #Symbol 16 | | '<' terms '>' #Anagram 17 | | term '&' term #And 18 | | '\\chain(' terms ')' #Chain 19 | | '[' terms ']' #Choice 20 | | term '{' COUNT '}' #Count 21 | | '(' terms '~' terms ')' #Interleave 22 | | '(' terms ')' #List 23 | | term '?' #Maybe 24 | | term '+' #OneOrMore 25 | | term '|' term #Or 26 | | '"' terms '"' #Quote 27 | | '\\b' #WordBoundary 28 | ; 29 | terms : term+; 30 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/words/CustomDictionary.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.words; 2 | 3 | import java.util.Collection; 4 | import java.util.Map; 5 | import java.util.SortedMap; 6 | import java.util.TreeMap; 7 | 8 | public class CustomDictionary implements Dictionary { 9 | 10 | private final SortedMap wordFrequencies = new TreeMap<>(); 11 | 12 | public CustomDictionary(Collection words) { 13 | for (String word : words) 14 | wordFrequencies.put(word.toUpperCase().replaceAll("[^A-Z]+", ""), 1L); 15 | } 16 | 17 | @Override 18 | public SortedMap getWordFrequencies() { 19 | return wordFrequencies; 20 | } 21 | 22 | @Override 23 | public Map> getBiWordFrequencies() { 24 | return Map.of(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /snap-app/src/parser/popup.css: -------------------------------------------------------------------------------- 1 | 2 | .popup { 3 | position: absolute; 4 | z-index: 3; 5 | padding: 15px; 6 | background-color: white; 7 | border: 5px black solid; 8 | border-radius: 5px; 9 | left: 50%; 10 | top: 50%; 11 | transform: translate(-50%, -50%); 12 | } 13 | 14 | .submit-section { 15 | position: absolute; 16 | bottom: 15px; 17 | right: 15px; 18 | } 19 | 20 | .grayed-out { 21 | opacity: .2; 22 | } 23 | 24 | .hide { 25 | display: none; 26 | } 27 | 28 | .small-button { 29 | min-width: 36px; 30 | } 31 | 32 | .center { 33 | font-size: 18px; 34 | margin-bottom: 20px; 35 | font-weight: 800; 36 | } 37 | 38 | /* Individual popup features */ 39 | 40 | .advanced-settings-popup { 41 | width: 300px; 42 | height: 150px; 43 | } 44 | 45 | .export-popup { 46 | width: 500px; 47 | height: 250px; 48 | } 49 | -------------------------------------------------------------------------------- /snap-server/src/test/java/com/kyc/snap/document/PdfTest.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.document; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.IOException; 6 | import java.util.List; 7 | 8 | import org.junit.Test; 9 | 10 | import com.kyc.snap.document.Document.DocumentText; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | public class PdfTest { 15 | 16 | final File pdfFile = new File("src/test/resources/dummy.pdf"); 17 | final String expectedText = "Dummy PDF file"; 18 | 19 | @Test 20 | public void test() throws IOException { 21 | try (Pdf pdf = new Pdf(new FileInputStream(pdfFile))) { 22 | assertThat(pdf.getNumPages()).isEqualTo(1); 23 | 24 | List texts = pdf.getTexts(0); 25 | assertThat(texts.stream().map(DocumentText::text).reduce("", String::concat)).isEqualTo(expectedText); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /snap-app/src/wordsearch/wordsearch.css: -------------------------------------------------------------------------------- 1 | .wordsearch .grid { 2 | width: 100%; 3 | height: calc(100vh - 450px); 4 | } 5 | 6 | .wordsearch .word-bank { 7 | width: 100%; 8 | height: 150px; 9 | } 10 | 11 | .wordsearch .input table td { 12 | text-align: center; 13 | font-family: monospace; 14 | font-size: 17px; 15 | height: 20px; 16 | width: 20px; 17 | } 18 | 19 | .wordsearch .output { 20 | height: calc(100vh - 150px); 21 | overflow: scroll; 22 | user-select: none; 23 | } 24 | 25 | .wordsearch .output .link { 26 | display: inline-flex; 27 | font-family: monospace; 28 | border: solid 3px transparent; 29 | border-radius: 0; 30 | margin: 2px; 31 | padding: 3px 5px; 32 | width: 180px; 33 | } 34 | 35 | .wordsearch .unused-letters { 36 | font-family: monospace; 37 | width: 400px; 38 | height: 100px; 39 | } 40 | 41 | .wordsearch input[type=button] { 42 | margin-right: 2px; 43 | } -------------------------------------------------------------------------------- /snap-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snap-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "classnames": "^2.3.1", 7 | "react": "^18.2.0", 8 | "react-dom": "^18.2.0", 9 | "react-github-corner": "^2.5.0", 10 | "react-router-dom": "^6.18.0", 11 | "react-scripts": "^5.0.1" 12 | }, 13 | "devDependencies": { 14 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11" 15 | }, 16 | "scripts": { 17 | "start": "REACT_APP_LOCAL=true react-scripts start", 18 | "build": "BUILD_PATH='../snap-server/src/main/resources/assets' react-scripts build" 19 | }, 20 | "eslintConfig": { 21 | "extends": [ 22 | "react-app", 23 | "react-app/jest" 24 | ] 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/words/Dictionary.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.words; 2 | 3 | import java.util.Map; 4 | import java.util.Set; 5 | import java.util.SortedMap; 6 | 7 | import com.google.common.collect.ImmutableSortedMap; 8 | 9 | public interface Dictionary { 10 | 11 | /** Returns all words in the dictionary in upper case. */ 12 | SortedMap getWordFrequencies(); 13 | 14 | Map> getBiWordFrequencies(); 15 | 16 | default Set getWords() { 17 | return getWordFrequencies().keySet(); 18 | } 19 | 20 | default Map getWordFrequencies(String prefix) { 21 | return getWordFrequencies().subMap(prefix, prefix + Character.MAX_VALUE); 22 | } 23 | 24 | default Map getWordFrequencies(String prevWord, String prefix) { 25 | return getBiWordFrequencies() 26 | .getOrDefault(prevWord, ImmutableSortedMap.of()) 27 | .subMap(prefix, prefix + Character.MAX_VALUE); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /snap-server/src/test/java/com/kyc/snap/store/FileStoreTest.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.store; 2 | 3 | import java.util.List; 4 | 5 | import org.junit.Test; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | public class FileStoreTest { 10 | 11 | final FileStore store = new FileStore(); 12 | 13 | @Test 14 | public void storeAndGetBlob() { 15 | byte[] blob = {1, 2, 3}; 16 | String id = store.storeBlob(blob); 17 | assertThat(store.getBlob(id)).isEqualTo(blob); 18 | } 19 | 20 | @Test 21 | public void storeAndGetObject() { 22 | Object object = new Struct(1, true, List.of("a", "b")); 23 | String id = store.storeObject(object); 24 | assertThat(store.getObject(id, Struct.class)).isEqualTo(object); 25 | 26 | Object newObject = new Struct(2, false, List.of()); 27 | store.updateObject(id, newObject); 28 | assertThat(store.getObject(id, Struct.class)).isEqualTo(newObject); 29 | } 30 | 31 | private record Struct(int num, boolean flag, List values) {} 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 kyc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/grid/Grid.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.grid; 2 | 3 | import java.util.List; 4 | 5 | import com.fasterxml.jackson.annotation.JsonIgnore; 6 | 7 | public record Grid(int numRows, int numCols, Square[][] squares) { 8 | 9 | public Grid(int numRows, int numCols) { 10 | this(numRows, numCols, new Square[numRows][numCols]); 11 | for (int i = 0; i < numRows; i++) 12 | for (int j = 0; j < numCols; j++) 13 | squares[i][j] = new Square(); 14 | } 15 | 16 | @JsonIgnore 17 | public Square square(int row, int col) { 18 | return squares[row][col]; 19 | } 20 | 21 | public static class Square { 22 | 23 | public int rgb = -1; 24 | public String text = ""; 25 | public Border topBorder = Border.NONE; 26 | public Border rightBorder = Border.NONE; 27 | public Border bottomBorder = Border.NONE; 28 | public Border leftBorder = Border.NONE; 29 | 30 | @JsonIgnore 31 | public List borders() { 32 | return List.of(topBorder, rightBorder, bottomBorder, leftBorder); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /snap-server/src/test/java/com/kyc/snap/util/MathUtilTest.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.util; 2 | 3 | import java.math.BigInteger; 4 | import java.util.List; 5 | 6 | import org.junit.Test; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | public class MathUtilTest { 11 | 12 | @Test 13 | public void testFactorial() { 14 | assertThat(MathUtil.factorial(0)).isEqualTo(BigInteger.valueOf(1)); 15 | assertThat(MathUtil.factorial(1)).isEqualTo(BigInteger.valueOf(1)); 16 | assertThat(MathUtil.factorial(2)).isEqualTo(BigInteger.valueOf(2)); 17 | assertThat(MathUtil.factorial(3)).isEqualTo(BigInteger.valueOf(6)); 18 | assertThat(MathUtil.factorial(10)).isEqualTo(BigInteger.valueOf(3628800)); 19 | } 20 | 21 | @Test 22 | public void testNumRearrangements() { 23 | assertThat(MathUtil.numRearrangements(List.of())).isEqualTo(1); 24 | assertThat(MathUtil.numRearrangements(List.of(1))).isEqualTo(1); 25 | assertThat(MathUtil.numRearrangements(List.of(1, 2, 3))).isEqualTo(6); 26 | assertThat(MathUtil.numRearrangements(List.of(1, 2, 1, 2, 3))).isEqualTo(30); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /snap-app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 10px; 3 | } 4 | 5 | img[src="./wrench.png"] { 6 | width: 48px; 7 | height: 48px; 8 | } 9 | 10 | .title { 11 | font-size: 40px; 12 | font-weight: 600; 13 | margin-bottom: 30px; 14 | } 15 | 16 | .inline { 17 | margin-right: 5px; 18 | display: inline; 19 | } 20 | 21 | .block { 22 | margin-bottom: 20px; 23 | } 24 | 25 | .link { 26 | color: #000; 27 | background-color: #f1f1f1; 28 | font-size: 18px; 29 | border-radius: 10%; 30 | cursor: pointer; 31 | } 32 | 33 | .link.selected { 34 | background-color: #cccccc; 35 | } 36 | 37 | .loading { 38 | border: 3px solid #f3f3f3; 39 | animation: spin 1s linear infinite; 40 | border-top: 3px solid #555; 41 | border-radius: 50%; 42 | width: 20px; 43 | height: 20px; 44 | display: inline-block; 45 | } 46 | 47 | @keyframes spin { 48 | 0% { transform: rotate(0deg); } 49 | 100% { transform: rotate(360deg); } 50 | } 51 | 52 | .input, .output { 53 | float: left; 54 | margin-right: 20px; 55 | } 56 | 57 | .input { 58 | width: calc(45% - 50px); 59 | } 60 | 61 | .output { 62 | width: calc(55% - 50px); 63 | } 64 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/words/WikipediaTitlesDictionary.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.words; 2 | 3 | import java.io.File; 4 | import java.io.FileNotFoundException; 5 | import java.util.Map; 6 | import java.util.Scanner; 7 | import java.util.SortedMap; 8 | import java.util.TreeMap; 9 | 10 | public class WikipediaTitlesDictionary implements Dictionary { 11 | 12 | public static final String FILE = "./data/wikipedia-titles"; 13 | 14 | private final SortedMap wordFrequencies = new TreeMap<>(); 15 | 16 | @Override 17 | public SortedMap getWordFrequencies() { 18 | if (wordFrequencies.isEmpty()) { 19 | try (Scanner scanner = new Scanner(new File(FILE))) { 20 | while (scanner.hasNext()) 21 | wordFrequencies.put(scanner.next().toUpperCase().replaceAll("[^A-Z]+", " ").trim(), 1L); 22 | } catch(FileNotFoundException e) { 23 | throw new RuntimeException(e); 24 | } 25 | } 26 | return wordFrequencies; 27 | } 28 | 29 | @Override 30 | public Map> getBiWordFrequencies() { 31 | return Map.of(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /snap-server/src/test/java/com/kyc/snap/SnapTest.java: -------------------------------------------------------------------------------- 1 | 2 | package com.kyc.snap; 3 | 4 | import org.junit.Test; 5 | 6 | import com.kyc.snap.solver.EnglishModel; 7 | import com.kyc.snap.solver.PregexSolver; 8 | import com.kyc.snap.words.EnglishDictionary; 9 | import com.kyc.snap.words.WikipediaTitlesDictionary; 10 | 11 | public class SnapTest { 12 | 13 | EnglishDictionary dictionary = new EnglishDictionary(); 14 | WikipediaTitlesDictionary wikipedia = new WikipediaTitlesDictionary(); 15 | PregexSolver pregex = new PregexSolver(new EnglishModel(dictionary)); 16 | 17 | @Test 18 | public void test() throws Exception { 19 | // Files.lines(Paths.get("./test")) 20 | // .forEach(System.out::println); 21 | // Map wordFrequencies = dictionary.getWordFrequencies(); 22 | // wordFrequencies.forEach((word, freq) -> { 23 | // if (freq < 10) 24 | // return; 25 | // System.out.println(word); 26 | // }); 27 | // wikipedia.getWordFrequencies().forEach((word, freq) -> { 28 | // String[] split = word.split(" "); 29 | // if (Arrays.stream(split).map(String::length).toList().equals(List.of(1, 2, 3))) 30 | // System.out.println(word); 31 | // }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/server/HostingClient.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.server; 2 | 3 | import feign.Feign; 4 | import feign.Headers; 5 | import feign.Param; 6 | import feign.RequestLine; 7 | import feign.jackson.JacksonDecoder; 8 | 9 | public class HostingClient { 10 | 11 | private final String serverOrigin = ServerProperties.get().hostingServerOrigin(); 12 | 13 | /** 14 | * Hosts a file on the configured public server, and returns the URL to access the file. 15 | */ 16 | public String hostFile(String contentType, byte[] data) { 17 | String hostingBaseUrl = getHostingBaseUrl(); 18 | HostingClientService hosting = Feign.builder() 19 | .decoder(new JacksonDecoder()) 20 | .target(HostingClientService.class, hostingBaseUrl); 21 | String fileId = hosting.uploadFile(contentType, data); 22 | return String.format("%s/%s", hostingBaseUrl, fileId); 23 | } 24 | 25 | interface HostingClientService { 26 | 27 | @RequestLine("POST /") 28 | @Headers("Content-type: {contentType}") 29 | String uploadFile(@Param("contentType") String contentType, byte[] data); 30 | } 31 | 32 | private String getHostingBaseUrl() { 33 | return String.format("%s/api/files", serverOrigin); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /snap-app/src/parser/parser.css: -------------------------------------------------------------------------------- 1 | .toolbar_options { 2 | float: right; 3 | margin-right: 20px; 4 | } 5 | 6 | .radio { 7 | background-color: lightgray; 8 | margin-right: 0px; 9 | padding: 4px; 10 | border: 1px solid black; 11 | cursor: pointer; 12 | } 13 | 14 | .radio.selected { 15 | background-color: peachpuff; 16 | font-weight: 600; 17 | } 18 | 19 | .radio:not(.selected):hover { 20 | background-color: burlywood; 21 | } 22 | 23 | .radio:first-child { 24 | border-top-left-radius: 5px; 25 | border-bottom-left-radius: 5px; 26 | } 27 | 28 | .radio:last-child { 29 | border-top-right-radius: 5px; 30 | border-bottom-right-radius: 5px; 31 | } 32 | 33 | .button { 34 | display: inline-block; 35 | text-align: center; 36 | background-color: lightgray; 37 | border: 1px solid black; 38 | cursor: pointer; 39 | padding: 3px; 40 | border-radius: 5px; 41 | } 42 | 43 | .button:hover { 44 | background-color: burlywood; 45 | transform: scale(1.05); 46 | } 47 | 48 | .big { 49 | font-size: 24px; 50 | padding: 15px; 51 | margin: 10px; 52 | width: 180px; 53 | border-radius: 10px; 54 | } 55 | 56 | .advanced { 57 | display: inline-block; 58 | margin-left: 20px; 59 | } 60 | 61 | .hidden { 62 | visibility: hidden; 63 | } 64 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/solver/EnglishTokens.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.solver; 2 | 3 | public enum EnglishTokens { 4 | ; 5 | 6 | public static final int NUM_LETTERS = 26; 7 | private static final double[][] IS = new double[NUM_LETTERS][NUM_LETTERS + 1]; 8 | private static final double[][] PROBABLY = new double[NUM_LETTERS][NUM_LETTERS + 1]; 9 | private static final double[] WILDCARD = new double[NUM_LETTERS + 1]; 10 | private static final double[] WORD_DELIMITER = new double[NUM_LETTERS + 1]; 11 | static { 12 | for (char c = 'A'; c <= 'Z'; c++) { 13 | IS[c - 'A'][c - '@'] = 1; 14 | PROBABLY[c - 'A'][c - '@'] = .8; 15 | for (char cc = 'A'; cc <= 'Z'; cc++) 16 | if (cc != c) 17 | PROBABLY[c - 'A'][cc - '@'] = .2 / (NUM_LETTERS - 1); 18 | WILDCARD[c - '@'] = 1. / NUM_LETTERS; 19 | } 20 | WORD_DELIMITER[0] = 1; 21 | } 22 | 23 | public static double[] is(char c) { 24 | assert c >= 'A' && c <= 'Z'; 25 | return IS[c - 'A']; 26 | } 27 | 28 | public static double[] probably(char c) { 29 | assert c >= 'A' && c <= 'Z'; 30 | return PROBABLY[c - 'A']; 31 | } 32 | 33 | public static double[] wildcard() { 34 | return WILDCARD; 35 | } 36 | 37 | public static double[] wordDelimiter() { 38 | return WORD_DELIMITER; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /snap-app/src/fetch.js: -------------------------------------------------------------------------------- 1 | const SERVER = process.env.REACT_APP_LOCAL ? "http://localhost:8080/api" : "/api"; 2 | 3 | function fetchBackend(path, args, onSuccess, onError) { 4 | fetch(SERVER + path, args).then(response => { 5 | if (response.ok) { 6 | onSuccess && onSuccess(response); 7 | } else { 8 | response.json().then(body => { 9 | alert(body.message); 10 | }); 11 | } 12 | }); 13 | } 14 | 15 | function get(args, onSuccess, onError) { 16 | fetchBackend(args.path, { method: 'GET' }, onSuccess, onError); 17 | } 18 | exports.get = get; 19 | 20 | exports.getJson = function ( args, onSuccess, onError) { 21 | get({ path: args.path }, response => { 22 | response.json().then(body => { 23 | onSuccess && onSuccess(body); 24 | }); 25 | }, onError); 26 | } 27 | 28 | function post(args, onSuccess, onError) { 29 | fetchBackend(args.path, { 30 | method: 'POST', 31 | body: args.body, 32 | headers: args.headers, 33 | }, response => { 34 | response.json().then(body => { 35 | onSuccess && onSuccess(body); 36 | }); 37 | }, onError); 38 | } 39 | exports.post = post; 40 | 41 | exports.postJson = function (args, onSuccess, onError) { 42 | post({ 43 | path: args.path, 44 | body: JSON.stringify(args.body), 45 | headers: { 'Content-type': 'application/json' }, 46 | }, onSuccess, onError); 47 | } 48 | -------------------------------------------------------------------------------- /snap-server/src/test/java/com/kyc/snap/words/WordSearchSolverTest.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.words; 2 | 3 | import java.util.List; 4 | 5 | import org.junit.Test; 6 | 7 | import com.kyc.snap.words.WordSearchSolver.Result; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | public class WordSearchSolverTest { 12 | 13 | static final EnglishDictionary dictionary = new EnglishDictionary(); 14 | static final WordSearchSolver solver = new WordSearchSolver(); 15 | 16 | @Test 17 | public void findStraight() { 18 | assertThat(solver.find(List.of("ABC", "DEF", "GHI"), dictionary, false, List.of(3)).results()) 19 | .extracting(Result::word) 20 | .contains("FED") 21 | .doesNotContain("BEG"); 22 | } 23 | 24 | @Test 25 | public void findBoggle() { 26 | assertThat(solver.find(List.of("ABC", "DEF", "GHI"), dictionary, true, List.of(3)).results()) 27 | .extracting(Result::word) 28 | .contains("FED", "BEG"); 29 | } 30 | 31 | @Test 32 | public void findFuzzy() { 33 | assertThat(solver.find(List.of("AFB", "CUD", "EXF", "GZH", "IYJ"), dictionary, false, List.of(3, 5)).results()) 34 | .extracting(Result::word) 35 | .contains("FUZZY"); 36 | } 37 | 38 | @Test 39 | public void findNotAlpha() { 40 | assertThat(solver.find(List.of("ALPHA#"), dictionary, false, List.of(5)).results()) 41 | .extracting(Result::word) 42 | .contains("ALPHA"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /snap-app/src/parser/popup.js: -------------------------------------------------------------------------------- 1 | import * as classNames from "classnames"; 2 | import React from "react"; 3 | import "./popup.css" 4 | 5 | export class Popup extends React.Component { 6 | 7 | render() { 8 | return ( 9 |
12 | {this.renderContent()} 13 | {this.renderSubmitSection()} 14 |
15 | ); 16 | } 17 | 18 | renderSubmitSection() { 19 | return ( 20 |
21 | 22 | {this.renderSubmitButton("Submit")} 23 |
24 | ); 25 | } 26 | 27 | renderSubmitButton(text) { 28 | if (this.state.awaitingServer) { 29 | return ( 30 | 33 | ); 34 | } else { 35 | return ( 36 | 45 | ); 46 | } 47 | } 48 | 49 | exit = () => { 50 | const { exit } = this.props; 51 | this.finish(); 52 | exit(); 53 | } 54 | 55 | finish = () => this.setState({ awaitingServer: false }); 56 | } 57 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/util/MathUtil.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.util; 2 | 3 | import java.math.BigInteger; 4 | import java.util.ArrayList; 5 | import java.util.Collections; 6 | import java.util.HashSet; 7 | import java.util.List; 8 | import java.util.function.Consumer; 9 | 10 | public enum MathUtil { 11 | ; 12 | 13 | public static BigInteger factorial(int n) { 14 | BigInteger factorial = BigInteger.ONE; 15 | for (int i = 1; i <= n; i++) 16 | factorial = factorial.multiply(BigInteger.valueOf(i)); 17 | return factorial; 18 | } 19 | 20 | /** 21 | * Returns the number of distinct rearrangements of the given values. 22 | */ 23 | public static long numRearrangements(List values) { 24 | BigInteger numRearrangements = factorial(values.size()); 25 | for (T value : new HashSet<>(values)) 26 | numRearrangements = numRearrangements.divide(factorial(Collections.frequency(values, value))); 27 | return numRearrangements.longValue(); 28 | } 29 | 30 | public static void forEachPermutation(List objects, Consumer> f) { 31 | forEachPermutationHelper(new ArrayList<>(objects), new ArrayList<>(), f); 32 | } 33 | 34 | private static void forEachPermutationHelper(List objects, List permutation, Consumer> f) { 35 | if (objects.isEmpty()) { 36 | f.accept(permutation); 37 | } 38 | for (int i = 0; i < objects.size(); i++) { 39 | T obj = objects.remove(i); 40 | permutation.add(obj); 41 | forEachPermutationHelper(objects, permutation, f); 42 | objects.add(i, obj); 43 | permutation.remove(permutation.size() - 1); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /snap-server/src/test/java/com/kyc/snap/words/PhoneticsUtilTest.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.words; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | public class PhoneticsUtilTest { 8 | 9 | @Test 10 | public void testPhoneticDifference() { 11 | assertThat(PhoneticsUtil.difference("B", "B")).isEqualTo(0); 12 | assertThat(PhoneticsUtil.difference("", "UH")).isEqualTo(0); 13 | assertThat(PhoneticsUtil.difference("EY", "EH")).isEqualTo(0); 14 | 15 | assertThat(PhoneticsUtil.difference("B", "P")).isEqualTo(1); 16 | assertThat(PhoneticsUtil.difference("B", "V")).isEqualTo(1); 17 | assertThat(PhoneticsUtil.difference("CH", "JH")).isEqualTo(1); 18 | assertThat(PhoneticsUtil.difference("G", "K")).isEqualTo(1); 19 | assertThat(PhoneticsUtil.difference("T", "TH")).isEqualTo(1); 20 | assertThat(PhoneticsUtil.difference("D", "DH")).isEqualTo(1); 21 | assertThat(PhoneticsUtil.difference("AE", "EY")).isEqualTo(1); 22 | assertThat(PhoneticsUtil.difference("IY", "EH")).isEqualTo(1); 23 | assertThat(PhoneticsUtil.difference("AA", "AO")).isEqualTo(1); 24 | 25 | assertThat(PhoneticsUtil.difference("B", "F")).isEqualTo(2); 26 | assertThat(PhoneticsUtil.difference("S", "SH")).isEqualTo(2); 27 | assertThat(PhoneticsUtil.difference("N", "NG")).isEqualTo(2); 28 | assertThat(PhoneticsUtil.difference("L", "R")).isEqualTo(2); 29 | assertThat(PhoneticsUtil.difference("AA", "AE")).isEqualTo(2); 30 | assertThat(PhoneticsUtil.difference("", "IH")).isEqualTo(2); 31 | 32 | assertThat(PhoneticsUtil.difference("B", "L")).isGreaterThan(5); 33 | assertThat(PhoneticsUtil.difference("D", "")).isGreaterThan(5); 34 | assertThat(PhoneticsUtil.difference("S", "AA")).isGreaterThan(5); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /snap-server/WebApp.gs: -------------------------------------------------------------------------------- 1 | /** 2 | * Processes a POST request. The request body should contain a command that is one of the following: 3 | * 4 | * - insertImage 5 | * - removeAllImages 6 | */ 7 | function doPost(e) { 8 | const params = JSON.parse(e.postData.contents); 9 | 10 | if (params.command === "insertImage") { 11 | insertImage(params); 12 | } else if (params.command === "removeAllImages") { 13 | removeAllImages(params); 14 | } 15 | } 16 | 17 | /** 18 | * Add an image to a sheet. The params object should contain the following fields: 19 | * 20 | * spreadsheetId: ID of spreadsheet 21 | * sheetId: ID of sheet 22 | * url: url of image, e.g. "https://www.google.com/images/srpr/logo3w.png" 23 | * column: 1-indexed column, e.g. 1 24 | * row: 1-indexed row, e.g. 1 25 | * offsetX: x-offset from cell position, e.g. 0 26 | * offsetY: y-offset from cell position, e.g. 0 27 | * width: width of image in sheet, e.g. 100 28 | * height: height of image in sheet, e.g. 100 29 | */ 30 | function insertImage(params) { 31 | const sheet = getSheet(params.spreadsheetId, params.sheetId); 32 | const image = sheet.insertImage(params.url, params.column, params.row, params.offsetX, params.offsetY); 33 | image.setWidth(params.width); 34 | image.setHeight(params.height); 35 | } 36 | 37 | /** 38 | * Removes all overlay images from a sheet. The params object should contain the following fields: 39 | * 40 | * spreadsheetId: ID of spreadsheet 41 | * sheetId: ID of sheet 42 | */ 43 | function removeAllImages(params) { 44 | const sheet = getSheet(params.spreadsheetId, params.sheetId); 45 | sheet.getImages().map(image => image.remove()); 46 | } 47 | 48 | function getSheet(spreadsheetId, sheetId) { 49 | for (const sheet of SpreadsheetApp.openById(spreadsheetId).getSheets()) { 50 | if (sheet.getSheetId() === sheetId) { 51 | return sheet; 52 | } 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/words/DatamuseUtil.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.words; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import java.util.Set; 6 | import java.util.stream.Collectors; 7 | 8 | import com.google.common.collect.Sets; 9 | 10 | import feign.Feign; 11 | import feign.QueryMap; 12 | import feign.RequestLine; 13 | import feign.jackson.JacksonDecoder; 14 | 15 | public enum DatamuseUtil { 16 | ; 17 | 18 | public static List getCommonWordsAfter(String word) { 19 | return getCommonWordsAfter(word, "*"); 20 | } 21 | 22 | public static List getCommonWordsAfter(String word, String pattern) { 23 | DatamuseService datamuse = Feign.builder() 24 | .decoder(new JacksonDecoder()) 25 | .target(DatamuseService.class, "https://api.datamuse.com"); 26 | return datamuse.getWords(Map.of("sp", pattern, "lc", word)); 27 | } 28 | 29 | public static List getCommonWordsBefore(String word) { 30 | return getCommonWordsBefore(word, "*"); 31 | } 32 | 33 | public static List getCommonWordsBefore(String word, String pattern) { 34 | DatamuseService datamuse = Feign.builder() 35 | .decoder(new JacksonDecoder()) 36 | .target(DatamuseService.class, "https://api.datamuse.com"); 37 | return datamuse.getWords(Map.of("sp", pattern, "rc", word)); 38 | } 39 | 40 | public static Set getCommonWordsBetween(String before, String after) { 41 | return Sets.intersection( 42 | getCommonWordsAfter(before).stream().map(WordResult::word).collect(Collectors.toSet()), 43 | getCommonWordsBefore(after).stream().map(WordResult::word).collect(Collectors.toSet())); 44 | } 45 | 46 | public record WordResult(String word, int score) {} 47 | 48 | interface DatamuseService { 49 | 50 | @RequestLine("GET /words") 51 | List getWords(@QueryMap Map queryMap); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/words/EnglishDictionary.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.words; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.Scanner; 8 | import java.util.SortedMap; 9 | import java.util.TreeMap; 10 | 11 | public class EnglishDictionary implements Dictionary { 12 | 13 | public static final String WORD_FREQUENCIES_FILE = "./data/count_1w.txt"; 14 | public static final String BIWORD_FREQUENCIES_FILE = "./data/count_2w.txt"; 15 | /** 16 | * The value to scale frequencies in the biword frequencies file. This value was estimated 17 | * heuristically such that all biword frequencies (which are greater than 100,000) automatically 18 | * map to frequencies in the top 1 percentile of single word frequencies. 19 | */ 20 | public static final int BIWORD_FREQUENCY_MULTIPLIER = 10000; 21 | 22 | private final SortedMap wordFrequencies = new TreeMap<>(); 23 | private final Map> biwordFrequencies = new HashMap<>(); 24 | 25 | public EnglishDictionary() { 26 | try (Scanner scanner = new Scanner(new File(WORD_FREQUENCIES_FILE)); 27 | Scanner scanner2 = new Scanner(new File(BIWORD_FREQUENCIES_FILE))) { 28 | while (scanner.hasNext()) 29 | wordFrequencies.put(scanner.next().toUpperCase(), scanner.nextLong()); 30 | while (scanner2.hasNext()) { 31 | biwordFrequencies.computeIfAbsent(scanner2.next().toUpperCase(), key -> new TreeMap<>()) 32 | .put(scanner2.next().toUpperCase(), scanner2.nextLong() * BIWORD_FREQUENCY_MULTIPLIER); 33 | } 34 | } catch (IOException e) { 35 | throw new RuntimeException(e); 36 | } 37 | } 38 | 39 | public SortedMap getWordFrequencies() { 40 | return wordFrequencies; 41 | } 42 | 43 | public Map> getBiWordFrequencies() { 44 | return biwordFrequencies; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Snap 2 | ==== 3 | 4 | Snap provides advanced tooling for puzzle hunts. Try Snap at [util.in](https://util.in). 5 | 6 | Features 7 | -------- 8 | 9 | ### [Crossword tool](https://util.in/parser) 10 | 11 | Given a crossword puzzle page, Snap automatically detects the crossword grid and clues. 12 | 13 | ![Parsing a crossword](docs/parse_crossword.gif) 14 | 15 | Snap then hooks up your Google Sheet so that filling in a clue answer automatically fills in the grid and letters in orthogonal clues. 16 | 17 | ![Exported crossword](docs/crossword.gif) 18 | 19 | ### [Parse grids](https://util.in/parser) 20 | 21 | Snap can handle many types of images. For example, here is an image of a grid with borders: 22 | 23 | ![Bordered grid](docs/bordered-grid.gif) 24 | 25 | ### [Parse blobs](https://util.in/parser) 26 | 27 | Easily import jigsaw pieces or other moving components into Google Sheets. 28 | 29 | ![Blobs](docs/blobs.gif) 30 | 31 | ### [Heavy duty anagram solver](https://util.in/solver) 32 | 33 | Snap has a powerful solver engine with a deep understanding of English. This allows it to solve for phrases and even sentences, which traditional tools cannot do. 34 | 35 | ![Anagram](docs/anagram.gif) 36 | 37 | ### [Wordsearch solver](https://util.in/wordsearch) 38 | 39 | Find words in a grid, with a nice visual UI and without needing a word bank. Both straight words and boggle mode are supported. 40 | 41 | ![Word search](docs/wordsearch.gif) 42 | 43 | 44 | Instructions 45 | ------------ 46 | 47 | Instructions for the crossword tool [here](../../wiki/Crossword-tool-tutorial). 48 | 49 | Instructions for parsing other grids are similar to that of the crossword tool; instead of clicking "Parse crossword", choose "Parse grid". 50 | 51 | Instructions for the anagram solver [here](../../wiki/Heavy-duty-anagram-solver). 52 | 53 | 54 | Development 55 | ----------- 56 | 57 | In the [snap-app](snap-app) directory, run `yarn install`, then `yarn build` to build the frontend asset files. (You can also develop on the frontend only with `yarn start`.) 58 | 59 | Follow the instructions [here](snap-server/README.md) to setup and start the server. 60 | 61 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/store/FileStore.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.store; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.UUID; 6 | 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import com.google.common.io.Files; 9 | 10 | public class FileStore implements Store { 11 | 12 | public static final String BASE_DIR = "./data/store"; 13 | 14 | private final File baseDir; 15 | private final ObjectMapper mapper; 16 | 17 | public FileStore() { 18 | baseDir = new File(BASE_DIR); 19 | baseDir.mkdirs(); 20 | mapper = new ObjectMapper(); 21 | } 22 | 23 | @Override 24 | public String storeBlob(byte[] blob) { 25 | UUID id = UUID.randomUUID(); 26 | try { 27 | Files.write(blob, new File(baseDir, id.toString())); 28 | return id.toString(); 29 | } catch (IOException e) { 30 | throw new RuntimeException(e); 31 | } 32 | } 33 | 34 | @Override 35 | public byte[] getBlob(String id) { 36 | try { 37 | return Files.asByteSource(new File(baseDir, id)).read(); 38 | } catch (IOException e) { 39 | throw new RuntimeException(e); 40 | } 41 | } 42 | 43 | @Override 44 | public String storeObject(Object object) { 45 | try { 46 | byte[] blob = mapper.writeValueAsBytes(object); 47 | return storeBlob(blob); 48 | } catch (IOException e) { 49 | throw new RuntimeException(e); 50 | } 51 | } 52 | 53 | @Override 54 | public void updateObject(String id, Object newObject) { 55 | try { 56 | byte[] blob = mapper.writeValueAsBytes(newObject); 57 | Files.write(blob, new File(baseDir, id)); 58 | } catch (IOException e) { 59 | throw new RuntimeException(e); 60 | } 61 | } 62 | 63 | @Override 64 | public T getObject(String id, Class clazz) { 65 | byte[] blob = getBlob(id); 66 | try { 67 | return mapper.readValue(blob, clazz); 68 | } catch (IOException e) { 69 | throw new RuntimeException(e); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /snap-server/src/test/java/com/kyc/snap/scraper/ScraperTest.java: -------------------------------------------------------------------------------- 1 | 2 | package com.kyc.snap.scraper; 3 | 4 | import java.io.IOException; 5 | import java.nio.charset.StandardCharsets; 6 | import java.util.HashSet; 7 | import java.util.Set; 8 | 9 | import org.apache.commons.io.IOUtils; 10 | import org.jsoup.nodes.Element; 11 | import org.junit.Test; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | 15 | /** 16 | * Tips for writing a scraper. 17 | * 18 | *

Ensure that Chrome is running with the remote debugging port on: 19 | * 20 | *

21 |  * "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222
22 |  * 
23 | * 24 | * To get a selector: right-click on an element, click Inspect, right click again, and click "Copy 25 | * as selector". 26 | */ 27 | public class ScraperTest extends Scraper { 28 | 29 | @Test 30 | public void test() { 31 | newTab("https://www.faa.gov/air_traffic/flight_info/aeronav/aero_data/Loc_ID_Search/Fixes_Waypoints/"); 32 | sleep(1000); 33 | js("$('select').val(1000)"); 34 | Set ids = new HashSet<>(); 35 | while (true) { 36 | Set newIds = new HashSet<>(ids); 37 | for (Element el : html().select("div > main > article > div > #contentTable > tbody > tr")) 38 | newIds.add(el.select("td:nth-child(1)").text()); 39 | if (newIds.equals(ids)) 40 | break; 41 | click("#pageBar > li.next > a"); 42 | sleep(500); 43 | ids = newIds; 44 | } 45 | assertThat(ids.size()).isGreaterThan(60000); 46 | assertThat(ids).contains("AAALL", "LAMBY", "ZZYZX"); 47 | } 48 | 49 | String getHtml() { 50 | try { 51 | Process process = Runtime.getRuntime().exec(new String[]{"osascript", "-e", 52 | "tell application \"Google Chrome\" to set sourceHTML to execute " 53 | + "front window's active tab javascript \"document.documentElement.outerHTML\""}); 54 | return IOUtils.toString(process.getInputStream(), StandardCharsets.UTF_8); 55 | } catch (IOException e) { 56 | throw new RuntimeException(e); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/google/GoogleAPIManager.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.google; 2 | 3 | import java.io.FileInputStream; 4 | import java.io.IOException; 5 | import java.security.GeneralSecurityException; 6 | import java.util.Set; 7 | 8 | import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; 9 | import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; 10 | import com.google.api.client.http.HttpTransport; 11 | import com.google.api.client.json.JsonFactory; 12 | import com.google.api.client.json.jackson2.JacksonFactory; 13 | import com.google.api.services.sheets.v4.Sheets; 14 | import com.google.api.services.sheets.v4.SheetsScopes; 15 | import com.google.api.services.slides.v1.Slides; 16 | 17 | public class GoogleAPIManager { 18 | 19 | public static final String CREDENTIALS_FILE = "./google-api-credentials.json"; 20 | 21 | private static final String APPLICATION_NAME = "Snap"; 22 | private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance(); 23 | 24 | private final GoogleCredential credential; 25 | private final Sheets sheets; 26 | private final Slides slides; 27 | 28 | public GoogleAPIManager() { 29 | try { 30 | HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport(); 31 | credential = GoogleCredential.fromStream(new FileInputStream(CREDENTIALS_FILE)) 32 | .createScoped(Set.of(SheetsScopes.DRIVE)); 33 | sheets = new Sheets.Builder(httpTransport, JSON_FACTORY, credential) 34 | .setApplicationName(APPLICATION_NAME) 35 | .build(); 36 | slides = new Slides.Builder(httpTransport, JSON_FACTORY, credential) 37 | .setApplicationName(APPLICATION_NAME) 38 | .build(); 39 | } catch (IOException | GeneralSecurityException e) { 40 | throw new RuntimeException(e); 41 | } 42 | } 43 | 44 | public SpreadsheetManager getSheet(String spreadsheetId, int sheetId) { 45 | return new SpreadsheetManager(credential, sheets.spreadsheets(), spreadsheetId, sheetId); 46 | } 47 | 48 | public PresentationManager getPresentation(String presentationId, String slideId) { 49 | return new PresentationManager(credential, slides.presentations(), presentationId, slideId); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/solver/GenericSolver.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.solver; 2 | 3 | import java.util.List; 4 | import java.util.function.BiConsumer; 5 | 6 | import javax.annotation.Nullable; 7 | 8 | public interface GenericSolver { 9 | 10 | int MAX_NUM_RESULTS = 100; 11 | 12 | /** 13 | * We're given a starting state and an object representing all possible transitions between states. 14 | * 15 | *

In each transition, a token is emitted with various probabilities. This method returns the most likely 16 | * sequence of tokens, given those probabilities, and the prior probabilities from the model. 17 | * 18 | *

For example, tokens may be letters, and a regex of "A." corresponds to a two-state FSM where the first state 19 | * emits A with 100% probability, and the second state emits all letters with equal probability. The prior would be 20 | * the likely n-grams and words in English. 21 | */ 22 | List solve(State start, Transitions transitions, PriorModel model); 23 | 24 | /** 25 | * For each state (first argument), calls the second argument on all possible next states (with the corresponding 26 | * token emission probabilities). For example, 27 | * 28 | *

29 |      * (state, transitions) -> {
30 |      *     // if we transition to state + 1, we emit token 0 or token 1 with equal probability
31 |      *     transitions.add(state + 1, [0.5, 0.5]);
32 |      *
33 |      *     // if we transition to state + 2, we emit token 0 with 100% probability
34 |      *     transitions.add(state + 2, [1.0, 0.0]);
35 |      * }
36 |      * 
37 | */ 38 | interface Transitions extends BiConsumer> {} 39 | 40 | interface TransitionConsumer { 41 | /** 42 | * Specifies a possible transition. A null nextState corresponds to the end state. A null tokenProbabilities 43 | * means that no token is emitted in this transition. 44 | */ 45 | void add(@Nullable State nextState, @Nullable double[] tokenProbabilities); 46 | } 47 | 48 | interface PriorModel { 49 | double[] getProbabilities(List tokens); 50 | 51 | String toMessage(List tokens); 52 | } 53 | 54 | record Result(String message, double score) {} 55 | } 56 | -------------------------------------------------------------------------------- /snap-server/src/main/java/com/kyc/snap/words/EnglishTrie.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.words; 2 | 3 | import java.util.Collection; 4 | 5 | import com.google.common.collect.ArrayListMultimap; 6 | import com.google.common.collect.Multimap; 7 | import com.kyc.snap.solver.EnglishTokens; 8 | 9 | public class EnglishTrie { 10 | 11 | public static final int NO_NODE = 0; 12 | 13 | private final Multimap wordsByNodeIndex = ArrayListMultimap.create(); 14 | private int[][] nodes; 15 | private int size = 1; 16 | 17 | public static EnglishTrie of(Dictionary dictionary, int maxNumDeletions) { 18 | EnglishTrie trie = new EnglishTrie(); 19 | for (String word : dictionary.getWords()) { 20 | trie.add(word, word); 21 | 22 | if (maxNumDeletions >= 1) 23 | for (int i = 0; i < word.length(); i++) 24 | trie.add(word.substring(0, i) + word.substring(i + 1), word); 25 | } 26 | return trie; 27 | } 28 | 29 | private EnglishTrie() { 30 | this.nodes = new int[1 << 20][EnglishTokens.NUM_LETTERS]; 31 | } 32 | 33 | public int startNodeIndex() { 34 | return 1; 35 | } 36 | 37 | public int getNodeIndex(int nodeIndex, char c) { 38 | return nodeIndex < nodes.length ? nodes[nodeIndex][c - 'A'] : NO_NODE; 39 | } 40 | 41 | public Collection getWords(int nodeIndex) { 42 | return wordsByNodeIndex.get(nodeIndex); 43 | } 44 | 45 | private void add(String fuzzyWord, String word) { 46 | int nodeIndex = startNodeIndex(); 47 | for (int i = 0; i < fuzzyWord.length(); i++) { 48 | int index = fuzzyWord.charAt(i) - 'A'; 49 | ensureSize(nodeIndex); 50 | if (nodes[nodeIndex][index] == 0) 51 | nodes[nodeIndex][index] = ++size; 52 | nodeIndex = nodes[nodeIndex][index]; 53 | } 54 | wordsByNodeIndex.put(nodeIndex, word); 55 | } 56 | 57 | private void ensureSize(int nodeIndex) { 58 | if (nodeIndex >= nodes.length) { 59 | int[][] prevNodes = nodes; 60 | nodes = new int[nodes.length * 2][nodes[0].length]; 61 | for (int i = 0; i < prevNodes.length; i++) 62 | System.arraycopy(prevNodes[i], 0, nodes[i], 0, nodes[0].length); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /snap-server/src/test/java/com/kyc/snap/grid/GridParserTests.java: -------------------------------------------------------------------------------- 1 | package com.kyc.snap.grid; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.io.File; 5 | import java.io.FileInputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | 9 | import org.junit.Test; 10 | 11 | import com.kyc.snap.document.Pdf; 12 | import com.kyc.snap.opencv.OpenCvManager; 13 | 14 | import javax.imageio.ImageIO; 15 | 16 | import static org.assertj.core.api.Assertions.assertThat; 17 | 18 | public class GridParserTests { 19 | 20 | final GridParser gridParser = new GridParser(new OpenCvManager()); 21 | 22 | @Test 23 | public void testFindGridLines() throws IOException { 24 | testFindGridLinesHelper("crosswerd.png", 15, 16); 25 | testFindGridLinesHelper("hugs.png", 19, 13); 26 | testFindGridLinesHelper("role_call.png", 18, 42); 27 | testFindGridLinesHelper("zsasz.png", 13, 14); 28 | testFindGridLinesHelper("kitchen_rush.pdf", 17, 17); 29 | testFindGridLinesHelper("pit.pdf", 12, 12); 30 | testFindGridLinesHelper("meatballs.pdf", 17, 17); 31 | testFindGridLinesHelper("hugs.pdf", 19, 13); 32 | } 33 | 34 | @Test 35 | public void testFindImplicitGridLines() throws IOException { 36 | BufferedImage image = getImage("./src/test/resources/blood_bowl.png"); 37 | GridLines gridLines = gridParser.findImplicitGridLines(image); 38 | assertThat(gridLines.horizontalLines().size() - 1).isEqualTo(18); 39 | assertThat(gridLines.verticalLines().size() - 1).isEqualTo(24); 40 | } 41 | 42 | void testFindGridLinesHelper(String filename, int numRows, int numCols) throws IOException { 43 | BufferedImage image = getImage("./src/test/resources/" + filename); 44 | GridLines gridLines = gridParser.findGridLines(image); 45 | gridLines = gridParser.getInterpolatedGridLines(gridLines); 46 | assertThat(gridLines.horizontalLines().size() - 1).isEqualTo(numRows); 47 | assertThat(gridLines.verticalLines().size() - 1).isEqualTo(numCols); 48 | } 49 | 50 | BufferedImage getImage(String path) throws IOException { 51 | if (path.endsWith(".png")) { 52 | return ImageIO.read(new File(path)); 53 | } else if (path.endsWith(".pdf")) { 54 | try (InputStream in = new FileInputStream(path); Pdf pdf = new Pdf(in)) { 55 | return pdf.toImage(0); 56 | } 57 | } 58 | throw new IllegalArgumentException(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /snap-server/README.md: -------------------------------------------------------------------------------- 1 | ## Development 2 | 3 | ### One-time setup 4 | 5 | To develop on and run the Snap server, you need JDK 11+. All commands should be run in this directory (snap-server). 6 | 7 | - Update the [gradle.properties](gradle.properties) configuration file with your platform, one of `linux-x86_64`, `macosx-x86_64`, or `windows-x86_64`. 8 | 9 | - Download required data files by running `./gradlew downloadFiles`. 10 | 11 | #### Setup Google API service account 12 | 13 | Running the crossword tool or grid parser requires a Google API service account. 14 | 15 | - Visit the [Google Cloud Platform](https://console.cloud.google.com/home) and create a project. In the "IAM & admin" tab, select "Service accounts" and click "Create service account". You can use "sheets-creator" as the name. Note the service account ID; this is the email address that you must share your Google Sheets with to use Snap. 16 | 17 | - Download a credentials file for the service account. Click "Create key" and download the JSON file. Name it `google-api-credentials.json` and save it in this directory. 18 | 19 | - Allow the service user to use Google Drive APIs. In the navigation menu of the Google Cloud console page, select "APIs & Services" and click "Enable APIs and services". Enable both the Google Drive API and Google Sheets API. 20 | 21 | - Adding overlay images is not well-supported in the Google Sheets API. The current workaround is to create a new [App Script](http://script.google.com). Give your service account access to the script's container Google Sheet, then copy the contents of [WebApp.gs](WebApp.gs) into the script. Then, in the "Publish" menu, click "Deploy as web app" and select "Execute the app as:" yourself instead of the user accessing. A popup dialog will display the URL of the published web app; it looks like `https://script.google.com/macros/s/.../exec`. Copy the URL into the `googleServerScriptUrl` field in the [gradle.properties](gradle.properties) file. Also update the `draftSpreadsheetId` field with the spreadsheet ID of the script container Google Sheet. Finally, in the "Run" menu, run any function and click through the Google popup to allow the script to be run. 22 | 23 | #### Install Google Chrome binary 24 | 25 | This is needed for running the crossword tool or grid parser with HTML URLs (it is not needed for images or PDFs). 26 | 27 | - Steps for Ubuntu can be found [here](https://blog.softhints.com/ubuntu-16-04-server-install-headless-google-chrome/). 28 | 29 | ### Starting the server 30 | 31 | Run `./gradlew run`. 32 | 33 | Visit the app at http://localhost:8080. 34 | 35 | -------------------------------------------------------------------------------- /snap-app/src/parser/cluesArea.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./cluesArea.css"; 3 | 4 | export class CluesArea extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | localChanges: undefined, 10 | }; 11 | this.timeout = undefined; 12 | } 13 | 14 | render() { 15 | const { crosswordClues, crosswordCluesInferred } = this.props; 16 | const { localChanges } = this.state; 17 | return
18 |

Enter crossword clues

19 | {crosswordCluesInferred 20 | ?

Clues automatically found! Make any fixes if needed here.

21 | :

The format is flexible, so copying and pasting directly from the puzzle probably works.

} 22 |