Ruby: PdfPageCount.rb

Está meio ruim de ler aqui na página por causa do tamanho das letras e do tamanho das linhas. Sugiro copiar e colar para um editor com sintaxe colorida. Obs.: eu testei com vários arquivos PDF, mas é certo que há erros à espreita. Considere-se avisado.

Versão usada nos testes de 2010-10-27:

#!/usr/bin/env ruby
# encoding: iso-8859-1

# Este programa mostra na tela o número de páginas de um
# arquivo PDF passado na linha de comando.
# Este código fonte é de domínio público (PUBLIC DOMAIN)
# Desenvolvido em Ruby 1.9.2
# Marcus.

def is_delimiter(ch)
    return ch =~ %r*[\[\]\s()<>{}/%]*
end

def skip_spaces(pdf_file)
    begin
        ch = pdf_file.getbyte.chr
    end while ch =~ /\s/
    pdf_file.seek -1, IO::SEEK_CUR
end

class Token
    attr_reader :t, :val
    def initialize(t, val)
        @t = t
        @val = val
    end
end

class TokenReader
    attr_reader :pdf_file
    attr_accessor :xref
    def initialize(pdf_file)
        @pdf_file = pdf_file
        @tok_buf = []
    end
    def unread_token(token)
        @tok_buf << token
    end
    def next_token
        if !@tok_buf.empty?
            return @tok_buf.pop
        end
        trata_word = lambda do |; word|
            word = read_word
            if ['obj', 'endobj', 'stream', 'endstream', 'true', 'false', 'null', 'xref', 'trailer'].find_index word
                return Token.new(word, nil)
            else
                raise "Erro no PDF no byte #{@pdf_file.tell} lendo #{word}"
            end
        end

        ch = nil

        loop do
            ch = @pdf_file.getbyte.chr
            if ch == '%'
                skip_comment
            elsif ch =~ /\s/
                skip_spaces(pdf_file)
            else
                break
            end
        end

        case ch
        when /[\d+.-]/
            @pdf_file.seek -1, IO::SEEK_CUR
            return Token.new('NUMBER', read_number)
        when '<'
            ch = @pdf_file.getbyte.chr
            if ch == '<'
                return Token.new('<<', nil)
            else
                @pdf_file.seek -1, IO::SEEK_CUR
                return Token.new('HX_STRING', read_hx_string)
            end
        when '>'
            ch = @pdf_file.getbyte.chr
            if ch == '>'
                return Token.new('>>', nil)
            else
                @pdf_file.seek -1, IO::SEEK_CUR
                return Token.new('>', nil) # Não deve acontecer, porque o read_hx_string vai consumir o '>'
            end
        when '('
            return Token.new('STRING', read_string)
        when '/'
            return Token.new('NAME', read_name)
        when '['
            return Token.new('[', nil)
        when ']'
            return Token.new(']', nil)
        when '{'
            return Token.new('CODE', read_code)
        when 'R'
            ch = @pdf_file.getbyte.chr
            @pdf_file.seek -1, IO::SEEK_CUR
            if is_delimiter ch
                return Token.new('R', nil)
            else
                @pdf_file.seek -1, IO::SEEK_CUR
                return trata_word.call
            end
        else
            @pdf_file.seek -1, IO::SEEK_CUR
            return trata_word.call
        end
    end
private
    def read_word
        result = ''
        ch = @pdf_file.getbyte.chr
        while !is_delimiter ch
            result << ch
            ch = @pdf_file.getbyte.chr
        end
        @pdf_file.seek -1, IO::SEEK_CUR
        return result
    end

    def read_number
        result = ''
        ch = @pdf_file.getbyte.chr
        while ch =~ /[\d.+-]/
            result << ch
            ch = @pdf_file.getbyte.chr
        end
        @pdf_file.seek -1, IO::SEEK_CUR
        return result
    end

    def skip_comment
        begin
            ch = @pdf_file.getbyte.chr
            if ch == "\r"
                ch = @pdf_file.getbyte.chr
                if ch != "\n"
                    @pdf_file.seek -1, IO::SEEK_CUR
                end
                return
            end
        end until ch == "\n"
    end

    def read_code
        # Tratando código como 1 token só porque
        # não estamos interessados no seu conteúdo.
        # Mas é claro que o código é composto de vários tokens!
        par_level = 0
        result = ''
        loop do
            ch = @pdf_file.getbyte.chr
            if ch == '{'
                par_level += 1
                result << ch
            elsif ch == '}'
                par_level -= 1
                if par_level < 0
                    return result
                else
                    result << ch
                end
            else
                result << ch
            end
        end
    end

    def read_name
        return read_word
    end

    def read_hx_string
        result = ''
        ch = @pdf_file.getbyte.chr
        while ch != '>'
            result << ch
            ch = @pdf_file.getbyte.chr
        end
        return result
    end

    def read_string
        par_level = 0
        escaped = false
        result = ''
        loop do
            ch = @pdf_file.getbyte.chr
            if escaped
                case ch
                when 'n'
                    result << "\n"
                when 'r'
                    result << "\r"
                when 'b'
                    result << "\b"
                when 'f'
                    result << "\f"
                when /\d/
                    # Não suportado ainda
                    result << "\\#{ch}"
                when '(', ')'
                    result << ch
                end
                escaped = false
            elsif ch == "\\"
                escaped = true
            elsif ch == '('
                par_level += 1
                result << '('
            elsif ch == ')'
                par_level -= 1
                if par_level < 0
                    return result
                end
            else
                result << ch
            end
        end
    end
