Images
A worksheet can carry floating images — a logo on an invoice, a 印影 stamp on a form, product photos in a catalog. Since v1.6.0 they are fully editable: insert from code or from a file picker, drag to move, drag a grip to resize, and they survive a save to xlsx.
Before v1.6.0 images were read-only — a picture loaded from an xlsx was displayed and then silently dropped when the workbook was saved. That is fixed: export now writes real
xl/mediaandxl/drawingsparts.
Edition: images are a Pro feature. Lite has display and editing switched off, but reads (getImage / getImages / getImageRect) and file import stay available — so a document opened in Lite keeps its images when written back out.
Inserting an image
addImage(source, anchor, options?) is synchronous and returns the new image’s id.
const ws = grid.worksheet
// From bytes — Uint8Array, ArrayBuffer, or a data: URL
const id = ws.addImage(bytes, { row: 1, column: 1, width: 240, height: 120 })
From a file picker, addImageFromFile() takes a File or Blob and defaults to the image’s natural size:
const input = document.querySelector('input[type=file]')
input.addEventListener('change', async () => {
const file = input.files?.[0]
if (file) await ws.addImageFromFile(file, { row: 1, column: 1 })
})
Anchors
Where an image sits is described by an anchor, in the same three flavours Excel uses. The shorthand forms below are normalized for you:
| You write | Anchor type | Behaviour |
|---|---|---|
{ row, column, width, height } | oneCell | Top-left pinned to a cell; moves with it, size fixed |
{ from, to } | twoCell | Spans two cell corners; moves and resizes with the cells |
{ x, y, width, height } | absolute | Pinned to the sheet, indifferent to rows and columns |
// oneCell — a logo that keeps its size wherever its cell ends up
ws.addImage(bytes, { row: 0, column: 0, width: 180, height: 60 })
// twoCell — a photo that fills B3:D10 and follows those columns' widths
ws.addImage(bytes, {
from: { row: 2, column: 1 },
to: { row: 10, column: 4 },
})
// absolute — a watermark at a fixed sheet position
ws.addImage(bytes, { x: 40, y: 20, width: 240, height: 120 })
A cell anchor point also takes optional offsetX / offsetY in pixels from the cell’s top-left corner.
Anchors follow row and column inserts and deletes, exactly as Excel behaves: deleting the rows under a logo no longer deletes the logo, while a twoCell image shrinks with the rows it spans.
Options
ws.addImage(bytes, anchor, {
name: 'Company logo', // Excel shape name
mimeType: 'image/png', // force the type instead of sniffing the bytes
})
Editing with the mouse
Out of the box, on a Pro sheet:
| Gesture | Result |
|---|---|
| Click an image | Selects it and draws eight resize grips |
| Drag the body | Moves it |
| Drag a grip | Resizes it — hold Shift to keep the aspect ratio |
| Delete | Removes the selected image |
Each drag is a single Ctrl+Z step.
Switch the interaction off while leaving the images visible and the API usable:
ws.setImageEditEnabled(false)
Sheet protection does the same thing implicitly.
To turn the feature off altogether — nothing drawn, nothing clickable, left out of PDF export — without touching the model:
ws.setImagesEnabled(false)
The model is untouched by that call, so the images still round-trip through xlsx and JSON. This is what Lite does internally.
Editing from code
// Move — to a cell anchor, or to a raw content-pixel point
ws.moveImage(id, { row: 5, column: 2 })
ws.moveImage(id, { x: 300, y: 180 })
// Resize, in pixels
ws.resizeImage(id, { width: 320, height: 160 })
// Replace the anchor outright (e.g. oneCell → twoCell)
ws.setImageAnchor(id, { from: { row: 2, column: 1 }, to: { row: 10, column: 4 } })
// Remove
ws.removeImage(id)
Each returns true when it applied, false when the id is unknown.
Reading images
const images = ws.getImages()
images.forEach((image) => {
console.log(image.id) // unique id
console.log(image.name) // Excel shape name, when the file had one
console.log(image.mimeType) // 'image/png', 'image/jpeg', …
console.log(image.data) // Uint8Array — the raw bytes
console.log(image.anchor) // { type: 'oneCell' | 'twoCell' | 'absolute', … }
})
// One image by id
const image = ws.getImage(id)
// Where it currently sits, in sheet content coordinates
// (header offset included, scroll not applied)
const rect = ws.getImageRect(id) // { x, y, width, height } | null
WorksheetImageInfo
| Property | Type | Description |
|---|---|---|
id | string | Unique identifier |
name | string? | Display name (Excel’s shape name) |
mimeType | string | MIME type of the bytes |
data | Uint8Array | Raw image bytes |
anchor | WorksheetImagePlacement | Normalized placement — oneCell, twoCell or absolute |
Displaying an image outside the grid
To render one in a sidebar or a download link, create a blob URL with createImageUrl(), and revoke it when you are done:
const url = ws.createImageUrl(id)
const img = document.createElement('img')
img.src = url
document.body.appendChild(img)
// Later — blob URLs persist until revoked
ws.revokeImageUrl(id)
The grid’s own image layer manages its URLs internally; you only need to manage the ones you create.
Reacting to changes
onImagesChange() fires whenever the image list changes, including immediately on subscription with the current images.
const unsubscribe = ws.onImagesChange((images) => {
console.log(`${images.length} image(s)`)
})
unsubscribe()
File I/O
| Format | Images |
|---|---|
| xlsx | Full round-trip via real xl/media + xl/drawings parts. Identical images are stored once. |
| ReoGrid JSON | Round-trips in sheet.images as base64. Pass toJson({ includeImages: false }) to leave the bytes out. |
| PDF export | Drawn, following the print scale and page breaks. |
| Browser print | Drawn since v1.6.0 — toPagedHtmlDocument({ showImages }) overrides the sheet’s setting. |
Because a photo round-trips byte-for-byte, a JSON document carrying images is photo-sized. When the JSON is a wire format and the bytes would dwarf it, turn them off:
const doc = grid.toJson({ includeImages: false })
Headless import
The browser loaders (grid.loadXlsx and the worksheet’s loadSheetFromBuffer) need the DOM. For a build tool that converts customer .xlsx files into templates outside a browser, use the exported XlsxImporter and hand its images over explicitly:
import { XlsxImporter, convertImageInfo } from '@reogrid/pro'
const importer = new XlsxImporter(worksheet)
const result = importer.load(bytes)
for (const image of result.images) {
worksheet.loadImageFromImport(convertImageInfo(image))
}
React example
import { useState } from 'react'
import { Reogrid } from '@reogrid/pro/react'
import type { ReogridInstance } from '@reogrid/pro/react'
import type { WorksheetImageInfo } from '@reogrid/pro'
export default function App() {
const [images, setImages] = useState<WorksheetImageInfo[]>([])
const [ws, setWs] = useState<ReogridInstance['worksheet'] | null>(null)
function onReady({ worksheet }: ReogridInstance) {
setWs(worksheet)
worksheet.onImagesChange(setImages)
}
async function onPick(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0]
if (file && ws) await ws.addImageFromFile(file, { row: 1, column: 1 })
}
return (
<>
<input type="file" accept="image/*" onChange={onPick} />
<Reogrid onReady={onReady} style={{ width: '100%', height: '400px' }} />
<p>{images.length} image(s)</p>
</>
)
}
Notes
- Supported formats. PNG, JPEG, GIF, SVG, BMP, TIFF and WebP — subject to what the browser’s
<img>element can decode. - Memory. Blob URLs from
createImageUrl()persist untilrevokeImageUrl()is called. - Lite. Images are not displayed and cannot be edited, but import,
getImages()and export still carry them, so a Lite viewer never destroys a Pro document’s pictures.
Related
- Images demo — a logo and a 承認 seal on an invoice: drag to move, drag a grip to resize.
- XLSX Import & Export — where the
xl/mediaround-trip happens. - ReoGrid JSON —
includeImages. - PDF Export — images in exported documents.
- Print to HTML —
showImages. - Protection — protecting a sheet also locks its images.
Sorry to hear that. What could be improved?