├── cross2sheet ├── __init__.py ├── test │ ├── __init__.py │ ├── test.py │ └── testdata.py ├── examples │ ├── __init__.py │ └── examples.py ├── web │ ├── download.py │ ├── templates │ │ ├── select.html │ │ └── convert.html │ ├── serial.py │ ├── __init__.py │ └── render.py ├── transforms.py ├── html14.py ├── htmltable.py ├── grid_features.py ├── excel.py ├── analysis.py ├── main.py └── image.py ├── .gitignore ├── .travis.yml ├── setup.py ├── LICENSE └── README.md /cross2sheet/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cross2sheet/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cross2sheet/examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.egg-info/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: python 4 | python: 5 | - "3.5" 6 | 7 | before_install: 8 | - pip install -U pip 9 | 10 | install: 11 | - pip install -Ue . 12 | - sudo apt-get -qq update 13 | - sudo apt-get -qq install ghostscript 14 | 15 | script: 16 | - python -m unittest -q cross2sheet.test.testdata 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | VERSION = "0.1.0" 3 | 4 | from setuptools import find_packages, setup 5 | 6 | setup( 7 | name="cross2sheet", 8 | version=VERSION, 9 | packages=find_packages(), 10 | install_requires=[ 11 | 'beautifulsoup4', 12 | 'numpy', 13 | 'openpyxl', 14 | 'opencv-python', 15 | 'Wand', 16 | ], 17 | extras_require = { 18 | 'web': ['Flask'], 19 | }, 20 | ) 21 | -------------------------------------------------------------------------------- /cross2sheet/web/download.py: -------------------------------------------------------------------------------- 1 | from cross2sheet.web.serial import TableData 2 | from cross2sheet.grid_features import Grid 3 | from cross2sheet.transforms import autonumber, outside_bars 4 | from cross2sheet.excel import to_openpyxl 5 | from io import BytesIO 6 | 7 | def form_data_to_excel(form): 8 | td=TableData.from_json(form['data']) 9 | g=Grid(td.height,td.width) 10 | if 'back' in form: 11 | g.features.extend(td.back) 12 | if 'bar' in form: 13 | g.features.extend(td.bars) 14 | if form['auto']=='text': 15 | g.features.extend(td.text) 16 | g.validate() 17 | if form['auto']=='auto': 18 | g.features.extend(autonumber(g)) 19 | g.features.extend(outside_bars(g)) 20 | i=BytesIO() 21 | to_openpyxl(g,text_in_cells='cells' in form,text_in_comments='comments' in form).save(i) 22 | i.seek(0) 23 | return i 24 | -------------------------------------------------------------------------------- /cross2sheet/web/templates/select.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Crossword grid parser

5 | {% if error_msg %} 6 |

Error: {{ error_msg }}

7 | {% endif %} 8 |

This program takes an image of a crossword grid and converts it into 9 | a spreadsheet that can be uploaded to Google Sheets.

10 |
Image files:
11 |

Right click on the crossword grid, select, "Save Image As", and then upload the image here. 12 |

13 |
PDF files:
14 |

Download the PDF, and then upload it here.

15 |
Otherwise:
16 |

Take a screenshot. Cropping the screenshot might help but usually isn't necessary.

17 |

Upload an image file

18 |
19 |
20 | 21 |
22 | {% if config.URLOPEN_ENABLED %} 23 |

Specify an image URL

