Pdf.js: получить цвет текста

У меня есть простой pdf файл, содержащий слова "Hello world", каждый в другом цвете.

Я загружаю PDF, например:

PDFJS.getDocument('test.pdf').then( onPDF );

function onPDF( pdf )
{
    pdf.getPage( 1 ).then( onPage );
}

function onPage( page )
{
    page.getTextContent().then( onText );
}

function onText( text )
{   
    console.log( JSON.stringify( text ) );
}

И я получаю вывод JSON следующим образом:

{
    "items" : [{
            "str" : "Hello ",
            "dir" : "ltr",
            "width" : 29.592,
            "height" : 12,
            "transform" : [12, 0, 0, 12, 56.8, 774.1],
            "fontName" : "g_font_1"
        }, {
            "str" : "world",
            "dir" : "ltr",
            "width" : 27.983999999999998,
            "height" : 12,
            "transform" : [12, 0, 0, 12, 86.5, 774.1],
            "fontName" : "g_font_1"
        }
    ],
    "styles" : {
        "g_font_1" : {
            "fontFamily" : "serif",
            "ascent" : 0.891,
            "descent" : 0.216
        }
    }
}

Однако я не смог найти способ определить цвет каждого слова. Когда я его выдаю, он отображается правильно, поэтому я знаю, что информация там где-то. Есть ли где-нибудь доступ к этому?

Ответ 1

Как сказано в разделе Respwned, нет простого ответа, который будет работать во всех случаях. При этом здесь есть два подхода, которые, похоже, работают достаточно хорошо. Оба имеют верхние и нижние стороны.

Подход 1

Внутри метод getTextContent использует то, что называется EvaluatorPreprocessor для анализа операторов PDF, и поддерживает графическое состояние. Так что мы можем сделать, реализовать пользовательский EvaluatorPreprocessor, перезаписать метод preprocessCommand и использовать его, чтобы добавить текущий цвет текста к графическому состоянию. Как только это будет установлено, в любое время создается новый текстовый фрагмент, мы можем добавить атрибут цвета и установить его в текущее состояние цвета.

Недостатками этого подхода являются:

  • Требуется изменить исходный код PDFJS. Это также сильно зависит от текущая реализация PDFJS, и может сломаться, если это изменилось.

  • Это не удастся в случаях, когда текст используется как путь к заполнению изображения. В некоторых создателях PDF (таких как Photoshop) способ создания цветного текста заключается в том, что он сначала создает обтравочный путь из всех заданных текстовых символов, а затем рисует сплошное изображение по пути. Таким образом, единственный способ вывести цвет заливки - это считывание значений пикселей из изображения, что потребовало бы рисования его на холсте. Даже подключение к paintChar здесь не поможет, так как цвет заливки появится только позже.

Потенциал роста, его довольно надежный и работает независимо от фона страницы. Он также не требует визуализации чего-либо для холста, поэтому его можно сделать полностью в фоновом потоке.

код

Все изменения сделаны в файле core/evaluator.js.

Сначала вы должны определить пользовательский оценщик, после определение EvaluatorPreprocessor.

