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
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/client/internals/PrintHandout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ const route = computed(() => props.route)

<template>
<div class="break-after-page">
<!--A4 specific, figure out better customization-->
<!-- A4 specific, figure out better customization -->
<div class="w-full mt-104 h-176 flex flex-col relative overflow-hidden">
<NoteDisplay v-if="route.meta?.slide!.noteHTML" :note-html="route.meta?.slide!.noteHTML"
class="w-full mx-auto px-2 handout-notes" />
<NoteDisplay
v-if="route.meta?.slide!.noteHTML" :note-html="route.meta?.slide!.noteHTML"
class="w-full mx-auto px-2 handout-notes"
/>

<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

<div class="absolute bottom-5 right-0 text-right text-[11px] ">
{{ index + 1 }}
Expand Down
110 changes: 57 additions & 53 deletions packages/slidev/node/commands/export.ts
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 ;)

Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export interface ExportOptions {
base?: string
format?: 'pdf' | 'png' | 'md'
output?: string
handout?: boolean,
cover?: boolean,
handout?: boolean
cover?: boolean
timeout?: number
dark?: boolean
routerMode?: 'hash' | 'history'
Expand Down Expand Up @@ -383,7 +383,6 @@ export async function exportSlides({
}

async function genNotesPdfOnePiece() {

const baseName = output.replace('.pdf', '')
const output_notes = `${baseName}-notes.pdf`

Expand All @@ -406,7 +405,6 @@ export async function exportSlides({
}

async function genCoverPdfOnePiece() {

const baseName = output.replace('.pdf', '')
const output_notes = `${baseName}-cover.pdf`

Expand All @@ -433,27 +431,27 @@ export async function exportSlides({
llx = 0,
lly = 30,
urx = 40,
ury = 230
ury = 230,
) =>
page.doc.context.register(
page.doc.context.obj({
Type: "Annot",
Subtype: "Link",
Type: 'Annot',
Subtype: 'Link',
Rect: [llx, lly, urx, ury],
Border: [0, 0, 0],
C: [0, 0, 1],
A: {
Type: "Action",
S: "URI",
Type: 'Action',
S: 'URI',
URI: PDFString.of(uri),
},
})
}),
)

async function mergeSlidesWithNotes(
slides: pdfLib.PDFDocument,
pdfNotes: pdfLib.PDFDocument,
pdfCover: pdfLib.PDFDocument | undefined
pdfCover: pdfLib.PDFDocument | undefined,
) {
const pdfSlidePages = slides.getPages()
const numSlides = pdfSlidePages.length
Expand Down Expand Up @@ -481,7 +479,7 @@ export async function exportSlides({

const currentPage = pdf.addPage(PageSizes.A4)

//firstPage.drawPage(slideEmbedded as pdfLib.PDFEmbeddedPage, {
// firstPage.drawPage(slideEmbedded as pdfLib.PDFEmbeddedPage, {
currentPage.drawPage(slideEmbedded, {
...slideEmbeddedDims,
x: currentPage.getWidth() / 2 - slideEmbeddedDims.width / 2,
Expand All @@ -500,8 +498,8 @@ export async function exportSlides({
})

let noteEmbeddedDims: {
width: number;
height: number;
width: number
height: number
}

/* add notes */
Expand All @@ -520,46 +518,51 @@ export async function exportSlides({
x: currentPage.getWidth() / 2 - noteEmbeddedDims.width / 2,
y: 0,
})
} catch (error) {
}
catch (error) {
console.error(`Could not embed note as page does not exist: ${error}`)
}

/* add links for slides */
const annots = pdfSlidePages[i].node.Annots()

const newLinkAnnotations: PDFRef[] = []; // Initialize an empty array to accumulate new link annotations
const newLinkAnnotations: PDFRef[] = [] // Initialize an empty array to accumulate new link annotations

try {
annots?.asArray().forEach((a) => {

const dict = slides.context.lookupMaybe(a, PDFDict)
if (!dict) return
if (!dict)
return

const aRecord = dict.get(asPDFName(`A`))
if (!aRecord) return
if (!aRecord)
return

const subtype = dict.get(PDFName.of("Subtype"))?.toString()
if (!subtype) return
const subtype = dict.get(PDFName.of('Subtype'))?.toString()
if (!subtype)
return

if (subtype === "/Link") {
const rect = dict.get(PDFName.of("Rect"))!
if (subtype === '/Link') {
const rect = dict.get(PDFName.of('Rect'))!
const link = slides.context.lookupMaybe(aRecord, PDFDict)
if (!link) return
if (!link)
return

const uri = link.get(asPDFName("URI"))!.toString().slice(1, -1) // get the original link, remove parenthesis
const uri = link.get(asPDFName('URI'))!.toString().slice(1, -1) // get the original link, remove parenthesis

const scale = slideEmbeddedDims.width / pdfSlidePages[i].getWidth(); // Calculate scale based on the width (or height)
const offsetX =
currentPage.getWidth() / 2 - slideEmbeddedDims.width / 2
const offsetY =
currentPage.getHeight() - slideEmbeddedDims.height - 30
const scale = slideEmbeddedDims.width / pdfSlidePages[i].getWidth() // Calculate scale based on the width (or height)
const offsetX
= currentPage.getWidth() / 2 - slideEmbeddedDims.width / 2
const offsetY
= currentPage.getHeight() - slideEmbeddedDims.height - 30

// @ts-expect-error missing types
const newRect = rect.array.map((value, index) => {
if (index % 2 === 0) {
// x values (llx, urx)
return value * scale + offsetX
} else {
}
else {
// y values (lly, ury)
// Y values need to be inverted due to PDF's coordinate system (0 at bottom)

Expand All @@ -580,9 +583,8 @@ export async function exportSlides({
newLinkAnnotations.push(newLink)
}
})


} catch (e) {
}
catch (e) {
console.error(e)
}

Expand All @@ -593,30 +595,33 @@ export async function exportSlides({
let dict: PDFDict | undefined
try {
dict = pdfNotes.context.lookupMaybe(a, PDFDict)
} catch (e) {
}
catch (e) {
}

if (!dict) return
if (!dict)
return

const aRecord = dict.get(PDFName.of(`A`))
const subtype = dict.get(PDFName.of("Subtype"))?.toString()
const subtype = dict.get(PDFName.of('Subtype'))?.toString()

if (subtype === "/Link") {
const rect = dict.get(PDFName.of("Rect"))!
if (subtype === '/Link') {
const rect = dict.get(PDFName.of('Rect'))!
const link = pdfNotes.context.lookupMaybe(aRecord, PDFDict)!
const uri = link.get(PDFName.of("URI"))!.toString().slice(1, -1)
const uri = link.get(PDFName.of('URI'))!.toString().slice(1, -1)

const scale = noteEmbeddedDims.width / notesPages[i].getWidth()
const offsetX = currentPage.getWidth() / 2 - noteEmbeddedDims.width / 2
const offsetY = 0; // Notes are drawn at the bottom, so offsetY is 0
const offsetY = 0 // Notes are drawn at the bottom, so offsetY is 0

// @ts-expect-error missing types
const newRect = rect.array.map((value, index) => {
if (index % 2 === 0) {
return value * scale + offsetX; // x values
} else {
return value * scale + offsetX // x values
}
else {
// y values need to be adjusted differently for notes
return -2 + offsetY + value * scale; // Adjust y values for position
return -2 + offsetY + value * scale // Adjust y values for position
}
})

Expand All @@ -626,29 +631,27 @@ export async function exportSlides({
newRect[0], // llx
newRect[1], // lly
newRect[2], // urx
newRect[3] // ury
newRect[3], // ury
)
newLinkAnnotations.push(newLink)
}
})


} catch (e) {
}
catch (e) {
console.error(e)
}

if (newLinkAnnotations.length > 0) {
currentPage.node.set(
PDFName.of("Annots"),
pdf.context.obj(newLinkAnnotations)
PDFName.of('Annots'),
pdf.context.obj(newLinkAnnotations),
)
}
}

return pdf
}


async function genHandoutAndMerge(pdfSlidesPath: string) {
if (format !== 'pdf')
throw new Error(`Unsupported exporting format for handout "${format}"`)
Expand All @@ -673,9 +676,10 @@ export async function exportSlides({

const pdf = await mergeSlidesWithNotes(pdfSlides, pdfNotes, pdfCover)

/* cleanup*/
/* cleanup */
await fs.unlink(notesPath)
if (cover && coverPath) await fs.unlink(coverPath)
if (cover && coverPath)
await fs.unlink(coverPath)

if (!pdf)
throw new Error('PDF could not be generated')
Expand Down
1 change: 0 additions & 1 deletion packages/types/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ declare module '#slidev/global-components/bottom' {
export default component
}


declare module '#slidev/global-components/handout-bottom' {
import type { ComponentOptions } from 'vue'

Expand Down
Loading