A PDF does not store text as words in paragraphs. It stores drawing instructions. Each page has a content stream, a list of operators that paint the page, and the text on it sits inside text objects: a block that starts with BT and ends with ET, picks a font and size with Tf, puts the pen somewhere with Tm or Td, and paints a string of character codes with Tj or TJ. The codes are not letters. The font turns each code into a shape (a glyph), and a separate map, the font’s encoding or its ToUnicode table, says which character each shape stands for. That is why copying, searching and editing a PDF can each go wrong in their own way.
Every excerpt below comes from a real file: the demo letter of PDF Textfix’s sample set, a synthetic one-page letter in IBM Plex Serif, and for two excerpts the demo cover letter, a one-page job application in the same face, both decompressed with qpdf on October 13, 2026.
What is a PDF made of?
A PDF is a collection of numbered objects, and the standard names their kinds in one sentence: “PDF includes eight basic types of objects: Boolean values, Integer and Real numbers, Strings, Names, Arrays, Dictionaries, Streams, and the null object.” The demo letter uses six of the eight:
| Object type | How it is written | Where the demo letter uses it |
|---|---|---|
| Number, integer or real | 1044, 540.5 |
The length of the page’s stream; the vertical position of a line on the page |
| String | (18 Orchard Close) |
The codes a line of text paints |
| Name | /Catalog, /TT2 |
Object types, dictionary keys, and the name the page gives a font |
| Array | [ /PDF /Text ] |
Lists, such as the page’s procedure sets or a font’s glyph widths |
| Dictionary | << /Type /Page … >> |
The catalog, the page and each font |
| Stream | stream … endstream |
Object 5, the page’s drawing instructions |
| Boolean and null | true, false, null |
Not used in the letter |
Any object can be numbered and referred to from elsewhere. The catalog, object 1, reads << /Pages 3 0 R /Type /Catalog >>, and 3 0 R points to object 3, the list of pages. The one page is object 4; it points to its drawing instructions in object 5 and to its fonts through object 6. After the objects comes the cross-reference table, which the standard says “shall contain a one-line entry for each indirect object, specifying the byte offset of that object within the body of the file”, and a trailer that tells a reader where the table starts. What saving does to your PDF walks through that layout line by line.
What is a content stream?
The standard’s definition is short: “A content stream is a PDF stream object whose data consists of a sequence of instructions describing the graphical elements to be painted on a page.” In the demo letter, as in many files, the stream is compressed, so it looks like noise in a text editor. qpdf can write a copy with every stream decompressed (its QDF mode), and then the page’s instructions are readable:
qpdf --qdf --object-streams=disable "Demo Letter.pdf" letter-qdf.pdf
Object 5 of the copy then reads as a list of operators: colors, the graphics state and, between them, the text. Nothing says “paragraph” or “heading”.
What is a text object, and what do BT, ET, Tf, Td and Tj mean?
The standard defines it this way: “A PDF text object consists of operators that may show text strings, move the text position, and set text state and certain other parameters.” Here is the street line of the recipient’s address, exactly as the stream holds it:
BT 11 0 0 11 64 661 Tm /TT2 1 Tf (18 Orchard Close) Tj ET
From left to right:
| Operator | Operands here | What it does |
|---|---|---|
BT |
none | Begins a text object |
Tm |
11 0 0 11 64 661 |
Sets the text matrix: scale by 11 (so 11-point text) and move the pen to 64, 661, measured in points from the bottom-left corner of the page |
Tf |
/TT2 1 |
Selects the font the page’s resources call TT2, at size 1 (the matrix above already scales it to 11) |
Tj |
(18 Orchard Close) |
Paints the string, one glyph per code, advancing the pen by each glyph’s width |
ET |
none | Ends the text object |
Td is the other common way to move the pen. It moves to the start of the next line by an offset from the start of the current one, so a producer can write a whole paragraph as one text object with a Td between lines. The demo letter does not: Quartz, the macOS framework that wrote it, places every line with its own Tm. An editor has to handle both.
Why is a word split into pieces in the content stream?
The first line of the letter’s body, “Thank you for booking a place at our talk”, is stored like this:
BT 11 0 0 11 64 556 Tm /TT2 1 Tf [ (Thank y) 10 (ou f) 6 (or b) -10 (o) -10 (oking a plac) 10 (e a) 10 (t our talk on the his) 7 (tor) -30 (y o) 6 (f the Har) -10 (tw) 7 (ell c) 3 (anal) -10 (, which tak) 15 (es) ] TJ ET
TJ paints an array: strings, with numbers between them that nudge the pen. The standard explains the sign: “In the default coordinate system, a positive adjustment has the effect of moving the next glyph painted either to the left or down by the given amount.” The unit is a thousandth of the font size, so 10 pulls the next glyph 0.11 points to the left at 11 points. These are kerning pairs: “y” and “o” sit a little closer together than their widths alone would put them.
So the word “you” is not stored in this line as “you”, but as “y”, a number, then “ou”. On screen you cannot tell. In the file it matters three ways:
- Search has to join the pieces first. A viewer’s search does that, with its own rules; a tool that searches the file’s bytes does not. In the demo invoice, the label “Due date” is stored as
[ (Due dat) 4 (e) ] TJ, so a byte search of the decompressed stream for the whole label finds 0 matches, and only the fragment “Due dat” is found. - Spaces may be glyphs or only gaps: a large
TJnumber can stand where a space looks to be, and copying has to guess. - Editing has to respect every number. Change “booking” to “reserving” and the pieces after it must still land where the file puts them, or the kerning of the rest of the line changes.
How do character codes become letters?
The bytes inside the parentheses are character codes, and what they mean depends on the font. In the demo letter, TT2 is object 9:
<< /BaseFont /AAAAAC+IBMPlexSerif-Regular /Encoding /MacRomanEncoding /FirstChar 32 /FontDescriptor 12 0 R /LastChar 255 /Subtype /TrueType /Type /Font /Widths [ … ] >>
Three things in that line decide what you see and what you can copy:
- The encoding, here
MacRomanEncoding, a table that gives each byte a glyph name: byte 65 is “A”, byte 142 (written\216in a PDF string) is “é”. The same byte means “Ž” inWinAnsiEncoding, the Windows table, which is why the encoding a file declares has to be the one its bytes were written in. - The font program, embedded in the file, which holds the actual shapes. The
AAAAAC+before the name marks a subset: the standard says the tag “shall consist of exactly six uppercase letters” and a plus sign. A subset normally holds only the glyphs the document uses; the demo letter keeps every Mac Roman letter of its fonts on purpose, so that typed English stays in the document’s font. - The map back to Unicode, which a viewer needs to copy or search. A standard encoding like MacRoman is enough for that. A font with custom codes needs a ToUnicode table. Of a font whose codes are plain glyph numbers, the standard says that without one “no information would be available about what the glyphs mean.”
When a font with custom codes has no such table, the page still looks right, but a copy pastes whatever the codes happen to mean in a standard table. That is the mechanism behind copied text that comes out as gibberish.
The demo cover letter shows the second point, a subset that holds only what the page draws. Its heading, the writer’s own name, reads:
BT 20 0 0 20 64 770 Tm /TT1 1 Tf [ (R) 22 (enee Oka) 1 (f) 21 (or) ] TJ ET
TT1 is object 8, the cover letter’s SemiBold weight:
<< /BaseFont /AAAAAB+IBMPlexSerif-SemiBold /Encoding /MacRomanEncoding /FirstChar 32 /FontDescriptor 11 0 R /LastChar 117 /Subtype /TrueType /Type /Font /Widths [ … ] >>
The cover letter never prints an accented letter, and its SemiBold lines need nothing past “u”, code 117. Code 142, which is “é” in Mac Roman, lies outside that range: the subset has no glyph and no width for it. Correct the name to “Renée Okafor”, as she spells it, and that one letter has to come from another font, the case why editing a PDF changes the font walks through.
Why does a PDF have no paragraphs?
Because nothing in a content stream needs them. Each line in the demo letter is its own text object at its own fixed position. The stream does not record that the line at 556 continues at 540.5, where a sentence ends, or how wide the column is. The app that made the file worked that out once, painted the result and kept none of the reasoning.
A tagged PDF adds a structure tree that marks paragraphs, headings and reading order for assistive technology. The tree is optional and does not change how the page is drawn; the demo letter has none.
This is the root of most broken-layout stories: an editor that rebuilds paragraphs has to guess where they are and reflow the words, often in another font. Where the guess is wrong, lines move.
What must an edit of one line do to keep everything else in place?
An edit that leaves the rest of the page as it was has to do three things:
- Write the new text in the same font. Each new character must become a code the line’s font can paint. If the subset lacks the glyph, it must come from another copy of the font in the file, the same font installed on the Mac, or a standard font, and the reader should be told which. PDF Textfix looks in that order and marks every character that did not come from the line’s own font: underlined when another copy of that font in the file drew it, orange when a font from outside the file did (the installed copy or a standard font). For a font whose licence forbids deriving a new font from it, PDF Textfix skips its other copies and its installed copy: the character comes from a different font when one fits, or is refused with the licence as the reason.
- Keep every other glyph where it was. The other lines and every other object on the page must not move. PDF Textfix checks, to within 0.0001 of a unit, that every glyph meant to stay put is where it was, and refuses the edit with the reason when one has moved. When the new text would run into the text after it, PDF Textfix moves the rest of the line, says so in the bar, and offers Keep Following Text in Place; Shift Rest of Line (⌃⌘R) does the same by hand.
- Leave alone what it does not understand. Text in a font made of pictures (a Type 3 font), invisible text such as the layer of a scan, vertical text and a few other cases are locked in PDF Textfix, and the line says why.
A line edit changes a few codes inside one text object, plus a font resource when a character needed another font. The guide to editing a PDF without changing its format shows what that looks like from the user’s side.
What happens to these objects when the file is saved?
An edit changes the content stream, so object 5 must be written again: either appended to the end of the file, which leaves the old object and its text in place, or as part of a new copy of the whole file. When you save an edit, PDF Textfix writes the whole file again without the content stream the edit replaced. Everything else is copied as it was, unless the file was damaged; PDF Textfix tells you when it repaired one. The same words can remain in links and the addresses they open, bookmarks, comments, form fields, a tagged file’s accessibility text, images and the file’s metadata, and fonts keep the shapes of all their characters. What saving does to your PDF measures both ways in bytes.
How can you look inside a PDF yourself?
Two free Homebrew packages are enough on a Mac:
brew install qpdf poppler
qpdf --qdf --object-streams=disable in.pdf readable.pdf # every stream decompressed
pdffonts in.pdf # fonts, encodings, subsets, ToUnicode
pdftotext -layout in.pdf - # the text as a viewer rebuilds it
Open readable.pdf in a plain text editor and search for BT: every hit starts a text object. pdffonts shows, per font, whether it is embedded, a subset, and has a ToUnicode map.
Questions people ask
Is the text in a PDF stored as plain text?
As codes, which become shapes through the font and characters through the encoding or a ToUnicode table. For English letters the codes are often close to ASCII; in other files they are glyph numbers that mean nothing without the font.
Why does search miss a word I can see on the page?
Usually because the word is stored in pieces, as in the TJ example above, or because its glyphs have no map back to characters, which copied PDF text that comes out as gibberish explains.
What does the plus sign in a font name like AAAAAC+IBMPlexSerif mean?
The six capital letters and the plus sign mark an embedded subset: the file usually carries only the glyphs the document used when it was made. A letter the author never typed is usually missing from it, which is why a new character may need a fallback font.
What does "text object not tagged" mean?
It comes from accessibility checks. A text object should belong to the structure tree, the tags that tell a screen reader what each part of the page is, or be marked as an artifact, such as a page number. Adobe’s checker words its rule this way: “This check reports whether all content in the document is tagged.” The fix: “Ensure that all content in the document is either included in the Tags tree, or marked as an artifact.” It is about accessibility, not about whether the text can be edited.
Can I edit the content stream by hand?
In a QDF copy of a test file, yes. In a real document it breaks easily: a longer string makes the table’s offsets wrong (qpdf’s fix-qdf repairs those), a character the subset lacks draws nothing, and a wider word overlaps the rest of the line.