nuxt-pdfmake icon indicating copy to clipboard operation
nuxt-pdfmake copied to clipboard

Save PDF file locally.

Open idesignzone opened this issue 2 years ago • 10 comments

Is it possible to save the PDF locally for example in /public folder? Do you have any suggesstions on how to impliment this in a way that a PDF is generated at the build time?

Thank you

idesignzone avatar Nov 22 '23 11:11 idesignzone

Hello @idesignzone ,

I don't think the underlying library was designed to work like that for the client.

If you want to, you can check out how to use pdfMake on the server in node. Here are two links that should help:

  • https://github.com/bpampuch/pdfmake/blob/0.1/dev-playground/server.js
  • https://pdfmake.github.io/docs/0.1/getting-started/server-side/

BayBreezy avatar Nov 22 '23 13:11 BayBreezy

This is what I have tried with this plugin

<script setup lang="ts">

const smallmargin = [0, 0, 0, 10] as any;
const bigmargin = [0, 0, 0, 20] as any;

const loadPdf = () => {

  const { $pdfMake } = useNuxtApp();

  $pdfMake
    .createPdf({
      content: [
        { text: "Basic QR Usage", margin: smallmargin },
        { qr: "text in QR", margin: bigmargin },
        { text: "Colored QR Code", margin: smallmargin },
      ],
    })
    .getDataUrl(async (dataUrl: any) => {
      const blob = new Blob([dataUrl], { type: "application/pdf" });
      console.log(blob);
      const pdfFile = new File([blob], "example.pdf");

      let formData = new FormData();
      formData.append("file", pdfFile);

      try {
        const response = await $fetch("/api/createPdf", {
          method: "POST",
          headers: {
            Accept: "application/json",
          },
          body: formData,
        });

        if (response.status !== "error") {
          console.log("response success");
        }
      } catch (err) {
        console.log(err);
      }
    });
};

onMounted(() => {
  loadPdf();
});
</script>

and in my server /api/createPdf

import formidable from "formidable";
import fs from "fs";
import path from "path";

export default defineEventHandler(async (event) => {
  let fileUrl = "";
  let oldPath = "";
  let newPath = "";

  const form = formidable({ multiples: true });
  const data = await new Promise((resolve, reject) => {
    form.parse(event.req, (err, fields, files) => {
      if (err) {
        reject(err);
      }
      if (!files.file) {
        resolve({
          status: "error",
          message: "Please upload a photo with name photo in the form",
        });
      }
      if (files.file.mimetype.startsWith("application/octet-stream")) {
        let fileName =
          Date.now() +
          Math.round(Math.random() * 100000) +
          files.file.originalFilename;
        oldPath = files.file.filepath;
        newPath = `${path.join("public", "uploads", fileName)}`;
        fileUrl = "./public/upload/" +fileName;
        fs.copyFileSync(oldPath, newPath);

        resolve({
          status: "ok",
          url: fileUrl,
        });
      } else {
        resolve({
          status: "error",
          message: "Please upload nothing but pdf.",
        });
      }
    });
  });
  return data;
});

I have been able to create a file in /public/uploads but the PDF file has a base64 body insterad.

image

No sure what I am doing wrong. I create a blob with dataUrl and send it to server as a file. any suggession how to get the correct body here?

idesignzone avatar Nov 22 '23 15:11 idesignzone

I have never tried this before so I am not sure. I will have to look into how formidable works.

based on the snippet, it would seem like the file is not being converted properly or something

BayBreezy avatar Nov 22 '23 16:11 BayBreezy

I managed to fix it by using "base64ToArrayBuffer"

working example for someone who will need to save the pdf locally:

<script setup lang="ts">
const smallmargin = [0, 0, 0, 10] as any;
const bigmargin = [0, 0, 0, 20] as any;

function base64ToArrayBuffer(data) {
  var bString = window.atob(data);
  var bLength = bString.length;
  var bytes = new Uint8Array(bLength);
  for (var i = 0; i < bLength; i++) {
    var ascii = bString.charCodeAt(i);
    bytes[i] = ascii;
  }
  return bytes;
}

