Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
jonpecar committed Sep 27, 2022
2 parents 8e0f5f7 + b19a672 commit c2cad00
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 46 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10"]
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]

steps:
- uses: actions/checkout@v3
Expand Down
12 changes: 2 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "qrImageIndexer"
<<<<<<< Updated upstream
version = "0.6.2"
=======
version = "0.7.3"
>>>>>>> Stashed changes
version = "0.7.2"
description = "Tool for indexing images with QR codes"
readme = "README.md"
authors = [{name = "Jonathan Pecar", email = "jonathan.pecar@gmail.com"}]
Expand All @@ -35,11 +31,7 @@ dev = ["pytest", "bumpver", "setuptools"]
[project.urls]
Homepage = "https://github.com/jonpecar/qrCodeImageSorter"
[tool.bumpver]
<<<<<<< Updated upstream
current_version = "0.6.2"
=======
current_version = "0.7.3"
>>>>>>> Stashed changes
current_version = "0.7.2"
version_pattern = "MAJOR.MINOR.PATCH"
commit_message = "bump version {old_version} -> {new_version}"
commit = true
Expand Down
6 changes: 1 addition & 5 deletions qrImageIndexer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
<<<<<<< Updated upstream
__version__ = "0.6.2"
=======
__version__ = "0.7.3"
>>>>>>> Stashed changes
__version__ = "0.7.2"
11 changes: 4 additions & 7 deletions qrImageIndexer/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from qrImageIndexer.qr_generator import load_text_file, print_struct_outline, unpack_data, generate_qr_code_structure
from qrImageIndexer.generate_qr_wrapper import generate_qr_pdf
from qrImageIndexer.photo_sorter import sort_directory
from qrImageIndexer.write_pdf_fpf2 import build_pdf_report

Expand Down Expand Up @@ -46,18 +47,14 @@ def main():
sliceable = args.pdf_type[0] == 'sliceable'

text_data = load_text_file(input)

data_struct = {}
unpack_data(text_data, args.qr_for_headings, data_struct, string_header)
image_struct = generate_qr_code_structure(data_struct)

if verbose:
print('Loaded text file: ' + input)
print('')
print('Read data structure: ')
print_struct_outline(image_struct)
print('Read lines: ')
print(text_data)

pdf = build_pdf_report(image_struct, args.repeat_table_headings, sliceable)
pdf = generate_qr_pdf(text_data, args.qr_for_headings, args.repeat_table_headings, sliceable, string_header)
pdf.output(output)
if verbose:
print('Saved pdf: ' + output)
Expand Down
11 changes: 11 additions & 0 deletions qrImageIndexer/generate_qr_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from distutils.command import build
from typing import List
from fpdf import FPDF
from qrImageIndexer.qr_generator import unpack_data, generate_qr_code_structure
from qrImageIndexer.write_pdf_fpf2 import build_pdf_report

