ReoGrid ReoGrid Web

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/media and xl/drawings parts.

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 writeAnchor typeBehaviour
{ row, column, width, height }oneCellTop-left pinned to a cell; moves with it, size fixed
{ from, to }twoCellSpans two cell corners; moves and resizes with the cells
{ x, y, width, height }absolutePinned 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:

GestureResult
Click an imageSelects it and draws eight resize grips
Drag the bodyMoves it
Drag a gripResizes it — hold Shift to keep the aspect ratio
DeleteRemoves 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

PropertyTypeDescription
idstringUnique identifier
namestring?Display name (Excel’s shape name)
mimeTypestringMIME type of the bytes
dataUint8ArrayRaw image bytes
anchorWorksheetImagePlacementNormalized 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

FormatImages
xlsxFull round-trip via real xl/media + xl/drawings parts. Identical images are stored once.
ReoGrid JSONRound-trips in sheet.images as base64. Pass toJson({ includeImages: false }) to leave the bytes out.
PDF exportDrawn, following the print scale and page breaks.
Browser printDrawn 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 until revokeImageUrl() 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.

Was this page helpful?
Stay Updated

Be first to know — get updates as they ship

Get notified of new releases, features, and announcements.
No spam — just updates that matter.