24 |
25 |
26 | 27 |
28 | {% endif %} 29 | 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 Daniel Gulotta 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 | 23 | -------------------------------------------------------------------------------- /cross2sheet/web/serial.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from cross2sheet.grid_features import BackgroundElt,TextElt,BorderElt 4 | 5 | class TableData: 6 | def __init__(self,width=None,height=None,back=None,bars=None,text=None,*,img=None): 7 | if img: 8 | g=img.grid() 9 | self.width=g.width 10 | self.height=g.height 11 | self.back=img.read_background() 12 | self.bars=img.read_bars() 13 | self.text=img.autonumber_if_text_found() 14 | else: 15 | self.width=width 16 | self.height=height 17 | self.back=back 18 | self.bars=bars 19 | self.text=text 20 | 21 | def to_json(self): 22 | d={ 23 | 'width' : self.width, 24 | 'height' : self.height, 25 | 'back' : [(r,c,e.color) for r,c,e in self.back], 26 | 'bars' : [(r,c,e.dirs) for r,c,e in self.bars], 27 | 'text' : [(r,c,e.text) for r,c,e in self.text] 28 | } 29 | return json.dumps(d) 30 | 31 | @staticmethod 32 | def from_json(s): 33 | d=json.loads(s) 34 | return TableData(width=d['width'],height=d['height'], 35 | back=[(r,c,BackgroundElt(b)) for r,c,b in d['back']], 36 | bars=[(r,c,BorderElt(b)) for r,c,b in d['bars']], 37 | text=[(r,c,TextElt(t)) for r,c,t in d['text']]) 38 | -------------------------------------------------------------------------------- /cross2sheet/transforms.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from cross2sheet.grid_features import * 3 | from cross2sheet.analysis import GridAnalyzer 4 | 5 | def autonumber(grid): 6 | numbers = [] 7 | analyzer = GridAnalyzer(grid) 8 | n=itertools.count(1) 9 | for y,x in analyzer.squares(): 10 | if (y,x) in analyzer.filled: 11 | continue 12 | if (analyzer.left_blocked(y,x) and not analyzer.right_blocked(y,x)) or (analyzer.top_blocked(y,x) and not analyzer.bottom_blocked(y,x)): 13 | numbers.append((y,x,TextElt(str(next(n))))) 14 | return numbers 15 | 16 | def _elt_xcoord(y,x,elt): 17 | if isinstance(elt,BorderElt) and frozenset(elt.dirs)==frozenset('L'): 18 | return x-1 19 | return x 20 | 21 | def _elt_ycoord(y,x,elt): 22 | if isinstance(elt,BorderElt) and frozenset(elt.dirs)==frozenset('T'): 23 | return y-1 24 | return y 25 | 26 | def outside_bars(grid): 27 | new_elts=[] 28 | for n in range(grid.height): 29 | new_elts.append((n,0,BorderElt('L'))) 30 | new_elts.append((n,grid.width-1,BorderElt('R'))) 31 | for n in range(grid.width): 32 | new_elts.append((0,n,BorderElt('T'))) 33 | new_elts.append((grid.height-1,n,BorderElt('B'))) 34 | return new_elts 35 | 36 | def pad(grid,rows,cols): 37 | grid.height+=rows 38 | grid.width+=cols 39 | grid.features=[(y+rows,x+cols,e) for y,x,e in grid.features] 40 | -------------------------------------------------------------------------------- /cross2sheet/html14.py: -------------------------------------------------------------------------------- 1 | """ 2 | Reads grids that are formatted in HTML in the same way as those from the 2014-2015 MIT 3 | Mystery Hunts (eg http://web.mit.edu/puzzle/www/2014/puzzle/sledgehammered/). 4 | """ 5 | 6 | import bisect, re 7 | from cross2sheet.grid_features import * 8 | from bs4 import BeautifulSoup 9 | 10 | _coords_re = re.compile(r'left:(\d+)px;top:(\d+)px;') 11 | _color_re = re.compile(r'border-top:#([0-9A-Fa-f]+)') 12 | 13 | def _coords(style): 14 | x,y = _coords_re.search(style).groups() 15 | return (int(y),int(x)) 16 | 17 | def _color(style): 18 | m=_color_re.search(style) 19 | return int(m.groups()[0],16) if m else 0 20 | 21 | def parse_html_grid(text): 22 | soup=BeautifulSoup(text) 23 | elts=[] 24 | xstartset=set() 25 | ystartset=set() 26 | for elt in soup.find_all('div',attrs={'class':'bk'}): 27 | y,x=_coords(elt.attrs['style']) 28 | col=_color(elt.attrs['style']) 29 | elts.append((y,x,BackgroundElt(col))) 30 | xstartset.add(x) 31 | ystartset.add(y) 32 | for elt in soup.find_all('div',attrs={'class':'nu'}): 33 | y,x=_coords(elt.attrs['style']) 34 | elts.append((y,x,TextElt(elt.text))) 35 | xstarts=sorted(xstartset) 36 | ystarts=sorted(ystartset) 37 | g=Grid(len(xstarts),len(ystarts)) 38 | g.features=[(bisect.bisect_right(xstarts,x)-1,bisect.bisect_right(ystarts,y)-1,c) for y,x,c in elts] 39 | return g 40 | -------------------------------------------------------------------------------- /cross2sheet/examples/examples.py: -------------------------------------------------------------------------------- 1 | from urllib.request import urlopen 2 | from cross2sheet.html14 import parse_html_grid 3 | from cross2sheet.htmltable import parse_html_table 4 | from cross2sheet.excel import save_xlsx, to_openpyxl_multi 5 | from cross2sheet.image import ImageGrid 6 | from cross2sheet.transforms import autonumber, outside_bars 7 | 8 | def ridfill(): 9 | req=urlopen('http://web.mit.edu/puzzle/www/2015/puzzle/rid_fill/') 10 | data=req.read() 11 | req.close() 12 | save_xlsx(parse_html_grid(data),'ridfill.xlsx') 13 | 14 | def fill_in_the_blanks(): 15 | req=urlopen('http://web.mit.edu/puzzle/www/2013/coinheist.com/get_smart/fill_in_the_blanks/index.html') 16 | data=req.read().decode() 17 | req.close() 18 | tables = data.split('') 19 | grids=[parse_html_table(t,styleattr='class',styledict={'b':0}) for t in tables] 20 | to_openpyxl_multi(grids[:-1]).save('fill_in_the_blanks.xlsx') 21 | 22 | def the_wicked_switch(): 23 | req=urlopen('http://web.mit.edu/puzzle/www/2012/puzzles/a_circus_line/the_wicked_switch/1.png') 24 | data=req.read() 25 | req.close() 26 | img=ImageGrid(data) 27 | grid=img.grid() 28 | grid.features.extend(img.read_background()) 29 | grid.features.extend(img.read_text_ocr()) 30 | grid.features.extend(outside_bars(grid)) 31 | save_xlsx(grid,'the_wicked_switch.xlsx') 32 | 33 | def a_puzzle_with_answer_nowhere_man(): 34 | req=urlopen('http://web.mit.edu/puzzle/www/2014/puzzle/puzzle_with_answer_nowhere_man/grid.png') 35 | data=req.read() 36 | req.close() 37 | img=ImageGrid(data) 38 | grid=img.grid() 39 | grid.features.extend(img.read_background()) 40 | grid.features.extend(img.read_bars()) 41 | grid.features.extend(outside_bars(grid)) 42 | grid.features.extend(autonumber(grid)) 43 | save_xlsx(grid,'nowhere_man.xlsx') 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cross2sheet [![Build Status](https://travis-ci.org/dgulotta/cross2sheet.svg?branch=master)](https://travis-ci.org/dgulotta/cross2sheet) 2 | =========== 3 | 4 | Features 5 | ======== 6 | The program reads a crossword grid and converts it into a spreadsheet. 7 | 8 | Supported input formats: 9 | * image files (any format supported by OpenCV or ImageMagick) 10 | * html, in the format that was used in the 2014-2015 MIT Mystery Hunts (see for example http://web.mit.edu/puzzle/www/2014/puzzle/sledgehammered/) 11 | * html tables, if the style information is encoded in a sufficiently simple way 12 | 13 | Supported output formats: 14 | * xlsx 15 | 16 | Installing 17 | ========== 18 | 19 | To install the required dependencies, run 20 | ``` 21 | pip install -e .[web] 22 | ``` 23 | You can remove the `web` option if you don't want to use the web interface. 24 | 25 | Web interface 26 | ============= 27 | 28 | Cross2sheet can be used via a web interface, as a command line program, or 29 | as a library. The web interface is the easiest to use. The code for the web 30 | interface can be found in the `cross2sheet/web` directory. 31 | 32 | To test the web interface locally: 33 | ``` 34 | export FLASK_APP=cross2sheet/web/__init__.py 35 | flask run 36 | ``` 37 | 38 | Instructions for deploying Flask applications can be found at 39 | https://flask.palletsprojects.com/en/1.1.x/deploying/. 40 | 41 | Command line interface 42 | ====================== 43 | 44 | The `cross2sheet.main` module includes a command line program that will 45 | hopefully work most of the time. For example, you can try the following: 46 | ``` 47 | python3 -m cross2sheet.main http://web.mit.edu/puzzle/www/2014/puzzle/puzzle_with_answer_nowhere_man/grid.png nowhere_man.xlsx 48 | python3 -m cross2sheet.main http://web.mit.edu/puzzle/www/2015/puzzle/rid_fill/ rid_fill.xlsx 49 | ``` 50 | To see the full list of options supported by the program, run `python3 -m cross2sheet.main -h`. 51 | 52 | API 53 | === 54 | 55 | The API is not particularly well documented, but there are some examples 56 | in `cross2sheet/examples/examples.py`. 57 | -------------------------------------------------------------------------------- /cross2sheet/web/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, send_file, redirect, url_for 2 | from cross2sheet.image import ImageGrid 3 | from cross2sheet.web.render import Table 4 | from cross2sheet.web.serial import TableData 5 | from cross2sheet.web.download import form_data_to_excel 6 | from urllib.request import urlopen 7 | from urllib.error import URLError 8 | from flask import flash 9 | app = Flask(__name__) 10 | app.config['MAX_CONTENT_LENGTH']=0x1000000 11 | 12 | def check_urlopen(): 13 | try: 14 | urlopen('http://www.example.com/') 15 | return True 16 | except URLError: 17 | return False 18 | 19 | if 'URLOPEN_ENABLED' not in app.config: 20 | app.config['URLOPEN_ENABLED']=check_urlopen() 21 | 22 | @app.route("/") 23 | def select(): 24 | return render_template('select.html') 25 | 26 | @app.route("/view",methods=['POST']) 27 | def display(): 28 | try: 29 | if 'file' in request.files: 30 | data=request.files['file'].read() 31 | elif request.form['url'].startswith('http'): 32 | data=urlopen(request.form['url']).read() 33 | else: 34 | return redirect(url_for('select')) 35 | img=ImageGrid(data) 36 | dim=img.dimensions() 37 | if dim[0]<=0 or dim[1]<=0: 38 | return render_template('select.html',error_msg='Failed to recognize crossword grid.') 39 | d=TableData(img=img) 40 | t=Table(d) 41 | return render_template('convert.html',table=t,data=d.to_json()) 42 | except ValueError: 43 | return render_template('select.html',error_msg='File format not recognized.') 44 | except URLError as e: 45 | return render_template('select.html',error_msg='Could not load url: {}.'.format(e.reason.strerror)) 46 | 47 | @app.route("/download",methods=['POST']) 48 | def download(): 49 | f=form_data_to_excel(request.form) 50 | return send_file(f,as_attachment=True,attachment_filename='grid.xlsx',mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') 51 | 52 | if __name__ == "__main__": 53 | app.run() 54 | -------------------------------------------------------------------------------- /cross2sheet/htmltable.py: -------------------------------------------------------------------------------- 1 | """ 2 | Reads grids that are formatted as HTML tables, provided a function that interprets style 3 | information. 4 | """ 5 | 6 | import bisect, re 7 | from cross2sheet.grid_features import * 8 | from bs4 import BeautifulSoup 9 | 10 | class TableParser: 11 | 12 | def __init__(self): 13 | self.elts = [] 14 | self.height = 0 15 | self.width = 0 16 | 17 | def grid(self): 18 | g = Grid(self.height,self.width) 19 | g.features.extend(self.elts) 20 | return g 21 | 22 | def add_element(self,y,x,e): 23 | self.elts.append((y,x,e)) 24 | if y>=self.height: 25 | self.height=y+1 26 | if x>=self.width: 27 | self.width=x+1 28 | 29 | def parse(self,table,stylefunc): 30 | for y,row in enumerate(table.find_all('tr')): 31 | for x,elt in enumerate(row.find_all('td')): 32 | for st in stylefunc(elt.attrs): 33 | self.add_element(y,x,st) 34 | if(elt.text): 35 | self.add_element(y,x,TextElt(elt.text)) 36 | 37 | # TODO: add some way of picking out a particular table 38 | def parse_html_table(text,stylefunc=None,styleattr=None,styledict=None): 39 | ''' 40 | Parses an html table. 41 | 42 | The ``stylefunc`` function should be a function that takes a dictionary of 43 | ```` element attributes and returns a list of ``grid_features.GridFeature`` 44 | objects that should be associated with that cell. 45 | ''' 46 | if stylefunc is None: 47 | if styleattr is None: 48 | raise ValueError('Either stylefunc or styleattr is required') 49 | def stylefunc(attrs): 50 | a = attrs.get(styleattr) 51 | if isinstance(a,list): 52 | a=' '.join(a) 53 | if a in styledict: 54 | return [BackgroundElt(styledict[a])] 55 | else: 56 | return [] 57 | soup=BeautifulSoup(text) 58 | parser=TableParser() 59 | for table in soup.find_all('table'): 60 | parser.parse(table,stylefunc) 61 | return parser.grid() 62 | -------------------------------------------------------------------------------- /cross2sheet/web/render.py: -------------------------------------------------------------------------------- 1 | from bs4 import Tag 2 | from cross2sheet.grid_features import Grid 3 | from cross2sheet.transforms import autonumber 4 | 5 | class Cell: 6 | def __init__(self): 7 | self.back=None 8 | self.borders=[] 9 | self.texts=[] 10 | 11 | def tag(self): 12 | t=Tag(name='td') 13 | if self.borders: 14 | t['class']=self.borders 15 | if self.back is not None: 16 | t['style']='background-color: #%06x;'%self.back 17 | for x in self.texts: 18 | t.append(x.text_tag()) 19 | for x in self.texts: 20 | t.append(x.div_tag()) 21 | return t 22 | 23 | class CellText: 24 | def __init__(self,cls,text): 25 | self.cls=cls 26 | self.text=text 27 | 28 | def text_tag(self): 29 | t=Tag(name='span') 30 | t['class']=self.cls+['n'] 31 | t.string=self.text 32 | return t 33 | 34 | def div_tag(self): 35 | t=Tag(name='div') 36 | t['class']=self.cls+['comment'] 37 | t['title']=self.text 38 | return t 39 | 40 | class Table: 41 | def __init__(self,data): 42 | self.cells = [[Cell() for x in range(data.width)] for y in range(data.height)] 43 | for r,c,e in data.back: 44 | self.cells[r][c].back=e.color 45 | for r,c,e in data.bars: 46 | self.cells[r][c].borders.extend(b.lower() for b in e.dirs) 47 | for r,c,e in data.text: 48 | self.cells[r][c].texts.append(CellText(['text'],e.text)) 49 | g=Grid(data.height,data.width) 50 | for s1,e1 in [('bar',data.bars),('nobar',[])]: 51 | for s2,e2 in [('back',data.back),('noback',[])]: 52 | g.features=e1+e2 53 | for r,c,e in autonumber(g): 54 | self.cells[r][c].texts.append(CellText(['auto',s1,s2],e.text)) 55 | 56 | def tag(self): 57 | tt=Tag(name='table') 58 | for r in self.cells: 59 | rt=Tag(name='tr') 60 | for c in r: 61 | rt.append(c.tag()) 62 | tt.append(rt) 63 | return tt 64 | 65 | def __html__(self): 66 | return str(self.tag()) 67 | -------------------------------------------------------------------------------- /cross2sheet/grid_features.py: -------------------------------------------------------------------------------- 1 | """ 2 | Defines elements that can appear in a grid. 3 | """ 4 | 5 | from inspect import Parameter, Signature 6 | 7 | class Grid: 8 | 9 | def __init__(self,height,width): 10 | self.height=height 11 | self.width=width 12 | self.features=[] 13 | 14 | def validate(self): 15 | if not (self.width in range(256) and self.height in range(7812)): 16 | raise ValueError 17 | if not all(self._valid_coords(r,c) and (e.validate() is None) 18 | for r,c,e in self.features): 19 | raise ValueError 20 | 21 | def _valid_coords(self,r,c): 22 | return r in range(self.height) and c in range(self.width) 23 | 24 | class GridFeature: 25 | 26 | def _as_tuple(self): 27 | return tuple(getattr(self,f) for f in self.fields) 28 | 29 | def __init__(self,*args,**kwargs): 30 | sig = Signature([Parameter(f,Parameter.POSITIONAL_OR_KEYWORD) for f in self.fields]) 31 | for k,v in sig.bind(*args,**kwargs).arguments.items(): 32 | setattr(self,k,v) 33 | 34 | def __eq__(self,other): 35 | return type(self)==type(other) and self._as_tuple()==other._as_tuple() 36 | 37 | def __repr__(self): 38 | return '%s(%s)'%(type(self).__name__,','.join(repr(t) for t in self._as_tuple())) 39 | 40 | def __hash__(self): 41 | return hash(self._as_tuple()) 42 | 43 | def validate(self): 44 | pass 45 | 46 | class BackgroundElt(GridFeature): 47 | 'The background color of the cell, in 8-bit RGB format' 48 | 49 | fields=['color'] 50 | 51 | def validate(self): 52 | if self.color not in range(1<<24): 53 | raise ValueError 54 | 55 | def __repr__(self): 56 | return 'BackgroundElt(0x%06x)'%self.color 57 | 58 | class TextElt(GridFeature): 59 | 'The text inside the cell' 60 | 61 | def validate(self): 62 | if not isinstance(self.text,str): 63 | raise TypeError 64 | if len(self.text)>=256: 65 | raise ValueError 66 | 67 | fields=['text'] 68 | 69 | class BorderElt(GridFeature): 70 | "The borders of the cell that should be drawn (some subset of 'LRTB')" 71 | 72 | def validate(self): 73 | if not isinstance(self.dirs,str): 74 | raise TypeError 75 | if not (len(self.dirs)<=4 and self.dirs.isalpha()): 76 | raise ValueError 77 | 78 | fields=['dirs'] 79 | 80 | -------------------------------------------------------------------------------- /cross2sheet/excel.py: -------------------------------------------------------------------------------- 1 | """ 2 | Converts grids to Excel format. 3 | """ 4 | 5 | from openpyxl import Workbook 6 | from openpyxl.styles import PatternFill 7 | from openpyxl.styles.borders import Border, Side 8 | from openpyxl.styles.colors import Color 9 | from openpyxl.comments import Comment 10 | from openpyxl.utils import get_column_letter 11 | import collections 12 | from cross2sheet.grid_features import * 13 | 14 | class _CellStyle: 15 | 16 | border_names = { 'L' : 'left', 'R' : 'right', 'T' : 'top', 'B' : 'bottom' } 17 | 18 | def __init__(self): 19 | self.color = None 20 | self.borders = set() 21 | 22 | def set_style(self,cell): 23 | if self.color is not None: 24 | cell.fill=PatternFill(patternType='solid',fgColor=Color('FF%06x'%self.color)) 25 | if self.borders: 26 | kwa = { self.border_names[b] : Side(style='thick') for b in self.borders } 27 | cell.border=Border(**kwa) 28 | 29 | def write_sheet(grid,ws,text_in_cells=True,text_in_comments=False,leave_white_blank=True): 30 | styles = collections.defaultdict(_CellStyle) 31 | for r,c,elt in grid.features: 32 | cell = ws.cell(row=r+1,column=c+1) 33 | if isinstance(elt,BackgroundElt): 34 | if not (elt.color==0xFFFFFF and leave_white_blank): 35 | styles[r,c].color=elt.color 36 | elif isinstance(elt,TextElt): 37 | if text_in_cells: 38 | cell.value=elt.text 39 | if text_in_comments: 40 | cell.comment=Comment(elt.text,'') 41 | elif isinstance(elt,BorderElt): 42 | styles[r,c].borders.update(elt.dirs) 43 | for (r,c),s in styles.items(): 44 | s.set_style(ws.cell(row=r+1,column=c+1)) 45 | for c in range(grid.width): 46 | ws.column_dimensions[get_column_letter(c+1)].width=3 47 | # Google Sheets seems to truncate sheets with no data at 10 columns, so 48 | # make sure the bottom right cell isn't empty 49 | bottom_right=ws.cell(row=grid.height,column=grid.width) 50 | if not bottom_right.value: 51 | bottom_right.value=' ' 52 | 53 | def to_openpyxl(grid,**kwargs): 54 | wb = Workbook() 55 | write_sheet(grid,wb.active,**kwargs) 56 | return wb 57 | 58 | def to_openpyxl_multi(grids,**kwargs): 59 | wb = Workbook() 60 | wb.remove_sheet(wb.active) 61 | for grid in grids: 62 | ws = wb.create_sheet() 63 | write_sheet(grid,ws,**kwargs) 64 | return wb 65 | 66 | def save_xlsx(grid,filename,**kwargs): 67 | return to_openpyxl(grid,**kwargs).save(filename) 68 | -------------------------------------------------------------------------------- /cross2sheet/web/templates/convert.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 31 | 36 | 55 | 60 | 65 | 70 | 75 | 80 | 109 | 110 | 111 | {{table}} 112 |
113 | Detect cell colors
114 | Detect bars
115 | Automatically number cells based on bars and dark squares
116 | Sequentially number cells that appear to have text
117 | No cell numbering
118 | Show numbers in cells
119 | Show numbers in comments
120 | 121 | 122 |
123 |

After downloading the spreadsheet, you can upload it to Google Sheets by selecting File > Import... and then clicking on the Upload tab.

124 | 125 | 126 | -------------------------------------------------------------------------------- /cross2sheet/analysis.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from cross2sheet.grid_features import BackgroundElt, BorderElt 3 | 4 | class GridAnalyzer: 5 | def __init__(self,grid): 6 | self.grid=grid 7 | self.filled=set() 8 | self.hbars=set() 9 | self.vbars=set() 10 | for r,c,e in grid.features: 11 | if isinstance(e,BackgroundElt): 12 | if e.color==0: 13 | self.filled.add((r,c)) 14 | elif isinstance(e,BorderElt): 15 | if 'T' in e.dirs: 16 | self.hbars.add((r,c)) 17 | if 'B' in e.dirs: 18 | self.hbars.add((r+1,c)) 19 | if 'L' in e.dirs: 20 | self.vbars.add((r,c)) 21 | if 'R' in e.dirs: 22 | self.vbars.add((r,c+1)) 23 | 24 | def is_blank(self,y,x): 25 | return (x>=0 and x=0 and yoldr: 15 | oldr=r 16 | i.write('\n') 17 | if e.color==0: 18 | i.write('#') 19 | elif e.color==0xffffff: 20 | i.write('.') 21 | else: 22 | i.write('O') 23 | return i.getvalue() 24 | 25 | def bars_to_string(g): 26 | ymax=g.height-1 27 | xmax=g.width-1 28 | grid=[[' ' for x in range(2*xmax+3)] for y in range(2*ymax+3)] 29 | for y,x,b in g.features: 30 | if not isinstance(b,BorderElt): 31 | continue 32 | for c in b.dirs: 33 | if c=='T': 34 | grid[2*y][2*x]='+' 35 | grid[2*y][2*x+1]='-' 36 | grid[2*y][2*x+2]='+' 37 | elif c=='L': 38 | grid[2*y][2*x]='+' 39 | grid[2*y+1][2*x]='|' 40 | grid[2*y+2][2*x]='+' 41 | elif c=='B': 42 | grid[2*y+2][2*x]='+' 43 | grid[2*y+2][2*x+1]='-' 44 | grid[2*y+2][2*x+2]='+' 45 | elif c=='R': 46 | grid[2*y][2*x+2]='+' 47 | grid[2*y+1][2*x+2]='|' 48 | grid[2*y+2][2*x+2]='+' 49 | return '\n'.join(''.join(r) for r in grid) 50 | 51 | def labels_to_string(g): 52 | grid=[['.' for x in range(g.width)] for y in range(g.height)] 53 | for y,x,t in g.features: 54 | if isinstance(t,TextElt): 55 | grid[y][x]='*' 56 | return '\n'.join(''.join(r) for r in grid) 57 | 58 | def print_tests(url,grid): 59 | print(' url={}'.format(url)) 60 | print(' rows={}'.format(grid.height)) 61 | print(' cols={}'.format(grid.width)) 62 | if any(isinstance(e,BackgroundElt) and e.color!=0xffffff for r,c,e in grid.features): 63 | print(" fill='''") 64 | print(grid_to_string(grid)) 65 | print("'''") 66 | if any(isinstance(e,BorderElt) for r,c,e in grid.features): 67 | bordered=Grid(grid.height,grid.width) 68 | bordered.features.extend(grid.features) 69 | bordered.features.extend(outside_bars(grid)) 70 | print(" bars='''") 71 | print(bars_to_string(bordered)) 72 | print("'''") 73 | if any(isinstance(e,TextElt) for r,c,e in grid.features): 74 | label_str = labels_to_string(grid) 75 | gr = Grid(grid.height,grid.width) 76 | gr.features.extend(autonumber(gr)) 77 | if label_str == labels_to_string(grid): 78 | print(" cells_with_text='auto'") 79 | else: 80 | print(" cells_with_text='''") 81 | print(label_str) 82 | print("'''") 83 | 84 | class ImageTest(unittest.TestCase): 85 | 86 | def setUp(self): 87 | url=self.url 88 | if url.startswith('20'): 89 | url='http://web.mit.edu/puzzle/www/'+url 90 | req=urlopen(url) 91 | data=req.read() 92 | req.close() 93 | self.img=ImageGrid(data) 94 | self.maxDiff=None 95 | 96 | def test_all(self): 97 | detected=(len(self.img.breaks[0])-1,len(self.img.breaks[1])-1) 98 | expected=(self.rows,self.cols) 99 | self.assertEqual(expected,detected,'wrong dimensions') 100 | if hasattr(self,'fill'): 101 | with self.subTest('fill'): 102 | grid=self.img.grid() 103 | grid.features.extend(self.img.read_background()) 104 | f=grid_to_string(grid) 105 | self.assertEqual(self.fill.strip(),f.strip()) 106 | if hasattr(self,'bars'): 107 | with self.subTest('bars'): 108 | grid=self.img.grid() 109 | grid.features.extend(self.img.read_bars()) 110 | grid.features.extend(outside_bars(grid)) 111 | b=bars_to_string(grid) 112 | self.assertEqual(self.bars.strip(),b.strip()) 113 | if hasattr(self,'cells_with_text'): 114 | with self.subTest('cells_with_text'): 115 | if self.cells_with_text=='auto': 116 | grid=self.img.grid() 117 | grid.features.extend(self.img.read_background()) 118 | grid.features.extend(self.img.read_bars()) 119 | grid.features.extend(autonumber(grid)) 120 | self.cells_with_text=labels_to_string(grid) 121 | grid=self.img.grid() 122 | grid.features.extend(self.img.autonumber_if_text_found()) 123 | t=labels_to_string(grid) 124 | self.assertEqual(self.cells_with_text.strip(),t.strip()) 125 | -------------------------------------------------------------------------------- /cross2sheet/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import argparse 4 | import urllib.request 5 | from cross2sheet.excel import save_xlsx 6 | from cross2sheet.html14 import parse_html_grid 7 | from cross2sheet.htmltable import parse_html_table 8 | from cross2sheet.transforms import autonumber, outside_bars, pad 9 | 10 | def read(string): 11 | if '://' in string: 12 | req=urllib.request.urlopen(string) 13 | data=req.read() 14 | req.close() 15 | return data 16 | else: 17 | with open(string,'rb') as f: 18 | return f.read() 19 | 20 | class NotRecognized(Exception): 21 | pass 22 | 23 | class ReadFailed(Exception): 24 | pass 25 | 26 | def read_image(data,args): 27 | try: 28 | from cross2sheet.image import ImageGrid 29 | except ImportError as e: 30 | if e.name in ('cv2','numpy'): 31 | raise NotRecognized('Image detection disabled because the module %s was not found.'%e.name) 32 | else: 33 | raise e 34 | try: 35 | img=ImageGrid(data) 36 | except ValueError: 37 | raise NotRecognized 38 | grid=img.grid() 39 | if args.detect_background: 40 | grid.features.extend(img.read_background(args.color_levels)) 41 | if args.detect_bars: 42 | grid.features.extend(img.read_bars()) 43 | if args.autonumber_cells_with_text: 44 | grid.features.extend(img.autonumber_if_text_found()) 45 | if args.ocr_text: 46 | grid.features.extend(img.read_text_ocr()) 47 | if args.autonumber is None: 48 | args.autonumber=not (args.autonumber_cells_with_text or args.ocr_text) 49 | return grid 50 | 51 | def read_html(data,args): 52 | try: 53 | data=data.decode() 54 | except UnicodeDecodeError: 55 | raise NotRecognized 56 | if '
=16] 39 | if not squares: 40 | return ([],[]) 41 | a=statistics.median(cv2.contourArea(c) for c in squares) 42 | p=statistics.median(cv2.arcLength(c,True) for c in squares) 43 | blanks=[c for c in con if abs(cv2.contourArea(c)-a)<.1*a and abs(cv2.arcLength(c,True)-p)<.05*p] 44 | dist=int(p/8) 45 | xc = [] 46 | yc = [] 47 | for c in blanks: 48 | x,y,w,h=cv2.boundingRect(c) 49 | xc.extend((x,x+w)) 50 | yc.extend((y,y+h)) 51 | xc.sort() 52 | yc.sort() 53 | return (self._squares_to_breaks(yc,dist),self._squares_to_breaks(xc,dist)) 54 | 55 | def grid(self): 56 | return Grid(*self.dimensions()) 57 | 58 | def dimensions(self): 59 | return (len(self.breaks[0])-1,len(self.breaks[1])-1) 60 | 61 | def read_background(self,color_resolution=2): 62 | cells = [] 63 | for r,ys in enumerate(self._cell_slices(0)): 64 | for c,xs in enumerate(self._cell_slices(1)): 65 | means = self.img[ys,xs].mean(axis=(0,1)) 66 | vals = [255*int(round(x*color_resolution/255.))//color_resolution for x in means] 67 | rgb = vals[0]|(vals[1]<<8)|(vals[2]<<16) 68 | cells.append((r,c,BackgroundElt(rgb))) 69 | return cells 70 | 71 | @staticmethod 72 | def _contour_is_square(c): 73 | if len(c)!=4: 74 | return False 75 | _,_,w,h = cv2.boundingRect(c) 76 | return 3*w>=2*h and 3*h>=2*w 77 | 78 | @staticmethod 79 | def _squares_to_breaks(coords,dist): 80 | start=None 81 | end=None 82 | breaks=[] 83 | for c in coords: 84 | if start is None: 85 | start=c 86 | end=c 87 | elif c-end>=dist: 88 | breaks.append((start,end)) 89 | start=c 90 | end=c 91 | else: 92 | end=c 93 | breaks.append((start,end)) 94 | return breaks 95 | 96 | def _cell_slices(self,idx): 97 | it1 = iter(self.breaks[idx]) 98 | it2 = iter(self.breaks[idx]) 99 | next(it2,None) 100 | for (_,a),(b,_) in zip(it1,it2): 101 | yield slice(a+1,b) 102 | 103 | # This isn't too reliable yet. Use transforms.autonumber if possible. 104 | def autonumber_if_text_found(self): 105 | n=itertools.count(1) 106 | numbers=[] 107 | for r,ys in enumerate(self._cell_slices(0)): 108 | for c,xs in enumerate(self._cell_slices(1)): 109 | if self._has_text_rect(self.gray[ys,xs]): 110 | numbers.append((r,c,TextElt(str(next(n))))) 111 | return numbers 112 | 113 | @staticmethod 114 | def _find_text_rect(img): 115 | _,thr = cv2.threshold(img,128,1,cv2.THRESH_BINARY) 116 | con = cv2.findContours(thr,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)[-2] 117 | if len(con)<=1: 118 | return None 119 | x,y,w,h=cv2.boundingRect(numpy.concatenate(con[:-1])) 120 | if w*h>=15: 121 | return (slice(y-1,y+h+1),slice(x-1,x+w+1)) 122 | 123 | @staticmethod 124 | def _has_text_rect(img): 125 | _,thr = cv2.threshold(img,128,255,cv2.THRESH_BINARY) 126 | con = cv2.findContours(thr,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)[-2] 127 | if len(con)>=2: 128 | return True 129 | elif len(con)==1: 130 | x,y,w,h=cv2.boundingRect(numpy.concatenate(con[0])) 131 | return cv2.arcLength(con[0],True)>2*w+2*h 132 | else: 133 | return False 134 | 135 | def read_bars(self): 136 | hbars=self._find_bars(lambda x,y: (x,y)) 137 | vbars=self._find_bars(lambda x,y: (y,x)) 138 | bars=[(y,x,BorderElt('T')) for y,x in hbars] 139 | bars.extend((y,x,BorderElt('L')) for y,x in vbars) 140 | return bars 141 | 142 | def _find_bars(self,coord): 143 | b0,b1=coord(0,1) 144 | if len(self.breaks[b0])<=2: 145 | return [] 146 | span = max(b-a+1 for a,b in self.breaks[b0]) 147 | means = [] 148 | for a,(c1,c2) in enumerate(self.breaks[b0][1:-1]): 149 | dy=(span-c2+c1)//2 150 | c2+=dy 151 | c1=c2-span 152 | for b,s in enumerate(self._cell_slices(b1)): 153 | means.append((a+1,b,self.gray[coord(slice(c1,c2+1),s)].mean())) 154 | smeans=sorted(m[2] for m in means) 155 | thr=smeans[max(range(len(smeans)-1),key=lambda j: smeans[j+1]-smeans[j])] 156 | return [coord(a,b) for a,b,m in means if m<=thr] 157 | 158 | # This isn't too reliable yet. Use transforms.autonumber if possible. 159 | def read_text_ocr(self,ocr_cmd='tesseract',allowed_chars='0123456789'): 160 | f,fn = tempfile.mkstemp(suffix='.png') 161 | os.close(f) 162 | elts=[] 163 | for r,ys in enumerate(self._cell_slices(0)): 164 | for c,xs in enumerate(self._cell_slices(1)): 165 | small = self.gray[ys,xs] 166 | rect = self._find_text_rect(small) 167 | if not rect: 168 | continue 169 | small = small[rect] 170 | cv2.imwrite(fn,small) 171 | txt=subprocess.check_output([ocr_cmd,fn,'stdout','--psm','8','-c','tessedit_char_whitelist=%s'%allowed_chars],stderr=subprocess.DEVNULL).decode().strip() 172 | if txt: 173 | elts.append((r,c,TextElt(txt))) 174 | os.remove(fn) 175 | return elts 176 | -------------------------------------------------------------------------------- /cross2sheet/test/testdata.py: -------------------------------------------------------------------------------- 1 | from cross2sheet.test.test import ImageTest 2 | import unittest 3 | 4 | class NowhereMan(ImageTest): 5 | url='2014/puzzle/puzzle_with_answer_nowhere_man/grid.png' 6 | rows=13 7 | cols=13 8 | fill=''' 9 | O............ 10 | .O........... 11 | ..O.......... 12 | ...O......... 13 | ....O........ 14 | ............. 15 | ............. 16 | ............. 17 | ........O.... 18 | .........O... 19 | ..........O.. 20 | ...........O. 21 | ............O 22 | ''' 23 | bars=''' 24 | +-+-+-+-+-+-+-+-+-+-+-+-+-+ 25 | | | | 26 | +-+ +-+ +-+ + +-+ +-+ + 27 | | | | | | | 28 | + + + + +-+ + +-+ +-+ + 29 | | | | | | | 30 | + +-+ + + + + + 31 | | | | | 32 | + + + + +-+ +-+ + +-+ + 33 | | | | | | | 34 | + + + + + +-+ +-+-+ + + 35 | | | | | | | | 36 | +-+ + + + +-+-+ + +-+ + 37 | | | | | | | 38 | + +-+ + +-+-+ + + + +-+ 39 | | | | | | | | 40 | + + +-+-+ +-+ + + + + + 41 | | | | | | | 42 | + +-+ + +-+ +-+ + + + + 43 | | | | | 44 | + + + + + +-+ + 45 | | | | | | | 46 | + +-+ +-+ + +-+ + + + + 47 | | | | | | | 48 | + +-+ +-+ + +-+ +-+ +-+ 49 | | | | 50 | +-+-+-+-+-+-+-+-+-+-+-+-+-+ 51 | ''' 52 | cells_with_text='auto' 53 | 54 | class CrossPollinationA(ImageTest): 55 | url='2014/puzzle/cross_pollination/A.png' 56 | rows=15 57 | cols=15 58 | fill=''' 59 | ......#....#... 60 | ......#....#... 61 | ......#........ 62 | ##......#...... 63 | ....##....#.... 64 | ...#.....#..... 65 | ...#........### 66 | ......###...... 67 | ###........#... 68 | .....#.....#... 69 | ....#....##.... 70 | ......#......## 71 | ........#...... 72 | ...#....#...... 73 | ...#....#...... 74 | ''' 75 | cells_with_text='auto' 76 | 77 | class CrossPollinationB(ImageTest): 78 | url='2014/puzzle/cross_pollination/B.png' 79 | rows=15 80 | cols=15 81 | fill=''' 82 | .....#.....#... 83 | .....#.....#... 84 | .....#......... 85 | ...#...#....... 86 | ....#.....#.... 87 | ....#.....#.... 88 | .....#.....#... 89 | ###...###...### 90 | ...#.....#..... 91 | ....#.....#.... 92 | ....#.....#.... 93 | .......#...#... 94 | .........#..... 95 | ...#.....#..... 96 | ...#.....#..... 97 | ''' 98 | #broken 99 | #cells_with_text='auto' 100 | 101 | class SamsYourUncle(ImageTest): 102 | url='2013/coinheist.com/feynman/sams_your_uncle/image00.png' 103 | rows=23 104 | cols=24 105 | cells_with_text=''' 106 | ..*.**.***********.****. 107 | *..**.**.....*..*....*.. 108 | **....*........*....*... 109 | ...*.*..*....*...***.*.. 110 | *.*.*..*..*..*..**..*... 111 | *......*.*..*.....*...*. 112 | **..*......**..**..*.*.. 113 | *..**.***.....*..*..*... 114 | *.....*.......*......... 115 | *.*.*.........*.*.**.... 116 | *....***......*......*.. 117 | *...*..*......*..*.**... 118 | *.....*.*.....*...*.*... 119 | *.*.*..*......***...*.*. 120 | **...*.*..*****...*..*.. 121 | *..**..*.*..*.*..**..... 122 | *....**.*.....*.*..**... 123 | *.*...*.....*.....*...*. 124 | *...*...*..*..*..*..**.. 125 | **.*..**..*..*.*.*...*.. 126 | .**.*.......*.....*..... 127 | *...*...*.....*..*..*... 128 | ........................ 129 | ''' 130 | bars=''' 131 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 132 | | | 133 | + +-+ +-+ +-+ + + + + + +-+ + + + 134 | | | | | | | | | | | | 135 | + +-+ + + + + + + + + + + +-+ + + + + 136 | | | | | | | | | | 137 | + + + + +-+ + + + + + + + + +-+-+ +-+ + + 138 | | | | | | | | | | | | 139 | + + +-+ + + +-+ +-+ + +-+ + +-+-+ +-+ +-+ + 140 | | | | | | | | | | | 141 | + +-+ + + +-+-+ +-+ + +-+ + + +-+ + 142 | | | | | | | 143 | + +-+ +-+ + + +-+ + +-+-+ +-+ + +-+ +-+ + 144 | | | | | | | | | 145 | + +-+-+ + +-+-+ + + + + + + + +-+ + + + 146 | | | | | | | | | 147 | + +-+ +-+-+ + + + +-+ + +-+ + 148 | | | | | 149 | + +-+ + +-+-+ + +-+-+-+-+ +-+ +-+ +-+ +-+ + 150 | | | | | | | | | 151 | + +-+ + +-+-+-+-+ + + + + +-+-+-+-+ + 152 | | | | | | 153 | + +-+ +-+ + + +-+ + + +-+ + +-+-+ +-+ + 154 | | | | | | | | | 155 | + +-+ +-+-+ + + +-+ + + +-+ + + +-+ +-+ + 156 | | | | | | | | 157 | + +-+-+-+-+ + + + + +-+-+-+-+ + +-+ + 158 | | | | | | | | | 159 | + +-+ +-+ +-+ +-+ +-+-+-+-+ +-+-+ + +-+ + 160 | | | | 161 | + +-+ + +-+ + + +-+ +-+-+ + 162 | | | | | | | | | 163 | + + + +-+ +-+ + + + + + +-+-+ + +-+-+ + 164 | | | | | | | | 165 | + +-+ +-+ + +-+ +-+-+ + +-+ + + +-+ +-+ + 166 | | | | | | | 167 | + +-+ +-+ + +-+ + +-+ +-+-+ + + +-+ + 168 | | | | | | | | | | | 169 | + +-+ +-+ +-+-+ + +-+ + +-+ +-+ + + +-+ + + 170 | | | | | | | | | | | | 171 | + + +-+ + +-+ + + + + + + + + + +-+ + + + + 172 | | | | | | | | | | | 173 | + + + + +-+ + + + + + + + + + + +-+ + 174 | | | | | | | | | | | | 175 | + + + +-+ + + + + + +-+ +-+ +-+ + 176 | | | 177 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 178 | ''' 179 | 180 | class FixesTheWavyZigzagJumble(ImageTest): 181 | url='2013/coinheist.com/rubik/fixes_the_wavy_zigzag_jumble/image00.png' 182 | rows=21 183 | cols=21 184 | bars=''' 185 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 186 | | | | | | | 187 | + +-+ +-+ + +-+ + + + + +-+ +-+ +-+ +-+ 188 | | | | | | | | | | 189 | + +-+ +-+ +-+ + + + +-+ +-+ + + +-+ 190 | | | | | | 191 | + +-+ + +-+ + +-+ +-+ +-+-+ + + + +-+ 192 | | | | | | | | | | | | | 193 | + + +-+-+-+ +-+-+ +-+ +-+-+-+ + +-+ +-+-+-+ 194 | | | | | | | | | 195 | + +-+ + + +-+ + +-+ +-+ +-+-+ +-+ 196 | | | | | | | 197 | + + + +-+ + +-+-+ + +-+ +-+ +-+ +-+ +-+ 198 | | | | | | | | | | | 199 | +-+ + +-+ +-+ +-+ + + +-+ +-+ +-+ +-+ +-+-+ 200 | | | | | | | | | | 201 | + +-+ +-+-+ +-+ + +-+ + +-+ + + + + +-+ 202 | | | | | | | | | | 203 | + + +-+ + + +-+-+ +-+ + + +-+ + +-+ + +-+ 204 | | | | | | | | | | | | 205 | + +-+-+ + + + +-+-+ + + + +-+ +-+ +-+ +-+ 206 | | | | | | | 207 | +-+ +-+ +-+ +-+ + + + +-+-+ + + + +-+-+ + 208 | | | | | | | | | | | | 209 | +-+ + +-+ + +-+ + + +-+ +-+-+ + + +-+ + + 210 | | | | | | | | | | 211 | +-+ + + + + +-+ + +-+ + +-+ +-+-+ +-+ + 212 | | | | | | | | | | 213 | +-+-+ +-+ +-+ +-+ +-+ + + +-+ +-+ +-+ + +-+ 214 | | | | | | | | | | | 215 | +-+ +-+ +-+ +-+ +-+ + +-+-+ + +-+ + + + 216 | | | | | | | 217 | +-+ +-+-+ +-+ +-+ + +-+ + + +-+ + 218 | | | | | | | | | 219 | +-+-+-+ +-+ + +-+-+-+ +-+ +-+-+ +-+-+-+ + + 220 | | | | | | | | | | | | | 221 | +-+ + + + +-+-+ +-+ +-+ + +-+ + +-+ + 222 | | | | | | 223 | +-+ + + +-+ +-+ + + + +-+ +-+ +-+ + 224 | | | | | | | | | | 225 | +-+ +-+ +-+ +-+ + + + + +-+ + +-+ +-+ + 226 | | | | | | | 227 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 228 | ''' 229 | cells_with_text='auto' 230 | 231 | class EvilInfluence(ImageTest): 232 | url='2012/puzzles/mayan_fair_lady/evil_influence/1s.png' 233 | rows=13 234 | cols=13 235 | bars=''' 236 | +-+-+-+-+-+-+-+-+-+-+-+-+-+ 237 | | | | | 238 | + + + +-+ + + + +-+ +-+ 239 | | | | | | 240 | + + + +-+ + + + + 241 | | | | | | 242 | + + +-+ + +-+-+ + + 243 | | | | | | 244 | + + + +-+ + +-+-+-+ + 245 | | | | | | | 246 | + + +-+-+ + +-+ + +-+ + + 247 | | | | | | | | 248 | +-+-+ +-+ +-+ +-+ + +-+ + 249 | | | | | 250 | + +-+ + +-+ +-+ +-+ +-+-+ 251 | | | | | | | | 252 | + + +-+ + +-+ + +-+-+ + + 253 | | | | | | | 254 | + +-+-+-+ + +-+ + + + 255 | | | | | | 256 | + + +-+-+ + +-+ + + 257 | | | | | | 258 | + + + + +-+ + + + 259 | | | | | | 260 | +-+ +-+ + + + +-+ + + + 261 | | | | | 262 | +-+-+-+-+-+-+-+-+-+-+-+-+-+ 263 | ''' 264 | cells_with_text='auto' 265 | 266 | class Laureate(ImageTest): 267 | url='2011/puzzles/civilization/laureate/assets/grid.png' 268 | rows=9 269 | cols=17 270 | bars=''' 271 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 272 | | | | | 273 | + + + + + + 274 | | | | | 275 | + + + + + + 276 | | | | | 277 | + + + +-+ + 278 | | | | 279 | +-+ +-+ +-+ + +-+ 280 | | | | 281 | + +-+ +-+ +-+-+ +-+-+ + +-+ + 282 | | | | | 283 | + +-+ + +-+ + + 284 | | | | 285 | + + + + 286 | | | | 287 | + + + + + 288 | | | | | 289 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 290 | ''' 291 | cells_with_text='auto' 292 | 293 | class NinetyNineCentsAClue(ImageTest): 294 | url='2009/puzzles/99_cents_a_clue/PUZZLE/grid.png' 295 | rows=15 296 | cols=15 297 | fill=''' 298 | .........#....O 299 | .#.#.#.#.#O#.#. 300 | .O...#......... 301 | .#.#.#.#.#.#O#. 302 | O........#..... 303 | .#.###O#.#.###. 304 | ..O.....#...... 305 | ##.#.#.#.#.#.## 306 | ......#..O..... 307 | .###.#.#O###.#. 308 | .....#......... 309 | .#O#.#.#.#.#.#. 310 | ......O..#..... 311 | .#.#.#.#.#.#O#. 312 | .....#.......O. 313 | ''' 314 | cells_with_text='auto' 315 | 316 | class WorldsTallestCryptic(ImageTest): 317 | url='2008/world_s_tallest_cryptic/images/worldstallest.jpg' 318 | rows=38 319 | cols=13 320 | bars=''' 321 | +-+-+-+-+-+-+-+-+-+-+-+-+-+ 322 | | | | | | | | 323 | + +-+-+ +-+-+ + + +-+ + + 324 | | | | | | | 325 | + + +-+ +-+ + + + +-+ + + 326 | | | | | | 327 | + + +-+ +-+ + + + +-+-+ + + 328 | | | | | | | | | | | 329 | + + + +-+ + + + +-+ +-+ + + 330 | | | | | | 331 | + + + +-+ + + + +-+ + + + + 332 | | | | | | | | 333 | + + + +-+ + + +-+-+ + + 334 | | | | | | 335 | +-+ +-+ + +-+-+ +-+ +-+ + 336 | | | | | | | | 337 | + + +-+ + +-+ +-+ + +-+ +-+ 338 | | | | | 339 | + +-+ +-+-+ +-+ +-+ + + 340 | | | | | 341 | + +-+ +-+-+ + +-+ + + + 342 | | | | | | | 343 | + + + +-+ + + + + + + + 344 | | | | | | | | 345 | + + +-+ + + + +-+ + + + + 346 | | | | | | | | 347 | + + +-+ +-+ + +-+ +-+ + + + 348 | | | | | | | | 349 | + +-+-+ +-+-+ + + +-+ + + 350 | | | | | | | 351 | + + +-+ +-+ + + + +-+ + + 352 | | | | | | 353 | + + +-+ +-+ + + + +-+-+ + + 354 | | | | | | | | | | | 355 | + + + +-+ + + + +-+ +-+ + + 356 | | | | | | 357 | + + + +-+ + + + +-+ + + + + 358 | | | | | | | | 359 | + + + +-+ + + +-+-+ + + 360 | | | | | | 361 | +-+ +-+ + +-+-+ +-+ +-+ + 362 | | | | | | | | 363 | + + +-+ + +-+ +-+ + +-+ +-+ 364 | | | | | 365 | + +-+ +-+-+ +-+ +-+ + + 366 | | | | | 367 | + +-+ +-+-+ + +-+ + + + 368 | | | | | | | 369 | + + + +-+ + + + + + + + 370 | | | | | | | | 371 | + + +-+ + + + +-+ + + + + 372 | | | | | | | | 373 | + + +-+ +-+ + +-+ +-+ + + + 374 | | | | | | | | 375 | + +-+-+ +-+-+ + + +-+ + + 376 | | | | | | | 377 | + + +-+ +-+ + + + +-+ + + 378 | | | | | | 379 | + + +-+ +-+ + + + +-+-+ + + 380 | | | | | | | | | | | 381 | + + + +-+ + + + +-+ +-+ + + 382 | | | | | | 383 | + + + +-+ + + + +-+ + + + + 384 | | | | | | | | 385 | + + + +-+ + + +-+-+ + + 386 | | | | | | 387 | +-+ +-+ + +-+-+ +-+ +-+ + 388 | | | | | | 389 | + +-+ + +-+ + +-+ +-+ 390 | | | | 391 | + +-+ +-+ + + 392 | | | | | 393 | + +-+ +-+ + +-+ + + 394 | | | | 395 | + +-+ +-+ +-+ +-+ + + 396 | | | | | 397 | +-+-+-+-+-+-+-+-+-+-+-+-+-+ 398 | ''' 399 | cells_with_text=''' 400 | **..**..*.*.. 401 | .*......*.... 402 | *.....*..*... 403 | ..**....*.*.. 404 | *............ 405 | ..*....*..... 406 | *...*.*.**.*. 407 | *.*..*....*.* 408 | .**.*........ 409 | *..*......... 410 | ..*....*..... 411 | *....*..*.... 412 | .**.*..*.**.. 413 | **..**..*.*.. 414 | .*......*.... 415 | *.....*..*... 416 | ..**....*.*.. 417 | *............ 418 | ..*....*..... 419 | *...*.*.**.*. 420 | *.*..*....*.* 421 | .**.*........ 422 | *..*......... 423 | ..*....*..... 424 | *....*..*.... 425 | .**.*..*.**.. 426 | **..**..*.*.. 427 | .*......*.... 428 | *.....*..*... 429 | ..**....*.*.. 430 | *............ 431 | ..*....*..... 432 | *...*.*.**.*. 433 | *.*..*...**.* 434 | **..*........ 435 | .*......*.... 436 | *.....**..... 437 | ****.*.*.**** 438 | ''' 439 | 440 | class GridWithAHoleInTheMiddle(ImageTest): 441 | url='2006/puzzles/cambridge/grid_with_a_hole_in_the_middle/grid.png' 442 | rows=11 443 | cols=11 444 | bars=''' 445 | +-+-+-+-+-+-+-+-+-+-+-+ 446 | | | | | 447 | + + +-+ + +-+ + + 448 | | | | | 449 | + +-+ + + +-+ + + 450 | | | | 451 | + + + +-+ + + + + + 452 | | | | | | | | | 453 | + + + +-+ + +-+ + + + + 454 | | | | 455 | +-+ +-+ +-+-+-+ +-+-+ 456 | | | | 457 | +-+-+ +-+-+-+ +-+ +-+ 458 | | | | 459 | + + + + +-+ + +-+ + + + 460 | | | | | | | | | 461 | + + + + + +-+ + + + 462 | | | | 463 | + + +-+ + + +-+ + 464 | | | | | 465 | + + +-+ + +-+ + + 466 | | | | | 467 | +-+-+-+-+-+-+-+-+-+-+-+ 468 | ''' 469 | cells_with_text='auto' 470 | 471 | class WhoaIKnowWindows(ImageTest): 472 | url='2003/www.acme-corp.com/teamGuest/Training/images/windows.jpg' 473 | rows=12 474 | cols=12 475 | bars=''' 476 | +-+-+-+-+-+-+-+-+-+-+-+-+ 477 | | | | 478 | +-+ +-+ +-+ +-+ +-+ 479 | | | | | 480 | +-+ + +-+ +-+ +-+ + + 481 | | | | | | 482 | + + + + + +-+ +-+ + + 483 | | | | | | | 484 | + + + + + +-+ +-+ +-+ + + 485 | | | | | | | | | 486 | + +-+ +-+ +-+ + +-+-+ + 487 | | | | | 488 | +-+-+ + + + + +-+-+ 489 | | | | | 490 | + +-+-+ + +-+ +-+ +-+ + 491 | | | | | | | | | 492 | + + +-+ +-+ +-+ + + + + + 493 | | | | | | | 494 | + + +-+ +-+ + + + + + 495 | | | | | | 496 | + + +-+ +-+ +-+ + +-+ 497 | | | | | 498 | +-+ +-+ +-+ +-+ +-+ 499 | | | | 500 | +-+-+-+-+-+-+-+-+-+-+-+-+ 501 | ''' 502 | cells_with_text='auto' 503 | 504 | class InnerTube(ImageTest): 505 | url='2000/set3/1/grid.jpg' 506 | rows=13 507 | cols=13 508 | bars=''' 509 | +-+-+-+-+-+-+-+-+-+-+-+-+-+ 510 | | | | 511 | +-+ +-+ +-+ + +-+-+ + + 512 | | | | | | | 513 | +-+ +-+ +-+ + + + + + + 514 | | | | | | | 515 | + + +-+ +-+ + + +-+ + 516 | | | | | 517 | + +-+ +-+ +-+ +-+ + + 518 | | | | | | | | 519 | + + + +-+ + +-+ + + 520 | | | | | 521 | + +-+ +-+-+ +-+ + + 522 | | | | | 523 | + + +-+ +-+-+ +-+ + 524 | | | | | 525 | + + +-+ + +-+ + + + 526 | | | | | | | | 527 | + + +-+ +-+ +-+ +-+ + 528 | | | | | 529 | + +-+ + + +-+ +-+ + + 530 | | | | | | | 531 | + + + + + + +-+ +-+ +-+ 532 | | | | | | | 533 | + + +-+-+ + +-+ +-+ +-+ 534 | | | | 535 | +-+-+-+-+-+-+-+-+-+-+-+-+-+ 536 | ''' 537 | cells_with_text='auto' 538 | 539 | class TheWickedSwitch(ImageTest): 540 | url='2012/puzzles/a_circus_line/the_wicked_switch/1.png' 541 | rows=13 542 | cols=13 543 | fill=''' 544 | ...#....#.... 545 | ...#....#.... 546 | ............. 547 | ....##...#... 548 | ###.......### 549 | ...#...#..... 550 | ......#...... 551 | .....#...#... 552 | ###.......### 553 | ...#...##.... 554 | ............. 555 | ....#....#... 556 | ....#....#... 557 | ''' 558 | cells_with_text='auto' 559 | 560 | class PacificOvertones(ImageTest): 561 | url='2012/puzzles/ben_bitdiddle/pacific_overtones/1.png' 562 | rows=15 563 | cols=15 564 | fill=''' 565 | ....##....#.... 566 | ....#.....#.... 567 | ....#.....#.... 568 | ###.....OO...## 569 | ...O........... 570 | ......##...O... 571 | .....##...#.... 572 | ###.........### 573 | ....#...##..... 574 | ...#...O#...... 575 | ...........#... 576 | ##...##.....### 577 | ....O.....#.... 578 | ....#.....#.... 579 | ....#....OO.... 580 | ''' 581 | cells_with_text=''' 582 | ****..****.**** 583 | *....*.....*... 584 | *....*.....*... 585 | ...**.....*.... 586 | ***.*...**...** 587 | *..*....*...*.. 588 | *......*...*... 589 | ...*.**...*.... 590 | ***..*....*.*** 591 | *...*....*..... 592 | *..*...**...*.. 593 | ..*....*...*... 594 | **...**....**** 595 | *....*.....*... 596 | *....*.....*... 597 | ''' 598 | 599 | class OneMoreTry(ImageTest): 600 | url='2011/puzzles/mega_man/one_more_try/assets/grid.png' 601 | rows=25 602 | cols=25 603 | fill=''' 604 | ###......##....#...##.... 605 | ##........#.........#.... 606 | #.........#.........#.... 607 | ......#....#.....##.....# 608 | .....#......#...#.....### 609 | ..#....#........#........ 610 | ##.......#....#.....#.... 611 | ....#...#..........##.... 612 | ...#....#...##....###.... 613 | ...#....#.......#...##... 614 | .......#...#....##......# 615 | ..#...#...#....#.......## 616 | ......#...........#...... 617 | ##.......#....#...#...#.. 618 | #......##....#...#....... 619 | ...##...#.......#....#... 620 | ....###....##...#....#... 621 | ....##..........#...#.... 622 | ....#.....#....#.......## 623 | ........#........#....#.. 624 | ###.....#...#......#..... 625 | #.....##.....#....#...... 626 | ....#.........#.........# 627 | ....#.........#........## 628 | ....##...#....##......### 629 | ''' 630 | cells_with_text='auto' 631 | 632 | class DualSingularities(ImageTest): 633 | url='2009/puzzles/dual_singularities/PUZZLE/grid3.png' 634 | rows=17 635 | cols=17 636 | fill=''' 637 | .....#.....#..... 638 | .....#.....#..... 639 | .....#........... 640 | ......#......#... 641 | ###.....#.....### 642 | ....#......#..... 643 | ...#......###.... 644 | ...#...#...#..... 645 | ........#........ 646 | .....#...#...#... 647 | ....###......#... 648 | .....#......#.... 649 | ###.....#.....### 650 | ...#......#...... 651 | ...........#..... 652 | .....#.....#..... 653 | .....#.....#..... 654 | ''' 655 | cells_with_text='auto' 656 | 657 | class Unspeakable(ImageTest): 658 | url='2007/puzzles/unspeakable/grid-1.png' 659 | rows=11 660 | cols=11 661 | bars=''' 662 | +-+-+-+-+-+-+-+-+-+-+-+ 663 | | | | | 664 | + + + + + + 665 | | | | | 666 | + + + + + + 667 | | | | | 668 | + +-+-+ + +-+ + +-+ + 669 | | | | 670 | +-+ +-+ +-+ +-+ + 671 | | | | | 672 | + + + +-+ + 673 | | | | | 674 | + +-+ + + + + 675 | | | | | 676 | + +-+ +-+-+ +-+ +-+ 677 | | | | | 678 | +-+-+ + + +-+ + +-+ + 679 | | | | | 680 | + + + + + + 681 | | | | | 682 | + + + + + 683 | | | | | 684 | +-+-+-+-+-+-+-+-+-+-+-+ 685 | ''' 686 | cells_with_text='auto' 687 | 688 | class DealingWithChange(ImageTest): 689 | url='2003/www.acme-corp.com/teamGuest/4/images/dealing-with-change.jpg' 690 | rows=19 691 | cols=19 692 | fill=''' 693 | .....###....#.....# 694 | ......#.....#.....# 695 | ......#.....#...... 696 | ..........#........ 697 | ...#...###...##.... 698 | ...#.....#.....#### 699 | ....###....#....... 700 | ###................ 701 | .......##...##..... 702 | ...#.....#.....#... 703 | .....##...##....... 704 | ................### 705 | .......#....###.... 706 | ####.....#.....#... 707 | ....##...###...#... 708 | ........#.......... 709 | ......#.....#...... 710 | #.....#.....#...... 711 | #.....#....###..... 712 | ''' 713 | #broken 714 | #detects many extraneous vertical bars 715 | #cells_with_text='auto' works if bar detection disabled 716 | 717 | class QED(ImageTest): 718 | url='2012/puzzles/ben_bitdiddle/qed/1.png' 719 | rows=9 720 | cols=9 721 | # the diagonal should be a very light gray; we don't detect this at the moment 722 | fill=''' 723 | ....#.... 724 | ....#.... 725 | ...#....# 726 | ##...#... 727 | ..#...#.. 728 | ...#...## 729 | #....#... 730 | ....#.... 731 | ....#.... 732 | ''' 733 | cells_with_text='auto' 734 | 735 | class PipeDream2(ImageTest): 736 | url='2011/puzzles/world1/pipe_dream_2/assets/grid.png' 737 | rows=7 738 | cols=7 739 | fill=''' 740 | ....O.. 741 | ..O...O 742 | O...O#. 743 | ...#... 744 | .#O...O 745 | O...O.. 746 | ..O.... 747 | ''' 748 | cells_with_text=''' 749 | .***..* 750 | *.....* 751 | *..**.* 752 | .**...* 753 | ..*.... 754 | ....... 755 | ***..** 756 | ''' 757 | 758 | class RumpledManWithABowlCut(ImageTest): 759 | url='2009/puzzles/the_rumpled_man_with_a_bowl_cut/PUZZLE/600px-acrostic.png' 760 | rows=3 761 | cols=18 762 | fill=''' 763 | ..#..#...#........ 764 | ..#....#...#.....# 765 | .........#.#.....# 766 | ''' 767 | cells_with_text=''' 768 | **.**.***.******** 769 | **.****.***.*****. 770 | *********.*.*****. 771 | ''' 772 | 773 | class GoodTimesInTheCasino(ImageTest): 774 | 775 | url='2011/puzzles/mega_man/good_times_in_the_casino/assets/grid.png' 776 | rows=13 777 | cols=21 778 | fill=''' 779 | ......##....#........ 780 | .#.#.#.#.#.#.#.#.#.#. 781 | .......#.......#..... 782 | ##.#.#.#####.#.#.#.#. 783 | .....#........#...... 784 | .#####.#.#.#.#.#.#.#. 785 | .......#.....#....... 786 | .#.#.#.#.#.#.#.#####. 787 | ......#........#..... 788 | .#.#.#.#.#####.#.#.## 789 | .....#.......#....... 790 | .#.#.#.#.#.#.#.#.#.#. 791 | ........#....##...... 792 | ''' 793 | cells_with_text='auto' 794 | 795 | class CurseOfTheAtlanteansTomb(ImageTest): 796 | url='2015/puzzle/the_curse_of_the_atlanteans_tomb/images/curse-of-the-atlantean-tomb.png' 797 | rows=19 798 | cols=39 799 | #broken 800 | #fill 801 | #cells_with_text='auto' 802 | 803 | class TwistsAndTurns(ImageTest): 804 | url='2003/www.acme-corp.com/teamGuest/6/images/twists-and-turns.jpg' 805 | rows=12 806 | cols=12 807 | bars=''' 808 | +-+-+-+-+-+-+-+-+-+-+-+-+ 809 | | | | | | 810 | + +-+ + +-+ + +-+ + +-+ + 811 | | | | | | | | | | 812 | +-+ + +-+ + + + +-+ + +-+ 813 | | | | | | | | | 814 | + +-+ + +-+-+-+-+ + +-+ + 815 | | | | | | | | 816 | +-+ +-+-+ + + + +-+-+ +-+ 817 | | | | | | | | 818 | + +-+ +-+-+ + +-+-+ +-+ + 819 | | | | | | 820 | +-+-+-+ +-+-+-+-+ +-+-+-+ 821 | | | | | | 822 | + +-+ +-+-+ + +-+-+ +-+ + 823 | | | | | | | | 824 | +-+ +-+-+ + + + +-+-+ +-+ 825 | | | | | | | | 826 | + +-+ + +-+-+-+-+ + +-+ + 827 | | | | | | | | | 828 | +-+ + +-+ + + + +-+ + +-+ 829 | | | | | | | | | | 830 | + +-+ + +-+ + +-+ + +-+ + 831 | | | | | | 832 | +-+-+-+-+-+-+-+-+-+-+-+-+ 833 | ''' 834 | 835 | class ThatsRight(ImageTest): 836 | url='2003/www.acme-corp.com/teamGuest/7/images/thats-right.jpg' 837 | rows=12 838 | cols=12 839 | cells_with_text=''' 840 | *.**.*.*..** 841 | .*.*........ 842 | ....*....**. 843 | ..*......... 844 | *....*.*..*. 845 | ....*....... 846 | ..***....... 847 | .*.......*** 848 | ..*.....*... 849 | .**....*.... 850 | ...*.**...*. 851 | **..*.*.*.*. 852 | ''' 853 | 854 | class PipeDream(ImageTest): 855 | url='2006/puzzles/epcot_center/pipe_dream/pipedreamgrid.png' 856 | rows=9 857 | cols=9 858 | fill=''' 859 | ......... 860 | ...#..... 861 | ......#.. 862 | ..#.....# 863 | ....#.... 864 | #.....#.. 865 | ..#...... 866 | .....#... 867 | ......... 868 | ''' 869 | cells_with_text=''' 870 | **.*.**.* 871 | ..*.*...* 872 | .....*.*. 873 | *..**.... 874 | *.*...*.* 875 | ..*...... 876 | *........ 877 | *.*...*.* 878 | *....**.* 879 | ''' 880 | 881 | class WhenNotInRome(ImageTest): 882 | url='2007/puzzles/when_not_in_rome/grid.png' 883 | rows=13 884 | cols=13 885 | fill=''' 886 | .......#..... 887 | .#.#.#.#.#.#. 888 | .....#....... 889 | .#.#.#.#.#.#. 890 | ........#.... 891 | .###.#.#.#.#. 892 | #.....#.....# 893 | .#.#.#.#.###. 894 | ....#........ 895 | .#.#.#.#.#.#. 896 | .......#..... 897 | .#.#.#.#.#.#. 898 | .....#....... 899 | ''' 900 | 901 | class HowFar(ImageTest): 902 | url='2016/puzzle/how_far/images/howfargrid.png' 903 | rows=17 904 | cols=17 905 | cells_with_text='auto' 906 | #broken 907 | #dark squares are considered as light squares with bars on all sides 908 | 909 | class ManInTheMoon(ImageTest): 910 | url='2016/puzzle/the_man_in_the_moon/images/puzzle.pdf' 911 | rows=12 912 | cols=12 913 | bars=''' 914 | +-+-+-+-+-+-+-+-+-+-+-+-+ 915 | | | | 916 | + +-+ + +-+ + +-+ + 917 | | | | | | | 918 | + + + +-+ +-+ + + + + 919 | | | | | | 920 | + + + +-+ + +-+ + + 921 | | | | | 922 | + +-+ + + +-+ + + 923 | | | | 924 | + +-+ +-+ +-+ +-+ + + + + 925 | | | | | | | | 926 | +-+ +-+ + + + + +-+ +-+ 927 | | | | | | | | 928 | + + + + +-+ +-+ +-+ +-+ + 929 | | | | 930 | + + +-+ + + +-+ + 931 | | | | | 932 | + + +-+ + +-+ + + + 933 | | | | | | 934 | + + + + +-+ +-+ + + + 935 | | | | | | | 936 | + +-+ + +-+ + +-+ + 937 | | | | 938 | +-+-+-+-+-+-+-+-+-+-+-+-+ 939 | ''' 940 | cells_with_text='auto' 941 | 942 | class CrossedSwords(ImageTest): 943 | url='2010/puzzles/1752/crossed_swords/crossed_swords.gif' 944 | rows=15 945 | cols=15 946 | bars=''' 947 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 948 | | | | | 949 | + +-+ +-+ +-+ +-+ + +-+ + + 950 | | | | | | 951 | + +-+ + + +-+ + +-+ + + 952 | | | | | 953 | + +-+ +-+ + + +-+ + +-+ + 954 | | | | | | | 955 | + +-+-+ +-+ + + +-+ +-+ 956 | | | | | | | 957 | +-+ +-+ + + +-+ +-+ + 958 | | | | | 959 | + +-+ + +-+-+ +-+ + + 960 | | | | | 961 | + +-+-+ +-+ +-+ +-+ + + 962 | | | | | 963 | + + +-+ +-+ +-+ +-+-+ + 964 | | | | | 965 | + + +-+ +-+-+ + +-+ + 966 | | | | | 967 | + +-+ +-+ + + +-+ +-+ 968 | | | | | | | 969 | +-+ +-+ + + +-+ +-+-+ + 970 | | | | | | | 971 | + +-+ + +-+ + + +-+ +-+ + 972 | | | | | 973 | + + +-+ + +-+ + + +-+ + 974 | | | | | | 975 | + + +-+ + +-+ +-+ +-+ +-+ + 976 | | | | | 977 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 978 | ''' 979 | #broken 980 | #cells_with_text works on my system but not on TravisCI 981 | #cells_with_text='auto' 982 | #highlighted squares on diagonal 983 | 984 | @unittest.skip('the pdf seems to have been corrupted') 985 | class IndescribableAmorphousCryptic(ImageTest): 986 | url='2016/puzzle/the_indescribable_amorphous_cryptic/images/puzzle.pdf' 987 | rows=13 988 | cols=13 989 | bars=''' 990 | +-+-+-+-+-+-+-+-+-+-+-+-+-+ 991 | | | | 992 | + +-+ +-+-+ +-+ +-+ + + 993 | | | | | | | | 994 | + + + +-+ + +-+ +-+ + + + + 995 | | | | | | 996 | + + + +-+ + +-+ +-+ + + + 997 | | | | | | 998 | +-+ +-+ + + +-+ +-+ + + + 999 | | | | | | | | 1000 | + + + + + + + +-+-+ + 1001 | | | | | 1002 | + +-+ + + +-+ + +-+ + 1003 | | | | | 1004 | + +-+ + +-+ + + +-+ + 1005 | | | | | 1006 | + +-+-+ + + + + + + + 1007 | | | | | | | | 1008 | + + + +-+ +-+ + + +-+ +-+ 1009 | | | | | | 1010 | + + + +-+ +-+ + +-+ + + + 1011 | | | | | | 1012 | + + + + +-+ +-+ + +-+ + + + 1013 | | | | | | | | 1014 | + + +-+ +-+ +-+-+ +-+ + 1015 | | | | 1016 | +-+-+-+-+-+-+-+-+-+-+-+-+-+ 1017 | ''' 1018 | cells_with_text='auto' 1019 | 1020 | class TheCapriciousType(ImageTest): 1021 | url='2010/puzzles/2000/the_capricious_type/the_capricious_type.gif' 1022 | rows=12 1023 | cols=12 1024 | bars=''' 1025 | +-+-+-+-+-+-+-+-+-+-+-+-+ 1026 | | | | 1027 | + +-+ +-+ +-+ +-+ + 1028 | | | | | 1029 | + + + + + +-+ + 1030 | | | | | | 1031 | + + + + + +-+ + 1032 | | | | | 1033 | + + +-+ + +-+ + + 1034 | | | | | | 1035 | + +-+ +-+ + + +-+ + 1036 | | | | 1037 | +-+ + + +-+ +-+-+ 1038 | | | | | 1039 | + +-+ +-+-+ +-+-+ + 1040 | | | | | | 1041 | + + +-+ + +-+ +-+ +-+ + 1042 | | | | | | | 1043 | + + + +-+ +-+ + + 1044 | | | | | 1045 | + +-+ +-+ + + +-+ + + + 1046 | | | | | | 1047 | + +-+ +-+ +-+ +-+ +-+ + 1048 | | | | 1049 | +-+-+-+-+-+-+-+-+-+-+-+-+ 1050 | ''' 1051 | #broken 1052 | #cells_with_text works on my system but not on TravisCI 1053 | #cells_with_text='auto' 1054 | 1055 | @unittest.skip('dimensions not detected correctly') 1056 | class CrossExamination(ImageTest): 1057 | url='2008/cross_examination/images/crossexamination.gif' 1058 | rows=13 1059 | cols=13 1060 | 1061 | class JoinedAtTheHip(ImageTest): 1062 | url='2006/puzzles/mcmurdo_station/joined_at_the_hip/puzzle.pdf' 1063 | rows=13 1064 | cols=13 1065 | bars=''' 1066 | +-+-+-+-+-+-+-+-+-+-+-+-+-+ 1067 | | | | | 1068 | + +-+ +-+ + +-+ + +-+ +-+ + 1069 | | | | | | 1070 | + + +-+ + +-+ + +-+ + + + 1071 | | | | | | | | | 1072 | +-+ + +-+ + +-+ + +-+ + + + 1073 | | | | | | | | 1074 | + + + +-+ + + +-+ +-+ + 1075 | | | | | 1076 | + + + +-+-+ + +-+ + +-+ + + 1077 | | | | | | | | | | 1078 | + +-+ +-+ +-+ + + +-+ + 1079 | | | | | 1080 | + +-+ + + +-+ +-+ +-+ + 1081 | | | | | | | | | | 1082 | + + +-+ + +-+ + +-+-+ + + + 1083 | | | | | 1084 | + +-+ +-+ + + +-+ + + + 1085 | | | | | | | | 1086 | + + + +-+ + +-+ + +-+ + +-+ 1087 | | | | | | | | | 1088 | + + + +-+ + +-+ + +-+ + + 1089 | | | | | | 1090 | + +-+ +-+ + +-+ + +-+ +-+ + 1091 | | | | | 1092 | +-+-+-+-+-+-+-+-+-+-+-+-+-+ 1093 | ''' 1094 | cells_with_text='auto' 1095 | 1096 | class MysteriousCryQuietHabit(ImageTest): 1097 | url='2006/puzzles/kuala_lumpur/mysterious_cry_quiet_habit/grid.png' 1098 | rows=26 1099 | cols=21 1100 | bars=''' 1101 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 1102 | | | | | 1103 | +-+-+-+ +-+-+-+-+ + 1104 | | | | 1105 | +-+-+-+ +-+-+ +-+ +-+ + 1106 | | | | | | | | | 1107 | + + + + + +-+ + + + 1108 | | | | | | | | | | | 1109 | + + + + + +-+ + +-+-+-+-+ + + 1110 | | | | | | | | | | | 1111 | + + + + + + + + +-+-+-+-+ + + 1112 | | | | | | | | | | | | | 1113 | + + +-+-+ +-+ +-+ + + + + 1114 | | | | | | | 1115 | + +-+-+-+-+-+ +-+ + + + + 1116 | | | | | | | | | 1117 | + + + +-+ +-+-+-+ +-+-+ + 1118 | | | | | | | 1119 | + +-+ +-+-+ +-+-+ +-+-+-+ +-+-+ + 1120 | | | | | | | | | 1121 | + + + + +-+-+ +-+ +-+ + 1122 | | | | | | | | | 1123 | + + + + + + + +-+ + 1124 | | | | | | | | | | | 1125 | + +-+ +-+-+ +-+ + +-+-+-+-+-+ + + 1126 | | | | | | | 1127 | + +-+ +-+-+-+-+ +-+-+-+-+-+-+ + + 1128 | | | | | | | 1129 | + + +-+-+-+-+-+-+ +-+-+-+ +-+-+-+-+-+ 1130 | | | | | | 1131 | + + +-+-+-+ +-+ + +-+ +-+ +-+-+-+-+-+ 1132 | | | | | | | | | | | | | 1133 | + + + +-+-+ +-+ +-+-+ + +-+ +-+ + 1134 | | | | | | | | | 1135 | + + + + +-+ +-+ +-+-+ + +-+ +-+ + 1136 | | | | | | | | | | | | | | | 1137 | + +-+ + + + + +-+ + + + + + 1138 | | | | | | | | | | | 1139 | + + + + + +-+-+-+-+ + +-+ + 1140 | | | | | | | | | 1141 | + + + +-+ +-+-+-+-+-+ + 1142 | | | | | 1143 | + + +-+-+-+-+-+-+-+ + 1144 | | | | | 1145 | + + +-+-+ +-+-+ +-+ + 1146 | | | | | | | | | 1147 | + + + + + + + + 1148 | | | | | | | | | 1149 | + + + + +-+-+ +-+-+-+ + 1150 | | | | | | | 1151 | + + + + +-+-+ +-+-+-+ + 1152 | | | | | | | | | 1153 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 1154 | ''' 1155 | 1156 | class CommonBonds05(ImageTest): 1157 | url='2005/setec/common_bonds/COMMON_BONDS.pdf' 1158 | rows=13 1159 | cols=13 1160 | bars=''' 1161 | +-+-+-+-+-+-+-+-+-+-+-+-+-+ 1162 | | | | | 1163 | + + +-+ + 1164 | | | | 1165 | + + + + 1166 | | | | 1167 | + +-+-+ +-+ +-+ +-+ + 1168 | | | | | | 1169 | + +-+ + +-+-+ + + +-+ + 1170 | | | | | | 1171 | +-+ +-+ + + +-+ + + 1172 | | | | 1173 | + + + + + + + + + + + 1174 | | | | | | | | | | | 1175 | + + + + + + + + + + + 1176 | | | | 1177 | + + +-+ + + +-+ +-+ 1178 | | | | | | 1179 | + +-+ + + +-+-+ + +-+ + 1180 | | | | | | 1181 | + +-+ +-+ +-+ +-+-+ + 1182 | | | | 1183 | + + + + 1184 | | | | 1185 | + +-+ + + 1186 | | | | | 1187 | +-+-+-+-+-+-+-+-+-+-+-+-+-+ 1188 | ''' 1189 | cells_with_text='auto' 1190 | 1191 | class SixOfOne(ImageTest): 1192 | url='2005/setec/six_of_one/six%20of%20one.pdf' 1193 | rows=13 1194 | cols=13 1195 | fill=''' 1196 | O............ 1197 | O...........O 1198 | ............. 1199 | ...O......... 1200 | .........O... 1201 | ..........O.. 1202 | ...........O. 1203 | ............. 1204 | .....O....... 1205 | ............. 1206 | ............. 1207 | .........O... 1208 | ....O........ 1209 | ''' 1210 | bars=''' 1211 | +-+-+-+-+-+-+-+-+-+-+-+-+-+ 1212 | | | | 1213 | +-+ +-+ + +-+ +-+ 1214 | | | | | 1215 | + + +-+ + +-+ +-+ 1216 | | | | | 1217 | + + + +-+ + +-+-+ +-+ 1218 | | | | | | | | 1219 | + + +-+ + + +-+ +-+ + 1220 | | | | | | 1221 | + + +-+ +-+-+ + +-+ + + 1222 | | | | | | | | 1223 | + +-+ +-+-+ +-+ + +-+ + 1224 | | | | | 1225 | + + + +-+ +-+ + +-+ + 1226 | | | | | 1227 | + + + +-+ +-+ +-+ + + 1228 | | | | | | 1229 | +-+ +-+ +-+ +-+ + + + + + 1230 | | | | | | | | | 1231 | + + + +-+ +-+ +-+ + + 1232 | | | | 1233 | + + + +-+-+ +-+-+ + + + 1234 | | | | | | | | 1235 | + + + +-+-+-+ +-+-+ + + + 1236 | | | | | 1237 | +-+-+-+-+-+-+-+-+-+-+-+-+-+ 1238 | ''' 1239 | cells_with_text=''' 1240 | .**..****.**. 1241 | .*........... 1242 | *...*....*... 1243 | ....*.....*.* 1244 | *....*....*.. 1245 | ..*...*...... 1246 | .*..*...*.... 1247 | *...*.....**. 1248 | ...*......... 1249 | *.*....*..... 1250 | *.......*.*.. 1251 | ............. 1252 | *............ 1253 | ''' 1254 | 1255 | class LeadWithHydrogen(ImageTest): 1256 | url='2017/assets/lead_with_hydrogen/puzzle.png' 1257 | rows=11 1258 | cols=11 1259 | fill=''' 1260 | .....#....# 1261 | .#.#.#.##.# 1262 | .....#..... 1263 | .#......#.# 1264 | ........... 1265 | ....#.#.... 1266 | ........... 1267 | #.#......#. 1268 | .....#..... 1269 | #.##.#.#.#. 1270 | #....#..... 1271 | ''' 1272 | cells_with_text='auto' 1273 | 1274 | class CrimesAgainstCruciverbalism(ImageTest): 1275 | url='2016/puzzle/crimes_against_cruciverbalism/images/puzzle.pdf' 1276 | rows=15 1277 | cols=15 1278 | fill=''' 1279 | ....#.....#.... 1280 | ....#.....#.... 1281 | ....#.....#.... 1282 | ....#....#..... 1283 | ...#....#...### 1284 | .......#...#... 1285 | ##...##...#.... 1286 | ............... 1287 | ....#...##...## 1288 | ...#...#....... 1289 | ###...#....#... 1290 | .....#....#.... 1291 | ....#.....#.... 1292 | ....#.....#.... 1293 | ....#.....#.... 1294 | ''' 1295 | cells_with_text='auto' 1296 | 1297 | class FleshedOut(ImageTest): 1298 | url='2017/assets/fleshed_out/image01.gif' 1299 | rows=11 1300 | cols=18 1301 | bars=''' 1302 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 1303 | | | | | 1304 | +-+ +-+ + +-+ + +-+ + 1305 | | | | | | | | 1306 | +-+ + + + + + + + + + + + + + + + + + 1307 | | | | | | | | | | | | | | | | | | 1308 | + + + + + +-+ + + + + + +-+ + + + 1309 | | | | | 1310 | + +-+ + +-+ + + + +-+ 1311 | | | | | | | 1312 | + +-+ +-+-+ +-+ +-+ +-+-+ +-+ +-+ + 1313 | | | | | 1314 | + +-+ +-+ +-+-+ +-+ +-+ +-+-+ +-+ + 1315 | | | | | | | 1316 | +-+ + + + +-+ + +-+ + 1317 | | | | | 1318 | + + + +-+ + + + + + +-+ + + + + + 1319 | | | | | | | | | | | | | | | | | | 1320 | + + + + + + + + + + + + + + + + + +-+ 1321 | | | | | | | | 1322 | + +-+ + +-+ + +-+ +-+ 1323 | | | | | 1324 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 1325 | ''' 1326 | cells_with_text='auto' 1327 | 1328 | del ImageTest 1329 | --------------------------------------------------------------------------------