Skip to content

Commit

Permalink
feat: implement preview functionality for pdf files (#4157)
Browse files Browse the repository at this point in the history
  • Loading branch information
yuri1969 authored Jul 8, 2024
1 parent 3272e1a commit e7e7cc5
Show file tree
Hide file tree
Showing 12 changed files with 740 additions and 16 deletions.
509 changes: 503 additions & 6 deletions ui/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"moment-timezone": "^0.5.45",
"node-modules-polyfill": "^0.1.4",
"nprogress": "^0.2.0",
"pdfjs-dist": "^4.3.136",
"posthog-js": "^1.138.2",
"cronstrue": "^2.50.0",
"throttle-debounce": "^5.0.0",
Expand Down
148 changes: 148 additions & 0 deletions ui/src/components/PdfPreview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<template>
<div>
<canvas id="pdf" />

<nav v-if="rendered">
<el-tooltip :content="$t('page.previous')" effect="light" :show-after="1500">
<el-button @click="onPrevPage">
<chevron-left />
</el-button>
</el-tooltip>
<span>
{{ pageNum }}
{{ $t("of") }}
{{ pdfDoc?.numPages }}
</span>
<el-tooltip :content="$t('page.next')" effect="light" :show-after="1500">
<el-button @click="onNextPage">
<chevron-right />
</el-button>
</el-tooltip>
</nav>
</div>
</template>

<script>
import * as pdfjs from "pdfjs-dist";
import ChevronLeft from "vue-material-design-icons/ChevronLeft.vue";
import ChevronRight from "vue-material-design-icons/ChevronRight.vue";

export default {
components: {
ChevronLeft,
ChevronRight
},
props: {
source: {
type: String,
required: true
}
},
data() {
// Can't be a reactive prop
this.pdfDoc = undefined;

return {
pageNum: 1,
rendered: false,
pageRendering: false,
pageNumPending: undefined,
scale: 1.5
}
},
computed: {
canvas() {
return document.getElementById("pdf");
},
context() {
return this.canvas.getContext("2d");
}
},
mounted() {
// Provide worker location
pdfjs.GlobalWorkerOptions.workerSrc = this.getWorkerUrl();

// Initial/first page rendering
this.initRender();
},
methods: {
getWorkerUrl() {
return new URL(
"pdfjs-dist/build/pdf.worker.min.mjs",
import.meta.url
).toString();
},
renderPage(pageNum) {
this.pageRendering = true;

// Using promise to fetch the page
this.pdfDoc.getPage(pageNum).then((page) => {
const viewport = page.getViewport({scale: this.scale});
this.canvas.height = viewport.height;
this.canvas.width = viewport.width;

// Render PDF page into canvas context
const renderContext = {
canvasContext: this.context,
viewport: viewport
};
const renderTask = page.render(renderContext);

// Wait for rendering to finish
renderTask.promise.then(() => {
this.rendered = true;
this.pageRendering = false;

if (this.pageNumPending) {
// New page rendering is pending
this.renderPage(this.pageNumPending);
this.pageNumPending = undefined;
}
});
});
},
initRender() {
// Decode PDF document
pdfjs.getDocument({data: atob(this.source)}).promise.then((pdf) => {
this.pdfDoc = pdf;

this.renderPage(this.pageNum);
}, () => {
// PDF loading error
this.$toast().error(this.$t("failed to render pdf"));
});
},
queueRenderPage(pageNum) {
if (this.pageRendering) {
this.pageNumPending = pageNum;
} else {
this.renderPage(pageNum);
}
},
onPrevPage() {
if (this.pageNum <= 1) {
return;
}
this.pageNum--;
this.queueRenderPage(this.pageNum);
},
onNextPage() {
if (this.pageNum >= this.pdfDoc.numPages) {
return;
}
this.pageNum++;
this.queueRenderPage(this.pageNum);
}
}
}
</script>

<style lang="scss" scoped>
nav {
display: flex;
gap: 1rem;
align-items: center;
justify-content: center;
margin-top: 0.5em;
}
</style>
4 changes: 3 additions & 1 deletion ui/src/components/executions/FilePreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
</el-alert>
<list-preview v-if="filePreview.type === 'LIST'" :value="filePreview.content" />
<img v-else-if="filePreview.type === 'IMAGE'" :src="imageContent" alt="Image output preview">
<pdf-preview v-else-if="filePreview.type === 'PDF'" :source="filePreview.content" />
<markdown v-else-if="filePreview.type === 'MARKDOWN'" :source="filePreview.content" />
<editor v-else :full-height="false" :input="true" :navbar="false" :model-value="filePreview.content" :lang="extensionToMonacoLang" read-only />
<el-form class="ks-horizontal max-size mt-3">
Expand Down Expand Up @@ -64,12 +65,13 @@
<script>
import Editor from "../inputs/Editor.vue";
import ListPreview from "../ListPreview.vue";
import PdfPreview from "../PdfPreview.vue";
import {mapGetters, mapState} from "vuex";
import Markdown from "../layout/Markdown.vue";
import Drawer from "../Drawer.vue";
export default {
components: {Markdown, ListPreview, Editor, Drawer},
components: {Markdown, ListPreview, PdfPreview, Editor, Drawer},
props: {
value: {
type: String,
Expand Down
7 changes: 6 additions & 1 deletion ui/src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,11 @@
"update": "Update value for key '{key}'",
"duplicate": "This key already exists"
},
"expiration": "Expiration"
"expiration": "Expiration",
"failed to render pdf": "Failed to render the PDF preview",
"page": {
"previous": "Previous page",
"next": "Next page"
}
}
}
7 changes: 6 additions & 1 deletion ui/src/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,11 @@
"update": "Mettre à jour la valeur pour la clé '{key}'",
"duplicate": "Cette clé existe déjà"
},
"expiration": "Expiration"
"expiration": "Expiration",
"failed to render pdf": "Impossible de rendre l'aperçu PDF",
"page": {
"previous": "Page précédente",
"next": "Page suivante"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.kestra.webserver.utils.filepreview;

import org.apache.commons.io.IOUtils;

import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;

public class Base64Render extends FileRender {

Base64Render(String extension, InputStream inputStream, Integer maxLine) throws IOException {
super(extension, maxLine);
this.content = Base64.getEncoder().encodeToString(IOUtils.toByteArray(inputStream));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public enum Type {
TEXT,
LIST,
IMAGE,
MARKDOWN
MARKDOWN,
PDF
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ public static FileRender of(String extension, InputStream filestream, Optional<C
return new ImageFileRender(extension, filestream, maxLine);
}

return switch (extension) {
return switch (extension.toLowerCase()) {
case "ion" -> new IonFileRender(extension, filestream, maxLine);
case "md" -> new DefaultFileRender(extension, filestream, DEFAULT_FILE_CHARSET, FileRender.Type.MARKDOWN, maxLine);
case "pdf" -> new PdfFileRender(extension, filestream, maxLine);
default -> new DefaultFileRender(extension, filestream, charset.orElse(DEFAULT_FILE_CHARSET), maxLine);
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
package io.kestra.webserver.utils.filepreview;

import lombok.Getter;
import org.apache.commons.io.IOUtils;

import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Base64;

@Getter
public class ImageFileRender extends FileRender {
public class ImageFileRender extends Base64Render {
ImageFileRender(String extension, InputStream inputStream, Integer maxLine) throws IOException {
super(extension, maxLine);
this.content = Base64.getEncoder().encodeToString(IOUtils.toByteArray(inputStream));
super(extension, inputStream, maxLine);
this.type = Type.IMAGE;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.kestra.webserver.utils.filepreview;

import lombok.Getter;

import java.io.IOException;
import java.io.InputStream;

@Getter
public class PdfFileRender extends Base64Render {
PdfFileRender(String extension, InputStream inputStream, Integer maxLine) throws IOException {
super(extension, inputStream, maxLine);
this.type = Type.PDF;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.kestra.webserver.utils.filepreview;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.stream.Stream;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

public class FileRenderBuilderTest {
@ParameterizedTest
@MethodSource("provideExtensions")
void of(String extension, Class returnedClass) throws IOException {
var emptyInput = new ByteArrayInputStream("".getBytes());
var charset = StandardCharsets.UTF_8;

assertThat(
FileRenderBuilder.of(extension, emptyInput, Optional.of(charset), 1000).getClass(),
is(returnedClass)
);
}

private static Stream<Arguments> provideExtensions() {
return Stream.of(
Arguments.of("ion", IonFileRender.class),
Arguments.of("md", DefaultFileRender.class),
Arguments.of("pdf", PdfFileRender.class),
Arguments.of("PDF", PdfFileRender.class),
Arguments.of("foobar", DefaultFileRender.class)
);
}
}

0 comments on commit e7e7cc5

Please sign in to comment.