var CustomEvaluatorPreprocessor = (function() {
    function CustomEvaluatorPreprocessor(stream, xref, stateManager, resources) {
        EvaluatorPreprocessor.call(this, stream, xref, stateManager);
        this.resources = resources;
        this.xref = xref;

        // set initial color state
        var state = this.stateManager.state;
        state.textRenderingMode = TextRenderingMode.FILL;
        state.fillColorSpace = ColorSpace.singletons.gray;
        state.fillColor = [0,0,0];
    }

    CustomEvaluatorPreprocessor.prototype = Object.create(EvaluatorPreprocessor.prototype);

    CustomEvaluatorPreprocessor.prototype.preprocessCommand = function(fn, args) {
        EvaluatorPreprocessor.prototype.preprocessCommand.call(this, fn, args);
        var state = this.stateManager.state;
        switch(fn) {
            case OPS.setFillColorSpace:
                state.fillColorSpace = ColorSpace.parse(args[0], this.xref, this.resources);
            break;
            case OPS.setFillColor:
                 var cs = state.fillColorSpace;
                 state.fillColor = cs.getRgb(args, 0);
            break;
            case OPS.setFillGray:
              state.fillColorSpace = ColorSpace.singletons.gray;
              state.fillColor = ColorSpace.singletons.gray.getRgb(args, 0);
            break;
            case OPS.setFillCMYKColor:
              state.fillColorSpace = ColorSpace.singletons.cmyk;
              state.fillColor = ColorSpace.singletons.cmyk.getRgb(args, 0);
            break;
            case OPS.setFillRGBColor:
                state.fillColorSpace = ColorSpace.singletons.rgb;
                state.fillColor = ColorSpace.singletons.rgb.getRgb(args, 0);
            break;
        }
    };

    return CustomEvaluatorPreprocessor;
})();

Затем вам нужно изменить метод getTextContent для использования нового оценщика:

var preprocessor = new CustomEvaluatorPreprocessor(stream, xref, stateManager, resources);

И наконец, в newTextChunk добавьте атрибут цвета:

color: stateManager.state.fillColor

Подход 2

Другой подход заключается в извлечении текстовых ограничивающих блоков через getTextContent, рендеринг страницы и для каждого текста, получение значений пикселей, которые находятся в пределах его границ, и принять это за цвет заливки.

Недостатками этого подхода являются:

  • Выделенные текстовые ограничивающие поля не всегда корректны, а в некоторых случаях могут даже полностью отключиться (например, повернутый текст). Если ограничивающая рамка не охватывает хотя бы частично фактический текст на холсте, то этот метод не удастся. Мы можем восстановиться после полных сбоев, проверив, что текстовые пиксели имеют дисперсию цвета больше порога. Обоснование заключается в том, что если ограничивающая рамка полностью соответствует фону, она будет иметь небольшую дисперсию, и в этом случае мы можем отказаться от цвета текста по умолчанию (или, возможно, даже цвета k ближайших соседей).
  • Метод предполагает, что текст темнее фона. В противном случае фон может быть ошибочно принят за цвет заливки. Обычно это не проблема, так как большинство документов имеют белый фон.

Поверхность - это простое и не требует возиться с исходным кодом PDFJS. Кроме того, он будет работать в случаях, когда текст используется в качестве обтравочного контура и заполняется изображением. Хотя это может стать туманным, когда у вас сложное заполнение изображений, и в этом случае выбор цвета текста становится неоднозначным.

Демо

http://jsfiddle.net/x2rajt5g/

Пример PDF для тестирования:

код

function parseColors(canvasImgData, texts) {
    var data = canvasImgData.data,
        width = canvasImgData.width,
        height = canvasImgData.height,
        defaultColor = [0, 0, 0],
        minVariance = 20;

    texts.forEach(function (t) {
        var left = Math.floor(t.transform[4]),
            w = Math.round(t.width),
            h = Math.round(t.height),
            bottom = Math.round(height - t.transform[5]),
            top = bottom - h,
            start = (left + (top * width)) * 4,
            color = [],
            best = Infinity,
            stat = new ImageStats();

        for (var i, v, row = 0; row < h; row++) {
            i = start + (row * width * 4);
            for (var col = 0; col < w; col++) {
                if ((v = data[i] + data[i + 1] + data[i + 2]) < best) { // the darker the "better"
                    best = v;
                    color[0] = data[i];
                    color[1] = data[i + 1];
                    color[2] = data[i + 2];
                }
                stat.addPixel(data[i], data[i+1], data[i+2]);
                i += 4;
            }
        }
        var stdDev = stat.getStdDev();
        t.color = stdDev < minVariance ? defaultColor : color;
    });
}

