slidev icon indicating copy to clipboard operation
slidev copied to clipboard

feat: PDF handout (slides on top, notes on bottom of page)

Open oripka opened this issue 11 months ago • 6 comments

This pull request introduces a new option for slide export. It allows for the creation of an A4 page with the slide on the top and the speaker notes on the bottom.

The footer of the pages is customizable using a custom component handout-bottom.vue, similar to global-bottom.vue for slides.

Optionally, it can create cover pages by passing the --cover option, which then renders another custom component handout-cover.vue. Both of these components need to be in your theme or project root.

I have added some example components to the starter.

Example Usage

cd demo/starter
node node_modules/@slidev/cli/bin/slidev.js export --handout --cover

The code creates 2 temporary files:

  • A cover PDF (also accessible via /cover)
  • A temporary handout PDF (also accessible via /handout), which forms the bottom part of the final handout

It then prepends the cover and merges slides and handouts. In this way, we can reuse the slide generation code. However, this breaks hyperlinks. Therefore, I have ensured to perform all necessary calculations to make them clickable also in the final PDF.

The final file will be: handout-slides-export.pdf

Caveat

~~This feature requires the inclusion of this pull request to work properly: https://github.com/slidevjs/slidev/pull/1419 Without it, each click exports a slide, which then desynchronizes the merge with the notes causing errors.~~

Possible enhancements.

Terminology

Since the term "notes" was already taken, I called the API "handout", but it might be confusing, as /handout is just a helper page to generate the lower part of the final handout and will be merged with the slides to create the final handout.

Fix pageNumber param

Somehow the pageNumber param does not get passed to HandoutBottom here: <HandoutBottom :pageNumber="index + 100" /> in PrintHandout.vue. This may be due to the way the component is imported. It would be nice to have, because for now, the page numbering is hardcoded in Slidev and not customizable.

Configure output name

Allow configuring the output name of the handout

handout-slides-export.pdf

oripka avatar Mar 12 '24 16:03 oripka

Deploy Preview for slidev ready!

Name Link
Latest commit 3e20b974e39405d5b9b180758cabb45f7d67e1c7
Latest deploy log https://app.netlify.com/sites/slidev/deploys/661e1f5074cf8900082f8394
Deploy Preview https://deploy-preview-1421--slidev.netlify.app
Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

netlify[bot] avatar Mar 12 '24 16:03 netlify[bot]

Thanks for your contribution! I think this feature is quite useful. btw, the commits I pushed earlier contain no actual function-related changes.

Thanks for having a look at it and cleaning it up. I really appreciate it. I fetched your changes and seems to work.

oripka avatar Mar 17 '24 12:03 oripka

Hello, there is an almost finished PR #1513 which allows users to provide their own print templates. Would that help?

kermanx avatar Apr 16 '24 06:04 kermanx

@KermanX I synced the current pull request with main. Unfortunately, I did not get around yet to implement the changes you mentioned.

oripka avatar Apr 16 '24 06:04 oripka

Hello, there is an almost finished PR #1513 which allows users to provide their own print templates. Would that help?

Ok, thanks for the info, I will have a look how to implement it using this PR.

oripka avatar Apr 16 '24 07:04 oripka

Hello, there is an almost finished PR #1513 which allows users to provide their own print templates. Would that help?

So I am starting to use this the mentioned PR, works but I have one question and two issue I am stuck with.

  1. How can I debug export easier, before I could go to /handout in the browser (with my code) but now that the --template parameter was introduced I don't understand how to do live debugging, as --template is only accepted when exporting and not when running in dev mode (so /print in dev mode is always the default print), so one always has to do a new export run to see the result. Makes development a bit painful.

  2. This is a template I am working on, I am stuck on how to properly scale the slide to fit to the A4 page with margins etc. it always ends up looking bad as content is overflowing. I am sure I use the wrong way to do that (slideWidth.value, provideLocal(injectionSlideScale, computed(() => 0.7)).

Screenshot 2024-04-16 at 18 01 47
<script setup lang="ts">
import { computed } from 'vue'
import { useNav } from '../../composables/useNav'

import NoteDisplay from '../../internals/NoteDisplay.vue'
import { usePrintStyle } from '../../composables/usePrintStyle'
import PrintSlide from '../../internals/PrintSlide.vue'
import { provideLocal } from '@vueuse/core'
import { injectionSlideScale } from '../../constants'
import { useStyleTag } from '@vueuse/core'


const { slidesToPrint, total } = useNav()
import { slideWidth } from '../../env'

slideWidth.value = 740
provideLocal(injectionSlideScale, computed(() => 1))
</script>

<template>
    <div class="w-full px-[1px]"> <!--1px helps to avoid cutting off a tiny bit of content by margins-->
        <div v-for="(slide, index) of slidesToPrint" :key="slide.no"
            class="mx-auto w-full flex flex-col gap-4 break-inside-avoid-page break-after-page">
            <div>

                <div class="relative border-1 border-red w-full">
                    <PrintSlide :key="slide.no" :route="slide" class="mx-auto w-full border-0.5 border-black" />
                </div>

                <div class="flex flex-col w-full mt-12">

                    <div class="h-124 border-red border-2">
                        <NoteDisplay v-if="slide.meta.slide.noteHTML" :note-html="slide.meta.slide.noteHTML"
                            class="max-w-full h-full" />
                    </div>

                    <div class="w-full flex flex-col gap-2">
                        <div class="h-[1px] bg-black w-full" />
                        <div class="mt-2 relative">
                            <div class="top-3.0 absolute left-0 !text-[11px]">© {{ new Date().getFullYear() }} Company
                            </div>
                            <div class="top-3.0 absolute right-0 !text-[11px]">Title - Version</div>
                        </div>
                        <div class="text-right text-[11px] mt-6">
                        {{ index + 1 }} / {{ total }}
                    </div>

                    </div>
             
                </div>

            </div>
        </div>
    </div>
</template>

<style>
@page {
    size: A4;
    margin-top: 1.5cm;
    margin-bottom: 1cm;
}

/* overwrite the class in PrintSlideClick.vue, we want to break every slide
 * 794px fits A4
 * 1 px border (2x 0.5)
 * 26 px margin
 * 740 px slide width
*/
.print-slide-container {
    @apply relative overflow-hidden !break-after-avoid translate-0;
}


html.print,
html.print body,
html.print #app {
    height: auto;
    overflow: auto;
}

html.print #page-root {
    height: auto;
    overflow: hidden;
}

html.print * {
    -webkit-print-color-adjust: exact;
}

html.print {
    width: 100%;
    height: 100%;
    overflow: visible;
}

html.print body {
    margin: 0 auto;
    border: 0;
    padding: 0;
    float: none;
    overflow: visible;
}

@page {
    size: A4;
    margin-top: 1.5cm;
    margin-bottom: 1.0cm;
    margin-left: 1cm;
    margin-right: 1cm;
}
</style>

  1. I also need to overwrite .print-slide-container to avoid page breaks after slides. Works but is a bit awkward to do it like that. Maybe this needs to be restructured as well
.print-slide-container {
    @apply relative overflow-hidden !break-after-avoid translate-0;
}

oripka avatar Apr 16 '24 16:04 oripka