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>
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
}
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
}
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)
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:
- Detecting browser HEIC support
- Decoding HEIC using
createImageBitmap - Reading and applying EXIF orientation
- Rendering to canvas and converting to JPEG
- Embedding into a multi-page PDF with jsPDF
- 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)