DEV Community

sunshey
sunshey

Posted on

How to Convert HEIC to PDF in the Browser with Vue 3 and Canvas

Converting HEIC (High Efficiency Image Container) files to PDF seems straightforward — decode the image, embed it in a PDF. But HEIC adds complexity: browser support varies, EXIF orientation handling is tricky, and batch processing requires careful memory management.

Here's how to build a browser-based HEIC-to-PDF converter with Vue 3.

The challenge

HEIC is not universally supported. While Safari and modern Chrome/Firefox support it natively, the support is inconsistent:

  • Safari: Full HEIC support
  • Chrome: HEIC support behind a flag (or in latest versions)
  • Firefox: Limited HEIC support
  • Mobile browsers: Varies by OS and version

The conversion approach must handle these variations gracefully.

The stack

  • Vue 3 with Composition API
  • HTML5 Canvas API for image decoding and rendering
  • jsPDF for PDF generation
  • Vite for bundling

The core implementation

<script setup lang="ts">
import { ref } from 'vue'
import jsPDF from 'jspdf'

const files = ref<File[]>([])
const converting = ref(false)
const progress = ref(0)
const progressTotal = ref(0)

async function convertHeicToPdf() {
  if (files.value.length === 0) return
  converting.value = true

  const pdf = new jsPDF({
    orientation: 'portrait',
    unit: 'mm',
    format: 'a4',
  })

  progressTotal.value = files.value.length

  for (let i = 0; i < files.value.length; i++) {
    progress.value = i + 1
    const file = files.value[i]

    try {
      // Decode HEIC using createImageBitmap (browser native)
      const bitmap = await createImageBitmap(file)

      // Handle EXIF orientation
      const orientedCanvas = await applyExifOrientation(bitmap, file)
      const imageData = orientedCanvas.toDataURL('image/jpeg', 0.92)

      // Add to PDF
      const pageWidth = pdf.internal.pageSize.getWidth()
      const pageHeight = pdf.internal.pageSize.getHeight()
      const imgWidth = orientedCanvas.width
      const imgHeight = orientedCanvas.height

      // Fit image to page while maintaining aspect ratio
      const ratio = Math.min(
        pageWidth / imgWidth,
        pageHeight / imgHeight
      )
      const drawWidth = imgWidth * ratio
      const drawHeight = imgHeight * ratio
      const x = (pageWidth - drawWidth) / 2
      const y = (pageHeight - drawHeight) / 2

      if (i > 0) pdf.addPage()
      pdf.addImage(imageData, 'JPEG', x, y, drawWidth, drawHeight)

    } catch (err) {
      console.error(`Failed to process ${file.name}:`, err)
    }
  }

  const pdfBlob = pdf.output('blob')
  downloadBlob(pdfBlob, 'heic-photos.pdf')
  converting.value = false
}

// Handle EXIF orientation from HEIC files
async function applyExifOrientation(
  bitmap: ImageBitmap,
  file: File
): Promise<HTMLCanvasElement> {
  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('2d')!

  // Read EXIF orientation
  const orientation = await readExifOrientation(file)

  // Set canvas size based on orientation
  if (orientation === 6 || orientation === 8) {
    // Rotate 90° CW or CCW
    canvas.width = bitmap.height
    canvas.height = bitmap.width
  } else {
    canvas.width = bitmap.width
    canvas.height = bitmap.height
  }

  // Apply rotation
  ctx.translate(canvas.width / 2, canvas.height / 2)
  if (orientation === 6) ctx.rotate(Math.PI / 2)
  else if (orientation === 8) ctx.rotate(-Math.PI / 2)
  else if (orientation === 3 || orientation === 4) ctx.rotate(Math.PI)
  else if (orientation === 5) {
    ctx.rotate(Math.PI / 2)
    ctx.scale(1, -1)
  }
  else if (orientation === 7) {
    ctx.rotate(-Math.PI / 2)
    ctx.scale(1, -1)
  }
  else if (orientation === 2) ctx.scale(-1, 1)
  else if (orientation === 4) ctx.scale(1, -1)

  ctx.drawImage(bitmap, -bitmap.width / 2, -bitmap.height / 2)
  return canvas
}

// Read EXIF orientation from file
async function readExifOrientation(file: File): Promise<number> {
  const buffer = await file.slice(0, 65536).arrayBuffer()
  const view = new DataView(buffer)

  // Check for JPEG/EXIF header
  if (view.getUint16(0) !== 0xFFD8) return 1 // Not JPEG, no orientation

  // Find SOF0 marker and read orientation
  let offset = 2
  while (offset < buffer.byteLength) {
    const marker = view.getUint16(offset)
    if (marker === 0xFFE1) {
      // APP1 marker - look for EXIF
      const exifOffset = offset + 2 + 4 // Skip marker + "Exif\0\0"
      if (view.getUint16(exifOffset) === 0x4949) {
        // Intel byte order
        const ifdOffset = view.getUint32(exifOffset + 4)
        const orientation = view.getUint16(exifOffset + ifdOffset + 27)
        return orientation || 1
      }
    }
    offset += 2
  }
  return 1
}
</script>
Enter fullscreen mode Exit fullscreen mode

Key implementation details

1. Browser HEIC support detection

Not all browsers support HEIC. Detect support and provide a fallback:

function isHeicSupported(): boolean {
  const img = new Image()
  return img.decode instanceof Function && 
         createImageBitmap instanceof Function
}
Enter fullscreen mode Exit fullscreen mode

If HEIC is not supported, suggest the user convert to JPG first using a browser-based image converter.

2. Memory management for batch processing

Processing many large HEIC files can exhaust browser memory. Key strategies:

  • Process files sequentially, not in parallel
  • Release bitmaps after each conversion
  • Show progress to prevent timeout perceptions
// Sequential processing with memory cleanup
for (let i = 0; i < files.value.length; i++) {
  const bitmap = await createImageBitmap(files.value[i])
  // ... process ...
  bitmap.close() // Release GPU memory
}
Enter fullscreen mode Exit fullscreen mode

3. EXIF orientation handling

HEIC files from iPhones store orientation in EXIF data. Without reading and applying this orientation, photos may appear rotated incorrectly in the PDF. The applyExifOrientation function reads EXIF and applies the correct rotation before rendering.

4. Image quality settings

The conversion uses JPEG compression at 92% quality:

const imageData = canvas.toDataURL('image/jpeg', 0.92)
Enter fullscreen mode Exit fullscreen mode

This balances file size and quality. For print-quality output, increase to 0.95-0.98. For web submission, 0.85-0.90 may be sufficient.

5. Multi-page PDF generation

jsPDF handles multi-page PDFs automatically with pdf.addPage(). Each HEIC file becomes one page. For multiple images per page, calculate grid positions and use pdf.image() with coordinates.

Summary

Building a browser-based HEIC-to-PDF converter involves:

  1. Detecting browser HEIC support
  2. Decoding HEIC using createImageBitmap
  3. Reading and applying EXIF orientation
  4. Rendering to canvas and converting to JPEG
  5. Embedding into a multi-page PDF with jsPDF
  6. Managing memory for batch processing

The result: iPhone photos converted to universally compatible PDFs, processed entirely locally. Try it at en.sotool.top/heic-to-pdf.

Top comments (0)