PDFIO.jl icon indicating copy to clipboard operation
PDFIO.jl copied to clipboard

Extracting text with a specific font with a rectangular region as selection area

Open kskyten opened this issue 6 years ago • 10 comments

It is common to use different fonts to denote semantic meaning (e.g italics for emphasis or larger font size for section titles). Is it possible to extract text that is in a specific font and size? Also, is it possible to specify a region where to extract from? I would like to be able to, for example, extract all the italic text inside a region.

kskyten avatar Apr 24 '19 09:04 kskyten

Not as an API. However, it's not hard to implement or extend pdPageExtractText for these purposes. If you plan to submit a PR, please feel free to do so.

sambitdash avatar Apr 24 '19 09:04 sambitdash

Can you give some hints on how to implement this?

kskyten avatar Apr 24 '19 09:04 kskyten

pdPageEvalContent is essentially the method to evaluate the content stream stack and populates intermittent values to the graphic state the stack. This stack / state is called GState.

You have to pass a rectangular selection area or font name or attribute as a parameter on the GState. Internally, GState is a stack of Dictionary objects. In evalContent!(tr::PDPageTextRun, state::GState) method filter the text which does not fit into your selection criteria and just pick up the TextRuns that are relevant. Once, that's done, show_text_layout! will sort the relevant text area and show only the selected text which is there in the GState[end][:text_layout].

sambitdash avatar Apr 24 '19 09:04 sambitdash

I made some progress with your advice, but then I got stuck. I added a query font to the state and I check whether it matches. The problem is that now the following text commands for non-matching fonts will not work, so I also added a variable to the state to indicate whether the current font matches the query. This did not work. I get a string "\0\0\0\0...\0" back. The code is also not very elegant, but I just wanted to see if I could get it working first. I mostly just used the existing functions, except for adding the state variables.

Here is my attempt:

import PDFIO.PD: evalContent!, GState,
       show_text_layout!, PDXObject, get_font, get_TextBox,
       TextLayout, offset_text_pos!

import Base.==

function (==)(a::PDPageElement, b::PDPageElement)
    ret = true
    for f in fieldnames(typeof(a))
        if getproperty(a, f) != getproperty(b, f)
            ret = false
        end
    end
    return ret
end

@inline function evalContent!(pdo::PDPageElement{:Tf}, state::GState)
    src = get(state, :source, Union{PDPage, PDXObject})
    fontname = pdo.operands[1]
    font = get_font(src, fontname)
    query = get(state, :query, PDPageElement{:Tf})

    if (font === CosNull) || (font != query)
        state[:matching_font] = false
        return state
    end

    state[:matching_font] = true
    state[:font] = (fontname, font)
    fontsize = get(pdo.operands[2])
    # PDF Spec expects any number so better to standardize to Float32
    state[:fontsize] = Float32(fontsize)
    return state
end

@inline function evalContent!(tr::PDPageTextRun, state::GState)
    if get(state, :matching_font, Bool)
        evalContent!(tr.elem, state)
        tfs = get(state, :fontsize, 0f0)
        th  = get(state, :Tz, Float32)/100f0
        ts  = get(state, :Ts, Float32)
        tc  = get(state, :Tc, Float32)
        tw  = get(state, :Tw, Float32)
        tm  = get(state, :Tm, Matrix{Float32})
        ctm = get(state, :CTM, Matrix{Float32})
        trm = tm*ctm

        (fontname, font) = get(state, :font,
                               (cn"", CosNull),
                               Tuple{CosName, PDFont})
        heap = get(state, :text_layout, Vector{TextLayout})
        text, w, h = get_TextBox(tr.ss, font, tfs, tc, tw, th)
        d = get(state, :h_profile, Dict{Int, Int})
        ih = round(Int, h*10)
        d[ih] = get(d, ih, 0) + length(text)
        tb = [0f0 0f0 1f0; w 0f0 1f0; w h 1f0; 0f0 h 1f0]*trm
        if !get(state, :in_artifact, false)
            tl = TextLayout(tb[1,1], tb[1,2], tb[2,1], tb[2,2],
                            tb[3,1], tb[3,2], tb[4,1], tb[4,2],
                            text, fontname, font.flags)
            push!(heap, tl)
        end
        offset_text_pos!(w, 0f0, state)
        return state
    else
        return state
    end
