Статьи

Извлечение таблиц из PDF-файлов в Javascript с помощью PDF.js

 Распространенной и сложной проблемой получения данных является извлечение таблиц из PDF. Ранее я описывал, как извлечь текст из PDF с помощью PDF.js , библиотеки рендеринга PDF, созданной Mozilla Labs.

Процесс рендеринга требует HTML-объекта canvas, а затем рисует на нем каждый объект (символ, линию, прямоугольник и т. Д.). Самый простой способ получить их список — перехватить все вызовы PDF.js, выполняемые функциями рисования на объекте canvas. (см. « Самомодифицирующиеся JavaScript » для аналогичной техники). Метод «set» ниже добавляет закрытие оболочки к каждой функции, которая регистрирует вызов.

function replace(ctx, key) {
  var val = ctx[key];
   if (typeof(val) == "function") {
     ctx[key] = function() {
      var args = Array.prototype.slice.call(arguments);
      console.log("Called " + key + "(" + args.join(",") + ")");
      return val.apply(ctx, args);
    }
  }
}
 
for (var k in context) {
  replace(context, k);
}
 
var renderContext = {
  canvasContext: context,
  viewport: viewport
};
 
page.render(renderContext);

Это позволяет нам увидеть серию звонков:

Called transform(1,0,0,1,150.42,539.67)
Called translate(0,0)
Called scale(1,-1)
Called scale(0.752625,0.752625)
Called measureText(C)
Called save()
Called scale(0.9701818181818181,1)
Called fillText(C,0,0)
Called restore()
Called restore()
Called save()
Called transform(1,0,0,1,150.42,539.6

Мы можем легко получить текст, отметив первый аргумент для каждого вызова «fillText»:

"Congregations Ranked by Growth and Decline in Membership and Worship Attendance, 2006 to 2011Philadelphia Presbytery - Table 16Net
Membership ChangeNet Worship ChangePercent ChangePercent ChangeWorship
 2006Worship 2011Membership 2006Membership 2011Abington, Abington-
143(74)-13.18%(57)0(15)0.00%(22)NumberRank3003001,085942Anchor,
Wrightstown0(23)0.00%(27)-12(25)-21.43%(52)NumberRank56449797Arch
Street, Philadelphia-117(71)-68.42%(117)27(5)90.00%
(2)NumberRank305717154Aston, Aston3(21)3.53%(22)-5(19)-9.43%
(31)NumberRank53488588BeaconNo reportboth yearsNo reportboth
yearsNumberRankBensalem, Bensalem-23(39)-13.94%(62)-28(36)-28.57%
(64)NumberRank9870165142Berean, Philadelphia106(4)44.92%(4)No
reportboth yearsNumberRank00236342Bethany Collegiate, Havertown-
188(76)-42.44%(110)43(3)21.29%(7)NumberRank202245443255Bethel,
Philadelphia-13(33)-13.68%(60)-27(35)-35.06%
(71)NumberRank77509582Bethesda, Philadelphia9(18)5.56%(18)No reportboth
yearsNumberRank1150162171Beverly Hills, Upper Darby-3(26)-3.03%
(32)-11(24)-20.00%(48)NumberRank55449996Bridesburg,
Philadelphia0(23)0.00%(27)No reportboth yearsNumberRank004444Bristol,
BristolNo reportboth yearsNo reportboth yearsNumberRankPage 1 of
10Report prepared by Research Services, Presbyterian Church (U.S.A.)1-
800-728-7228, ext #204006-Oct-12"

Примечательно, что это не отслеживает окончания строк, и не все символы записываются в ожидаемом порядке (первая строка отображается после второй).

Призывы к преобразованию, переводу и масштабированию контролируют место размещения текста. Метод fillText также принимает набор параметров (x, y), который перемещает отдельные буквы между словами. Точная позиция — это комбинация последовательных операций, которые моделируются как стек матричных операций.

К счастью, PDF.js отслеживает вывод этих операций по мере их рендеринга, поэтому нам не нужно пересчитывать его.

Таким образом, мы можем сделать метод, который записывает буквы и их реальные позиции. Этот метод принимает объект внутреннего контекста, тип перехода состояния и аргументы перехода. Этот метод затем вызывается из функции ‘record’, указанной выше.

var chars = [];
var cur = {};
 
function record(ctx, state, args) {
  if (state === 'fillText') {
    var c = args[0];
    cur.c = c;
    cur.x = ctx._transformMatrix[4] + args[1];
    cur.y = ctx._transformMatrix[5] + args[2];
 
    chars[chars.length] = cur;
    cur = {};
  }
}

Эти результаты могут быть отсортированы по позиции (х и у). Метод sort упорядочивает буквы по позициям — если они сдвинуты на небольшую величину вверх или вниз, они считаются одной строкой.

chars.sort(
  function(a, b) {
    var dx = b.x - a.x;
    var dy = b.y - a.y;
 
    if (Math.abs(dy) < 0.5) {
      return dx * -1;
    } else {
      return dy * -1;
    }
  }
);

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

Для этого мы определяем функцию, которая может преобразовывать массив букв и позиций в вывод в стиле CSV. Это отслеживает от буквы к букве — если он видит «большое» изменение у, он создает новую строку. Если он видит «большое» изменение в x, он рассматривает его как новый столбец.

Реальная проблема — определить «большой», который для моего тестового PDF был около 15 и 20, для dx и dy.

function getText(marks, ex, ey, v) {
  var x = marks[0].x;
  var y = marks[0].y;
 
  var txt = '';
  for (var i = 0; i < marks.length; i++) {
    var c = marks[i];
    var dx = c.x - x;
    var dy = c.y - y;
 
    if (Math.abs(dy) > ey) {
      txt += "\"\n\"";
      if (marks[i+1]) {
        // line feed - start from position of next line
        x = marks[i+1].x;
      }
    }
 
    if (Math.abs(dx) > ex) {
      txt += "\",\"";
    }
 
    if (v) {
      console.log(dx + ", " + dy);
    }
 
    txt += c.c;
 
    x = c.x;
    y = c.y;
  }
 
  return txt;
}

Этот алгоритм не обрабатывает новые строки в строках, и, как ни странно, столбцы выходят не в правильном порядке, но они выглядят непоследовательно. Строки с большими пробелами (например, em-dash) обнаруживаются как имеющие несколько столбцов, но это может быть очищено позже — вот пример выходных данных.

Вы можете увидеть пример ниже, и окончательный источник доступен на github .

Congregations Ranked by Growth and Decline in M","embership and W","orship Attendance, 2006 to 2011"
"","Philadelphia Presbytery"," - Table 16"
"","Net ","Membership ","Change"
"","Net Worship ","Change","Percent ","Change","Percent ","Change","Worship"," 2006","Worship"," 2011","Membership"," 2006","Membership"," 2011"
"","Abington, Abington","-143","(74)","-13.18%(57)","0","(15)","0.00%(22)","Number","Rank","300","300","1,085","942"
"","Anchor, Wrightstown","0","(23)","0.00%(27)","-12","(25)","-21.43%(52)","Number","Rank","56","44","97","97"
"","Arch Street, Philadelphia","-117","(71)","-68.42%","(117)","27(5)","90.00%(2)","Number","Rank","30","57","171","54"
"","Aston, Aston","3","(21)","3.53%(22)","-5","(19)","-9.43%(31)","Number","Rank","53","48","85","88"
"","Beacon","No report","both years","No report","both years","Number","Rank"
"","Bensalem, Bensalem","-23","(39)","-13.94%(62)","-28","(36)","-28.57%(64)","Number","Rank","98","70","165","142"
"","Berean, Philadelphia","106(4)","44.92%(4)","No report","both years","Number","Rank","0","0","236","342"
"","Bethany Collegiate, Havertown","-188","(76)","-42.44%","(110)","43(3)","21.29%(7)","Number","Rank","202","245","443","255"
"","Bethel, Philadelphia","-13","(33)","-13.68%(60)","-27","(35)","-35.06%(71)","Number","Rank","77","50","95","82"
"","Bethesda, Philadelphia","9","(18)","5.56%(18)","No report","both years","Number","Rank","115","0","162","171"
"","Beverly Hills, Upper Darby","-3","(26)","-3.03%(32)","-11","(24)","-20.00%(48)","Number","Rank","55","44","99","96"
"","Bridesburg, Philadelphia","0","(23)","0.00%(27)","No report","both years","Number","Rank","0","0","44","44"
"","Bristol, Bristol","No report","both years","No report","both years","Number","Rank"
"","Page 1 of 10","Report prepared by Research Services, Presbyterian Church (U.S.A.)","1-800-728-7228, ext #2040","06-Oct-12"