---
title: "Read, edit and remove PDF tags in C#"
url: https://recursiveai.org/onepdf/cookbook/edit-pdf-tags/
description: "Read, fix and remove the tags of a tagged PDF in C#: walk the structure tree, correct a heading level in place, or strip the tags. Runnable code."
version: "1.15.0"
---

# Read, edit and remove PDF tags in C#

`doc.StructureTree` reads a tagged PDF’s structure tree into objects you can change in place, and `doc.RemoveTags()` takes the tags out. The program below fixes a skipped heading level in the IRS Form W-9, a public tagged PDF, then shows the tags removed.

```shell
dotnet new console -o PdfTags && cd PdfTags
dotnet add package RecursiveAI.OnePdf --version 1.15.0
curl -LO https://www.irs.gov/pub/irs-pdf/fw9.pdf
```

Put this in `Program.cs` and run `dotnet run`:

```csharp
using OnePdf;
using OnePdf.Text;

// Read and edit the tags: the W-9 jumps from an H2 to an H4.
using (PdfDocument doc = PdfDocument.Open("fw9.pdf",
    new PdfOpenOptions { AllowEditing = true }))
{
    PdfStructureTree tree = doc.StructureTree!;   // null when the file is untagged
    int count = tree.Elements().Count();
    Console.WriteLine($"{count} elements, {tree.Diagnostics.Count} problems");
    Console.WriteLine($"Heading findings: {HeadingFindings(doc)}");

    int previous = 0;
    // depth first, document order
    foreach (PdfStructureNode node in tree.Elements())
    {
        if (node.StandardType is not ['H', >= '1' and <= '6']) continue;
        int level = node.StandardType[1] - '0';
        if (level > previous + 1)
        {
            string text = node.GetText();
            int page = node.PageIndex + 1;
            string fix = $"{node.StandardType} -> H{previous + 1}";
            Console.WriteLine($"  page {page}: \"{text}\" {fix}");
            node.SetType($"H{previous + 1}");
            level = previous + 1;
        }
        previous = level;
    }
    Console.WriteLine($"Heading findings after: {HeadingFindings(doc)}");
    doc.Save("fw9-headings.pdf");
}

// Or take the tags out entirely, to tag the file again from scratch.
using (PdfDocument doc = PdfDocument.Open("fw9.pdf",
    new PdfOpenOptions { AllowEditing = true }))
{
    string before = Text(doc);
    PdfRemoveTagsResult removed = doc.RemoveTags();
    Console.WriteLine($"Removed {removed.SequencesRemoved} marked sequences");
    doc.Save("fw9-untagged.pdf");
    using PdfDocument check = PdfDocument.Open("fw9-untagged.pdf");
    Console.WriteLine($"Tagged: {check.IsTagged}, same text: {Text(check) == before}");
}

static int HeadingFindings(PdfDocument doc) =>
    doc.CheckPdfUA().Count(v => v.Checkpoint == "14");
static string Text(PdfDocument doc) =>
    string.Concat(doc.ExtractText(new PdfTextOptions { UseStructureTree = false })
        .Select(p => p.Text));
```

```text
708 elements, 0 problems
Heading findings: 1
  page 3: "Line 1" H4 -> H3
Heading findings after: 0
Removed 478 marked sequences
Tagged: False, same text: True
```

## How it works

- **`doc.StructureTree` reads the tags into typed objects.** Each `PdfStructureNode` has its `Type` as written and the `StandardType` it maps to through the role map, `Alt`, `ActualText`, `Language`, the table and list attributes (`Scope`, `ColSpan`, `RowSpan`, `ListNumbering`), its `Parent`, `Children` and page, and `GetText()` for the text it covers.
- **Edits change the document at once** and keep the tree’s bookkeeping consistent: `SetType`, the properties above, `tree.MapRole`, `MoveTo`, `WrapChildren`, `InsertChild` (a missing table cell, for example), and `Remove`, either keeping the children (`Unwrap`) or turning the content below into artifacts (`ToArtifacts`).
- **`RemoveTags` takes out the structure tree, `/MarkInfo`, every `/StructParents` and each marked-content sequence with an `/MCID`.** Only the marking operators change, so pages paint and extract as before. Afterwards `Tag` and `ApplyTagging` accept the file: see [Tagging existing PDFs](https://recursiveai.org/onepdf/docs/tagging/).

## Limits and good to know

- Reading never throws on a damaged tree. Cycles, shared elements and the like are listed in `tree.Diagnostics`, and `tree.FindMissingContent()` lists references to content that isn’t there.
- A tree read before `RemoveTags`, tagging or `RefreshStructureTree()` is stale and throws when you read its children. Read `doc.StructureTree` again.
- `RemoveTags(new PdfRemoveTagsOptions { DryRun = true })` reports what it would remove without changing anything, and doesn’t need `AllowEditing`. It also finds leftover marked content in files that have no tree.
- `ArtifactUntaggedGraphics()` marks paths, images and forms drawn outside any tag as artifacts in a tagged file, which clears that PDF/UA finding when only graphics were behind it. Untagged text is counted, never wrapped: what it is stays your decision.
- An inserted `TH` has no `Scope`. Set it at once, or the PDF/UA check reports the cell.

Every call and its limits are in [Structure tree and removing tags](https://recursiveai.org/onepdf/docs/structure/). For fonts the file doesn’t embed, see [embed missing fonts](https://recursiveai.org/onepdf/cookbook/embed-missing-fonts/).
