ScreenshotNeo

BlogHow-to

How to Convert an Image to PDF in Nuxt.js

Convert a selected image into a downloadable PDF in Nuxt.js with client-side rendering, CORS guidance, large-file handling, and troubleshooting.

By the ScreenshotNeo team4 October 20269 min read

To convert an image to a downloadable PDF in Nuxt.js, run the conversion in the browser: read the selected file, decode it as an image, add it to a PDF document, and download the result. Keep browser APIs out of Nuxt’s server-rendering path. This guide uses jsPDF; confirm the current installation and API details in the jsPDF project documentation before adopting it in production.

1. Choose where conversion runs

For a user selecting an image and immediately downloading a PDF, client-side conversion is usually the simplest fit. Nuxt renders universally by default: code can run on the server before the browser hydrates the page. Browser APIs such as File, Image, document, and Canvas are not available in that server environment. Nuxt advises placing libraries that rely on browser APIs behind a client-side boundary. See Nuxt rendering concepts.

The sample below uses a file input and a client-only component. It accepts one image, fits it proportionally on a single PDF page, and offers a download. It deliberately does not fetch arbitrary remote URLs: those introduce CORS and server-side request risks.

2. Install the PDF library

From the Nuxt project directory, install jsPDF using your package manager:

npm install jspdf

The component imports jsPDF only when the user submits a file. That keeps the library out of server rendering. Check the project’s documentation for current package requirements and APIs.

3. Add a client-side image-to-PDF component

Create components/ImageToPdf.client.vue. The .client.vue suffix makes this a client-only component in Nuxt.

<template>
  <section>
    <label for="image-file">Choose an image</label>
    <input
      id="image-file"
      type="file"
      accept="image/png,image/jpeg,image/webp,image/gif"
      @change="onFileChange"
    />

    <p v-if="error" role="alert">{{ error }}</p>
    <p v-else-if="selectedFile">Selected: {{ selectedFile.name }}</p>

    <button type="button" :disabled="!selectedFile || busy" @click="convert">
      {{ busy ? 'Creating PDF…' : 'Download PDF' }}
    </button>
  </section>
</template>

<script setup>
import { ref } from 'vue'

const selectedFile = ref(null)
const error = ref('')
const busy = ref(false)
const allowedTypes = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif'])
const maxBytes = 20 * 1024 * 1024

function onFileChange(event) {
  error.value = ''
  const file = event.target.files?.[0] ?? null
  selectedFile.value = null

  if (!file) return
  if (!allowedTypes.has(file.type)) {
    error.value = 'Choose a PNG, JPEG, WebP, or GIF image.'
    event.target.value = ''
    return
  }
  if (file.size > maxBytes) {
    error.value = 'The image is larger than the 20 MB example limit.'
    event.target.value = ''
    return
  }
  selectedFile.value = file
}

function loadImage(file) {
  return new Promise((resolve, reject) => {
    const objectUrl = URL.createObjectURL(file)
    const image = new Image()
    image.onload = () => resolve({ image, objectUrl })
    image.onerror = () => {
      URL.revokeObjectURL(objectUrl)
      reject(new Error('The selected file could not be decoded as an image.'))
    }
    image.src = objectUrl
  })
}

