├── 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 |
22 | {% if config.URLOPEN_ENABLED %}
23 | Specify an image URL
24 |
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 [](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 |
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 |
--------------------------------------------------------------------------------
|