Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 22 commits into
base: main
Choose a base branch
from

Conversation

oripka
Copy link
Contributor

@oripka oripka commented Mar 12, 2024

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: #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: 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

Copy link

netlify bot commented Mar 12, 2024

Deploy Preview for slidev failed.

Name Link
🔨 Latest commit 5b931c7
🔍 Latest deploy log https://app.netlify.com/sites/slidev/deploys/673b90c53638830008175588

@oripka oripka changed the title feat: PDF handout (slides on top, notes bottom of page) feat: PDF handout (slides on top, notes on bottom of page) Mar 12, 2024
Copy link
Member

@KermanX KermanX left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we simplify the handout print logic here? There seems to be some duplicated code. And is that possible to have the cover and the content on a single page, so that we don't need to concat them and recalculate the links manually?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept the cover and content separate because I use the same cover layout not only for a slide handout but also for a lab guide hand out (which is another PDF but generated from a Nuxt content site).

In general I like the idea of having the content on a single page. Now that you mention it I don't remember why I did it in two steps..

Copy link
Contributor Author

@oripka oripka Mar 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way I generate the lab guide from a nuxt content site and the issue is that the content is not really structured in pages (or slides). Alas a single nuxt content page can stretch over various print pages. So one does not know exactly where to do a page break (which is not a problem with Slides). For this reason I just export all nuxt content articles to a single PDF (the browser automatically does the page breaks) and generate the footer with the page numbers separately using my slidev template. Of course this is very specific and certainly out of scope of Slidev. But I just wanted to mention my reason for implementing it in such a weird way ;)

packages/client/composables/useNav.ts Show resolved Hide resolved
join(root, 'HandoutBottom.vue'),
]
}
else if (layer === 'handout-cover') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure whether handout-cover should be a global layer or a layout.

const { isPrintMode } = useNav()

watchEffect(() => {
const html = document.body.parentNode as HTMLElement
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think maybe we don't need a .print class here. Because when this component is loaded, the page must be in the print mode.

@oripka
Copy link
Contributor Author

oripka commented Mar 17, 2024

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.


<div class="">
<HandoutBottom :pageNumber="index + 100" />
<HandoutBottom :page-number="index + 100" />
<!-- I would like to do this in HandoutBottom, but somehow props don't get passed. -->
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove the hard-coded page numbering then

@KermanX
Copy link
Member

KermanX commented Apr 16, 2024

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

@oripka
Copy link
Contributor Author

oripka commented Apr 16, 2024

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

@oripka
Copy link
Contributor Author

oripka commented Apr 16, 2024

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
Copy link
Contributor Author

oripka commented Apr 16, 2024

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;
}

{
name: 'cover',
path: '/cover',
component: () => import('../pages/cover/print.vue'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this component be in /handout without being a route?

Comment on lines +616 to +619
.option('cover', {
type: 'boolean',
describe: 'prepend cover to handout, needs handout-cover.vue in project',
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we don't need this option? We could do it automatically when handout-cover.vue presents.

Comment on lines +620 to +623
.option('handout', {
type: 'boolean',
describe: 'Export handout with slides on top and notes on bottom, optionally prepending a cover',
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be --type=handout (default to slides). So later, we could merge export-notes to --type=notes and unify into a single command.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see @KermanX is doing --template in #1513

We could first discuss there

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants