76 lines
2.3 KiB
Python
76 lines
2.3 KiB
Python
"""Answer provenance and reproducible annotation spacing (200 dpi pixels)."""
|
|
from hashlib import sha256
|
|
from pathlib import Path
|
|
import shutil
|
|
|
|
import pymupdf
|
|
|
|
from copienator import configuration
|
|
from copienator.json_io import atomic_write_json, read_json
|
|
|
|
|
|
def fingerprint(path):
|
|
return sha256(Path(path).read_bytes()).hexdigest()
|
|
|
|
|
|
def metadata_path(pdf):
|
|
return Path(pdf).with_suffix('.layout.json')
|
|
|
|
|
|
def answer_parts(pdf):
|
|
"""Ignore stale sidecars after a PDF is replaced or manually edited."""
|
|
if pdf is None or not Path(pdf).is_file():
|
|
return []
|
|
try:
|
|
data = read_json(metadata_path(pdf), default={})
|
|
if data.get('sha256') == fingerprint(pdf):
|
|
parts = data.get('parts', [])
|
|
if isinstance(parts, list) and all(isinstance(part, dict) for part in parts):
|
|
return parts
|
|
except (OSError, ValueError, TypeError, AttributeError):
|
|
pass
|
|
return []
|
|
|
|
|
|
def save_parts(pdf, parts):
|
|
atomic_write_json(metadata_path(pdf), {
|
|
'version': 1, 'sha256': fingerprint(pdf), 'parts': parts,
|
|
})
|
|
|
|
|
|
def page_parts(pdf):
|
|
parts = answer_parts(pdf)
|
|
if not Path(pdf).is_file():
|
|
return []
|
|
with pymupdf.open(pdf) as document:
|
|
if len(parts) == len(document):
|
|
return parts
|
|
return [{'margin_status': 'unknown'} for _ in document]
|
|
|
|
|
|
def copy_answer_pdf(source, destination):
|
|
parts = answer_parts(source)
|
|
shutil.copy(source, destination)
|
|
save_parts(destination, parts)
|
|
|
|
|
|
def move_answer_pdf(source, destination):
|
|
parts = answer_parts(source)
|
|
shutil.move(str(source), str(destination))
|
|
save_parts(destination, parts)
|
|
metadata_path(source).unlink(missing_ok=True)
|
|
|
|
|
|
def layout_for(pdf):
|
|
low = any(part.get('margin_status') == 'low' for part in answer_parts(pdf))
|
|
margin = (configuration.ANNOTATION_LOW_MARGIN_LEFT if low
|
|
else configuration.ANNOTATION_MARGIN_LEFT)
|
|
if type(margin) is not int or margin < 0:
|
|
raise ValueError('Annotation left margins must be non-negative integers')
|
|
return {'version': 1, 'margin_left': margin, 'narrow_left_pad_limit': 300}
|
|
|
|
|
|
def legacy_layout():
|
|
# Old annotation scans were produced with these literal constants.
|
|
return {'version': 1, 'margin_left': 300, 'narrow_left_pad_limit': 300}
|