def generate_qr_pdf(text_data: List[List[str]], qr_for_headings : bool,
repeat_headings : bool, sort_sliceable : bool, string_header : str = '') -> FPDF:
data_struct = unpack_data(text_data, qr_for_headings, string_header)
iamge_struct = generate_qr_code_structure(data_struct)
return build_pdf_report(iamge_struct, repeat_headings, sort_sliceable)
36 changes: 28 additions & 8 deletions qrImageIndexer/photo_sorter.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ def sort_directory(input_dir : str, output_dir : str, string_header : str = '',
List[str] of all paths found in QR codes
"""

found_directories = []


non_image_dir = os.path.join(output_dir, 'non_image_files')

Expand All @@ -215,24 +215,44 @@ def sort_directory(input_dir : str, output_dir : str, string_header : str = '',
os.makedirs(output_dir, exist_ok=True)


found_directories = sort_directory_exisitng_results(results, input_dir, output_dir, verbose)

found_directories.sort()
return found_directories

def sort_directory_exisitng_results(results : Dict[str, str], input_dir : str, output_dir : str, verbose : bool = False) -> List[str]:
"""
Takes results from the QR code scanning and uses that information to sort the images. Function separated from
above for better integration with GUI code.
Parameters:
results: Dictionary of results including image path and QR code result
input_dir: input directory containing photos as string
output_dir: output directory to save images in
verbose: whether or not to write verbose output to the terminal
Returns:
List[str] of all paths found in QR codes
"""
found_directories = []
non_image_dir = os.path.join(output_dir, 'non_image_files')
image_paths = get_image_paths(input_dir, non_image_dir, verbose)

if verbose:
print('Sorting image files')
current_path = os.path.join(output_dir, 'unsorted')
for image_path in tqdm.tqdm(image_paths) if verbose else image_paths:
_, image = os.path.split(image_path)
qr_string = results[image_path]
qr_string = ''
if image_path in results:
qr_string = results[image_path]
if qr_string:
if qr_string.startswith(string_header):
qr_string = qr_string[len(string_header):]
qr_string = sanitise_path(qr_string)
current_path = os.path.join(output_dir, qr_string)
if qr_string not in found_directories:
found_directories.append(qr_string)

os.makedirs(current_path, exist_ok=True)
shutil.copyfile(image_path, os.path.join(current_path, image))

found_directories.sort()

return found_directories


37 changes: 29 additions & 8 deletions qrImageIndexer/qr_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,28 @@ def generate_qr_code_structure(data_structure : Dict[str, Tuple[Dict, str]]) ->
result[key] = (sub_struct, image)
return result

def unpack_data(data : List[List[str]], gen_qr_headings : bool, data_structure : Dict[str, Tuple[Dict, str]], string_header : str = '',
index : int = 0, previous_levels : str = '') -> int:
def unpack_data(data : List[List[str]], gen_qr_headings : bool, string_header : str = '') -> Dict[str, Tuple[Dict, str]]:
"""
Function to unpack a tabulated text file where items are grouped by tab depth. Called recursively for each loweer level of
Function to unpack tabulated text where items are grouped by tab depth. Calls the recursive function to remove the
complexity from view of potential users.
Inputs:
data - List of string representing data
gen_qr_headings - Boolean indicating if QR codes are to be built for headings or just for final elements
string_header - String to include as header for all QR code values to help distinguish from general QR codes
Returns:
index - Data structure in dictionary as required by the remainder of the tool
"""
_, data_structure = unpack_data_recurse(data, gen_qr_headings, string_header=string_header)
return data_structure

def unpack_data_recurse(data : List[List[str]], gen_qr_headings : bool, data_structure : Dict[str, Tuple[Dict, str]] = None, string_header : str = '',
index : int = 0, previous_levels : str = '') -> Tuple[int, Dict[str, Tuple[Dict, str]]]:
"""
Function to unpack a tabulated text where items are grouped by tab depth. Called recursively for each loweer level of
the data structure.
Inputs:
Expand All @@ -88,11 +106,14 @@ def unpack_data(data : List[List[str]], gen_qr_headings : bool, data_structure :
code for sorting
Returns:
Returns below in a Tuple:
index - integer representing current index of the process
data_struct - Data structure in dictionary as required by the remainder of the tool
"""
if data_structure is None:
data_structure = {}
#Determine target indent from first entry. All subsequent should be the same.
target_indent = count_leading_indent(data[index])
while index < len(data):
Expand All @@ -102,7 +123,7 @@ def unpack_data(data : List[List[str]], gen_qr_headings : bool, data_structure :
continue
# Check if this index is less indented than the target. If so we need to return a level.
if count_leading_indent(data[index]) < target_indent:
return index
return index, data_structure

# Get some data for this iteration
raw_line = data[index][target_indent]
Expand All @@ -127,12 +148,12 @@ def unpack_data(data : List[List[str]], gen_qr_headings : bool, data_structure :
# Check if the next line is further indented or if it is less indented. If more indented then recursively call function.
# Otherwise increment
if indent_diff < 0:
index = unpack_data(data, gen_qr_headings, next_data_struct, string_header, index + 1, next_level_str)
index, _ = unpack_data_recurse(data, gen_qr_headings, next_data_struct, string_header, index + 1, next_level_str)
else:
index += 1


return index
return index, data_structure

def count_leading_indent(line : List[str]) -> int:
"""
Expand Down Expand Up @@ -179,4 +200,4 @@ def build_indent_string_print(indent_level : int):
for _ in range(indent_level):
result += '\t'
result += '|___'
return result
return result
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ fpdf2
qrcode
tqdm
opencv-python
PyPDF2
PyPDF2
pytest
pytest-mock
65 changes: 59 additions & 6 deletions test/test_qr_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from os import path
from PIL import ImageChops


def build_demo_tsv(file_path : pathlib.Path):
content = '''Level1-1
Level1-2
Expand Down Expand Up @@ -33,6 +34,25 @@ def demo_tsv_expected_data():
data.append(['', '', 'Level3'])
return data

def demo_tsv_drop_index():
data = []
data.append(['Level1-1'])
data.append(['', 'Level2-1'])
data.append(['', 'Level2-2'])
data.append(['', '', 'Level3'])
data.append(['Level1-2'])
return data

def demo_data_struct_drop_indexinclude_headers():
test_struct = {}
test_struct['Level1-1'] = ({}, 'Level1-1' + path.sep)
test_struct['Level1-1'][0]['Level2-1'] = ({}, 'Level1-1' + path.sep + 'Level2-1' + path.sep)
test_struct['Level1-1'][0]['Level2-2'] = ({}, 'Level1-1' + path.sep + 'Level2-2' + path.sep)
test_struct['Level1-1'][0]['Level2-2'][0]['Level3'] = ({}, 'Level1-1' + path.sep + 'Level2-2' + path.sep + 'Level3' + path.sep)
test_struct['Level1-2'] = ({}, 'Level1-2' + path.sep)
return test_struct


def demo_data_blank_line_end():
data = []
data.append(['Level1-1'])
Expand Down Expand Up @@ -148,38 +168,71 @@ def test_count_leading_indent():

def test_unpack_data_include_headers():
result_struct = {}
qr_generator.unpack_data(demo_tsv_expected_data(), True, result_struct)
qr_generator.unpack_data_recurse(demo_tsv_expected_data(), True, result_struct)

assert demo_data_struct_include_headers() == result_struct

def test_unpack_data_include_headers_recurse_equivalence():
result_struct_1 = {}
qr_generator.unpack_data_recurse(demo_tsv_expected_data(), True, result_struct_1)
result_struct_2 = qr_generator.unpack_data(demo_tsv_expected_data(), True)

assert result_struct_1 == result_struct_2

def test_unpack_data_blank_line_end():
result_struct = {}
qr_generator.unpack_data(demo_data_blank_line_end(), True, result_struct)
qr_generator.unpack_data_recurse(demo_data_blank_line_end(), True, result_struct)

assert demo_data_struct_include_headers() == result_struct

def test_unpack_data_blank_line_end_recurse_equivalence():
result_struct_1 = {}
qr_generator.unpack_data_recurse(demo_data_blank_line_end(), True, result_struct_1)
result_struct_2 = qr_generator.unpack_data(demo_data_blank_line_end(), True)

assert result_struct_1 == result_struct_2

def test_unpack_data_blank_line_mid():
result_struct = {}
qr_generator.unpack_data(demo_data_blank_line_mid(), True, result_struct)
qr_generator.unpack_data_recurse(demo_data_blank_line_mid(), True, result_struct)

assert demo_data_struct_include_headers() == result_struct

def test_unpack_data_no_headers():
result_struct = {}
qr_generator.unpack_data(demo_tsv_expected_data(), False, result_struct)
qr_generator.unpack_data_recurse(demo_tsv_expected_data(), False, result_struct)

assert demo_data_struct_no_headers() == result_struct

def test_unpack_data_no_headers_recurse_equivalence():
result_struct_1 = {}
qr_generator.unpack_data_recurse(demo_tsv_expected_data(), False, result_struct_1)
result_struct_2 = qr_generator.unpack_data(demo_tsv_expected_data(), False)

assert result_struct_1 == result_struct_2

def test_unpack_data_include_headers_QRHeader():
result_struct = {}
qr_generator.unpack_data(demo_tsv_expected_data(), True, result_struct, r'{image}')
qr_generator.unpack_data_recurse(demo_tsv_expected_data(), True, result_struct, r'{image}')

assert demo_data_struct_include_headers_with_QRHeader() == result_struct

def test_unpack_data_include_headers_QRHeader_recurse_equivalence():
result_struct_1 = {}
qr_generator.unpack_data_recurse(demo_tsv_expected_data(), True, result_struct_1, r'{image}')
result_struct_2 = qr_generator.unpack_data(demo_tsv_expected_data(), True, r'{image}')

assert result_struct_1 == result_struct_2

def test_structure_qr_builder():
expected_struct = demo_data_struct_no_headers_images()
generated_struct = qr_generator.generate_qr_code_structure(demo_data_struct_no_headers())
assert not ImageChops.difference(expected_struct['Level1-1'][1], generated_struct['Level1-1'][1]).getbbox()
assert not ImageChops.difference(expected_struct['Level1-2'][0]['Level2-1'][1], generated_struct['Level1-2'][0]['Level2-1'][1]).getbbox()
assert not ImageChops.difference(expected_struct['Level1-2'][0]['Level2-2'][0]['Level3'][1], generated_struct['Level1-2'][0]['Level2-2'][0]['Level3'][1]).getbbox()
assert expected_struct['Level1-2'][0]['Level2-2'][1] == generated_struct['Level1-2'][0]['Level2-2'][1]
assert expected_struct['Level1-2'][0]['Level2-2'][1] == generated_struct['Level1-2'][0]['Level2-2'][1]

def test_unpack_data_drop_leve():
result_struct = qr_generator.unpack_data(demo_tsv_drop_index(), True)

assert result_struct == demo_data_struct_drop_indexinclude_headers()
50 changes: 50 additions & 0 deletions test/test_qr_pdf_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from qrImageIndexer import generate_qr_wrapper
from unittest import mock

def mock_qr_generator_functions(mocker : mock):
unpack_data = mocker.patch('qrImageIndexer.generate_qr_wrapper.unpack_data')
generate_qr_structure = mocker.patch('qrImageIndexer.generate_qr_wrapper.generate_qr_code_structure')
build_pdf = mocker.patch('qrImageIndexer.generate_qr_wrapper.build_pdf_report')

return (unpack_data, generate_qr_structure, build_pdf)


def test_single_func_pdf_builder_all_true(mocker):
(unpack_data, generate_qr_structure, build_pdf) = mock_qr_generator_functions(mocker)
result = generate_qr_wrapper.generate_qr_pdf([['Line 1', ''], ['', 'Line 1 Indent']],
True, True, True, r'{header}')

unpack_data.assert_called_once_with([['Line 1', ''], ['', 'Line 1 Indent']], True, r'{header}')
generate_qr_structure.assert_called_once_with(unpack_data.return_value)
build_pdf.assert_called_once_with(generate_qr_structure.return_value, True, True)
assert result == build_pdf.return_value

def test_single_func_pdf_builder_no_qr_heading(mocker):
(unpack_data, generate_qr_structure, build_pdf) = mock_qr_generator_functions(mocker)
result = generate_qr_wrapper.generate_qr_pdf([['Line 1', ''], ['', 'Line 1 Indent']],
False, True, True, r'{header}')

unpack_data.assert_called_once_with([['Line 1', ''], ['', 'Line 1 Indent']], False, r'{header}')
generate_qr_structure.assert_called_once_with(unpack_data.return_value)
build_pdf.assert_called_once_with(generate_qr_structure.return_value, True, True)
assert result == build_pdf.return_value

def test_single_func_pdf_builder_no_title_all_heading(mocker):
(unpack_data, generate_qr_structure, build_pdf) = mock_qr_generator_functions(mocker)
result = generate_qr_wrapper.generate_qr_pdf([['Line 1', ''], ['', 'Line 1 Indent']],
True, False, True, r'{header}')

unpack_data.assert_called_once_with([['Line 1', ''], ['', 'Line 1 Indent']], True, r'{header}')
generate_qr_structure.assert_called_once_with(unpack_data.return_value)
build_pdf.assert_called_once_with(generate_qr_structure.return_value, False, True)
assert result == build_pdf.return_value

def test_single_func_pdf_builder_not_sliceable(mocker):
(unpack_data, generate_qr_structure, build_pdf) = mock_qr_generator_functions(mocker)
result = generate_qr_wrapper.generate_qr_pdf([['Line 1', ''], ['', 'Line 1 Indent']],
True, True, False, r'{header}')

unpack_data.assert_called_once_with([['Line 1', ''], ['', 'Line 1 Indent']], True, r'{header}')
generate_qr_structure.assert_called_once_with(unpack_data.return_value)
build_pdf.assert_called_once_with(generate_qr_structure.return_value, True, False)
assert result == build_pdf.return_value

0 comments on commit c2cad00

Please sign in to comment.