diff --git a/ptulsconv/commands.py b/ptulsconv/commands.py index 28aea9b..8a5d0db 100644 --- a/ptulsconv/commands.py +++ b/ptulsconv/commands.py @@ -15,6 +15,7 @@ from .validations import * from ptulsconv.pdf.supervisor_1pg import output_report as output_supervisor_1pg from ptulsconv.pdf.line_count import output_report as output_line_count from ptulsconv.pdf.talent_sides import output_report as output_talent_sides +from ptulsconv.pdf.summary_log import output_report as output_summary # field_map maps tags in the text export to fields in FMPXMLRESULT # - tuple field 0 is a list of tags, the first tag with contents will be used as source @@ -208,10 +209,14 @@ def convert(input_file, output_format='fmpxml', start=None, end=None, select_ree validate_non_empty_field(parsed, field='QN'), validate_non_empty_field(parsed, field='CN'), validate_non_empty_field(parsed, field='Char'), + validate_non_empty_field(parsed, field='Title'), validate_dependent_value(parsed, key_field='CN', dependent_field='Char'), validate_dependent_value(parsed, key_field='CN', - dependent_field='Actor'),): + dependent_field='Actor'), + validate_unique_count(parsed, field='Title', count=1), + validate_unique_count(parsed, field='Spotting', count=1), + validate_unique_count(parsed, field='Supervisor', count=1)): print_warning(warning.report_message()) @@ -220,9 +225,12 @@ def convert(input_file, output_format='fmpxml', start=None, end=None, select_ree elif output_format == 'full': print("Sorry, the `full` output type is not yet supported.") normalized_records = normalize_record_keys(parsed) + output_supervisor_1pg(normalized_records) output_talent_sides(normalized_records) output_line_count(normalized_records) + output_summary(normalized_records) + elif output_format == 'fmpxml': if xsl is None: fmp_dump(parsed, input_file, output) diff --git a/ptulsconv/pdf/common.py b/ptulsconv/pdf/common.py index d3de29b..c72c61f 100644 --- a/ptulsconv/pdf/common.py +++ b/ptulsconv/pdf/common.py @@ -1,6 +1,51 @@ from reportlab.pdfbase.pdfmetrics import (getAscent, getDescent) +from reportlab.lib.units import inch +from reportlab.pdfgen import canvas +import datetime +# This is from https://code.activestate.com/recipes/576832/ for +# generating page count messages +class NumberedCanvas(canvas.Canvas): + def __init__(self, *args, **kwargs): + canvas.Canvas.__init__(self, *args, **kwargs) + self._saved_page_states = [] + self._report_date = datetime.datetime.now() + def showPage(self): + self._saved_page_states.append(dict(self.__dict__)) + self._startPage() + + def save(self): + """add page info to each page (page x of y)""" + num_pages = len(self._saved_page_states) + for state in self._saved_page_states: + self.__dict__.update(state) + self.draw_page_number(num_pages) + canvas.Canvas.showPage(self) + canvas.Canvas.save(self) + + def draw_page_number(self, page_count): + self.saveState() + self.setFont("Futura", 10) + self.drawString(0.5 * inch, 0.5 * inch, "Page %d of %d" % (self._pageNumber, page_count)) + right_edge = self._pagesize[0] - 0.5 * inch + self.drawRightString(right_edge, 0.5 * inch, self._report_date.strftime("%c")) + topline = self.beginPath() + topline.moveTo(0.5 * inch, 0.75 * inch) + topline.lineTo(right_edge, 0.75 * inch) + self.setLineWidth(0.5) + self.drawPath(topline) + self.restoreState() + +def time_format(mins): + if mins < 60.: + return "%im" % round(mins) + else: + m = round(mins) + hh, mm = divmod(m, 60) + return "%ih%im" % (hh, mm) + +## draws the title block inside the given rect def draw_title_block(canvas, rect, record): (supervisor, client,), title = rect.divide_y([16., 16., ]) title.draw_text_cell(canvas, record['Title'], "Futura", 18, inset_y=2.) @@ -56,8 +101,12 @@ class GRect: elif at <= 0: return self, None else: - return (GRect(self.min_x, self.min_y, at, self.height), - GRect(self.min_x + at, self.y, self.width - at, self.height)) + if direction == 'l': + return (GRect(self.min_x, self.min_y, at, self.height), + GRect(self.min_x + at, self.y, self.width - at, self.height)) + else: + return (GRect(self.max_x - at, self.y, at, self.height), + GRect(self.min_x, self.y, self.width - at, self.height)) def split_y(self, at, direction='u'): if at >= self.height: diff --git a/ptulsconv/pdf/line_count.py b/ptulsconv/pdf/line_count.py index 129b4a7..9c87d20 100644 --- a/ptulsconv/pdf/line_count.py +++ b/ptulsconv/pdf/line_count.py @@ -10,7 +10,7 @@ from reportlab.lib import colors from reportlab.lib.styles import getSampleStyleSheet from reportlab.platypus import Paragraph, Table, TableStyle -from .common import GRect +from .common import GRect, time_format, NumberedCanvas import datetime @@ -26,14 +26,6 @@ def build_columns(records, show_priorities = False): else: return str(l) - def time_format(mins): - if mins < 60.: - return "%im" % round(mins) - else: - m = round(mins) - hh, mm = divmod(m, 60) - return "%ih%im" % (hh, mm) - columns.append({ 'heading': '#', 'value_getter': lambda recs: recs[0]['Character Number'], @@ -180,7 +172,7 @@ def output_report(records): page = page.inset(inch * 0.5) title_box, table_box = page.split_y(inch, 'd') - c = Canvas('Line Count.pdf', pagesize=(letter[1], letter[0])) + c = NumberedCanvas('Line Count.pdf', pagesize=(letter[1], letter[0])) c.setFont('Futura', 18.) c.drawCentredString(title_box.center_x, title_box.center_y, "Line Count") diff --git a/ptulsconv/pdf/summary_log.py b/ptulsconv/pdf/summary_log.py new file mode 100644 index 0000000..9039792 --- /dev/null +++ b/ptulsconv/pdf/summary_log.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- + +from .common import GRect, draw_title_block, time_format, NumberedCanvas +from reportlab.lib.units import inch +from reportlab.lib.pagesizes import letter, landscape, portrait + +from reportlab.platypus import BaseDocTemplate, Paragraph, Spacer, \ + KeepTogether, Table, HRFlowable, PageTemplate, Frame +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib import colors + +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont + + +def build_aux_data_field(line): + entries = list() + if 'Reason' in line.keys(): + entries.append("Reason: " + line["Reason"]) + if 'Note' in line.keys(): + entries.append("Note: " + line["Note"]) + if 'Requested by' in line.keys(): + entries.append("Requested by: " + line["Requested by"]) + if 'Shot' in line.keys(): + entries.append("Shot: " + line["Shot"]) + + tag_field = "" + for tag in line.keys(): + if line[tag] == tag and tag != 'ADR': + tag_field += "" + tag + " " + + entries.append(tag_field) + + return "
".join(entries) + + +def build_story(lines): + story = list() + + this_scene = None + scene_style = getSampleStyleSheet()['Normal'] + scene_style.fontName = 'Futura' + scene_style.leftIndent = 0. + line_style = getSampleStyleSheet()['Normal'] + line_style.fontName = 'Futura' + + for line in lines: + table_style = [('VALIGN', (0, 0), (-1, -1), 'TOP'), ('LEFTPADDING', (0, 0), (0, 0), 0.0)] + + if 'Omitted' in line.keys(): + cue_number_field = "" + line['Cue Number'] + "
" + \ + line['Character Name'] + "" + table_style.append(('BACKGROUND', (0, 0), (-1, 0), colors.lightpink)) + else: + cue_number_field = line['Cue Number'] + "
" + line['Character Name'] + "" + + time_data = time_format(line.get('Time Budget Mins', 0.)) + + if 'Priority' in line.keys(): + time_data = time_data + "
" + "P:" + int(line['Priority']) + + aux_data_field = build_aux_data_field(line) + + line_table_data = [[Paragraph(cue_number_field, line_style), + Paragraph(line['PT.Clip.Start'] + "
" + line['PT.Clip.Finish'], line_style), + Paragraph(line['Line'], line_style), + Paragraph(time_data, line_style), + Paragraph(aux_data_field, line_style) + ]] + + line_table = Table(data=line_table_data, + colWidths=[inch, inch, inch * 3., 0.5 * inch, inch * 2.], + style=table_style) + + if line['Scene'] != this_scene: + this_scene = line['Scene'] + story.append(KeepTogether([ + Spacer(1., 0.25 * inch), + Paragraph("" + this_scene + "", scene_style), + line_table])) + else: + line_table.setStyle(table_style + [('LINEABOVE', (0, 0), (-1,0), .5, colors.gray)]) + story.append(KeepTogether([line_table])) + + return story + + + +def output_report(records): + page_size = portrait(letter) + page_box = GRect(inch * 0.5, inch * 0.5, page_size[0] - inch, page_size[1] - inch) + title_box, page_box = page_box.split_y(0.875 * inch, 'd') + footer_box, page_box = page_box.split_y(0.25 * inch, 'u') + + title_block, header_block = title_box.split_x(inch * 4., direction='r') + + pdfmetrics.registerFont(TTFont('Futura', 'Futura.ttc')) + + page_template = PageTemplate(id="Main", + frames=[Frame(page_box.min_x, page_box.min_y, page_box.width, page_box.height)], + onPage=lambda canvas, _: draw_title_block(canvas, title_block, lines[0])) + + lines = sorted(records['events'], key=lambda line: line['PT.Clip.Start_Seconds']) + + doc = BaseDocTemplate("Summary.pdf", + pagesize=page_size, leftMargin=0.5 * inch, + rightMargin=0.5 * inch, topMargin=0.5 * inch, + bottomMargin=0.5 * inch) + + doc.addPageTemplates([page_template]) + + story = build_story(lines) + + doc.build(story, canvasmaker=NumberedCanvas) diff --git a/ptulsconv/pdf/talent_sides.py b/ptulsconv/pdf/talent_sides.py index 3187b8a..00d009c 100644 --- a/ptulsconv/pdf/talent_sides.py +++ b/ptulsconv/pdf/talent_sides.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from .common import GRect, draw_title_block +from .common import GRect, draw_title_block, NumberedCanvas from reportlab.lib.units import inch from reportlab.lib.pagesizes import letter @@ -69,4 +69,4 @@ def output_report(records): ) ) - doc.build(story) + doc.build(story, canvasmaker=NumberedCanvas) diff --git a/ptulsconv/validations.py b/ptulsconv/validations.py index 3b04c04..298a78c 100644 --- a/ptulsconv/validations.py +++ b/ptulsconv/validations.py @@ -10,6 +10,11 @@ class ValidationError: def report_message(self): return f"{self.message}: event at {self.event['PT.Clip.Start']} on track {self.event['PT.Track.Name']}" +def validate_unique_count(input_dict, field='Title', count=1): + values = set(list(map(lambda e: e[field], input_dict['events']))) + if len(values) > count: + yield ValidationError(message="Field {} has too many values (max={}): {}".format(field, count, values)) + def validate_value(input_dict, key_field, predicate): for event in input_dict['events']: val = event[key_field]