sambitdash / PDFIO.jl

PDF Reader Library for Native Julia.
Other
127 stars 13 forks source link

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

Open kskyten opened 5 years ago

kskyten commented 5 years ago

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.

sambitdash commented 5 years ago

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.

kskyten commented 5 years ago

Can you give some hints on how to implement this?

sambitdash commented 5 years ago

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].

kskyten commented 5 years ago

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
sambitdash commented 5 years ago

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.

kskyten commented 5 years ago

Thank you! It works great.

sambitdash commented 5 years ago

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.

Nosferican commented 3 years ago

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

sambitdash commented 3 years ago

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.

Nosferican commented 3 years ago

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.