async function convert() {
  if (!selectedFile.value || busy.value) return
  busy.value = true
  error.value = ''
  let objectUrl

  try {
    const { image, objectUrl: url } = await loadImage(selectedFile.value)
    objectUrl = url

    const { jsPDF } = await import('jspdf')
    const pdf = new jsPDF({ orientation: 'portrait', unit: 'pt', format: 'a4' })
    const pageWidth = pdf.internal.pageSize.getWidth()
    const pageHeight = pdf.internal.pageSize.getHeight()
    const margin = 24
    const scale = Math.min(
      (pageWidth - margin * 2) / image.naturalWidth,
      (pageHeight - margin * 2) / image.naturalHeight,
    )
    const width = image.naturalWidth * scale
    const height = image.naturalHeight * scale
    const x = (pageWidth - width) / 2
    const y = (pageHeight - height) / 2

    // PNG preserves transparency; JPEG uses lossy compression.
    const format = selectedFile.value.type === 'image/png' ? 'PNG' : 'JPEG'
    pdf.addImage(image, format, x, y, width, height)

    const baseName = selectedFile.value.name.replace(/\.[^.]+$/, '') || 'image'
    pdf.save(`${baseName}.pdf`)
  } catch (cause) {
    error.value = cause instanceof Error ? cause.message : 'Could not create the PDF.'
  } finally {
    if (objectUrl) URL.revokeObjectURL(objectUrl)
    busy.value = false
  }
}
</script>

Use it from a page, for example pages/image-to-pdf.vue:

<template>
  <main>
    <h1>Image to PDF</h1>
    <ImageToPdf />
  </main>
</template>

Review the generated file in the browsers and devices your application supports. The example’s 20 MB limit is an application choice, not a universal safe limit. Validate file size and type on any server that accepts uploads; a browser’s MIME type is not a security boundary.

4. Tune page size, orientation, and image placement

The sample uses A4 portrait, point units, and a 24-point margin. To change the page, adjust the jsPDF constructor’s orientation, unit, and format options according to the current jsPDF documentation. The scaling calculation keeps the full image visible and centered without stretching it.

  • Fit to page: use the smaller width or height scale, as in the sample. This can leave whitespace on one axis.
  • Fill the page: use the larger scale, then crop the overflow. This requires explicit crop positioning.
  • Original dimensions: derive PDF page dimensions from the image’s pixel dimensions and a chosen conversion scale. Pixels are not a physical page size until you choose a unit or DPI.
  • Multiple images: add each image and create a new PDF page between images. Decide whether pages share one size or follow each image’s aspect ratio.
  • Transparency: PNG can preserve transparent pixels in image data when the PDF library supports the format path used. JPEG has no alpha channel; flatten transparency against a chosen background before encoding if needed.

For portrait or landscape, a simple aspect-ratio rule can select orientation before creating the PDF: compare image.naturalWidth and image.naturalHeight. For mixed-orientation multi-page PDFs, create each page with dimensions appropriate to that image if the library API supports it, and confirm the behavior in its documentation.

5. Handle larger images without unnecessary data URLs

Canvas toDataURL() creates a large encoded string containing the image data. MDN notes that this can have performance implications for large images and recommends considering toBlob() with URL.createObjectURL() as an alternative. The sample passes the decoded image to the PDF library directly, avoiding an explicit data URL conversion in application code, but the library still needs memory to encode and construct the PDF.

For large photographs or batches, set a reasonable input limit, show progress or a busy state, release object URLs after use, and avoid keeping duplicate image strings in reactive state. If the browser runs out of memory, downscale the image on a canvas before passing it to the PDF library, or move document generation to a server designed for the workload. Downscaling reduces detail, so choose dimensions based on the required print and viewing quality.

6. Remote images, app assets, and CORS

Remote image URLs

When loading a remote image into Canvas and then reading or exporting the canvas, the remote origin must allow CORS access. Otherwise, the canvas is not origin-clean and reading its pixels or exporting can fail with a security error. See MDN’s CORS-enabled images guide. Setting an image’s crossOrigin property in the browser does not grant permission by itself; the remote server must send suitable CORS headers.

If you control the image host, configure its CORS policy for your app’s origin and set image.crossOrigin = 'anonymous' before assigning image.src. If you do not control it, use an authorized server-side fetch or ask the image owner for an export route. Do not create an open proxy that fetches arbitrary user-supplied URLs: that can expose server-side network resources.

Nuxt assets