const loadPdf = () => {
  const { $pdfMake } = useNuxtApp();

  $pdfMake
    .createPdf({
      content: [
        { text: "Basic QR Usage", margin: smallmargin },
        { qr: "text in QR", margin: bigmargin },
        { text: "Colored QR Code", margin: smallmargin },
      ],
    })
    .getDataUrl(async (dataUrl: any) => {
      const b64 = dataUrl.split("base64,")[1];
      var bufferArray = base64ToArrayBuffer(b64);
      var blob = new Blob([bufferArray], { type: "application/pdf" });

      const pdfFile = new File([blob], "example.pdf");

      let formData = new FormData();
      formData.append("file", pdfFile);

      try {
        const response = await $fetch("/api/createPdf", {
          method: "POST",
          headers: {
            Accept: "application/json",
          },
          body: formData,
        });

        if (response.status !== "error") {
          console.log("response success");
        }
      } catch (err) {
        console.log(err);
      }
    });
};

onMounted(() => {
  loadPdf();
});
</script>

and in server /api/createPdf

import formidable from "formidable";
import fs from "fs";
import path from "path";

export default defineEventHandler(async (event) => {
  let fileUrl = "";
  let oldPath = "";
  let newPath = "";

  const form = formidable({ multiples: true });
  const data = await new Promise((resolve, reject) => {
    form.parse(event.req, (err, fields, files) => {
      if (err) {
        reject(err);
      }
      if (!files.file) {
        resolve({
          status: "error",
          message: "Please upload a photo with name photo in the form",
        });
      }
      if (files.file.mimetype.startsWith("application/octet-stream")) {
        let fileName =
          Date.now() +
          Math.round(Math.random() * 100000) +
          files.file.originalFilename;
        oldPath = files.file.filepath;
        newPath = `${path.join("public", "uploads", fileName)}`;
        fileUrl = "./public/upload/" +fileName;
        fs.copyFileSync(oldPath, newPath);

        resolve({
          status: "ok",
          url: fileUrl,
        });
      } else {
        resolve({
          status: "error",
          message: "Please upload nothing but pdf.",
        });
      }
    });
  });
  return data;
});

idesignzone avatar Nov 22 '23 16:11 idesignzone

@idesignzone love it!

Thanks for figuring this one out. Like I said, I always just want to use it on the client side, never tried anything on the server. Gonna leave this open for a bit. I may need to add it to the docs for those who come looking

BayBreezy avatar Nov 22 '23 17:11 BayBreezy

I would still like to know how to use this plugin in server. When I try this on server I get "createPdf" not defined. const { $pdfMake } = useNuxtApp(); does not work on the server.

according pdfMake we can use it on the server. I just can't figure out the nuxt way.

idesignzone avatar Nov 22 '23 19:11 idesignzone

Yeah.. it will be undefined because the module was not setup to work on the server, only the client. i will look into getting a server function added in the future.

You can take a look at this repo to see how they got pdfmake to work in node: https://github.com/jeanpierm/node-pdfmake-example

BayBreezy avatar Nov 22 '23 19:11 BayBreezy

The challenge I see is getting the fonts to work properly... In creating this module, getting the fonts to load consistently was a challenge.. Like I said, I will look into it and see what I can come up with

BayBreezy avatar Nov 22 '23 19:11 BayBreezy

I created a module so it will save PDF file locally at build time. This simplified the process thanks to "PdfPrinter". I just load fonts locally.

here is the example:

// modules/pdf.js

import { defineNuxtModule } from "nuxt/kit";
import PdfPrinter from "pdfmake";
import fs from "fs";

export default defineNuxtModule({
  setup() {
    var fonts = {
      Roboto: {
        normal: "fonts/Roboto-Regular.ttf",
        bold: "fonts/Roboto-Medium.ttf",
        italics: "fonts/Roboto-Italic.ttf",
        bolditalics: "fonts/Roboto-MediumItalic.ttf",
      },
    };
    var printer = new PdfPrinter(fonts);

    var docDefinition = {
      content: [{ text: "some text" }, { text: "some more text" }],
    };

    var pdfDoc = printer.createPdfKitDocument(docDefinition);
    pdfDoc.pipe(fs.createWriteStream("tmp/document.pdf"));
    pdfDoc.end();
  },
});

idesignzone avatar Nov 22 '23 20:11 idesignzone

I mean, if that works for your use case then good. I am not sure what type of app you are creating but as long as you got it to work, that is fine

BayBreezy avatar Nov 22 '23 22:11 BayBreezy