From fee9f49f1cc86dd4e3555e1416ee6eb71c9b4b5b Mon Sep 17 00:00:00 2001 From: Taka Date: Tue, 27 Apr 2021 02:47:05 +1000 Subject: [PATCH] Added a web interface for preparing data files. --- {bin => asl_rulebook2/bin}/dump_pdf.py | 0 {bin => asl_rulebook2/bin}/extract_pages.py | 0 {bin => asl_rulebook2/bin}/fixup_mmp_pdf.py | 69 +- {bin => asl_rulebook2/bin}/prepare_pdf.py | 70 +- asl_rulebook2/extract/all.py | 19 +- asl_rulebook2/extract/base.py | 12 - asl_rulebook2/extract/content.py | 6 +- asl_rulebook2/extract/index.py | 6 +- asl_rulebook2/tests/test_extract.py | 18 +- asl_rulebook2/tests/utils.py | 19 + asl_rulebook2/utils.py | 11 + asl_rulebook2/webapp/__init__.py | 1 + asl_rulebook2/webapp/globvars.py | 2 + asl_rulebook2/webapp/main.py | 20 +- asl_rulebook2/webapp/prepare.py | 214 + asl_rulebook2/webapp/run_server.py | 24 +- asl_rulebook2/webapp/static/css/global.css | 1 + asl_rulebook2/webapp/static/css/prepare.css | 37 + .../webapp/static/images/download.png | Bin 0 -> 2558 bytes asl_rulebook2/webapp/static/images/eASLRB.png | Bin 0 -> 12508 bytes asl_rulebook2/webapp/static/images/error.png | Bin 0 -> 1233 bytes .../webapp/static/images/warning.png | Bin 0 -> 1115 bytes asl_rulebook2/webapp/static/prepare.js | 364 + .../webapp/static/socketio/socket.io.js | 6046 +++++++++++++++++ .../webapp/static/socketio/socket.io.min.js | 7 + asl_rulebook2/webapp/templates/prepare.html | 39 + asl_rulebook2/webapp/tests/test_prepare.py | 126 + asl_rulebook2/webapp/tests/test_startup.py | 20 +- asl_rulebook2/webapp/tests/utils.py | 19 +- asl_rulebook2/webapp/utils.py | 5 + conftest.py | 12 + requirements.txt | 1 + setup.py | 6 - 33 files changed, 7047 insertions(+), 127 deletions(-) rename {bin => asl_rulebook2/bin}/dump_pdf.py (100%) rename {bin => asl_rulebook2/bin}/extract_pages.py (100%) rename {bin => asl_rulebook2/bin}/fixup_mmp_pdf.py (68%) rename {bin => asl_rulebook2/bin}/prepare_pdf.py (74%) create mode 100644 asl_rulebook2/tests/utils.py create mode 100644 asl_rulebook2/webapp/prepare.py create mode 100644 asl_rulebook2/webapp/static/css/prepare.css create mode 100644 asl_rulebook2/webapp/static/images/download.png create mode 100644 asl_rulebook2/webapp/static/images/eASLRB.png create mode 100644 asl_rulebook2/webapp/static/images/error.png create mode 100644 asl_rulebook2/webapp/static/images/warning.png create mode 100644 asl_rulebook2/webapp/static/prepare.js create mode 100644 asl_rulebook2/webapp/static/socketio/socket.io.js create mode 100644 asl_rulebook2/webapp/static/socketio/socket.io.min.js create mode 100644 asl_rulebook2/webapp/templates/prepare.html create mode 100644 asl_rulebook2/webapp/tests/test_prepare.py diff --git a/bin/dump_pdf.py b/asl_rulebook2/bin/dump_pdf.py similarity index 100% rename from bin/dump_pdf.py rename to asl_rulebook2/bin/dump_pdf.py diff --git a/bin/extract_pages.py b/asl_rulebook2/bin/extract_pages.py similarity index 100% rename from bin/extract_pages.py rename to asl_rulebook2/bin/extract_pages.py diff --git a/bin/fixup_mmp_pdf.py b/asl_rulebook2/bin/fixup_mmp_pdf.py similarity index 68% rename from bin/fixup_mmp_pdf.py rename to asl_rulebook2/bin/fixup_mmp_pdf.py index e23e5c9..091178f 100755 --- a/bin/fixup_mmp_pdf.py +++ b/asl_rulebook2/bin/fixup_mmp_pdf.py @@ -2,29 +2,26 @@ """ Fixup issues in the MMP eASLRB. """ import os -import math from pikepdf import Pdf, Page, OutlineItem, Encryption, make_page_destination import click +from asl_rulebook2.utils import log_msg_stderr + # --------------------------------------------------------------------- -def fixup_easlrb( fname, output_fname, optimize_web, rotate, log=None ): - """Fixup the eASLRB.""" +def fixup_mmp_pdf( fname, output_fname, optimize_web, rotate, log=None ): + """Fixup the MMP eASLRB PDF.""" def log_msg( msg_type, msg, *args, **kwargs ): if not log: return if isinstance( msg, list ): msg = "\n".join( msg ) - data = kwargs.pop( "data", None ) msg = msg.format( *args, **kwargs ) - log( msg_type, msg, data=data ) + log( msg_type, msg ) - def percentage( curr, total ): - return math.floor( 100 * float(curr) / float(total) ) - - # NOTE: It would be nice to use the targetes file to get the TOC entries and annotations + # NOTE: It would be nice to use the targets file to get the TOC entries and annotations # to point to the exact point on the page, but figuring out the text associated with each # annotiation is extremely messy (annotations are simply a rectangle on a page, so we need # to figure out which elements lie within that rectangle, and since things are not always @@ -32,24 +29,23 @@ def fixup_easlrb( fname, output_fname, optimize_web, rotate, log=None ): with Pdf.open( fname ) as pdf: - log_msg( "start", "Loaded PDF: {}".format( fname ), data=[ - ( "PDF version", pdf.pdf_version ), - ( "# pages", len(pdf.pages) ), - ] ) + log_msg( "start", "Loaded PDF: {}\n- PDF version = {}\n- #pages = {}".format( + fname, pdf.pdf_version, len(pdf.pages) ) + ) log_msg( None, "" ) # fixup bookmarks in the TOC - log_msg( "toc", "Fixing up the TOC..." ) + log_msg( "progress", "Fixing up the TOC..." ) def walk_toc( items, depth ): for item_no,item in enumerate(items): if item.destination[0].Type != "/Page" or item.destination[1] != "/Fit" \ or item.page_location is not None or item.page_location_kwargs != {}: - log_msg( "toc:warning", "Unexpected TOC item: {}/{}".format( depth, item_no ) ) + log_msg( "warning", "Unexpected TOC item: {}/{}".format( depth, item_no ) ) continue page = Page( item.destination[0] ) page_height = page.mediabox[3] bullet = "#" if depth <= 1 else "-" - log_msg( "toc:detail", " {}{} {} => p{}", + log_msg( "verbose", " {}{} {} => p{}", depth*" ", bullet, item.title, 1+page.index ) walk_toc( item.children, depth+1 ) @@ -60,16 +56,13 @@ def fixup_easlrb( fname, output_fname, optimize_web, rotate, log=None ): with pdf.open_outline() as outline: walk_toc( outline.root, 0 ) # NOTE: The TOC will be updated when we exit the context manager, and can take some time. - log_msg( "toc", "Installing the new TOC..." ) + log_msg( "progress", "Installing the new TOC..." ) log_msg( None, "" ) # fixup up each page - log_msg( "annoations", "Fixing up the content..." ) + log_msg( "progress", "Fixing up the content..." ) for page_no, raw_page in enumerate(pdf.pages): - log_msg( "annotations:progress", "- page {}", - 1+page_no, - data = { "percentage": percentage( page_no, len(pdf.pages) ) } - ) + log_msg( "verbose", "- page {}", 1+page_no ) if rotate: # force pages to be landscape (so that we don't get an h-scrollbar in Firefox # when we set the zoom to "fit width"). @@ -83,21 +76,20 @@ def fixup_easlrb( fname, output_fname, optimize_web, rotate, log=None ): dest = annot.get( "/Dest" ) if dest: page_no = Page( dest[0] ).index - log_msg( "annotations:detail", " - {} => p{}", + log_msg( "verbose", " - {} => p{}", repr(annot.Rect), 1+page_no ) annot.Dest = make_page_destination( pdf, page_no, "XYZ", top=page_height ) log_msg( None, "" ) # save the updated PDF - log_msg( "save", "Saving updated PDF: {}", output_fname ) + log_msg( "progress", "Saving the fixed-up PDF..." ) # NOTE: Setting a blank password will encrypt the file, but doesn't require the user to enter a password # when opening the file (but it will be marked as "SECURE" in the UI). enc = Encryption( owner="", user="" ) def save_progress( pct ): - log_msg( "save:progress", "- Saved {}%...", pct, - data = { "percentage": pct } - ) + if pct > 0 and pct % 10 == 0: + log_msg( "verbose", "- Saved {}%.", pct ) pdf.save( output_fname, encryption=enc, linearize=optimize_web, progress = save_progress ) @@ -107,9 +99,9 @@ def fixup_easlrb( fname, output_fname, optimize_web, rotate, log=None ): new_size = os.path.getsize( output_fname ) ratio = round( 100 * float(new_size) / float(old_size) ) - 100 if ratio == 0: - log_msg( "save", "The updated PDF file is about the same size as the original file." ) + log_msg( "verbose", "The updated PDF file is about the same size as the original file." ) else: - log_msg( "save", "The updated PDF file is about {}% {} than the original file.", + log_msg( "verbose", "The updated PDF file is about {}% {} than the original file.", abs(ratio), "larger" if ratio > 0 else "smaller" ) @@ -120,23 +112,18 @@ def fixup_easlrb( fname, output_fname, optimize_web, rotate, log=None ): @click.option( "--output","-o", required=True, type=click.Path(dir_okay=False), help="Where to save the fixed-up PDF." ) @click.option( "--optimize-web", is_flag=True, default=False, help="Optimize for use in a browser (larger file)." ) @click.option( "--rotate", is_flag=True, default=False, help="Rotate landscape pages." ) -@click.option( "--verbose","-v", is_flag=True, default=False, help="Verbose output." ) @click.option( "--progress","-p", is_flag=True, default=False, help="Log progress." ) -def main( pdf_file, output, optimize_web, rotate, verbose, progress ): +@click.option( "--verbose","-v", is_flag=True, default=False, help="Verbose output." ) +def main( pdf_file, output, optimize_web, rotate, progress, verbose ): """Fixup the eASLRB.""" - def log_msg( msg_type, msg, data=None ): - if not msg_type: - msg_type = "" - if msg_type.endswith( ":detail" ) and not verbose: + def log_msg( msg_type, msg ): + if msg_type in ("progress", "start", None) and not progress: return - if msg_type.endswith( ":progress" ) and not progress: + if msg_type == "verbose" and not verbose: return - print( msg ) - if msg_type == "start": - for k, v in data: - print( "- {:<12} {}".format( k+":", v ) ) - fixup_easlrb( pdf_file, output, optimize_web, rotate, log=log_msg ) + log_msg_stderr( msg_type, msg ) + fixup_mmp_pdf( pdf_file, output, optimize_web, rotate, log=log_msg ) if __name__ == "__main__": main() #pylint: disable=no-value-for-parameter diff --git a/bin/prepare_pdf.py b/asl_rulebook2/bin/prepare_pdf.py similarity index 74% rename from bin/prepare_pdf.py rename to asl_rulebook2/bin/prepare_pdf.py index 313ab47..071767d 100755 --- a/bin/prepare_pdf.py +++ b/asl_rulebook2/bin/prepare_pdf.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -""" Add named destinations to a PDF file. """ +""" Prepare the MMP eASLRB PDF. """ import subprocess import json @@ -8,7 +8,7 @@ import datetime import click -from asl_rulebook2.utils import TempFile +from asl_rulebook2.utils import TempFile, log_msg_stderr # NOTE: "screen" gives significant savings (~65%) but scanned PDF's become very blurry. The main MMP eASLRB # is not too bad, but some images are also a bit unclear. "ebook" gives no savings for scanned PDF's, but @@ -23,22 +23,8 @@ _COMPRESSION_CHOICES = [ # --------------------------------------------------------------------- -@click.command() -@click.argument( "pdf_file", nargs=1, type=click.Path(exists=True,dir_okay=False) ) -@click.option( "--title", help="Document title." ) -@click.option( "--targets","-t","targets_fname", required=True, type=click.Path(dir_okay=False), - help="Target definition file." -) -@click.option( "--yoffset", default=5, help="Offset to add to y co-ordinates." ) -@click.option( "--output","-o","output_fname", required=True, type=click.Path(dir_okay=False), - help="Output PDF file." -) -@click.option( "--compression", type=click.Choice(_COMPRESSION_CHOICES), default="ebook", - help="Level of compression." -) -@click.option( "--gs","gs_path", default="gs", help="Path to the Ghostscript executable." ) -def main( pdf_file, title, targets_fname, yoffset, output_fname, compression, gs_path ): - """Add named destinations to a PDF file.""" +def prepare_pdf( pdf_file, title, targets_fname, yoffset, output_fname, compression, gs_path, log_msg ): + """Prepare the MMP eASLRB PDF.""" # load the targets with open( targets_fname, "r" ) as fp: @@ -48,7 +34,7 @@ def main( pdf_file, title, targets_fname, yoffset, output_fname, compression, gs # compress the PDF if compression and compression != "none": - print( "Compressing the PDF ({})...".format( compression ) ) + log_msg( "progress", "Compressing the PDF ({})...".format( compression ) ) compressed_file.close( delete=False ) args = [ gs_path, "-sDEVICE=pdfwrite", "-dNOPAUSE", "-dQUIET", "-dBATCH", "-dPDFSETTINGS=/{}".format( compression ), @@ -58,11 +44,13 @@ def main( pdf_file, title, targets_fname, yoffset, output_fname, compression, gs start_time = time.time() subprocess.run( args, check=True ) elapsed_time = time.time() - start_time - print( "- Elapsed time: {}".format( datetime.timedelta(seconds=int(elapsed_time)) ) ) + log_msg( "timestamp", "- Elapsed time: {}".format( + datetime.timedelta( seconds=int(elapsed_time) ) ) + ) pdf_file = compressed_file.name # generate the pdfmarks - print( "Generating the pdfmarks..." ) + log_msg( "progress", "Generating the pdfmarks..." ) if title: print( "[ /Title ({})".format( title ), file=pdfmarks_file ) else: @@ -84,8 +72,7 @@ def main( pdf_file, title, targets_fname, yoffset, output_fname, compression, gs pdfmarks_file.close( delete=False ) # generate the pdfmark'ed document - print( "Generating the pdfmark'ed document..." ) - print( "- {} => {}".format( pdf_file, output_fname ) ) + log_msg( "progress", "Adding targets to the PDF..." ) args = [ gs_path, "-q", "-dBATCH", "-dNOPAUSE", "-sDEVICE=pdfwrite" ] args.extend( [ "-o", output_fname ] ) args.extend( [ "-f", pdf_file ] ) @@ -93,9 +80,44 @@ def main( pdf_file, title, targets_fname, yoffset, output_fname, compression, gs start_time = time.time() subprocess.run( args, check=True ) elapsed_time = time.time() - start_time - print( "- Elapsed time: {}".format( datetime.timedelta(seconds=int(elapsed_time)) ) ) + log_msg( "timestamp", "- Elapsed time: {}".format( + datetime.timedelta( seconds=int(elapsed_time) ) ) + ) # --------------------------------------------------------------------- +@click.command() +@click.argument( "pdf_file", nargs=1, type=click.Path(exists=True,dir_okay=False) ) +@click.option( "--title", help="Document title." ) +@click.option( "--targets","-t","targets_fname", required=True, type=click.Path(dir_okay=False), + help="Target definition file." +) +@click.option( "--yoffset", default=5, help="Offset to add to y co-ordinates." ) +@click.option( "--output","-o","output_fname", required=True, type=click.Path(dir_okay=False), + help="Output PDF file." +) +@click.option( "--compression", type=click.Choice(_COMPRESSION_CHOICES), default="ebook", + help="Level of compression." +) +@click.option( "--gs","gs_path", default="gs", help="Path to the Ghostscript executable." ) +@click.option( "--progress","-p", is_flag=True, default=False, help="Log progress." ) +def main( pdf_file, title, targets_fname, yoffset, output_fname, compression, gs_path, progress ): + """Prepare the MMP eASLRB PDF.""" + + # initialize + def log_msg( msg_type, msg ): + if msg_type in ("progress", "start", "timestamp", None) and not progress: + return + log_msg_stderr( msg_type, msg ) + + # prepare the PDF + prepare_pdf( + pdf_file, title, + targets_fname, yoffset, + output_fname, compression, + gs_path, + log_msg + ) + if __name__ == "__main__": main() #pylint: disable=no-value-for-parameter diff --git a/asl_rulebook2/extract/all.py b/asl_rulebook2/extract/all.py index 581c905..5cb4ab1 100755 --- a/asl_rulebook2/extract/all.py +++ b/asl_rulebook2/extract/all.py @@ -8,10 +8,11 @@ import importlib import click -from asl_rulebook2.pdf import PdfDoc -from asl_rulebook2.extract.base import ExtractBase, log_msg_stderr +from asl_rulebook2.extract.base import ExtractBase from asl_rulebook2.extract.index import ExtractIndex from asl_rulebook2.extract.content import ExtractContent +from asl_rulebook2.pdf import PdfDoc +from asl_rulebook2.utils import log_msg_stderr # --------------------------------------------------------------------- @@ -34,13 +35,13 @@ class ExtractAll( ExtractBase ): default_args.update( getattr( mod, "_DEFAULT_ARGS" ) ) # extract the index - self.log_msg( "progress", "\nExtracting the index..." ) + self.log_msg( "status", "\nExtracting the index..." ) args = ExtractBase.parse_args( self._args, default_args ) self.extract_index = ExtractIndex( args, self._log ) self.extract_index.extract_index( pdf ) # extract the content - self.log_msg( "progress", "\nExtracting the content..." ) + self.log_msg( "status", "\nExtracting the content..." ) args = ExtractBase.parse_args( self._args, default_args ) self.extract_content = ExtractContent( args, self._log ) self.extract_content.extract_content( pdf ) @@ -125,13 +126,16 @@ class ExtractAll( ExtractBase ): ) @click.option( "--save-index","save_index_fname", required=True, help="Where to save the extracted index." ) @click.option( "--save-targets","save_targets_fname", required=True, help="Where to save the extracted targets." ) +@click.option( "--save-chapters","save_chapters_fname", required=True, help="Where to save the extracted chaopters." ) @click.option( "--save-footnotes","save_footnotes_fname", required=True, help="Where to save the extracted footnotes." ) -def main( pdf_file, args, progress, output_fmt, save_index_fname, save_targets_fname, save_footnotes_fname ): +def main( pdf_file, args, progress, output_fmt, + save_index_fname, save_targets_fname, save_chapters_fname, save_footnotes_fname +): """Extract everything we need from the MMP eASLRB.""" # extract everything def log_msg( msg_type, msg ): - if msg_type == "progress" and not progress: + if msg_type in ("status", "progress") and not progress: return log_msg_stderr( msg_type, msg ) extract = ExtractAll( args, log_msg ) @@ -142,9 +146,10 @@ def main( pdf_file, args, progress, output_fmt, save_index_fname, save_targets_f # save the results with open( save_index_fname, "w", encoding="utf-8" ) as index_out, \ open( save_targets_fname, "w", encoding="utf-8" ) as targets_out, \ + open( save_chapters_fname, "w", encoding="utf-8" ) as chapters_out, \ open( save_footnotes_fname, "w", encoding="utf-8" ) as footnotes_out: getattr( extract.extract_index, "save_as_"+output_fmt )( index_out ) - getattr( extract.extract_content, "save_as_"+output_fmt )( targets_out, footnotes_out ) + getattr( extract.extract_content, "save_as_"+output_fmt )( targets_out, chapters_out, footnotes_out ) if __name__ == "__main__": main() #pylint: disable=no-value-for-parameter diff --git a/asl_rulebook2/extract/base.py b/asl_rulebook2/extract/base.py index a09ce0c..be5a19b 100644 --- a/asl_rulebook2/extract/base.py +++ b/asl_rulebook2/extract/base.py @@ -1,9 +1,5 @@ """ Base class for the extraction classes. """ -import sys - -import click - # --------------------------------------------------------------------- class ExtractBase: @@ -50,11 +46,3 @@ class ExtractBase: return msg = msg.format( *args, **kwargs ) self._log( msg_type, msg ) - -# --------------------------------------------------------------------- - -def log_msg_stderr( msg_type, msg ): - """Log a message to stderr.""" - if msg_type == "warning": - msg = click.style( "WARNING: {}".format( msg ), fg="yellow" ) - click.echo( msg, file=sys.stderr ) diff --git a/asl_rulebook2/extract/content.py b/asl_rulebook2/extract/content.py index 4305264..a809314 100755 --- a/asl_rulebook2/extract/content.py +++ b/asl_rulebook2/extract/content.py @@ -9,9 +9,9 @@ import math import click from pdfminer.layout import LTChar -from asl_rulebook2.extract.base import ExtractBase, log_msg_stderr +from asl_rulebook2.extract.base import ExtractBase from asl_rulebook2.pdf import PdfDoc, PageIterator, PageElemIterator -from asl_rulebook2.utils import parse_page_numbers, fixup_text, append_text, remove_trailing, jsonval +from asl_rulebook2.utils import parse_page_numbers, fixup_text, append_text, remove_trailing, jsonval, log_msg_stderr # NOTE: Characters are laid out individually on the page, and we generally want to process them top-to-bottom, # left-to-right, but in some cases, alignment is messed up (e.g. the bounding boxes don't line up properly @@ -104,7 +104,7 @@ class ExtractContent( ExtractBase ): self._curr_pageid = "{}{}".format( # nb: this is the ASL page# (e.g. "A42"), not the PDF page# self._curr_chapter, curr_chapter_pageno ) - self.log_msg( "progress", "- Processing page {} ({})...", page_no, self._curr_pageid ) + self.log_msg( "progress", "- Analyzing page {} ({}).", page_no, self._curr_pageid ) # process each element on the page curr_caption = None diff --git a/asl_rulebook2/extract/index.py b/asl_rulebook2/extract/index.py index d301a2e..e605fda 100755 --- a/asl_rulebook2/extract/index.py +++ b/asl_rulebook2/extract/index.py @@ -8,9 +8,9 @@ import re import click from pdfminer.layout import LTChar -from asl_rulebook2.extract.base import ExtractBase, log_msg_stderr +from asl_rulebook2.extract.base import ExtractBase from asl_rulebook2.pdf import PdfDoc, PageIterator, PageElemIterator -from asl_rulebook2.utils import parse_page_numbers, fixup_text, extract_parens_content, jsonval +from asl_rulebook2.utils import parse_page_numbers, fixup_text, extract_parens_content, jsonval, log_msg_stderr # --------------------------------------------------------------------- @@ -49,7 +49,7 @@ class ExtractIndex( ExtractBase ): if page_no not in page_nos: self.log_msg( "progress", "- Skipping page {}.", page_no ) continue - self.log_msg( "progress", "- Processing page {}...", page_no ) + self.log_msg( "progress", "- Analyzing page {}.", page_no ) # process each element on the page self._prev_y0 = 99999 diff --git a/asl_rulebook2/tests/test_extract.py b/asl_rulebook2/tests/test_extract.py index c17a897..808a1c0 100644 --- a/asl_rulebook2/tests/test_extract.py +++ b/asl_rulebook2/tests/test_extract.py @@ -10,6 +10,7 @@ from asl_rulebook2.extract.index import ExtractIndex from asl_rulebook2.extract.content import ExtractContent from asl_rulebook2.extract.all import ExtractAll from asl_rulebook2.tests import pytest_options +from asl_rulebook2.tests.utils import for_each_easlrb_version # --------------------------------------------------------------------- @@ -34,7 +35,7 @@ def test_extract_index(): assert open( fname, "r", encoding="utf-8" ).read() == buf # run the test - _for_each_version( do_test ) + for_each_easlrb_version( do_test ) # --------------------------------------------------------------------- @@ -65,7 +66,7 @@ def test_extract_content(): assert open( fname2, "r", encoding="utf-8" ).read() == footnotes_buf # run the test - _for_each_version( do_test ) + for_each_easlrb_version( do_test ) # --------------------------------------------------------------------- @@ -101,21 +102,10 @@ def test_extract_all(): assert open( fname2, "r", encoding="utf-8" ).read() == footnotes_buf # run the test - _for_each_version( do_test ) + for_each_easlrb_version( do_test ) # --------------------------------------------------------------------- -def _for_each_version( func ): - """Run tests for each version of the eASLRB.""" - base_dir = pytest_options.easlrb_path - ncalls = 0 - for name in os.listdir( base_dir ): - dname = os.path.join( base_dir, name ) - if os.path.isfile( os.path.join( dname, "eASLRB.pdf" ) ): - func( dname ) - ncalls += 1 - assert ncalls > 0 - def _check_log_msg( msg_type, msg ): """Check a log message.""" assert msg_type not in ( "warning", "error" ), \ diff --git a/asl_rulebook2/tests/utils.py b/asl_rulebook2/tests/utils.py new file mode 100644 index 0000000..2ec40b6 --- /dev/null +++ b/asl_rulebook2/tests/utils.py @@ -0,0 +1,19 @@ +""" Helper utilities. """ + +import os + +from asl_rulebook2.tests import pytest_options + +# --------------------------------------------------------------------- + +def for_each_easlrb_version( func ): + """Run tests for each version of the eASLRB.""" + assert pytest_options.easlrb_path + base_dir = pytest_options.easlrb_path + ncalls = 0 + for name in os.listdir( base_dir ): + dname = os.path.join( base_dir, name ) + if os.path.isfile( os.path.join( dname, "eASLRB.pdf" ) ): + func( dname ) + ncalls += 1 + assert ncalls > 0 diff --git a/asl_rulebook2/utils.py b/asl_rulebook2/utils.py index 5c9db95..d946605 100644 --- a/asl_rulebook2/utils.py +++ b/asl_rulebook2/utils.py @@ -1,5 +1,6 @@ """ Miscellaneous utilities. """ +import sys import os import pathlib import tempfile @@ -8,6 +9,8 @@ import math from io import StringIO from html.parser import HTMLParser +import click + # --------------------------------------------------------------------- class TempFile: @@ -160,6 +163,14 @@ def jsonval( val ): assert False, "Unknown JSON data type: {}".format( type(val) ) return '"???"' +def log_msg_stderr( msg_type, msg ): + """Log a message to stderr.""" + if msg_type == "warning": + msg = click.style( "WARNING: {}".format( msg ), fg="yellow" ) + elif msg_type == "error": + msg = click.style( "ERROR: {}".format( msg ), fg="red" ) + click.echo( msg, file=sys.stderr ) + def change_extn( fname, extn ): """Change a filename's extension.""" return pathlib.Path( fname ).with_suffix( extn ) diff --git a/asl_rulebook2/webapp/__init__.py b/asl_rulebook2/webapp/__init__.py index 40004ab..09054ed 100644 --- a/asl_rulebook2/webapp/__init__.py +++ b/asl_rulebook2/webapp/__init__.py @@ -76,6 +76,7 @@ import asl_rulebook2.webapp.startup #pylint: disable=wrong-import-position,cycli import asl_rulebook2.webapp.content #pylint: disable=wrong-import-position,cyclic-import import asl_rulebook2.webapp.search #pylint: disable=wrong-import-position,cyclic-import import asl_rulebook2.webapp.rule_info #pylint: disable=wrong-import-position,cyclic-import +import asl_rulebook2.webapp.prepare #pylint: disable=wrong-import-position,cyclic-import from asl_rulebook2.webapp import globvars #pylint: disable=wrong-import-position,cyclic-import app.before_request( globvars.on_request ) diff --git a/asl_rulebook2/webapp/globvars.py b/asl_rulebook2/webapp/globvars.py index 68717ee..5aa8c13 100644 --- a/asl_rulebook2/webapp/globvars.py +++ b/asl_rulebook2/webapp/globvars.py @@ -9,6 +9,8 @@ from asl_rulebook2.webapp.config.constants import APP_NAME, APP_VERSION cleanup_handlers = [] +socketio_server = None + # --------------------------------------------------------------------- _init_lock = threading.Lock() diff --git a/asl_rulebook2/webapp/main.py b/asl_rulebook2/webapp/main.py index 4914187..cdffe88 100644 --- a/asl_rulebook2/webapp/main.py +++ b/asl_rulebook2/webapp/main.py @@ -8,17 +8,27 @@ import logging from flask import render_template, jsonify, abort from asl_rulebook2.webapp import app, globvars, shutdown_event -from asl_rulebook2.webapp.utils import parse_int +from asl_rulebook2.webapp.utils import parse_int, get_gs_path # --------------------------------------------------------------------- @app.route( "/" ) def main(): """Return the main page.""" - from asl_rulebook2.webapp.asop import user_css_url - return render_template( "index.html", - ASOP_CSS_URL = user_css_url - ) + if app.config.get( "DATA_DIR" ): + # return the main page + from asl_rulebook2.webapp.asop import user_css_url + return render_template( "index.html", + ASOP_CSS_URL = user_css_url + ) + else: + # NOTE: If a data directory has not been configured, this is probably the first time the user + # has run the application, so we show the page that explains how to set things up. + # NOTE: Check for Ghostscript before we start. + args = {} + if get_gs_path(): + args["HAVE_GHOSTSCRIPT"] = 1 + return render_template( "prepare.html", **args ) # --------------------------------------------------------------------- diff --git a/asl_rulebook2/webapp/prepare.py b/asl_rulebook2/webapp/prepare.py new file mode 100644 index 0000000..222df8f --- /dev/null +++ b/asl_rulebook2/webapp/prepare.py @@ -0,0 +1,214 @@ +""" Analyze the MMP eASLRB PDF and prepare the data files. """ + +import threading +import zipfile +import io +import time +import base64 +import traceback +import logging + +from flask import request, send_file, abort, url_for + +from asl_rulebook2.extract.all import ExtractAll +from asl_rulebook2.bin.prepare_pdf import prepare_pdf +from asl_rulebook2.bin.fixup_mmp_pdf import fixup_mmp_pdf +from asl_rulebook2.pdf import PdfDoc +from asl_rulebook2.utils import TempFile +from asl_rulebook2.webapp import app, globvars +from asl_rulebook2.webapp.utils import get_gs_path + +_zip_data_download = None + +_logger = logging.getLogger( "prepare" ) + +# --------------------------------------------------------------------- + +@app.route( "/prepare", methods=["POST"] ) +def prepare_data_files(): + """Prepare the data files.""" + + # initialize + args = dict( request.json ) + download_url = url_for( "download_prepared_data" ) + + # initialize the socketio server + sio = globvars.socketio_server + if not sio: + raise RuntimeError( "The socketio server has not been started." ) + @sio.on( "start" ) + def on_start( data ): #pylint: disable=unused-variable,unused-argument + # start the worker thread that prepares the data files + # NOTE: We don't do this when the POST request comes in, but wait until the client + # tells us it's ready (otherwise, it might miss the first event or two). + def worker(): + try: + _do_prepare_data_files( args, download_url ) + except Exception as ex: #pylint: disable=broad-except + _logger.error( "PREPARE ERROR: %s\n%s", ex, traceback.format_exc() ) + globvars.socketio_server.emit( "error", str(ex) ) + threading.Thread( target=worker, daemon=True ).start() + + return "ok" + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +def _do_prepare_data_files( args, download_url ): + + # initialize + sio = globvars.socketio_server + pdf_data = args.get( "pdfData" ) + if not pdf_data: + # no data was sent - this is a test of logging progress messages. + del args["pdfData"] + _test_progress( **args ) + return + pdf_data = base64.b64decode( pdf_data ) + + def on_done( zip_data ): + global _zip_data_download + _zip_data_download = zip_data + sio.emit( "done", download_url ) + + # check if we should just return a pre-prepared ZIP file (for testing porpoises) + fname = app.config.get( "PREPARED_ZIP" ) + if fname: + with open( fname, "rb" ) as fp: + on_done( fp.read() ) + return + + with TempFile() as input_file, TempFile() as prepared_file: + + # save the PDF file data + input_file.write( pdf_data ) + input_file.close( delete=False ) + _logger.info( "Saved PDF file (#bytes=%d): %s", len(pdf_data), input_file.name ) + + # initialize logging + msg_types = set() + def log_msg( msg_type, msg ): + msg = msg.lstrip() + if msg_type == "status": + _logger.info( "[STATUS]: %s", msg ) + elif msg_type == "warning": + _logger.warning( "[WARNING]: %s", msg ) + elif msg_type == "error": + _logger.error( "[ERROR]: %s", msg ) + else: + _logger.debug( "[%s] %s", msg_type, msg ) + if msg.startswith( "- " ): + msg = msg[2:] + sio.emit( msg_type, msg ) + msg_types.add( msg_type ) + + # NOTE: The plan was to allow the user to change the default parameters in the UI, + # but this can be done (ahem) later. For now, if they really need to change something, + # they can prepare the data files from the command-line. + args = [] + + # extract everything we need from the PDF + log_msg( "status", "Opening the PDF..." ) + extract = ExtractAll( args, log_msg ) + with PdfDoc( input_file.name ) as pdf: + extract.extract_all( pdf ) + index_buf = io.StringIO() + extract.extract_index.save_as_json( index_buf ) + targets_buf, chapters_buf, footnotes_buf = io.StringIO(), io.StringIO(), io.StringIO() + extract.extract_content.save_as_json( targets_buf, chapters_buf, footnotes_buf ) + file_data = { + "index": index_buf.getvalue(), + "targets": targets_buf.getvalue(), + "chapters": chapters_buf.getvalue(), + "footnotes": footnotes_buf.getvalue(), + } + + # prepare the PDF + gs_path = get_gs_path() + if not gs_path: + raise RuntimeError( "Ghostscript is not available." ) + with TempFile( mode="w", encoding="utf-8" ) as targets_file: + log_msg( "status", "Preparing the final PDF..." ) + # save the extracted targets + targets_file.temp_file.write( file_data["targets"] ) + targets_file.close( delete=False ) + # prepare the PDF + prepared_file.close( delete=False ) + prepare_pdf( input_file.name, + "ASL Rulebook", + targets_file.name, 5, + prepared_file.name, "ebook", + gs_path, + log_msg + ) + + # fixup the PDF + with TempFile() as fixedup_file: + log_msg( "status", "Fixing up the final PDF..." ) + fixedup_file.close( delete=False ) + fixup_mmp_pdf( prepared_file.name, + fixedup_file.name, + True, True, + log_msg + ) + # read the final PDF data + with open( fixedup_file.name, "rb" ) as fp: + pdf_data = fp.read() + + # prepare the ZIP for the user to download + log_msg( "status", "Preparing the download ZIP..." ) + zip_data = io.BytesIO() + with zipfile.ZipFile( zip_data, "w", zipfile.ZIP_DEFLATED ) as zip_file: + fname_stem = "ASL Rulebook" + zip_file.writestr( fname_stem+".pdf", pdf_data ) + for key in file_data: + fname = "{}.{}".format( fname_stem, key ) + zip_file.writestr( fname, file_data[key] ) + zip_data = zip_data.getvalue() + + # notify the front-end that we're done + on_done( zip_data ) + _logger.debug( "Message types seen: %s", + " ; ".join( sorted( str(mt) for mt in msg_types ) ) + ) + + # NOTE: We don't bother shutting down the socketio server, since the user + # has to restart the server, using the newly-prepared data files. + +# --------------------------------------------------------------------- + +@app.route( "/prepare/download" ) +def download_prepared_data(): + """Download the prepared data ZIP file.""" + if not _zip_data_download: + abort( 404 ) + return send_file( + io.BytesIO( _zip_data_download ), + as_attachment=True, attachment_filename="asl-rulebook2.zip" + ) + +# --------------------------------------------------------------------- + +def _test_progress( npasses=100, status=10, warnings=None, errors=None, delay=0.1 ): + """Test progress messages.""" + + # initialize + warnings = [ int(w) for w in warnings.split(",") ] if warnings else [] + errors = [ int(e) for e in errors.split(",") ] if errors else [] + + # generate progress messages + sio = globvars.socketio_server + status_no = 0 + for i in range( int(npasses) ): + # check if we should start a new status block + if i % status == 0: + status_no += 1 + sio.emit( "status", "Status #{}".format( status_no ) ) + # issue the next progress message + if 1+i in warnings: + sio.emit( "warning", "Progress {}: warning".format( 1+i ) ) + if 1+i in errors: + sio.emit( "error", "Progress {}: error".format( 1+i ) ) + else: + sio.emit( "progress", "Progress {}.".format( 1+i ) ) + time.sleep( float( delay ) ) + sio.emit( "done" ) diff --git a/asl_rulebook2/webapp/run_server.py b/asl_rulebook2/webapp/run_server.py index 48b1905..e76a453 100755 --- a/asl_rulebook2/webapp/run_server.py +++ b/asl_rulebook2/webapp/run_server.py @@ -9,7 +9,7 @@ import glob import click -from asl_rulebook2.webapp import app +from asl_rulebook2.webapp import app, globvars # --------------------------------------------------------------------- @@ -79,11 +79,33 @@ def main( bind_addr, data_dir, force_init_delay, flask_debug ): _ = urllib.request.urlopen( url ) threading.Thread( target=_start_server, daemon=True ).start() + # check if the user needs to prepare their data files + if not app.config.get( "DATA_DIR" ): + # yup - initialize the socketio server + init_prepare_socketio( app ) + # run the server app.run( host=host, port=port, debug=flask_debug, extra_files = extra_files ) +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +def init_prepare_socketio( flask_app ): + """Initialize the socketio server needed to prepare the data files.""" + # NOTE: We only set this up if it's needed (i.e. because there is no data directory, + # and the user needs to prepare their data files), rather than always having it running + # on the off-chance that the user might need it :-/ + # NOTE: socketio doesn't really work well with threads, and it's tricky to get it to + # send events to the client if we're using e.g. eventlet: + # https://stackoverflow.com/questions/43801884/how-to-run-python-socketio-in-thread + # https://python-socketio.readthedocs.io/en/latest/server.html#standard-threads + # Using native threads is less-performant, but it's not an issue for us, and it works :-/ + import socketio + sio = socketio.Server( async_mode="threading" ) + flask_app.wsgi_app = socketio.WSGIApp( sio, flask_app.wsgi_app ) + globvars.socketio_server = sio + # --------------------------------------------------------------------- if __name__ == "__main__": diff --git a/asl_rulebook2/webapp/static/css/global.css b/asl_rulebook2/webapp/static/css/global.css index ff07028..38adf17 100644 --- a/asl_rulebook2/webapp/static/css/global.css +++ b/asl_rulebook2/webapp/static/css/global.css @@ -19,6 +19,7 @@ ul ul ul { list-style-image: url(../images/bullet3.png) ; } .exc .auto-ruleid { color: #555 ; } .auto-ruleid { color: red ; } .auto-ruleid:hover { background: #ffffcc ; } +span.pre { font-family: monospace ; } /* notification balloons */ #growls-br { bottom: 22px ; right: 0 ; max-height: 40% ; } diff --git a/asl_rulebook2/webapp/static/css/prepare.css b/asl_rulebook2/webapp/static/css/prepare.css new file mode 100644 index 0000000..4599b40 --- /dev/null +++ b/asl_rulebook2/webapp/static/css/prepare.css @@ -0,0 +1,37 @@ +p { margin: 5px 0 ; } +code { display: block ; margin: 5px 0 5px 20px ; } +.info { + margin-top: 10px ; min-height: 25px ; + padding-left: 30px ; background: no-repeat url(../images/info.png) ; + font-size: 80% ; font-style: italic ; color: #444 ; +} + +#prepare-app { height: 100% ; display: flex ; } +#header { margin-bottom: 5px ; } +#main { width: 100% ; margin: 10px ; display: flex ; flex-direction: column ; } + +#fatal-error { margin-bottom: 10px ; font-size: 120% ; font-weight: bold ; } + +#upload-panel { align-self: start ; border: 1px solid black ; border-radius: 5px ; padding: 10px ; } +#upload-panel button { height: 70px ; margin-right: 10px ; } +#upload-panel button img { margin-top: 3px ; height: 60px ; } + +#progress-panel { + flex-grow: 1 ; overflow-y: auto ; + border: 1px solid black ; border-radius: 5px ; padding: 10px ; + font-family: monospace ; font-size: 90% ; +} +#progress-panel .progress { font-style: italic ; } +#progress-panel .status { margin: 5px 0 ; } +#progress-panel .status:first-of-type { margin-top: 0 ; } +#progress-panel .status table { margin-left: 2px ; } +#progress-panel .status table td { vertical-align: top ; } +#progress-panel .status img.icon { height: 15px ; margin: 1px 3px 0 0 ; } + +#download-panel { + position: fixed ; bottom: 18px ; right: 18px ; width: 75% ; + border: 1px solid black ; border-radius: 5px ; background: white ; + padding: 10px ; +} +#download-panel button { height: 40px ; margin-right: 10px ; padding: 5px ; } +#download-panel button img { height: 30px ; } diff --git a/asl_rulebook2/webapp/static/images/download.png b/asl_rulebook2/webapp/static/images/download.png new file mode 100644 index 0000000000000000000000000000000000000000..9e7ea54a31604be5cc0e79038cc97ba22df0b1b0 GIT binary patch literal 2558 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 a-e*{qZrviX(({$X50H~mlB^Xs3Hd(~>0I*w literal 0 HcmV?d00001 diff --git a/asl_rulebook2/webapp/static/images/error.png b/asl_rulebook2/webapp/static/images/error.png new file mode 100644 index 0000000000000000000000000000000000000000..f161e9e3e0be5059757454ab57365d6b0bd84683 GIT binary patch literal 1233 zcmV;?1TOoDP)YB}7++rb0;U8g?vLgny8q5V7k5>LMW| zgw&uyQ42!Z6^qh3y4*n$W_cBh6;!%=655^y(aV!I$!^ zm$_tWFE9p-8GDB^2oVDiwrx5Kl{_#FOplg|kN%e+mrO;0$)5Dc#K6gS2p>O&k$4$B z5&^(%G_d9us9(!bxpWzOZhi`w94!^=&lTj7siS&--*n>4`@@58zlk1=fX~;xH4UT$ z&jZ&5+re!#n7@3L@^=>>O4}YAEfr_~Rgg=jj;g)EnO8pjBo;e#U<2&)VJ$6VRcink z{e2jHac~^49dK-F*KhLR+?S?woD-WR$|iUe$Pa(?!SKKvqu}?W_Q|9A#z+hE(2ul~y4KVEqVWS+E3-*kY<*zUyez`+C1YU8z6DSrJe z_1kyzYyJIx%W2{Kqc1Lu_Yd|#I1IK;;?={f+?*XY=gO17X8`Kj-|qz`h7OHDs|l?p z#jn1he*10~II#_$Or6yX|9JYt)5yg|n4gDc3pyQ!4(EE^We z%;)}q9b(so`?Bd%Ckcjvv`b|QKU~4Ho$TDT-=S6m%OVmAZWCm}0Y6w4R;9{nrE2cj z7N0Mhe)j~yU=QuNGKHUh!Lyz5(Ngi_)k@X078k*?2nP&Ml+1=eQDO!HIvp&t!KSoq zg6rAzTdxxg7_{fh6n>t;bDZ(fQt`rCS>CEQz_Ku;07Z#yq)1f|wu34`S{B>jx|U79 zmLcf((O#%f`0WOs>u!dFl-+L?5JId!UW=(>c03UaNE^TOK+4QIT+_1YBWZ%VM!Qm_ zFnbTr^|r#hf{Y;r!nRpeQ~-0MMMC6P1h}q?Pf_rx#B#~hc}-Qb>C_NG721nS6!JU6 zbIH_cpGr(uRa_xxdIE&VZwREEUg=o4YXt-0AezGX$euxZ6iI8@q);g0c{{?Rc;4h- zG=%GUxPCt??GAJ&?Rtu)>&p%7U@xxc5sie{_fiPIrn1s%^Vg#}JRx?5OF7k}$xJld ziwoF+9?Y7#RWMpAJ`$bIRJFN^7Y-q$1XZQ6yv+Sl1y9KAPHjQT8AI6PyZYmJQX=AU z78XsU<4ir-5nG3ys^)hcJ2KqcY(Tw^=Qu3YmT9^Y55+`P^C${=X8(mdUm2SC^5?4st3soBTAK+A~|9xdXCE5#&Y2omg v-rtu|1A*?+?{rLev6lB%R-U``U--nQ8lhVr(E~A(>VaSZY3JUiuVc1){? zi*=gXF_#C|%NbUb$3JCtV_f7PJ{vLm=a=yrrgEv{yg(E{eWveImooerY_F%RDJ+Y|o3Jcd+-0)5a-(J)j zpwo5j@1K2o-$Xjr)%z{_Z@S_1nro+G_7%-O9iF#2O1rsK{_o0gPQCgs=Vg4v+*?0Z z%u9%1Oz(X3H8Stcoae{RR(4%aKe$$i=cG!t|J#?37BU&8OFce$IYGA}+l)Czcn#~L zc7K6L%|Ev6laB^2OpQ}okk=H-R^5AW&c4g?XI>_(Zwx)QeobOZT-qj!TbEqD8}&Zw zR{7gsdEc;?C#>}`^T)aWJ~moJh^`Oc@BAk~p8dzhrOnIMFA9+m`?#c8>)ArP{kJ(X zWZu60lvOQy#&x|I->OY#7;U{)8tC}#D_s4sKTC)suco)`h0dCLOzEsoI6SnjSeLWD z`o7?wF$en*HV?-y^PX#*uif=sbSl^3LlsKh_un&>erR>LwQ4QvrIQ9PQ*KmOoMess zWU#3vK2rT=c~ekggiVM-Ql0Z6eg1Qc-0%3QvQ4y^xWP+hjg!jzw|^xc>m+@7)^RI% z?fryTQDHCI7i+9yIR3@s_QUrJe;gHhzwi6sYsU=T{Ju9;u1}odzbf$yuVLw0ow$l@ zx2ytzlJtxHJdJ;)e>Byfbhv!?5x2?XJE~iJ<_VNb%lgEh?Kr;4JwQabg{MWTeW$70 zxhG}Ql+!P5_~^X&bZ%^AZ^?hnW$XVbwdtMlU^4DEdHr9oo}sKw W(C6ly<#xb4#Ng@b=d#Wzp$P!f$mfax literal 0 HcmV?d00001 diff --git a/asl_rulebook2/webapp/static/prepare.js b/asl_rulebook2/webapp/static/prepare.js new file mode 100644 index 0000000..e3076a9 --- /dev/null +++ b/asl_rulebook2/webapp/static/prepare.js @@ -0,0 +1,364 @@ +// create the main application +export const gPrepareApp = Vue.createApp( { //eslint-disable-line no-undef + template: "", +} ) ; +$(document).ready( () => { + gPrepareApp.mount( "#prepare-app" ) ; +} ) ; + +// parse any URL parameters +let gUrlParams = new URLSearchParams( window.location.search.substring(1) ) ; + +let gProgressPanel = null ; + +// -------------------------------------------------------------------- + +gPrepareApp.component( "prepare-app", { + + data() { return { + isLoaded: false, + isProcessing: false, + downloadUrl: null, + fatalErrorMsg: gHaveGhostscript ? null : "Ghostscript is not available.", //eslint-disable-line no-undef + fatalErrorIconUrl: makeImageUrl( "error.png" ), + } ; }, + + template: ` +
+ +
+ + {{fatalErrorMsg}} +
+ + + +