Nuxt distinguishes files in public/, which are served at stable public paths, from files in assets/, which are processed by the build tool. For a path known only at runtime, use Nuxt’s public-asset approach instead of assuming a build-time asset import will resolve dynamically. See Nuxt assets documentation. Nuxt Image formats such as WebP or JPEG affect image delivery; they do not perform PDF conversion. See Nuxt Picture format configuration.

7. When to generate the PDF on the server

Client-side generation fits an immediate download from a local file. Consider a Nuxt server route when generation must happen without a browser, when a PDF must be stored or emailed, or when processing policy requires central validation. That design needs its own decisions: validate and limit uploads, authenticate requests, constrain accepted formats and dimensions, set request and execution limits, and account for the memory and temporary storage limits of the deployment runtime. Use a library that supports the chosen server runtime; browser Canvas code cannot simply be assumed to work in a server route.

Nuxt module-directory listings include PDF-related modules such as @i2d/nuxt-pdf-frame and @jasy/nuxt. Treat these as discovery leads, then check each package’s own documentation, current version, maintenance, browser support, and runtime suitability before choosing it.

8. Troubleshooting

Symptom Likely cause Fix
window, document, or Image is undefined Browser-only code ran during server rendering. Move it to a .client.vue component, a client-only plugin, or a client-only lifecycle path. Avoid top-level browser API access in universal modules.
Canvas export fails with a security error The image came from another origin without suitable CORS permission. Configure CORS on the image server, use an authorized same-origin route, or work from a user-selected file. Setting crossOrigin alone is insufficient.
The PDF is blank The image had not decoded when it was added, the format was unsupported by the chosen library path, or dimensions were zero. Wait for image load, check naturalWidth and naturalHeight, and verify supported image formats against the library documentation.
The downloaded PDF is too large or conversion freezes High-resolution pixels and intermediate encodings consume memory and processing time. Set a file-size limit, downscale when acceptable, avoid data URL copies, and consider server processing for large jobs.
Transparent areas appear with an unexpected background The chosen image format or PDF embedding path flattened transparency. Use a transparency-capable format path, or deliberately composite the image onto a background before adding it.
The filename is malformed The source filename has no extension or contains characters unsuitable for the download environment. Normalize and sanitize the basename, and provide a safe fallback such as image.pdf.
Import fails only in production Dependency version, bundler output, or server/client chunk behavior differs from development. Keep the import inside the client action or client component, inspect the production build error, and check the library’s current Nuxt and bundler compatibility.

9. Reliability, performance, and cost

Browser conversion avoids sending the selected file to your server, which can simplify privacy and hosting costs, but it uses the visitor’s device memory and CPU. Large images, older phones, and multiple pages can make the interface unresponsive. Disable repeat submissions while processing, report errors, release object URLs, and test realistic file sizes on target devices.

Server conversion gives the application more control over validation and storage, but adds compute, bandwidth, temporary-file handling, and operational limits. Set explicit upload and processing limits and make retries safe. For either approach, define page size, image scaling, transparency, and multi-page behavior as product requirements rather than relying on implicit defaults.

Or skip the browser setup

If what you need is a screenshot of a web page as an image or PDF, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. It is for capturing a page, rather than converting an arbitrary image file selected from a user’s device.

cURL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the ScreenshotNeo API documentation for request options. Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000.

Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.

FAQ

Can I convert a Nuxt Image URL directly to PDF?

Yes, if the browser can load the image and its origin permits access when pixel data must be read. For cross-origin sources, confirm CORS first.

Does Nuxt Image create the PDF?

No. Nuxt Image can transform or deliver images in configured formats. A PDF-generation library or a server-side document workflow is still needed.

Can one PDF contain several images?

Yes. Add each image to a page, create additional pages as needed, and define how page size and orientation behave when image dimensions differ.

Should I use a Nuxt PDF module?

Possibly. Verify its current docs and package health, and confirm it matches your rendering location and deployment runtime before relying on a module-directory summary.

Sources