end

def expect(tok_found, tok_expected)
    if tok_found != tok_expected
        raise "Encontrou #{tok_found} quando esperava #{tok_expected}"
    end
end

class Reference
    attr_reader :obj_nr, :gen_nr
    def initialize(obj_nr, gen_nr)
        @obj_nr = obj_nr
        @gen_nr = gen_nr
    end
end

class XrefItem
    attr_accessor :type, :id, :byte_pos, :gen_nr
end

class Xref
    def initialize
        @objs = {}
    end

    def parse_xref(token_reader)
        token = token_reader.next_token
        expect(token.t, 'xref')
        ch = nil
        begin
            token = token_reader.next_token
            expect(token.t, 'NUMBER')
            start_id = token.val.to_i

            token = token_reader.next_token
            expect(token.t, 'NUMBER')
            nr_of_lines = token.val.to_i

            skip_spaces(token_reader.pdf_file)

            if nr_of_lines != 0
                nr_of_expected_bytes = 20 * nr_of_lines
                str_xref = token_reader.pdf_file.read(nr_of_expected_bytes)
                add_block(start_id, nr_of_lines, str_xref)
            end
            ch = token_reader.pdf_file.getbyte.chr
            token_reader.pdf_file.seek -1, IO::SEEK_CUR
        end while ch =~ /\d/
    end

    def get_obj(ref) # Já que arrays têm seu hash calculado por valor, vamos usar
        return @objs[[ref.obj_nr, ref.gen_nr]]
    end
private
    def add_block(start_id, nr_of_lines, str_block)
        (0 ... nr_of_lines).each do |i|
            add_obj(i + start_id, str_block[20*i ... 20*(i+1)])
        end
    end
    def add_obj(id, str_line)
        xref_item = XrefItem.new
        xref_item.type = str_line[17]
        xref_item.id = id
        if xref_item.type == 'n'
            xref_item.byte_pos = str_line[0..9].to_i
        end
        xref_item.gen_nr = str_line[11..15].to_i
        ref = xref_item.id, xref_item.gen_nr
        if !@objs.has_key? ref
            @objs[ref] = xref_item
        end
    end
end

def parse_trailer(token_reader)
    token = token_reader.next_token
    expect(token.t, 'trailer')
    token = token_reader.next_token
    expect(token.t, '<<')
    return parse_dict(token_reader)
end

def parse_object(token_reader)
    obj = lambda do |; result, token|
        result = parse_object(token_reader)
        token = token_reader.next_token
        expect(token.t, 'endobj')
        return result
    end
    token1 = token_reader.next_token
    case token1.t
    when 'NUMBER'
        token2 = token_reader.next_token
        if token2.t == 'NUMBER'
            token3 = token_reader.next_token
            case token3.t
            when 'obj'
                return obj.call
            when 'R'
                return Reference.new(token1.val.to_i, token2.val.to_i)
            else
                token_reader.unread_token(token3)
                token_reader.unread_token(token2)
                return token1
            end
        else
            token_reader.unread_token(token2)
            return token1
        end
    when '['
        return parse_array(token_reader)
    when '<<'
        dictionary = parse_dict(token_reader)
        token4 = token_reader.next_token
        if token4.t == 'stream'
            return read_stream(token_reader, dictionary)
        else
            token_reader.unread_token(token4)
            return dictionary
        end
    when 'obj'
        return obj.call
    else
        return token1
    end
end

