D 2: PdfPageCount.d

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:

import std.array, std.ctype, std.string,
       std.algorithm, std.file, std.typetuple,
       std.typecons, std.regex, std.stdio,
       std.utf, std.conv;

bool isDelimiter(dchar ch) {
    if(canFind("()<>[]{}/%"d, ch)) {
        return true;
    }
    return cast(bool) isspace(ch);
}

struct PdfReference {
    long objNum, genNum;
}

struct PdfToken {
    string t, value;
}

class PdfObject {}

enum fields = [
    tuple(q{double}, q{number}),
    tuple(q{string}, q{str}),
    tuple(q{PdfObject[string]}, q{dict}),
    tuple(q{PdfObject[]}, q{array}),
    tuple(q{PdfReference}, q{reference}),
    tuple(q{PdfToken}, q{token}),
    tuple(q{Tuple!(PdfObject[string], ubyte[])}, q{stream})];

string strAllSubClasses(Tuple!(string, string)[] fields) {
    string result;
    foreach(f; fields) {
        result ~= "class Pdf" ~ to!(string)(toupper(f.field[1][0])) ~ (f.field[1][1 .. $]) ~ "Obj : PdfObject {" ~
            f.field[0] ~ ' ' ~ f.field[1] ~ ';' ~ // declare field
            q{this(} ~ f.field[0] ~ " value) {" ~ // declare constructor
                f.field[1] ~ q{ = value; } ~
            "}"
        "}";
    }
    return result;
}

mixin(strAllSubClasses(fields));

void skipSpaces(File* aPdfFile) {
    ubyte[1] by = void;
    do {
        aPdfFile.rawRead(by);
    } while(isspace(by[0]));
    aPdfFile.seek(-1L, SEEK_CUR);
}

class PdfTokenReader {
private:
    PdfToken[] tokenBuffer;
public:
    PdfXref xref;
    File* pdfFile;
    this(File* aPdfFile) {
        pdfFile = aPdfFile;
    }
    void unreadToken(PdfToken token) {
        tokenBuffer ~= token;
    }
    PdfToken nextToken() {
        if(tokenBuffer.length != 0) {
            scope(success) popBack(tokenBuffer);
            return tokenBuffer.back;
        }
        ubyte[1] by = void;
        while(true) {
            pdfFile.rawRead(by);
            if(by[0] == '%') {
                skipComment();
            } else if(isspace(by[0])) {
                skipSpaces(pdfFile);
            } else {
                break;
            }
        }
        PdfToken trataWord() {
            auto word = readWord();
            switch(word) {
                case "obj", "endobj", "stream", "endstream",
                    "true", "false", "null", "xref", "trailer":
                    return PdfToken(word, null);
                default:
                    throw new Exception(
                        "Erro no PDF no byte " ~ to!string(pdfFile.tell())
                        ~ " lendo " ~ word);
            }
        }

        if(isdigit(by[0]) || canFind(".+-"d, by[0])) {
            pdfFile.seek(-1L, SEEK_CUR);
            return PdfToken("NUMBER", readNumber());
        } else {
            switch(by[0]) {
                case '<':
                    pdfFile.rawRead(by);
                    if(by[0] == '<') {
                        return PdfToken("<<", null);
                    } else {
                        pdfFile.seek(-1L, SEEK_CUR);
                        return PdfToken("HX_STRING", readHxString());
                    }
                case '>':
                    pdfFile.rawRead(by);
                    if(by[0] == '>') {
                        return PdfToken(">>", null);
                    } else {
                        pdfFile.seek(-1L, SEEK_CUR);
                        return PdfToken(">", null); // Não deve acontecer, porque o readHxString vai consumir o '>'
                    }
                case '(':
                    return PdfToken("STRING", readString());
                case '/':
                    return PdfToken("NAME", readName());
                case '[':
                    return PdfToken("[", null);
                case ']':
                    return PdfToken("]", null);
                case '{':
                    return PdfToken("CODE", readCode());
                case 'R':
                    pdfFile.rawRead(by);
                    pdfFile.seek(-1L, SEEK_CUR);
                    if(isDelimiter(cast(dchar)by[0])) {
                        return PdfToken("R", null);
                    } else {
                        pdfFile.seek(-1L, SEEK_CUR);
                        return trataWord();
                    }
                default:
                    pdfFile.seek(-1L, SEEK_CUR);
                    return trataWord();
            }
        }
    }
private:
    string readWord() {
        string result;
        ubyte[1] by = void;
        pdfFile.rawRead(by);
        while(!isDelimiter(cast(dchar)by[0])) {
            // segundo um teste básico que eu fiz, isto não causa
            // realocação a cada caracter.
            result ~= by[0];
            pdfFile.rawRead(by);
        }
        pdfFile.seek(-1L, SEEK_CUR);
        return result;
    }
    string readNumber() {
        string result;
        ubyte[1] by = void;
        pdfFile.rawRead(by);
        while(isdigit(by[0]) || canFind(".+-"d, by[0])) {
            result ~= by[0];
            pdfFile.rawRead(by);
        }
        pdfFile.seek(-1L, SEEK_CUR);
        return result;
    }
    void skipComment() {
        ubyte[1] by = void;
        while(true) {
            pdfFile.rawRead(by);
            if(by[0] == '\r') {
                pdfFile.rawRead(by);
                if(by[0] != '\n') {
                    pdfFile.seek(-1L, SEEK_CUR);
                }
                return;
            } else if(by[0] == '\n') {
                return;
            }
        }
    }
    string readCode() {
        // 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!
        auto parLevel = 0;
        string result;
        ubyte[1] by = void;
        while(true) {
            pdfFile.rawRead(by);
            if(by[0] == '{') {
                ++parLevel;
                result ~= '{';
            } else if(by[0] == '}') {
                --parLevel;
                if(parLevel < 0) {
                    return result;
                } else {
                    result ~= '}';
                }
            } else {
                result ~= by[0];
            }
        }
    }
    string readName() {
        return readWord();
    }
    string readHxString() {
        string result;
        ubyte[1] by = void;
        while(pdfFile.rawRead(by), by[0] != '>') {
            result ~= by[0];
        }
        return result;
    }
    string readString() {
        auto parLevel = 0;
        auto escaped = false;
        string result;
        ubyte[1] by = void;
        while(true) {
            pdfFile.rawRead(by);
            if(escaped) {
                switch(by[0]) {
                    case 'n': result ~= '\n'; break;
                    case 'r': result ~= '\r'; break;
                    case 'b': result ~= '\b'; break;
                    case 'f': result ~= '\f'; break;
                    case '(': result ~= '(';  break;
                    case ')': result ~= ')';  break;
                    default:
                        if(isdigit(by[0])) {
                            // Não suportado ainda
                            result ~= '\\' ~ cast(string)by;
                        }
                        break;
                }
                escaped = false;
            } else if(by[0] == '\\') {
                escaped = true;
            } else if(by[0] == '(') {
                ++parLevel;
                result ~= '(';
            } else if(by[0] == ')') {
                --parLevel;
                if(parLevel < 0) {
                    return result;
                }
            } else {
                result ~= by[0];
            }
        }
    }
}

void expect(string tok1, string tok2) {
    if(tok1 != tok2) {
        throw new Exception("Encontrou " ~ tok1 ~ " quando esperava " ~ tok2);
    }
}

struct PdfXrefItem {
    ubyte type;
    long id, bytePos, genNr;
}

class PdfXref {
private:
    PdfXrefItem[PdfReference] objs;
public:
    void parseXref(PdfTokenReader tokenReader) {
        auto token = tokenReader.nextToken();
        ubyte[1] by;
        expect(token.t, "xref");
        do {
            token = tokenReader.nextToken();
            expect(token.t, "NUMBER");
            auto startId = to!long(token.value);

            token = tokenReader.nextToken();
            expect(token.t, "NUMBER");
            auto nrOfLines = to!int(token.value);

            skipSpaces(tokenReader.pdfFile);
            if(nrOfLines != 0) {
                auto nrOfExpectedBytes = 20 * nrOfLines;
                auto bytesXref = new ubyte[nrOfExpectedBytes];
                tokenReader.pdfFile.rawRead(bytesXref);
                addBlock(startId, nrOfLines, bytesXref);
            }

            skipSpaces(tokenReader.pdfFile);
            tokenReader.pdfFile.rawRead(by);
            tokenReader.pdfFile.seek(-1L, SEEK_CUR);
        } while(isdigit(by[0]));
    }
    PdfXrefItem getObj(PdfReference reference) {
        return objs[reference];
    }
private:
    void addBlock(long startId, int nrOfLines, ubyte[] bytesXref) {
        for(auto i = 0; i < nrOfLines; ++i) {
            addObj(i + startId, bytesXref[20*i .. 20*(i+1)]);
        }
    }
    void addObj(long id, ubyte[] bytesLine) {
        auto strLine = bytesToDString(bytesLine);
        PdfXrefItem xrefItem;
        xrefItem.type = bytesLine[17];
        xrefItem.id = id;
        if(bytesLine[17] == 'n') {
            xrefItem.bytePos = to!long(strLine[0 .. 10]);
        }
        xrefItem.genNr = to!long(strLine[11 .. 16]);
        auto key = PdfReference(xrefItem.id, xrefItem.genNr); 
        if(key !in objs) {
            objs[key] = xrefItem;
        }
    }
}

PdfObject parseObject(PdfTokenReader tokenReader) {
    PdfObject objhelper() {
        auto result = parseObject(tokenReader);
        auto token = tokenReader.nextToken();
        expect(token.t, "endobj");
        return result;
    }
    auto token1 = tokenReader.nextToken();
    switch(token1.t) {
        case "NUMBER": {
            auto token2 = tokenReader.nextToken();
            if(token2.t == "NUMBER") {
                auto token3 = tokenReader.nextToken();
                switch(token3.t) {
                    case "obj":
                        return objhelper();
                    case "R": {
                        auto reference = PdfReference(to!long(token1.value), to!long(token2.value));
                        return new PdfReferenceObj(reference);
                    }
                    default:
                        tokenReader.unreadToken(token3);
                        tokenReader.unreadToken(token2);
                        return new PdfNumberObj(to!double(token1.value));
                }
            } else {
                tokenReader.unreadToken(token2);
                return new PdfNumberObj(to!double(token1.value));
            }
        }
        case "[":
            return parseArray(tokenReader);
        case "<<": {
            auto dictionary = parseDict(tokenReader);
            auto token4 = tokenReader.nextToken();
            auto position = tokenReader.pdfFile.tell();
            if(token4.t == "stream") {
                return readStream(tokenReader, dictionary.dict);
            } else {
                tokenReader.unreadToken(token4);
                return dictionary;
            }
        }
        case "obj":
            return objhelper();
        case "STRING", "HX_STRING":
            return new PdfStrObj(token1.value);
        default:
            return new PdfTokenObj(token1);
    }
}

PdfDictObj parseDict(PdfTokenReader tokenReader) {
    PdfObject[string] result;
    while(true) {
        auto keyToken = tokenReader.nextToken();
        if(keyToken.t == ">>") {
            break;
        } else {
            auto objeto = parseObject(tokenReader);
            if(cast(PdfTokenObj)objeto && (cast(PdfTokenObj)objeto).token.t == ">>") {
                throw new Exception(">> inesperado em pos = " ~ to!string(tokenReader.pdfFile.tell()));
            } else {
                result[keyToken.value] = objeto;
            }
        }
    }
    return new PdfDictObj(result);
}

PdfArrayObj parseArray(PdfTokenReader tokenReader) {
    PdfObject[] result;
    while(true) {
        auto token = tokenReader.nextToken();
        if(token.t == "]") {
            break;
        } else {
            tokenReader.unreadToken(token);
            result ~= parseObject(tokenReader);
        }
    }
    return new PdfArrayObj(result);
}

PdfStreamObj readStream(PdfTokenReader tokenReader, PdfObject[string] dict) {
    ubyte[1] by = void;
    tokenReader.pdfFile.rawRead(by);
    if(by[0] == '\r') { // É \r\n?
        tokenReader.pdfFile.rawRead(by); // leu \r, ignorar \n
    } else if(by[0] == '\n') {
        // ignorar \n
    } else {
        tokenReader.pdfFile.seek(-1L, SEEK_CUR);
    }
    PdfObject lenObj = dict["Length"];
    if(auto lenReferenceObj = cast(PdfReferenceObj) lenObj) {
        long savedPos = tokenReader.pdfFile.tell();
        lenObj = findIndirectObj(tokenReader, lenReferenceObj.reference);
        tokenReader.pdfFile.seek(savedPos, SEEK_SET);
    }
    if(auto lenNumberObj = cast(PdfNumberObj) lenObj) {
        auto len = cast(uint)lenNumberObj.number;
        auto bytes = new ubyte[len];
        // faz de conta que lê o stream
        tokenReader.pdfFile.seek(len, SEEK_CUR);
        auto token = tokenReader.nextToken();
        expect(token.t, "endstream");
        return new PdfStreamObj(tuple(dict, bytes));
    } else {
        throw new Exception("Erro: /Length não é um número. Pos = " ~ to!string(tokenReader.pdfFile.tell()));
    }
}

PdfObject readIndirectObj(PdfTokenReader tokenReader, PdfXrefItem xrefItem) {
    if(xrefItem.type == 'n') {
        tokenReader.pdfFile.seek(xrefItem.bytePos, SEEK_SET);
        return parseObject(tokenReader);
    } else {
        throw new Exception("Procurando objeto inválido " ~ to!string(xrefItem.id));
    }
}

PdfObject findIndirectObj(PdfTokenReader tokenReader, PdfReference reference) {
    auto xrefItem = tokenReader.xref.getObj(reference);
    return readIndirectObj(tokenReader, xrefItem);
}

PdfObject parseTrailer(PdfTokenReader tokenReader) {
    auto token = tokenReader.nextToken();
    expect(token.t, "trailer");
    token = tokenReader.nextToken();
    expect(token.t, "<<");
    return parseDict(tokenReader);
}

dstring bytesToDString(ubyte[] bytes) {
    dchar[] result = new dchar[bytes.length];
    foreach(i, b; bytes) {
        result[i] = cast(dchar)b;
    }
    return cast(dstring)result;
}

int getNumberOfPages(string fileName) {
    immutable bufferSize = 5;
    auto pdfFile = File(fileName, "rb");
    scope(exit) // shouldn't be needed, but some tests were failing without this
        pdfFile.close();
    ubyte[bufferSize] comecoPdf = void;
    pdfFile.rawRead(comecoPdf);
    if(comecoPdf != cast(ubyte[])"%PDF-") {
        throw new Exception("Não é um arquivo PDF");
    }
    immutable endBufferSize = cast(int) min(getSize(fileName), 400UL);
    pdfFile.seek(-endBufferSize, SEEK_END);
    ubyte[] startxref = new ubyte[endBufferSize];
    pdfFile.rawRead(startxref);
    auto strStartxref = bytesToDString(startxref);
    try {
        validate(strStartxref);
    } catch(UtfException e) {
        writeln("Deu erro: " ~ e.toString());
        return -1;
    }
    auto regExMatch = match(strStartxref, regex(r"startxref\s+(\d+)\s+%%EOF\s*"d));
    if(regExMatch.empty()) {
        throw new Exception("Não achou o número de páginas (startxref)");
    }
    auto tokenReader = new PdfTokenReader(&pdfFile);
    int linearized() {
        pdfFile.seek(0L, SEEK_SET);
        auto firstObj = parseObject(tokenReader);
        if(auto firstDict = cast(PdfDictObj) firstObj) {
            if("Linearized" in firstDict.dict) {
                auto n = cast(PdfNumberObj)(firstDict.dict.get("N", null));
                if(n) {
                    return cast(int) n.number;
                } else {
                    throw new Exception("Não achou o número de páginas (/N)");
                }
            } else {
                throw new Exception("Não achou o número de páginas (/Linearized)");
            }
        } else {
            throw new Exception("Não achou o número de páginas (/Linearized)");
        }
    }
    auto pos = to!long(regExMatch.captures()[1]);
    if(pos == 0L) {
        return linearized();
    }
    pdfFile.seek(pos, SEEK_SET);
    auto token = tokenReader.nextToken();
    if(token.t == "xref") {
        tokenReader.unreadToken(token);
        PdfObject[string][] trailers;
        auto xref = new PdfXref();
        tokenReader.xref = xref;
        while(true) {
            xref.parseXref(tokenReader);
            auto trailer = (cast(PdfDictObj)parseTrailer(tokenReader)).dict;
            trailers ~= trailer;
            if(PdfObject* prev = ("Prev" in trailer)) {
                if(auto nPrev = cast(PdfNumberObj) *prev) {
                    pdfFile.seek(cast(long) nPrev.number, SEEK_SET);
                } else {
                    throw new Exception("/Prev não é um número");
                }
            } else {
                break;
            }
        }
        PdfObject catalog = null;
        foreach(tr; trailers) {
            if(PdfObject* root = "Root" in tr) {
                if(auto refRoot = cast(PdfReferenceObj) *root) {
                    catalog = findIndirectObj(tokenReader, refRoot.reference);
                    break;
                } else {
                    throw new Exception("/Root não é uma referência");
                }
            }
        }
        if(catalog is null) throw new Exception("Não achou o número de páginas (catalog)");

        auto catalogDict = cast(PdfDictObj) catalog;
        if(!catalogDict) throw new Exception("catalog não é um dict");

        auto pagesRef = cast(PdfReferenceObj) catalogDict.dict.get("Pages", null);
        if(!pagesRef) throw new Exception("/Pages não é uma referência");

        auto pages = cast(PdfDictObj)findIndirectObj(tokenReader, pagesRef.reference);
        if(!pages) throw new Exception("pages não é um dict");

        auto n = cast(PdfNumberObj) pages.dict.get("Count", null);
        if(!n) throw new Exception("Não achou o número de páginas. /Count não é um número.");

        return cast(int) n.number;
    } else {
        return linearized();
    }
}

void main(string[] args) {
    if(args.length < 2) {
        writeln("Uso: PdfPageCount <nome-arquivo.pdf>");
    } else {
        int sucessos = 0, falhas = 0;
        foreach(fileName; args[1 .. $]) {
            try {
                writefln("Número de páginas de %s: %d", fileName, getNumberOfPages(fileName));
                ++sucessos;
            } catch(Exception e) {
                writeln(e.msg);
                writefln("Ocorreu um erro ao processar %s. Continuando...", fileName);
                ++falhas;
            }
        }
        if(args.length > 2) {
            writefln("Fim do processo. %d sucesso(s), %d falha(s).", sucessos, falhas);
        }
    }
}

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

import std.array, std.ctype, std.string,
       std.algorithm, std.file, std.typetuple,
       std.typecons, std.regex, std.stdio,
       std.utf, std.conv;

/+
Este programa mostra na tela o número de páginas de um
ou mais arquivos PDF passados na linha de comando.
Este código fonte é de domínio público (PUBLIC DOMAIN)
Desenvolvido em D 2.049
Marcus.
+/

bool isDelimiter(dchar ch) {
    if(canFind("()<>[]{}/%"d, ch)) {
        return true;
    }
    return cast(bool) isspace(ch);
}

struct PdfReference {
    long objNum, genNum;
}

struct PdfToken {
    string t, value;
}

class PdfObject {}

enum fields = [
    tuple(q{double}, q{number}),
    tuple(q{string}, q{str}),
    tuple(q{PdfObject[string]}, q{dict}),
    tuple(q{PdfObject[]}, q{array}),
    tuple(q{PdfReference}, q{reference}),
    tuple(q{PdfToken}, q{token}),
    tuple(q{Tuple!(PdfObject[string], ubyte[])}, q{stream})];

string strAllSubClasses(Tuple!(string, string)[] fields) {
    string result;
    foreach(f; fields) {
        result ~= "class Pdf" ~ to!(string)(toupper(f.field[1][0])) ~ (f.field[1][1 .. $]) ~ "Obj : PdfObject {" ~
            f.field[0] ~ ' ' ~ f.field[1] ~ ';' ~ // declare field
            q{this(} ~ f.field[0] ~ " value) {" ~ // declare constructor
                f.field[1] ~ q{ = value; } ~
            "}"
        "}";
    }
    return result;
}

mixin(strAllSubClasses(fields));

void skipSpaces(File aPdfFile) {
    ubyte[1] by = void;
    while(true) {
        aPdfFile.rawRead(by);
        if(!isspace(cast(dchar)by[0])) {
            aPdfFile.seek(-1L, SEEK_CUR);
            return;
        }
    }
}

class PdfTokenReader {
private:
    PdfToken[] tokenBuffer;
public:
    PdfXref xref;
    File pdfFile;
    this(File aPdfFile) {
        pdfFile = aPdfFile;
    }
    void unreadToken(PdfToken token) {
        tokenBuffer ~= token;
    }
    PdfToken nextToken() {
        if(tokenBuffer.length != 0) {
            scope(success) popBack(tokenBuffer);
            return tokenBuffer.back;
        }
        ubyte[1] by = void;
        while(true) {
            pdfFile.rawRead(by);
            if(by[0] == cast(ubyte)'%') {
                skipComment();
            } else if(isspace(cast(dchar)by[0])) {
                skipSpaces(pdfFile);
            } else {
                pdfFile.seek(-1L, SEEK_CUR);
                break;
            }
        }
        PdfToken trataWord() {
            auto word = readWord();
            switch(word) {
                case "obj", "endobj", "stream", "endstream",
                    "true", "false", "null", "xref", "trailer":
                    return PdfToken(word, null);
                default:
                    throw new Exception(
                        "Erro no PDF no byte " ~ to!string(pdfFile.tell())
                        ~ " lendo " ~ word);
            }
        }
        pdfFile.rawRead(by);
        if(isdigit(cast(dchar)by[0]) || canFind(".+-"d, cast(dchar)by[0])) {
            pdfFile.seek(-1L, SEEK_CUR);
            return PdfToken("NUMBER", readNumber());
        } else {
            switch(by[0]) {
                case '<':
                    pdfFile.rawRead(by);
                    if(by[0] == cast(ubyte)'<') {
                        return PdfToken("<<", null);
                    } else {
                        pdfFile.seek(-1L, SEEK_CUR);
                        return PdfToken("HX_STRING", readHxString());
                    }
                case '>':
                    pdfFile.rawRead(by);
                    if(by[0] == cast(ubyte)'>') {
                        return PdfToken(">>", null);
                    } else {
                        pdfFile.seek(-1L, SEEK_CUR);
                        return PdfToken(">", null); // Não deve acontecer, porque o readHxString vai consumir o '>'
                    }
                case '(':
                    return PdfToken("STRING", readString());
                case '/':
                    return PdfToken("NAME", readName());
                case '[':
                    return PdfToken("[", null);
                case ']':
                    return PdfToken("]", null);
                case '{':
                    return PdfToken("CODE", readCode());
                case 'R':
                    pdfFile.rawRead(by);
                    pdfFile.seek(-1L, SEEK_CUR);
                    if(isDelimiter(cast(dchar)by[0])) {
                        return PdfToken("R", null);
                    } else {
                        pdfFile.seek(-1L, SEEK_CUR);
                        return trataWord();
                    }
                default:
                    pdfFile.seek(-1L, SEEK_CUR);
                    return trataWord();
            }
        }
    }
private:
    string readWord() {
        string result;
        ubyte[1] by = void;
        pdfFile.rawRead(by);
        while(!isDelimiter(cast(dchar)by[0])) {
            // segundo um teste básico que eu fiz, isto não causa
            // realocação a cada caracter.
            result ~= by[0];
            pdfFile.rawRead(by);
        }
        pdfFile.seek(-1L, SEEK_CUR);
        return result;
    }
    string readNumber() {
        string result;
        ubyte[1] by = void;
        pdfFile.rawRead(by);
        while(isdigit(cast(dchar)by[0]) || canFind(".+-"d, cast(dchar)by[0])) {
            result ~= by[0];
            pdfFile.rawRead(by);
        }
        pdfFile.seek(-1L, SEEK_CUR);
        return result;
    }
    void skipComment() {
        ubyte[1] by = void;
        while(true) {
            pdfFile.rawRead(by);
            if(by[0] == '\r') {
                pdfFile.rawRead(by);
                if(by[0] == '\n') {
                    return;
                } else {
                    pdfFile.seek(-1L, SEEK_CUR);
                }
            } else if(by[0] == '\n') {
                return;
            }
        }
    }
    string readCode() {
        // 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!
        auto parLevel = 0;
        string result;
        ubyte[1] by = void;
        while(true) {
            pdfFile.rawRead(by);
            if(by[0] == cast(ubyte)'{') {
                ++parLevel;
                result ~= '{';
            } else if(by[0] == cast(ubyte)'}') {
                --parLevel;
                if(parLevel < 0) {
                    return result;
                } else {
                    result ~= '}';
                }
            } else {
                result ~= by[0];
            }
        }
    }
    string readName() {
        return readWord();
    }
    string readHxString() {
        string result;
        ubyte[1] by = void;
        while(pdfFile.rawRead(by), by[0] != cast(ubyte)'>') {
            result ~= by[0];
        }
        return result;
    }
    string readString() {
        auto parLevel = 0;
        auto escaped = false;
        string result;
        ubyte[1] by = void;
        while(true) {
            pdfFile.rawRead(by);
            if(escaped) {
                switch(by[0]) {
                    case 'n': result ~= '\n'; break;
                    case 'r': result ~= '\r'; break;
                    case 'b': result ~= '\b'; break;
                    case 'f': result ~= '\f'; break;
                    case '(': result ~= '(';  break;
                    case ')': result ~= ')';  break;
                    default:
                        if(isdigit(cast(dchar)by[0])) {
                            // Não suportado ainda
                            result ~= '\\' ~ cast(string)by;
                        }
                        break;
                }
                escaped = false;
            } else if(by[0] == cast(ubyte)'\\') {
                escaped = true;
            } else if(by[0] == cast(ubyte)'(') {
                ++parLevel;
                result ~= '(';
            } else if(by[0] == cast(ubyte)')') {
                --parLevel;
                if(parLevel < 0) {
                    return result;
                }
            } else {
                result ~= by[0];
            }
        }
    }
}

void expect(string tok1, string tok2) {
    if(tok1 != tok2) {
        throw new Exception("Encontrou " ~ tok1 ~ " quando esperava " ~ tok2);
    }
}

struct PdfXrefItem {
    ubyte type;
    long id, bytePos, genNr;
}

long readNumber(dstring str, int pos) {
    auto endPos = pos;
    while(true) {
        if(!canFind(".+-", str[endPos]) && !isdigit(str[endPos])) {
            return to!long(str[pos..endPos]);
        }
        ++endPos;
    }
}

class PdfXref {
private:
    PdfXrefItem[PdfReference] objs;
public:
    void parseXref(PdfTokenReader tokenReader) {
        auto token = tokenReader.nextToken();
        ubyte[1] by;
        expect(token.t, "xref");
        do {
            token = tokenReader.nextToken();
            expect(token.t, "NUMBER");
            auto startId = to!long(token.value);

            token = tokenReader.nextToken();
            expect(token.t, "NUMBER");
            auto nrOfLines = to!int(token.value);

            skipSpaces(tokenReader.pdfFile);
            if(nrOfLines != 0) {
                auto nrOfExpectedBytes = 20 * nrOfLines;
                auto bytesXref = new ubyte[nrOfExpectedBytes];
                tokenReader.pdfFile.rawRead(bytesXref);
                addBlock(startId, nrOfLines, bytesXref);
            }

            skipSpaces(tokenReader.pdfFile);
            tokenReader.pdfFile.rawRead(by);
            tokenReader.pdfFile.seek(-1L, SEEK_CUR);
        } while(isdigit(cast(dchar)by[0]));
    }
    void addBlock(long startId, int nrOfLines, ubyte[] bytesXref) {
        for(auto i = 0; i < nrOfLines; ++i) {
            addObj(i + startId, bytesXref[20*i .. 20*(i+1)]);
        }
    }
    void addObj(long id, ubyte[] bytesLine) {
        auto strLine = bytesToDString(bytesLine);
        PdfXrefItem xrefItem;
        xrefItem.type = bytesLine[17];
        xrefItem.id = id;
        if(bytesLine[17] == 'n') {
            xrefItem.bytePos = readNumber(strLine, 0);
        }
        xrefItem.genNr = readNumber(strLine, 11);
        auto key = PdfReference(xrefItem.id, xrefItem.genNr);
        if(key !in objs) {
            objs[key] = xrefItem;
        }
    }
    PdfXrefItem getObj(PdfReference reference) {
        return objs[reference];
    }
}

PdfObject parseObject(PdfTokenReader tokenReader) {
    PdfObject objhelper() {
        auto result = parseObject(tokenReader);
        auto token = tokenReader.nextToken();
        expect(token.t, "endobj");
        return result;
    }
    auto token1 = tokenReader.nextToken();
    switch(token1.t) {
        case "NUMBER": {
            auto token2 = tokenReader.nextToken();
            if(token2.t == "NUMBER") {
                auto token3 = tokenReader.nextToken();
                switch(token3.t) {
                    case "obj":
                        return objhelper();
                    case "R": {
                        auto reference = PdfReference(to!long(token1.value), to!long(token2.value));
                        return new PdfReferenceObj(reference);
                    }
                    default:
                        tokenReader.unreadToken(token3);
                        tokenReader.unreadToken(token2);
                        return new PdfNumberObj(to!double(token1.value));
                }
            } else {
                tokenReader.unreadToken(token2);
                return new PdfNumberObj(to!double(token1.value));
            }
        }
        case "[":
            return parseArray(tokenReader);
        case "<<": {
            auto dictionary = parseDict(tokenReader);
            auto token4 = tokenReader.nextToken();
            auto position = tokenReader.pdfFile.tell();
            if(token4.t == "stream") {
                return readStream(tokenReader, dictionary.dict);
            } else {
                tokenReader.unreadToken(token4);
                return dictionary;
            }
        }
        case "obj":
            return objhelper();
        case "STRING", "HX_STRING":
            return new PdfStrObj(token1.value);
        default:
            return new PdfTokenObj(token1);
    }
}

PdfDictObj parseDict(PdfTokenReader tokenReader) {
    PdfObject[string] result;
    while(true) {
        auto keyToken = tokenReader.nextToken();
        if(keyToken.t == ">>") {
            break;
        } else {
            auto objeto = parseObject(tokenReader);
            if(cast(PdfTokenObj)objeto && (cast(PdfTokenObj)objeto).token.t == ">>") {
                throw new Exception(">> inesperado em pos = " ~ to!string(tokenReader.pdfFile.tell()));
            } else {
                result[keyToken.value] = objeto;
            }
        }
    }
    return new PdfDictObj(result);
}

PdfArrayObj parseArray(PdfTokenReader tokenReader) {
    PdfObject[] result;
    while(true) {
        auto token = tokenReader.nextToken();
        if(token.t == "]") {
            break;
        } else {
            tokenReader.unreadToken(token);
            result ~= parseObject(tokenReader);
        }
    }
    return new PdfArrayObj(result);
}

PdfStreamObj readStream(PdfTokenReader tokenReader, PdfObject[string] dict) {
    ubyte[1] by = void;
    tokenReader.pdfFile.rawRead(by);
    if(by[0] == cast(ubyte)'\r') { // É \r\n?
        tokenReader.pdfFile.rawRead(by); // leu \r, ignorar \n
    } else if(by[0] == cast(ubyte)'\n') {
        // ignorar \n
    } else {
        tokenReader.pdfFile.seek(-1L, SEEK_CUR);
    }
    PdfObject lenObj = dict["Length"];
    if(auto lenReferenceObj = cast(PdfReferenceObj) lenObj) {
        long savedPos = tokenReader.pdfFile.tell();
        lenObj = findIndirectObj(tokenReader, lenReferenceObj.reference);
        tokenReader.pdfFile.seek(savedPos, SEEK_SET);
    }
    if(auto lenNumberObj = cast(PdfNumberObj) lenObj) {
        auto len = cast(uint)lenNumberObj.number;
        auto bytes = new ubyte[len];
        // faz de conta que lê o stream
        tokenReader.pdfFile.seek(len, SEEK_CUR);
        auto token = tokenReader.nextToken();
        expect(token.t, "endstream");
        return new PdfStreamObj(tuple(dict, bytes));
    } else {
        throw new Exception("Erro: /Length não é um número. Pos = " ~ to!string(tokenReader.pdfFile.tell()));
    }
}

PdfObject readIndirectObj(PdfTokenReader tokenReader, PdfXrefItem xrefItem) {
    if(xrefItem.type == cast(ubyte)'n') {
        tokenReader.pdfFile.seek(xrefItem.bytePos, SEEK_SET);
        return parseObject(tokenReader);
    } else {
        throw new Exception("Procurando objeto inválido " ~ to!string(xrefItem.id));
    }
}

PdfObject findIndirectObj(PdfTokenReader tokenReader, PdfReference reference) {
    auto xrefItem = tokenReader.xref.getObj(reference);
    return readIndirectObj(tokenReader, xrefItem);
}

PdfObject parseTrailer(PdfTokenReader tokenReader) {
    auto token = tokenReader.nextToken();
    expect(token.t, "trailer");
    token = tokenReader.nextToken();
    expect(token.t, "<<");
    return parseDict(tokenReader);
}

dstring bytesToDString(ubyte[] bytes) {
    dchar[] result = new dchar[bytes.length];
    foreach(i, b; bytes) {
        result[i] = cast(dchar)b;
    }
    return cast(dstring)result;
}

int getNumberOfPages(string fileName) {
    immutable bufferSize = 5;
    auto pdfFile = File(fileName, "rb");
    scope(exit) // shouldn't be needed
        pdfFile.close();
    ubyte[bufferSize] comecoPdf = void;
    pdfFile.rawRead(comecoPdf);
    if(comecoPdf != cast(ubyte[])"%PDF-") {
        throw new Exception("Não é um arquivo PDF");
    }
    immutable endBufferSize = cast(int) min(getSize(fileName), 400UL);
    pdfFile.seek(-endBufferSize, SEEK_END);
    ubyte[] startxref = new ubyte[endBufferSize];
    pdfFile.rawRead(startxref);
    auto strStartxref = bytesToDString(startxref);
    try {
        validate(strStartxref);
    } catch(UtfException e) {
        writeln("Deu erro: " ~ e.toString());
        return -1;
    }
    auto regExMatch = match(strStartxref, regex(r"startxref\s+(\d+)\s+%%EOF\s*"d));
    if(regExMatch.empty()) {
        throw new Exception("Não achou o número de páginas (startxref)");
    }
    auto tokenReader = new PdfTokenReader(pdfFile); // FIXME: this is wrong, passing pdfFile by value
    int linearized() {
        pdfFile.seek(0L, SEEK_SET);
        auto firstObj = parseObject(tokenReader);
        if(auto firstDict = cast(PdfDictObj) firstObj) {
            if("Linearized" in firstDict.dict) {
                auto n = cast(PdfNumberObj)(firstDict.dict.get("N", null));
                if(n) {
                    return cast(int) n.number;
                } else {
                    throw new Exception("Não achou o número de páginas (/N)");
                }
            } else {
                throw new Exception("Não achou o número de páginas (/Linearized)");
            }
        } else {
            throw new Exception("Não achou o número de páginas (/Linearized)");
        }
    }
    auto pos = to!long(regExMatch.captures()[1]);
    if(pos == 0L) {
        return linearized();
    }
    pdfFile.seek(pos, SEEK_SET);
    auto token = tokenReader.nextToken();
    if(token.t == "xref") {
        tokenReader.unreadToken(token);
        PdfObject[string][] trailers;
        auto xref = new PdfXref();
        tokenReader.xref = xref;
        while(true) {
            xref.parseXref(tokenReader);
            auto trailer = (cast(PdfDictObj)parseTrailer(tokenReader)).dict;
            trailers ~= trailer;
            if(PdfObject* prev = ("Prev" in trailer)) {
                if(auto nPrev = cast(PdfNumberObj) *prev) {
                    pdfFile.seek(cast(long) nPrev.number, SEEK_SET);
                } else {
                    throw new Exception("/Prev não é um número");
                }
            } else {
                break;
            }
        }
        PdfObject catalog = null;
        foreach(tr; trailers) {
            if(PdfObject* root = "Root" in tr) {
                if(auto refRoot = cast(PdfReferenceObj) *root) {
                    catalog = findIndirectObj(tokenReader, refRoot.reference);
                    break;
                } else {
                    throw new Exception("/Root não é uma referência");
                }
            }
        }
        if(catalog is null) throw new Exception("Não achou o número de páginas (catalog)");

        auto catalogDict = cast(PdfDictObj) catalog;
        if(!catalogDict) throw new Exception("catalog não é um dict");

        auto pagesRef = cast(PdfReferenceObj) catalogDict.dict.get("Pages", null);
        if(!pagesRef) throw new Exception("/Pages não é uma referência");

        auto pages = cast(PdfDictObj)findIndirectObj(tokenReader, pagesRef.reference);
        if(!pages) throw new Exception("pages não é um dict");

        auto n = cast(PdfNumberObj) pages.dict.get("Count", null);
        if(!n) throw new Exception("Não achou o número de páginas. /Count não é um número.");

        return cast(int) n.number;
    } else {
        return linearized();
    }
}

void main(string[] args) {
    if(args.length < 2) {
        writeln("Uso: PdfPageCount <nome-arquivo.pdf>");
    } else {
        int sucessos = 0, falhas = 0;
        foreach(fileName; args[1 .. $]) {
            try {
                writefln("Número de páginas de %s: %d", fileName, getNumberOfPages(fileName));
                ++sucessos;
            } catch(Exception e) {
                writeln(e.msg);
                writefln("Ocorreu um erro ao processar %s. Continuando...", fileName);
                ++falhas;
            }
        }
        if(args.length > 2) {
            writefln("Fim do processo. %d sucesso(s), %d falha(s).", sucessos, falhas);
        }
    }
}

3 pensamentos sobre “D 2: PdfPageCount.d

  1. Comparação de desempenho lendo PDFs « Visions of hope

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