From 8c1fb84c29d47f2b9a8447da9f42f769ac42c820 Mon Sep 17 00:00:00 2001 From: Lucas Miranda Date: Wed, 18 Feb 2026 21:34:22 -0300 Subject: [PATCH] fix: base64 choice and scaling text --- TutorialMaker/Lib/TutorialExporter.py | 440 +++++++++++++++++++------- TutorialMaker/Lib/TutorialPainter.py | 6 +- 2 files changed, 321 insertions(+), 125 deletions(-) diff --git a/TutorialMaker/Lib/TutorialExporter.py b/TutorialMaker/Lib/TutorialExporter.py index c554af5..a9e038e 100644 --- a/TutorialMaker/Lib/TutorialExporter.py +++ b/TutorialMaker/Lib/TutorialExporter.py @@ -534,11 +534,26 @@ class Theme(Enum): overflow: hidden; page-break-after: always; page-break-inside: avoid; - min-height: 600px; + height: 600px; + display: flex; + flex-direction: column; + }} + + /* Stacked content */ + .slideContent, .textContent, .blankContent {{ + flex: 1 1 0; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + padding: 40px; }} - /* Content Wrappers */ - .slideContent, .coverContent, .backCoverContent, .sectionContent, .textContent, .blankContent {{ + /* Centered content */ + .coverContent, .backCoverContent, .sectionContent {{ + flex: 0 1 auto; + min-height: 0; + overflow: hidden; padding: 40px; }} @@ -546,7 +561,6 @@ class Theme(Enum): .cover {{ background: linear-gradient(135deg, {primary} 0%, {secondary} 100%); color: white; - display: flex; align-items: center; justify-content: center; }} @@ -589,45 +603,54 @@ class Theme(Enum): /* Regular Slides */ .slideTitle {{ + flex-shrink: 0; font-size: 2.2rem; font-weight: 600; color: {primary}; - margin-bottom: 30px; + margin-bottom: 20px; padding-bottom: 15px; border-bottom: 3px solid {primary}; }} .containerImage {{ - text-align: center; - margin: 30px 0; + flex: 1 1 0; + min-height: 0; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; background: #fafafa; - padding: 20px; + padding: 10px; border-radius: 4px; }} .slideImage {{ - max-width: 90%; + max-width: 100%; + max-height: 100%; + width: auto; height: auto; + object-fit: contain; border-radius: 4px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); }} .slideDescription {{ - font-size: 1.2rem; - line-height: 1.8; + flex-shrink: 1; + min-height: 0; + overflow: hidden; + font-size: 1.1rem; + line-height: 1.6; color: #444; text-align: justify; - margin-top: 25px; + margin-top: 10px; }} /* Back Cover */ .backCover {{ background: linear-gradient(135deg, {secondary} 0%, {primary} 100%); color: white; - min-height: 600px; - display: flex; - flex-direction: column; align-items: center; + justify-content: center; }} .backCoverContent {{ @@ -677,8 +700,6 @@ class Theme(Enum): .sectionSlide {{ background: linear-gradient(135deg, {primary} 0%, {secondary} 100%); color: white; - min-height: 600px; - display: flex; align-items: center; justify-content: center; }} @@ -697,17 +718,21 @@ class Theme(Enum): }} .textTitle {{ + flex-shrink: 0; font-size: 2.2rem; font-weight: 600; color: {primary}; - margin-bottom: 30px; + margin-bottom: 20px; padding-bottom: 15px; border-bottom: 3px solid {primary}; }} .textBody {{ - font-size: 1.2rem; - line-height: 1.8; + flex-shrink: 1; + min-height: 0; + overflow: hidden; + font-size: 1.1rem; + line-height: 1.6; color: #444; text-align: justify; }} @@ -717,40 +742,48 @@ class Theme(Enum): background: white; }} - .blankTitle {{ - font-size: 2.2rem; - font-weight: 600; - color: {primary}; - margin-bottom: 30px; - padding-bottom: 15px; - border-bottom: 3px solid {primary}; - }} - .blankBody {{ - font-size: 1.2rem; - line-height: 1.8; + flex-shrink: 1; + min-height: 0; + overflow: hidden; + font-size: 1.1rem; + line-height: 1.6; color: #444; - text-align: justify; + }} + + .slideContent p, .textContent p, .blankContent p {{ + margin: 0 0 0.4em; }} /* Print Styles */ @media print {{ + * {{ + print-color-adjust: exact; + -webkit-print-color-adjust: exact; + }} + body {{ background: white; padding: 0; }} .slide, .cover, .backCover, .sectionSlide, .textSlide, .blankSlide {{ + max-width: none; + width: 297mm; + height: 148.5mm; margin: 0; box-shadow: none; border-radius: 0; - min-height: 100vh; - height: auto; + overflow: hidden; + }} + + .slideContent, .textContent, .blankContent {{ + overflow: hidden; }} }} @page {{ - size: A4 landscape; + size: 297mm 148.5mm; margin: 0; }} """, @@ -777,16 +810,29 @@ class Theme(Enum): overflow: hidden; page-break-after: always; page-break-inside: avoid; - min-height: 600px; + height: 600px; + display: flex; + flex-direction: column; + }} + + .slideContent, .textContent, .blankContent {{ + flex: 1 1 0; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + padding: 50px; }} - .slideContent, .coverContent, .backCoverContent, .sectionContent, .textContent, .blankContent {{ + .coverContent, .backCoverContent, .sectionContent {{ + flex: 0 1 auto; + min-height: 0; + overflow: hidden; padding: 50px; }} .cover {{ background: white; - display: flex; align-items: center; justify-content: center; }} @@ -828,10 +874,11 @@ class Theme(Enum): }} .slideTitle {{ + flex-shrink: 0; font-size: 2.5rem; font-weight: 600; color: {primary}; - margin-bottom: 30px; + margin-bottom: 20px; padding-bottom: 15px; border-bottom: 2px solid {primary}; text-align: center; @@ -839,31 +886,40 @@ class Theme(Enum): }} .containerImage {{ - text-align: center; - margin: 30px 0; - padding: 20px; + flex: 1 1 0; + min-height: 0; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + padding: 10px; border: 1px solid #ddd; }} .slideImage {{ - max-width: 85%; + max-width: 100%; + max-height: 100%; + width: auto; height: auto; + object-fit: contain; border: 1px solid {primary}; }} .slideDescription {{ - font-size: 1.15rem; - line-height: 1.9; + flex-shrink: 1; + min-height: 0; + overflow: hidden; + font-size: 1.1rem; + line-height: 1.6; color: {text}; text-align: justify; - margin-top: 25px; + margin-top: 10px; }} .backCover {{ background: white; - display: flex; - flex-direction: column; align-items: center; + justify-content: center; }} .backCover .coverTitle {{ @@ -905,8 +961,6 @@ class Theme(Enum): /* Section Slide */ .sectionSlide {{ background: #fefefe; - min-height: 600px; - display: flex; align-items: center; justify-content: center; }} @@ -931,10 +985,11 @@ class Theme(Enum): }} .textTitle {{ + flex-shrink: 0; font-size: 2.5rem; font-weight: 600; color: {primary}; - margin-bottom: 30px; + margin-bottom: 20px; padding-bottom: 15px; border-bottom: 2px solid {primary}; text-align: center; @@ -942,8 +997,11 @@ class Theme(Enum): }} .textBody {{ - font-size: 1.15rem; - line-height: 1.9; + flex-shrink: 1; + min-height: 0; + overflow: hidden; + font-size: 1.1rem; + line-height: 1.6; color: {text}; text-align: justify; }} @@ -953,40 +1011,46 @@ class Theme(Enum): background: #fefefe; }} - .blankTitle {{ - font-size: 2.5rem; - font-weight: 600; - color: {primary}; - margin-bottom: 30px; - padding-bottom: 15px; - border-bottom: 2px solid {primary}; - text-align: center; - font-family: Georgia, serif; - }} - .blankBody {{ - font-size: 1.15rem; - line-height: 1.9; + flex-shrink: 1; + min-height: 0; + overflow: hidden; + font-size: 1.1rem; + line-height: 1.6; color: {text}; - text-align: justify; + }} + + .slideContent p, .textContent p, .blankContent p {{ + margin: 0 0 0.4em; }} @media print {{ + * {{ + print-color-adjust: exact; + -webkit-print-color-adjust: exact; + }} + body {{ background: white; padding: 0; }} .slide, .cover, .backCover, .sectionSlide, .textSlide, .blankSlide {{ + max-width: none; + width: 297mm; + height: 148.5mm; margin: 0; border: none; - min-height: 100vh; - height: auto; + overflow: hidden; + }} + + .slideContent, .textContent, .blankContent {{ + overflow: hidden; }} }} @page {{ - size: A4 landscape; + size: 297mm 148.5mm; margin: 0; }} """, @@ -1012,15 +1076,28 @@ class Theme(Enum): overflow: hidden; page-break-after: always; page-break-inside: avoid; - min-height: 600px; + height: 600px; + display: flex; + flex-direction: column; }} - .slideContent, .coverContent, .backCoverContent, .sectionContent, .textContent, .blankContent {{ + .slideContent, .textContent, .blankContent {{ + flex: 1 1 0; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + padding: 60px; + }} + + .coverContent, .backCoverContent, .sectionContent {{ + flex: 0 1 auto; + min-height: 0; + overflow: hidden; padding: 60px; }} .cover {{ - display: flex; align-items: center; justify-content: center; border-left: 8px solid {primary}; @@ -1056,35 +1133,46 @@ class Theme(Enum): }} .slideTitle {{ + flex-shrink: 0; font-size: 2rem; font-weight: 400; color: {primary}; - margin-bottom: 30px; + margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid {primary}; }} .containerImage {{ - text-align: center; - margin: 30px 0; + flex: 1 1 0; + min-height: 0; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + padding: 10px; }} .slideImage {{ max-width: 100%; + max-height: 100%; + width: auto; height: auto; + object-fit: contain; }} .slideDescription {{ + flex-shrink: 1; + min-height: 0; + overflow: hidden; font-size: 1.1rem; - line-height: 1.8; + line-height: 1.6; color: {text}; - margin-top: 25px; + margin-top: 10px; }} .backCover {{ - display: flex; - flex-direction: column; align-items: center; + justify-content: center; border-left: 8px solid {primary}; }} @@ -1127,8 +1215,6 @@ class Theme(Enum): .sectionSlide {{ background: white; border-left: 8px solid {primary}; - display: flex; - min-height: 600px; align-items: center; justify-content: center; }} @@ -1148,17 +1234,21 @@ class Theme(Enum): }} .textTitle {{ + flex-shrink: 0; font-size: 2rem; font-weight: 400; color: {primary}; - margin-bottom: 30px; + margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid {primary}; }} .textBody {{ + flex-shrink: 1; + min-height: 0; + overflow: hidden; font-size: 1.1rem; - line-height: 1.8; + line-height: 1.6; color: {text}; }} @@ -1167,36 +1257,45 @@ class Theme(Enum): background: white; }} - .blankTitle {{ - font-size: 2rem; - font-weight: 400; - color: {primary}; - margin-bottom: 30px; - padding-bottom: 10px; - border-bottom: 1px solid {primary}; - }} - .blankBody {{ + flex-shrink: 1; + min-height: 0; + overflow: hidden; font-size: 1.1rem; - line-height: 1.8; + line-height: 1.6; color: {text}; }} + .slideContent p, .textContent p, .blankContent p {{ + margin: 0 0 0.4em; + }} + @media print {{ + * {{ + print-color-adjust: exact; + -webkit-print-color-adjust: exact; + }} + body {{ background: white; padding: 0; }} .slide, .cover, .backCover, .sectionSlide, .textSlide, .blankSlide {{ + max-width: none; + width: 297mm; + height: 148.5mm; margin: 0; - min-height: 100vh; - height: auto; + overflow: hidden; + }} + + .slideContent, .textContent, .blankContent {{ + overflow: hidden; }} }} @page {{ - size: A4 landscape; + size: 297mm 148.5mm; margin: 0; }} """ @@ -1212,7 +1311,7 @@ def __init__(self, title: str, author: str, date: str, description: str): self.Description = description pass - def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): + def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN, embed: bool = False): template = HTML_TEMPLATES[theme]['cover'] colors = PALETTE_COLORS[palette] return template.format( @@ -1223,7 +1322,7 @@ def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): **colors ) - def ToMarkdown(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): + def ToMarkdown(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN, embed: bool = False): template = MARKDOWN_TEMPLATES[theme]['cover'] colors = PALETTE_COLORS[palette] return template.format( @@ -1239,7 +1338,7 @@ def __init__(self, title: str, Acknowledgments: str): self.Title = title self.Acknowledgments = Acknowledgments - def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): + def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN, embed: bool = False): if isinstance(self.Acknowledgments, dict): items = "".join( f"
  • {k}

    {v}

  • " @@ -1257,7 +1356,7 @@ def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): **colors ) - def ToMarkdown(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): + def ToMarkdown(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN, embed: bool = False): colors = PALETTE_COLORS[palette] if isinstance(self.Acknowledgments, dict): @@ -1289,13 +1388,18 @@ def ToMarkdown(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODER ) class SimpleSlide(): - def __init__(self, Title: str, Description: str, ImagePath: str): + def __init__(self, Title: str, Description: str, ImagePath: str, base_dir: str = None): self.Title = Title self.Description = Description self.ImagePath = ImagePath + self.BaseDir = base_dir - def _get_image_data_url(self, image_path): - if not image_path or not os.path.exists(image_path): + def _get_image_data_url(self, image_path, base_dir: str = None): + if not image_path: + return image_path + if base_dir and not os.path.isabs(image_path): + image_path = os.path.join(base_dir, image_path) + if not os.path.exists(image_path): return image_path try: @@ -1319,11 +1423,14 @@ def _get_image_data_url(self, image_path): print(f"Warning: Could not convert image to base64 ({image_path}): {e}") return image_path - def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): + def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN, embed: bool = False): template = HTML_TEMPLATES[theme]['slide'] colors = PALETTE_COLORS[palette] - - image_src = self._get_image_data_url(self.ImagePath) + base_dir = self.BaseDir + if embed: + image_src = self._get_image_data_url(self.ImagePath, base_dir=base_dir) + else: + image_src = self.ImagePath or "" return template.format( title=self.Title, @@ -1333,11 +1440,14 @@ def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): **colors ) - def ToMarkdown(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): + def ToMarkdown(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN, embed: bool = False): template = MARKDOWN_TEMPLATES[theme]['slide'] colors = PALETTE_COLORS[palette] - - image_src = self._get_image_data_url(self.ImagePath) + base_dir = self.BaseDir + if embed: + image_src = self._get_image_data_url(self.ImagePath, base_dir=base_dir) + else: + image_src = self.ImagePath or "" return template.format( title=self.Title, @@ -1351,7 +1461,7 @@ class SimpleSection(): def __init__(self, Title: str): self.Title = Title - def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): + def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN, embed: bool = False): template = HTML_TEMPLATES[theme]['section'] colors = PALETTE_COLORS[palette] return template.format( @@ -1359,7 +1469,7 @@ def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): **colors ) - def ToMarkdown(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): + def ToMarkdown(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN, embed: bool = False): template = MARKDOWN_TEMPLATES[theme]['section'] colors = PALETTE_COLORS[palette] return template.format( @@ -1372,7 +1482,7 @@ def __init__(self, Title: str, Body: str): self.Title = Title self.Body = Body - def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): + def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN, embed: bool = False): template = HTML_TEMPLATES[theme]['text'] colors = PALETTE_COLORS[palette] return template.format( @@ -1381,7 +1491,7 @@ def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): **colors ) - def ToMarkdown(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): + def ToMarkdown(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN, embed: bool = False): template = MARKDOWN_TEMPLATES[theme]['text'] colors = PALETTE_COLORS[palette] return template.format( @@ -1394,12 +1504,12 @@ class BlankSlide(): def __init__(self): pass - def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): + def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN, embed: bool = False): template = HTML_TEMPLATES[theme]['blank'] colors = PALETTE_COLORS[palette] return template.format(**colors) - def ToMarkdown(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): + def ToMarkdown(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN, embed: bool = False): template = MARKDOWN_TEMPLATES[theme]['blank'] colors = PALETTE_COLORS[palette] return template.format(**colors) @@ -1419,9 +1529,10 @@ def __init__(self, Model:SlideModel = SlideModel.SimpleSlide): self.Model = Model # Model will be used later to create more than one type of slide class TutorialExporter(): - def __init__(self, Slides: list[SlidePage], Title: str): + def __init__(self, Slides: list[SlidePage], Title: str, html_dir: str = None): self.Slides = Slides self.Title = Title + self.HtmlDir = html_dir self.Html = """ @@ -1435,18 +1546,103 @@ def __init__(self, Slides: list[SlidePage], Title: str): + """ self.Markdown = "" - def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): - body = "".join([slide.Model.ToHtml(palette=palette, theme=theme) for slide in self.Slides]) + def ToHtml(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN, embed: bool = False): + body = "".join([slide.Model.ToHtml(palette=palette, theme=theme, embed=embed) for slide in self.Slides]) colors = PALETTE_COLORS[palette] css = CSS_TEMPLATES[theme].format(**colors) return self.Html.format(self.Title, body, css) - def ToMarkdown(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): - md = "".join([slide.Model.ToMarkdown(palette=palette, theme=theme) for slide in self.Slides]) + def ToMarkdown(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN, embed: bool = False): + md = "".join([slide.Model.ToMarkdown(palette=palette, theme=theme, embed=embed) for slide in self.Slides]) self.Markdown = md return md @@ -1462,7 +1658,7 @@ def ToPdf(self, palette: Palette = Palette.BLUE, theme: Theme = Theme.MODERN): printer.setOrientation(qt.QPrinter.Landscape) doc = qt.QTextDocument() - doc.setHtml(self.ToHtml(palette=palette, theme=theme)) + doc.setHtml(self.ToHtml(palette=palette, theme=theme, embed=True)) doc.setPageSize(qt.QSizeF(printer.pageRect().size())) doc.print_(printer) diff --git a/TutorialMaker/Lib/TutorialPainter.py b/TutorialMaker/Lib/TutorialPainter.py index 9246a46..46697ae 100644 --- a/TutorialMaker/Lib/TutorialPainter.py +++ b/TutorialMaker/Lib/TutorialPainter.py @@ -842,11 +842,11 @@ def GenerateHTMLfromAnnotatedTutorial(self, path): ) elif slide.SlideLayout == AnnotatorSlideLayoutType.Screenshot: - full_image_path = os.path.join(localizedScreenshotsPath, slide.imagePath) page = Exporter.SimpleSlide( slide.SlideTitle, self.SimpleParser(slide.SlideBody), - full_image_path + slide.imagePath, + base_dir=localizedScreenshotsPath ) elif slide.SlideLayout == AnnotatorSlideLayoutType.Blank: @@ -860,7 +860,7 @@ def GenerateHTMLfromAnnotatedTutorial(self, path): tutorialPath = localizedScreenshotsPath + f"/{clean_title}" - export = Exporter.TutorialExporter(pages, self.TutorialInfo["title"]) + export = Exporter.TutorialExporter(pages, self.TutorialInfo["title"], html_dir=localizedScreenshotsPath) html = export.ToHtml() markdown = export.ToMarkdown() # pdf = export.ToPdf()