def parse_dict(token_reader)
    result = {}
    loop do
        key_token = token_reader.next_token
        if key_token.t == '>>'
            return result
        else
            objeto = parse_object(token_reader)
            if objeto.is_a?(Token) && objeto.t == '>>'
                raise ">> inesperado em pos = #{token_reader.pdf_file.tell}"
            else
                result[key_token.val] = objeto
            end
        end
    end
end

def parse_array(token_reader)
    result = []
    loop do
        token = token_reader.next_token
        if token.t == ']'
            return result
        else
            token_reader.unread_token(token)
            result << parse_object(token_reader)
        end
    end
end

def read_stream(token_reader, dictionary)
    ch = token_reader.pdf_file.getbyte.chr
    if ch == "\r"
        token_reader.pdf_file.getbyte # leu \r, ignorar \n
    elsif ch == "\n"
        ;# ignorar \n
    else
        token_reader.pdf_file.seek -1, IO::SEEK_CUR
    end
    len = dictionary["Length"]
    if len.is_a? Reference
        saved_pos = token_reader.pdf_file.tell
        len = find_indirect_obj(token_reader, len)
        token_reader.pdf_file.seek(len, IO::SEEK_SET)
    end
    token_reader.pdf_file.seek(len.val.to_i, IO::SEEK_CUR) # faz de conta que lê
    token = token_reader.next_token
    expect(token.t, 'endstream')
    return dictionary
end

def read_indirect_obj(token_reader, xref_item)
    if xref_item.type == 'n'
        token_reader.pdf_file.seek(xref_item.byte_pos, IO::SEEK_SET)
        return parse_object token_reader
    else
        raise "Procurando objeto inválido #{xref_item.id}"
    end
end

def find_indirect_obj(token_reader, ref)
    return read_indirect_obj(token_reader, token_reader.xref.get_obj(ref))
end

def get_number_of_pages(file_name)
    open(file_name, 'rb') do |pdf_file|
        comeco_pdf = pdf_file.read(5)
        if comeco_pdf != '%PDF-'
            raise 'Não é um arquivo PDF'
        end
        end_buffer_size = [400, File::size(file_name)].min
        pdf_file.seek(-end_buffer_size, IO::SEEK_END)
        startxref = pdf_file.read(end_buffer_size)
        match_startxref = /startxref\s+(\d+)\s+%%EOF/.match(startxref)
        if !match_startxref
            raise 'Não achou o número de páginas (startxref)'
        else
            token_reader = TokenReader.new(pdf_file)
            linearized = lambda do |; first_obj|
                pdf_file.seek 0, IO::SEEK_SET
                first_obj = parse_object(token_reader)
                if first_obj.has_key? 'Linearized'
                    return first_obj['N'].val.to_i
                else
                    raise 'Não achou o número de páginas (/Linearized)'
                end
            end
            pos = match_startxref[1].to_i
            if pos == 0
                return linearized.call
            else
                pdf_file.seek(pos, IO::SEEK_SET)
                token = token_reader.next_token
                if token.t == 'xref'
                    token_reader.unread_token(token)
                    trailers = []
                    xref = Xref.new
                    token_reader.xref = xref
                    loop do
                        xref.parse_xref(token_reader)
                        trailer = parse_trailer(token_reader)
                        trailers << trailer
                        if trailer.has_key? 'Prev'
                            pdf_file.seek(trailer['Prev'].val.to_i, IO::SEEK_SET)
                        else
                            break
                        end
                    end
                    catalog = nil
                    trailers.each do |tr|
                        if tr.has_key? 'Root'
                            catalog = find_indirect_obj(token_reader, tr['Root'])
                            break
                        end
                    end
                    if catalog == nil
                        raise 'Não achou o número de páginas (catalog)'
                    end
                    pages_ref = catalog['Pages']
                    pages = find_indirect_obj(token_reader, pages_ref)
                    return pages['Count'].val.to_i
                else
                    return linearized.call
                end
            end
        end
    end
end

if ARGV.length < 1
    puts 'Uso: PdfPageCount <nome-do-arquivo.pdf>'
else
    sucessos = falhas = 0
    ARGV.each do |file_name|
        begin
            puts "Número de páginas de #{file_name}: #{get_number_of_pages(file_name)}"
            sucessos += 1
        rescue => e
            puts e.to_s
            puts "Ocorreu um erro ao processar #{file_name}. Continuando..."
            falhas += 1
        end
        #STDOUT.flush
    end
    if ARGV.length > 1
        puts "Fim do processo. #{sucessos} sucesso(s), #{falhas} falha(s)."
    end
end

Um pensamento sobre “Ruby: PdfPageCount.rb

  1. Comparação de desempenho (2) « Visions of hope

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s