diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 2159927..004e847 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -57,7 +57,7 @@ jobs: run: | python -m pip install --upgrade pip pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip ./setup.py install - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names diff --git a/README.md b/README.md index 6104c81..834db83 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,11 @@ By making heavy use of digitized sheet music, `pymusco` provides a way to addres Digitizing original sheet music can be illegal depending on countries and editors. `pymusco` encourages in no way to trespass the law. `pymusco` doesn't have to be used with material subject to copyright. +## how to install + +``` +python ./setup.py install +``` ## how to use diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4f049be --- /dev/null +++ b/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup + +setup( + name='pymusco', + version=1.0, + description='python musical score tool', + url='https://github.com/g-raffy/pymusco', + author='Guillaume Raffy', + author_email='guillaume.raffy@univ-rennes1.fr', + license='MIT', + packages=['pymusco'], + package_dir={ + '': 'src' + }, + scripts = [ + 'src/apps/pymusco' + ], + install_requires=[ + 'PyPDF2>= 3.0.0', # the syntax has changed between PyPDF2 2.x and PyPDF2 3.x + 'pillow', + 'opencv-python' + ], + zip_safe=False) diff --git a/src/pymusco/__init__.py b/src/pymusco/__init__.py index f723fc2..bde9d77 100644 --- a/src/pymusco/__init__.py +++ b/src/pymusco/__init__.py @@ -1,10 +1,15 @@ from .core import load_commented_json +from .core import InstrumentId +from .core import VoiceId +from .core import TrackId +from .core import Clef from .core import Instrument from .core import Track from .core import Orchestra from .core import load_orchestra from .core import TableOfContents from .core import InstrumentNotFound +from .core import ITrackSelector from .main import scan_to_stub from .main import stub_to_print from .main import split_double_pages diff --git a/src/pymusco/core.py b/src/pymusco/core.py index 70fc287..1d68a22 100644 --- a/src/pymusco/core.py +++ b/src/pymusco/core.py @@ -231,29 +231,35 @@ def load_orchestra(orchestra_file_path: Path) -> Orchestra: return dict_to_orchestra(load_commented_json(orchestra_file_path)) - - # class Clef(Enum): # TREBLE = 1 # BASS = 2 +InstrumentId = str # the identifier of an instrument "bb trombone" +VoiceId = int # each instrument usually has multiple tracks (often 3), that's what we call voices +TrackId = str # the identifier of a track in the form "bb trombone 2 bc" +Clef = str # 'tc' for treble clef, 'bc' for bass clef - -class Track(object): - track_id: str # the identifier of a track in the form "bb trombone 2 bc" +class Track(): + track_id: TrackId # the identifier of a track in the form "bb trombone 2 bc" orchestra: Orchestra # the catalog of available instruments to use (the track is expected to use one of them) + instrument: InstrumentId + voice: VoiceId + clef: Clef + is_solo: bool + is_disabled: bool # for tracks that we want to ignore (eg a track that is present in a stub more than once) - def __init__(self, track_id: str, orchestra): + def __init__(self, track_id: TrackId, orchestra: Orchestra): """ :param str track_id: the identifier of a track in the form "bb trombone 2 bc" :param Orchestra orchestra: """ - assert isinstance(track_id, str) + assert isinstance(track_id, TrackId) self.orchestra = orchestra self.instrument = None self.voice = None - self.clef = None # 'tc' for treble clef, 'bc' for bass clef + self.clef = None self.is_solo = False - self.is_disabled = False # for tracks that we want to ignore (eg a track that is present in a stub more than once) + self.is_disabled = False parts = track_id.split(' ') instrument_first_part_index = 0 instrument_last_part_index = len(parts) - 1 @@ -305,7 +311,7 @@ def __eq__(self, other): """ return hash(self.get_id()) == hash(other.get_id()) - def get_id(self): + def get_id(self) -> TrackId: """ :return str: the identifier of this track in the form "bb trombone 2 tc" """ @@ -389,7 +395,7 @@ def __str__(self): def tracks(self) -> List[Track]: return self.track_to_page.keys() - def add_toc_item(self, track_id: str, page_index: int): + def add_toc_item(self, track_id: TrackId, page_index: int): """ :param str track_id: :param int page_index: diff --git a/src/pymusco/pdf.py b/src/pymusco/pdf.py index 3a7c238..f938af9 100644 --- a/src/pymusco/pdf.py +++ b/src/pymusco/pdf.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3.8 +from typing import List, Tuple, Any import struct import subprocess import os @@ -17,7 +18,7 @@ # https://stackoverflow.com/questions/2693820/extract-images-from-pdf-without-resampling-in-python/34116472#34116472 -def tiff_header_for_ccitt(width, height, img_size, ccitt_group=4): +def tiff_header_for_ccitt(width: int, height: int, img_size: int, ccitt_group: int = 4): tiff_header_struct = '<' + '2s' + 'h' + 'l' + 'h' + 'hhll' * 8 + 'h' return struct.pack(tiff_header_struct, b'II', # Byte order indication: Little indian @@ -36,7 +37,7 @@ def tiff_header_for_ccitt(width, height, img_size, ccitt_group=4): ) -def extract_pdf_stream_image(pdf_stream, image_dir, image_name): +def extract_pdf_stream_image(pdf_stream: PyPDF2.generic.EncodedStreamObject, image_dir: Path, image_name: str): """ :param PyPDF2.generic.EncodedStreamObject pdf_stream: a pdf node which is supposed to contain an image :param str image_dir: where to save the image of the given name_object @@ -150,7 +151,7 @@ def extract_pdf_stream_image(pdf_stream, image_dir, image_name): return saved_image_file_path -def find_pdf_page_raster_image(pdf_page): +def find_pdf_page_raster_image(pdf_page: PyPDF2.PageObject) -> PyPDF2.generic.EncodedStreamObject: """ finds the first raster image in this page @@ -165,12 +166,12 @@ def find_pdf_page_raster_image(pdf_page): return None -def extract_pdf_page_main_image(pdf_page: PyPDF2.PageObject, image_dir: Path, image_name: str): +def extract_pdf_page_main_image(pdf_page: PyPDF2.PageObject, image_dir: Path, image_name: str) -> Path: """ :param PyPDF2.pdf.PageObject pdf_page: - :param str image_dir: where to save the image of the given name_object + :param Path image_dir: where to save the image of the given name_object :param str image_name: the name of the saved file image, without file extension - :return str: the saved image file path with file extension + :return Path: the saved image file path with file extension """ pdf_stream = find_pdf_page_raster_image(pdf_page) @@ -219,12 +220,12 @@ def extract_pdf_page_main_image(pdf_page: PyPDF2.PageObject, image_dir: Path, im return saved_image_file_path -def extract_pdf_page(pdf_page: PyPDF2.PageObject, image_dir: Path, image_name: str): +def extract_pdf_page(pdf_page: PyPDF2.PageObject, image_dir: Path, image_name: str) -> Path: """ :param PyPDF2.pdf.PageObject pdf_page: - :param str image_dir: where to save the image of the given name_object + :param Path image_dir: where to save the image of the given name_object :param str image_name: the name of the saved file image, without file extension - :return str: the saved image file path with file extension + :return Path: the saved image file path with file extension """ saved_image_file_path = (image_dir / image_name).with_suffix('.pdf') with open(saved_image_file_path, 'wb') as pdf_file: @@ -234,7 +235,7 @@ def extract_pdf_page(pdf_page: PyPDF2.PageObject, image_dir: Path, image_name: s return saved_image_file_path -def extract_pdf_page_images(pdf_page, image_folder='/tmp'): +def extract_pdf_page_images(pdf_page: PyPDF2.PageObject, image_folder='/tmp'): """ :param PyPDF2.pdf.PageObject pdf_page: :param str image_folder: @@ -271,8 +272,24 @@ def pdf_page_to_png(pdf_page: PyPDF2.PageObject, resolution=72) -> cv2.Mat: return image - -def add_bookmarks(pdf_in_filename, bookmarks_tree, pdf_out_filename=None): +# example from https://python.hotexamples.com/site/file?hash=0xfd1eb9884f4c714b3c3d9173d13ed1b2d7175ca4d7ed3b64dcad71bec46326b9 +# bookmarks_tree example +# [ +# (u'Foreword', 0, []), +# (u'Chapter 1: Introduction', 1, +# [ +# (u'1.1 Python', 1, +# [ +# (u'1.1.1 Basic syntax', 1, []), +# (u'1.1.2 Hello world', 2, []) +# ] +# ), +# (u'1.2 Exercises', 3, []) +# ] +# ), +# (u'Chapter 2: Conclusion', 4, []) +# ] +def add_bookmarks(pdf_in_filename: Path, bookmarks_tree: List[Tuple[str, int, List[Any]]], pdf_out_filename: Path = None): """Add bookmarks to existing PDF files Home: https://github.com/RussellLuo/pdfbookmarker @@ -313,7 +330,7 @@ def crawl_tree(tree, parent): pdf_out.write(output_stream) -def add_stamp(src_pdf_file_path, dst_pdf_file_path, stamp_file_path, scale=1.0, tx=500.0, ty=770.0): +def add_stamp(src_pdf_file_path: Path, dst_pdf_file_path: Path, stamp_file_path: Path, scale: float = 1.0, tx: float = 500.0, ty: float = 770.0): """ warning! this function has a side effect : it removes the bookmark! @@ -355,7 +372,7 @@ def add_stamp(src_pdf_file_path, dst_pdf_file_path, stamp_file_path, scale=1.0, shutil.copyfile(tmp_dst_pdf_file_path, dst_pdf_file_path) -def check_pdf(src_pdf_file_path): +def check_pdf(src_pdf_file_path: Path): """ the purpose of this function is to detect inconsistencies in the given pdf file an exception is raised if the pdf is malformed diff --git a/src/pymusco/piece.py b/src/pymusco/piece.py index 9ffd95d..25d196a 100644 --- a/src/pymusco/piece.py +++ b/src/pymusco/piece.py @@ -1,15 +1,18 @@ from pathlib import Path import os import json +from typing import List, Dict, Any from .main import StampDesc from .main import scan_to_stub from .main import stub_to_print +from .core import TrackId from .core import TableOfContents from .core import load_commented_json from .core import Orchestra +from .core import ITrackSelector from .tssingle import SingleTrackSelector -def dict_to_toc(toc_as_dict, orchestra): +def dict_to_toc(toc_as_dict: Dict[str, Any], orchestra: Orchestra): """ Parameters ---------- @@ -28,7 +31,7 @@ def dict_to_toc(toc_as_dict, orchestra): return toc -def toc_to_dict(toc): +def toc_to_dict(toc: TableOfContents) -> Dict[str, Any]: """ Parameters ---------- @@ -43,7 +46,7 @@ def toc_to_dict(toc): return toc_as_dict -def dict_to_stamp_desc(stamp_desc_as_dict, piece_desc_file_path): +def dict_to_stamp_desc(stamp_desc_as_dict: Dict[str, Any], piece_desc_file_path: Path) -> StampDesc: abs_stamp_file_path = None stamp_file_path = Path(stamp_desc_as_dict['stamp_image_path']) allowed_image_suffixes = ['.pdf', '.png'] @@ -61,7 +64,7 @@ def dict_to_stamp_desc(stamp_desc_as_dict, piece_desc_file_path): ty=stamp_desc_as_dict['ty']) -def dict_to_piece(piece_as_dict, orchestra, piece_desc_file_path): +def dict_to_piece(piece_as_dict: Dict[str, Any], orchestra: Orchestra, piece_desc_file_path: Path) -> 'Piece': """ Parameters ---------- @@ -87,7 +90,7 @@ def dict_to_piece(piece_as_dict, orchestra, piece_desc_file_path): return piece -def piece_to_dict(piece): +def piece_to_dict(piece: 'Piece') -> Dict[str, Any]: """ Parameters ---------- @@ -105,7 +108,7 @@ def piece_to_dict(piece): return piece_as_dict -def load_piece_description(piece_desc_file_path: Path, orchestra): +def load_piece_description(piece_desc_file_path: Path, orchestra: Orchestra) -> 'Piece': """ Parameters @@ -120,7 +123,7 @@ def load_piece_description(piece_desc_file_path: Path, orchestra): return dict_to_piece(load_commented_json(piece_desc_file_path), orchestra, piece_desc_file_path) -def save_piece_description(piece, piece_desc_file_path): +def save_piece_description(piece: 'Piece', piece_desc_file_path: Path): """ Parameters ---------- @@ -136,7 +139,7 @@ def save_piece_description(piece, piece_desc_file_path): class Vector2(object): - def __init__(self, x, y): + def __init__(self, x: float, y: float): assert isinstance(x, float) assert isinstance(y, float) self.x = x @@ -144,8 +147,15 @@ def __init__(self, x, y): class Piece(object): - - def __init__(self, uid, title, orchestra, scan_toc, missing_tracks=None, stamp_descs=None, page_info_line_y_pos=1.0): + uid: int # unique number identifying a track (eg 42) + title: str # the title of the piece, eg 'alligator alley' + orchestra: Orchestra # the inventory of musical instruments + scan_toc: TableOfContents + missing_tracks: Dict[TrackId, str] # stores for each missing track its id and a message indicating the reason why it's missing + stamp_descs: List[StampDesc] + page_info_line_y_pos: float + + def __init__(self, uid: int, title: str, orchestra: Orchestra, scan_toc: TableOfContents, missing_tracks: Dict[TrackId, str] = None, stamp_descs: List[StampDesc] = None, page_info_line_y_pos: float=1.0): """ Parameters ---------- @@ -169,7 +179,7 @@ def __init__(self, uid, title, orchestra, scan_toc, missing_tracks=None, stamp_d # self.stamp_pos = Vector2(14.0, 4.0) @property - def label(self): + def label(self) -> str: return f"{self.uid:03d}-{self.title.replace(' ', '-')}" # def get_stub_toc(self): @@ -183,8 +193,10 @@ def label(self): class CatalogPiece(object): + piece: Piece + catalog: 'Catalog' - def __init__(self, piece, catalog): + def __init__(self, piece: Piece, catalog: 'Catalog'): self.piece = piece self.catalog = catalog @@ -213,7 +225,7 @@ def build_stub(self): # stub_toc.shift_page_indices(num_toc_pages) # return stub_toc - def build_print(self, track_selector, prints_dir=None): + def build_print(self, track_selector: ITrackSelector, prints_dir=None): if prints_dir is None: prints_dir = self.catalog.prints_dir @@ -223,7 +235,7 @@ def build_print(self, track_selector, prints_dir=None): track_selector=track_selector, orchestra=self.catalog.orchestra) - def extract_single_track(self, track_id, output_dir=None): + def extract_single_track(self, track_id: TrackId, output_dir: Path = None): """ :param str track_id: eg 'bb trumpet 3' """ @@ -238,7 +250,7 @@ def extract_single_track(self, track_id, output_dir=None): track_selector=track_selector, orchestra=self.catalog.orchestra) - def build_all(self, musician_count): + def build_all(self, musician_count: Dict[str, int]): self.build_stub() self.build_print(musician_count) @@ -251,6 +263,7 @@ class Catalog(object): stubs_dir: Path prints_dir: Path orchestra: Orchestra + pieces: Dict[int, Piece] def __init__(self, piece_desc_dir: Path, scans_dir: Path, stubs_dir: Path, prints_dir: Path, orchestra: Orchestra): """ @@ -280,8 +293,8 @@ def __init__(self, piece_desc_dir: Path, scans_dir: Path, stubs_dir: Path, print piece = load_piece_description(desc_file_path, orchestra) self.add(CatalogPiece(piece, self)) - def add(self, piece): + def add(self, piece: Piece): self.pieces[piece.uid] = piece - def get(self, uid): + def get(self, uid: int): return self.pieces[uid] diff --git a/src/pymusco/tssingle.py b/src/pymusco/tssingle.py index 13e94d0..fb07211 100644 --- a/src/pymusco/tssingle.py +++ b/src/pymusco/tssingle.py @@ -6,14 +6,14 @@ from .core import ITrackSelector from .core import Track - +from .core import Orchestra class SingleTrackSelector(ITrackSelector): """ a track selector that extracts the given track. """ - - def __init__(self, selected_track_id, orchestra): + orchestra: Orchestra + def __init__(self, selected_track_id, orchestra: Orchestra): """ :param str selected_track: :param Orchestra orchestra: the inventory of musical instruments