EmbedPDF

Rich text

A text box (a free text annotation) can mix styles: a bold word, a red sentence, a bigger heading. Double-click the box below, select a word, and use the buttons, or press Ctrl/Cmd with B, I or U while you type:

Loading live preview…

<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { SelectionLayer, selectionPlugin } from '@embedpdf/vue/selection';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/vue/annotation';
import { localEngine } from '@embedpdf/engine';
import RichTextToolbar from './RichTextToolbar.vue';
import SelectRichText from './SelectRichText.vue';

import '../rich-text.css';

const engine = localEngine();
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  selectionPlugin(),
  annotationPlugin(),
];

const notice = async (): Promise<OpenInput> => {
  const response = await fetch('/pdfs/notice.pdf');
  return { kind: 'bytes', id: 'notice', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: notice }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <SelectRichText />
      <RichTextToolbar />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <SelectionLayer />
          <AnnotationLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>

How it’s stored#

A text box has a body style, which is how its text looks by default, and paragraphs of runs. A run is a piece of text with the styles it changes, and nothing else: a bold word in a regular box is one run with { weight: 700 }.

annotation.richText;
// {
//   body: { family: 'Helvetica', size: 12, color: '#000000' },
//   paragraphs: [{ runs: [{ text: 'Paid ' }, { text: 'in full', style: { weight: 700 } }] }],
// }

The engine writes it the way Acrobat does, so the formatting is the same there. The engine docs have the full model: Free text.

Format from your toolbar#

The same calls work while someone types in a box and when boxes are only selected:

await annotation.selection.update({ bold: true });
await annotation.selection.update({ fontSize: 24, fontColor: '#c00000' });
await annotation.text.toggleFormat('italic'); // italic on, or off if all of it already is

Where a change lands depends on what’s selected:

  • Words selected while typing: the change styles those words.
  • A caret while typing: the change is for the text typed next, as in a word processor. Nothing changes until someone types, and moving the caret drops it.
  • Otherwise: it styles all the text of every selected box. A word with its own style follows the change: turn the box red, and a blue word turns red too.

A property that isn’t about text, such as opacity or the border, always applies to the whole box.

To show the state in your toolbar, useAnnotationProperties() reports what the next change would style:

  • Words selected: values.bold is true when every selected word is bold, and mixed lists what the words disagree on.
  • A caret: the style of the text before it (at the start of a line, the text after it), with the changes made at the caret since.
  • A selected box: all of its text, the way selected words read.

Text boxes also give you textFormat properties, so a style panel shows the bold, italic and underline buttons without extra code.

The engine’s own update keeps the PDF’s rule: fontColor, fontSize and fontFamily change the box’s body style, and a word with its own style keeps it. See Free text.

When it’s saved#

Typing is written after a short pause, at every format change, and when editing ends, so the engine never gets half a word. documents.download() waits for the last write, so the file you download has everything the user typed.

Show your own fonts#

The engine puts a font into the PDF, and the browser draws the text while people type, in the same font: the fonts plugin (fontsPlugin(), from the fonts entry) hands the browser the file the engine writes with. Register the font with the engine:

web-font.ts
import { localEngine } from '@embedpdf/engine';

export const engine = localEngine(); // the engine you give the viewer

const data = await fetch('/fonts/brand-sans.ttf').then((response) => response.arrayBuffer());

await engine.fonts.register({ key: 'brand-sans', data });

The key is the font’s name: fontFamily: 'brand-sans' on a text box. The standard PDF fonts need nothing: they show in the engine’s own faces. Without the fonts plugin, a box shows in its standard font, or in Helvetica. More in Fonts.

Draw your own text box#

The annotation layer’s text box already edits rich text. If you draw text boxes yourself, useRichTextEditor() makes your element the editor: it draws the runs, turns typing back into runs, keeps the caret when the style changes, and handles pasting, input methods and the format shortcuts:

branded-text-box.vue
<script setup lang="ts">
import { useRichTextEditor, type AnnotationRendererProps } from '@embedpdf/vue/annotation';

const props = defineProps<AnnotationRendererProps>();

const { ref: editorRef, editing, style } = useRichTextEditor(() => props.annotation);
</script>

<template>
  <!-- The box fills its frame (`width` and `height` 100% in your CSS) and draws it; the
       editor inside fills the box. -->
  <div :class="['text-box', { editing }]">
    <div :ref="editorRef" :style="style" />
  </div>
</template>

Loading live preview…

<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { SelectionLayer, selectionPlugin } from '@embedpdf/vue/selection';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/vue/annotation';
import type { AnnotationRenderer } from '@embedpdf/vue/annotation';
import { localEngine } from '@embedpdf/engine';
import BrandedTextBox from './BrandedTextBox.vue';
import FormatButtons from './FormatButtons.vue';
import SelectTextBox from './SelectTextBox.vue';

import '../branded-text-box.css';

const engine = localEngine();
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin(),
  selectionPlugin(),
  annotationPlugin(),
];

const notice = async (): Promise<OpenInput> => {
  const response = await fetch('/pdfs/notice.pdf');
  return { kind: 'bytes', id: 'notice', bytes: new Uint8Array(await response.arrayBuffer()) };
};

const RENDERERS: AnnotationRenderer[] = [
  { for: (annotation) => annotation.subtype === 'free-text', component: BrandedTextBox },
];
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: notice }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <SelectTextBox />
      <FormatButtons />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <SelectionLayer />
          <AnnotationLayer :renderers="RENDERERS" />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>

Try it: double-click the box and type. The buttons format the selected words, as on the built-in box.

style is the box’s body style as CSS: font, size, color and alignment. It also makes the element fill its parent: the text is laid out at 16 px or more and drawn back at the box’s size, so iOS doesn’t zoom the page in when the box takes focus. Draw the box’s fill, border and padding on the parent, as above; anything on the element itself would scale with the text. The formatting calls above work on your element as they do on the built-in one.

Was this page helpful?

Your feedback goes directly to the documentation team.