end

@inline function evalContent!(pdo::PDPageElement{:TD}, state::GState)
    if get(state, :matching_font, Bool)
        tx = Float32(get(pdo.operands[1]))
        ty = Float32(get(pdo.operands[2]))

        state[:TL] = -ty
        set_text_pos!(tx, ty, state)
    else
        return state
    end
end

function evaluate(src, objs, query)
    state = GState{:PDFIO}()
    state[:source] = src
    state[:query] = query
    state[:matching_font] = false

    for o in objs
        evalContent!(o, state)
    end

    io = IOBuffer()
    show_text_layout!(io, state)
    String(io.data)
end

kskyten avatar Apr 24 '19 17:04 kskyten

You can initialize :clipping_rect in pdPageExtractText

You can go to this location: https://github.com/sambitdash/PDFIO.jl/blob/95000b69625cfbd51cf7825470def0d4df9192aa/src/PDPageElement.jl#L653

This code will for example exclude all Italic fonts.

    if !get(state, :in_artifact, false) && !pdFontIsItalic(font)
        tl = TextLayout(tb[1,1], tb[1,2], tb[2,1], tb[2,2],
                        tb[3,1], tb[3,2], tb[4,1], tb[4,2],
                        text, fontname, font.flags)
        r = CDRect(tl)
        if intersects(r, get(state, :clipping_rect, CDRect{Float32})
            push!(heap, tl)
        end
    end

You do not need to override any other method to my belief as overriding them will clearly affect the PDF graphics state and transformation matrices may be seriously affected, thus affecting the renderer logic. Without understanding PDF specification page rendering any such changes can detrimental to the output. Will recommend reading the chapter on PDF text rendering for the same.

sambitdash avatar Apr 24 '19 18:04 sambitdash

Thank you! It works great.

kskyten avatar Apr 24 '19 19:04 kskyten

Now that it worked, you can make a modification to the pdPageExtractText which can take a clipping rectangle path as input or certain font characteristics as input parameter and submit a PR.

sambitdash avatar Apr 24 '19 20:04 sambitdash

Is there an example of how to extract all bold text in a pdf page?

Nosferican avatar Nov 09 '20 22:11 Nosferican

You can initialize :clipping_rect in pdPageExtractText

You can go to this location: https://github.com/sambitdash/PDFIO.jl/blob/95000b69625cfbd51cf7825470def0d4df9192aa/src/PDPageElement.jl#L653

This code will for example exclude all Italic fonts.

    if !get(state, :in_artifact, false) && !pdFontIsItalic(font)
        tl = TextLayout(tb[1,1], tb[1,2], tb[2,1], tb[2,2],
                        tb[3,1], tb[3,2], tb[4,1], tb[4,2],
                        text, fontname, font.flags)
        r = CDRect(tl)
        if intersects(r, get(state, :clipping_rect, CDRect{Float32})
            push!(heap, tl)
        end
    end

You do not need to override any other method to my belief as overriding them will clearly affect the PDF graphics state and transformation matrices may be seriously affected, thus affecting the renderer logic. Without understanding PDF specification page rendering any such changes can detrimental to the output. Will recommend reading the chapter on PDF text rendering for the same.

Use pdFontIsBold instead of pdFontIsItalic in the code.

sambitdash avatar Nov 11 '20 06:11 sambitdash

I ended up working with the text itself but it would be nice to have a code snippet / example from the PDF page to running the custom pdPageExtractText.

Nosferican avatar Nov 12 '20 18:11 Nosferican