Skip to content

feat: Support inline image(BI) - #89

Open
kvii wants to merge 3 commits into
ledongthuc:masterfrom
kvii:pr_BI
Open

kvii wants to merge 3 commits into
ledongthuc:masterfrom
kvii:pr_BI

Conversation

@kvii

@kvii kvii commented Sep 17, 2026 •

Copy link
Copy Markdown

Close: #30

Saving image data as string because I don't want to break current API.
Add Value.RawBytes for getting image data conveniently.

An example of extracting inline image from 99.pdf:

bb, _ := os.ReadFile("99.pdf")
br := bytes.NewReader(bb)
pr, _ := pdf.NewReader(br, br.Size())

var i int
contents := pr.Page(1).V.Key("Contents")
pdf.Interpret(contents, func(stk *pdf.Stack, op string) {
	if op != "BI" {
		return
	}

	bi := stk.Pop()
	w := int(bi.Key("W").Int64()) // 95
	h := int(bi.Key("H").Int64()) // 96
	// decode Image Data
	id := bi.Key("ID").RawBytes()
	br := bytes.NewReader(id)
	rd := ccitt.NewReader(br, ccitt.MSB, ccitt.Group4, w, h, nil)
	bs, _ := io.ReadAll(rd)

	// write image
	img := image.NewGray(image.Rect(0, 0, w, h))
	for y := range h {
		for x := range w {
			idx := y*((w+7)/8) + x/8
			if idx >= len(bs) {
				break
			}
			bitIdx := 7 - (x % 8)
			bit := (bs[idx] >> bitIdx) & 1
			img.SetGray(x, y, color.Gray{Y: bit * 255}) // 0->0, 1->255
		}
	}
	var buf bytes.Buffer
	err = png.Encode(&buf, img)
	if err != nil {
		t.Fatal(err)
	}
	out := buf.Bytes()

	i++
	os.WriteFile(fmt.Sprintf("out%d.png", i), out, 0666)
})

Cwooper added a commit to Cwooper/pdf that referenced this pull request Oct 5, 2026
The binary data after an inline image's ID was read as tokens, so an
unbalanced "(" in it opened a string that swallowed the rest of the
page, and other bytes counted as malformed operands. Interpret now skips
the data up to the first EI between whitespace, and its callers still
see the ID and EI operators. This is only a skip: it leaves room for
parsing the image dictionary and data, as upstream PR ledongthuc#89 proposes.
Cwooper added a commit to Cwooper/pdf that referenced this pull request Oct 5, 2026
The binary data after an inline image's ID was read as tokens, so an
unbalanced "(" in it opened a string that swallowed the rest of the
page, and other bytes counted as malformed operands. Interpret now skips
the data up to the first EI between whitespace, and its callers still
see the ID and EI operators. This is only a skip: it leaves room for
parsing the image dictionary and data, as upstream PR ledongthuc#89 proposes.
Cwooper added a commit to Cwooper/pdf that referenced this pull request Oct 6, 2026
The binary data after an inline image's ID was read as tokens, so an
unbalanced "(" in it opened a string that swallowed the rest of the
page, and other bytes counted as malformed operands. Interpret now skips
the data up to the first EI between whitespace, and its callers still
see the ID and EI operators. This is only a skip: it leaves room for
parsing the image dictionary and data, as upstream PR ledongthuc#89 proposes.
@ledongthuc

Copy link
Copy Markdown
Owner

@kvii

Thank you for this contribution. Inline image support is useful. Some problems must be fixed before this PR can be merged.

  1. The loop does not stop (blocking)

The function readInlineImage in lex.go does not stop if it does not find EI. At the end of the data, readByte returns '\n' again and again. The loop runs forever and the memory use increases. Interpret calls this function, so one invalid content stream stops text extraction.

To reproduce, use these content streams:

  • BI /W 8 /H 1 /BPC 8 /CS /G ID <4 bytes>, with no EI.
  • BI /W 8 /H 1 /BPC 8 /CS /G ID <4 bytes>\r EI Q. The code only matches \nEI.
  • BI /W 8 /H 1 /BPC 8 /CS /G ID <4 bytes> EI, where EI is at the end of the stream.

The loop must stop at the end of the stream. It must return an error in this case.

  1. The end of the image data is not detected correctly (blocking)

The code searches for \nEI followed by a space or a newline. This causes two problems:

  • Image data that contains \nEI is cut too early.
  • EI after \r, or at the end of the stream, is not found.

Also, unreadByte is called four times. This can give the wrong result when the buffer reloads.

Use the image size to find the end of the data. The size comes from W, H, BPC and CS when the data is not filtered. If you cannot use the size, limit the search to the stream length.

  1. Remove the debug output

lex.go has fmt.Printf("DEBUG: ..."). Remove this line. Library code must not write to standard output.

  1. Fix the control flow
  • The check for io.EOF comes after the check for nil. Put the io.EOF check first.
  • errorf panics. The continue after b.errorf never runs. Remove it.
  1. Add tests

Add unit tests for these cases:

  • A normal inline image.
  • EI after \n, after \r, and at the end of the stream.
  • Binary image data that contains the bytes EI.
  • A truncated stream.

A test with a real PDF that has an inline image would also help. The example in the description uses 99.pdf, which is not in the repository.

Other comments

RawBytes is clear and matches RawString. No change is needed.

Please reply when you update the PR. Thank you.

@kvii

kvii commented Oct 29, 2026

Copy link
Copy Markdown
Author

@ledongthuc Your review is so careful that it makes me ashamed. I made a lot of mistakes. I've updated the code. Here are the changes:

  1. I never thought about that. This is my fault.
  2. There is no perfect way to read image data, since image data can contain any bytes. Using unreadByte is a total mistake. BI has no field like "length", and we can't infer the length of image data from "W" or "H". readByte will stop at the end of the stream when a malformed PDF is encountered. This behavior comes from NewSectionReader in Value.Reader.
  3. This code was copied from Value.readDict. I think I followed the convention of this project.
  4. Same as 3.
  5. Decoding the data from 99.pdf requires "golang.org/x/image/ccitt", and parsing inline images is not part of this project. I've already added a test in my private test project. I have no idea how to add this test properly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

crash when encountering some CJK text amongst English

2 participants