function ImageStats() {
    this.pixelCount = 0;
    this.pixels = [];
    this.rgb = [];
    this.mean = 0;
    this.stdDev = 0;
}

ImageStats.prototype = {
    addPixel: function (r, g, b) {
        if (!this.rgb.length) {
            this.rgb[0] = r;
            this.rgb[1] = g;
            this.rgb[2] = b;
        } else {
            this.rgb[0] += r;
            this.rgb[1] += g;
            this.rgb[2] += b;
        }
        this.pixelCount++;
        this.pixels.push([r,g,b]);
    },

    getStdDev: function() {
        var mean = [
            this.rgb[0] / this.pixelCount,
            this.rgb[1] / this.pixelCount,
            this.rgb[2] / this.pixelCount
        ];
        var diff = [0,0,0];
        this.pixels.forEach(function(p) {
            diff[0] += Math.pow(mean[0] - p[0], 2);
            diff[1] += Math.pow(mean[1] - p[1], 2);
            diff[2] += Math.pow(mean[2] - p[2], 2);
        });
        diff[0] = Math.sqrt(diff[0] / this.pixelCount);
        diff[1] = Math.sqrt(diff[1] / this.pixelCount);
        diff[2] = Math.sqrt(diff[2] / this.pixelCount);
        return diff[0] + diff[1] + diff[2];
    }
};

Ответ 2

Этот вопрос на самом деле чрезвычайно тяжелый, если вы хотите сделать это до совершенства... или это может быть относительно легко, если вы можете жить с решениями, которые работают только некоторое время.

Прежде всего, поймите, что getTextContent предназначен для поиска текста с возможностью поиска и что все, что он намеревался сделать.

В приведенных выше комментариях было высказано предположение, что вы используете page.getOperatorList(), но в основном переопределяете всю модель чертежа PDF в своем коде... что в основном глупо, потому что самый большой кусок PDFJS делает именно это... за исключением не для извлечения текста, а с целью переноса на холст. Итак, что вы хотите сделать, это взломать canvas.js, чтобы вместо того, чтобы просто устанавливать свои внутренние регуляторы, он также выполняет некоторые обратные вызовы для вашего кода. Увы, если вы пойдете так, вы не сможете использовать фондовый PDFJS, и я скорее сомневаюсь, что ваша цель выделения цвета будет считаться очень полезной для основной цели PDFJS, поэтому ваши изменения, скорее всего, не получат принятый вверх по течению, поэтому вам, вероятно, придется поддерживать свою собственную вилку PDFJS.

После этого ужасного предупреждения, то, что вы хотели бы минимально изменить, - это функции, в которых PDFJS анализировал операторы цвета PDF и задает свой собственный цвет холста. Это происходит вокруг строки 1566 (ofvas.js) в function setFillColorN. Вам также потребуется перехватить текстовый рендер... который скорее является средством визуализации символов на уровне canvas.js, а именно CanvasGraphics_paintChar вокруг строки 1270. При использовании этих двух кнопок вы получите поток обратных вызовов для изменения цвета, чередующихся между последовательностями рисования символов. Таким образом, вы можете легко восстановить цвет символьных последовательностей из этого.. в простых случаях цвета.

И теперь я добираюсь до действительно уродливой части: тот факт, что PDF имеет чрезвычайно сложную цветовую модель. Сначала есть два цвета для рисования всего, включая текст: цвет заливки и штрих (контур). Пока что не слишком страшно, но цвет является индексом в ColorSpace... которого несколько, RGB - только одна возможность. Затем есть также режимы альфа и композиция, поэтому слои (различных альфа) могут привести к другому окончательному цвету в зависимости от режима компоновки. И в PDFJS нет ни одного места, где он накапливает цвет из слоев. Он просто [поверх] рисует их по мере их поступления. Поэтому, если вы только извлекаете изменения цвета заливки и игнорируете альфа, компоновку и т.д., Это будет работать, но не для сложных документов.

Надеюсь, что это поможет.