Skip to content

Instantly share code, notes, and snippets.

@kgorin8
Created September 17, 2019 09:54
Show Gist options
  • Save kgorin8/4c8b5eaf71b9410846124a4543bbd790 to your computer and use it in GitHub Desktop.
Save kgorin8/4c8b5eaf71b9410846124a4543bbd790 to your computer and use it in GitHub Desktop.
*&---------------------------------------------------------------------*
*& Include ZMM_PDF_PARSER
*& https://sapboard.ru/forum/viewtopic.php?f=13&t=51283
*&---------------------------------------------------------------------*
* Текстовый элемент, загруженный из PDF
TYPES: BEGIN OF t_text_element,
num_element TYPE I,
num_page TYPE I,
X TYPE I,
Y TYPE I,
font(20) TYPE C,
text TYPE STRING,
textUcase TYPE STRING,
END OF t_text_element.
TYPES: tt_text_element TYPE t_text_element OCCURS 0.
* Класс для парсинга PDF (разделение и структурирование его текстовых элементов) и
* удобной работы с данными элементами
CLASS lcl_pdf_parser DEFINITION.
PUBLIC SECTION.
CLASS-METHODS: try_read_number IMPORTING
pi_number TYPE ANY
CHANGING
pc_number TYPE ANY
EXCEPTIONS
INVALID_FORMAT.
METHODS: load_pdf_from_server IMPORTING
pi_filename TYPE C
EXCEPTIONS
ERROR_IN_LOADING
ERROR_IN_PARSING,
load_pdf_from_gui IMPORTING
pi_filename TYPE C
EXCEPTIONS
ERROR_IN_LOADING
ERROR_IN_PARSING,
load_pdf_from_itab IMPORTING
pi_it_filedata TYPE STANDARD TABLE
EXCEPTIONS
ERROR_IN_PARSING,
is_loaded EXPORTING
pe_loaded TYPE C,
get_num_pages EXPORTING
pe_num_pages TYPE I,
get_all_elements EXPORTING
pe_elements TYPE tt_text_element,
find_text IMPORTING
pi_num_page TYPE I DEFAULT 1
pi_text TYPE C
pi_match_case TYPE C DEFAULT SPACE
EXPORTING
pe_elements TYPE tt_text_element,
find_text_below IMPORTING
pi_element TYPE t_text_element
pi_accuracy TYPE I
EXPORTING
pe_elements TYPE tt_text_element,
find_text_right IMPORTING
pi_element TYPE t_text_element
pi_accuracy TYPE I
EXPORTING
pe_elements TYPE tt_text_element,
find_text_in_box IMPORTING
pi_num_page TYPE I
pi_left TYPE I
pi_top TYPE I
pi_right TYPE I
pi_bottom TYPE I
EXPORTING
pe_elements TYPE tt_text_element.
PRIVATE SECTION.
DATA: mt_filelines TYPE TABLE OF STRING,
mt_elements TYPE TABLE OF t_text_element,
m_num_pages TYPE I.
METHODS: parse.
ENDCLASS.
CLASS lcl_pdf_parser IMPLEMENTATION.
" Загружает файл PDF из папки на сервере
METHOD load_pdf_from_server.
DATA: BEGIN OF lwa_xline,
XLINE(3000) TYPE X,
END OF lwa_xline.
DATA: lh_all_file TYPE STRING,
lh_len TYPE I,
lh_len1 TYPE I,
lh_line(3000) TYPE C.
DATA: c_conv TYPE REF TO CL_ABAP_CONV_IN_CE.
" Загружаем
OPEN DATASET pi_filename FOR INPUT IN BINARY MODE.
IF sy-subrc <> 0.
"PERFORM log_msg USING '' '' 'ZCAT' c_error c_important '402' pi_filename '' '' ''.
RAISE ERROR_IN_LOADING.
ENDIF.
DO.
CLEAR lh_len.
READ DATASET pi_filename INTO lwa_xline-xline ACTUAL LENGTH lh_len.
IF lh_len > 0.
c_conv = cl_abap_conv_in_ce=>create( input = lwa_xline-xline
replacement = space
encoding = '1504' ).
c_conv->read( EXPORTING n = lh_len IMPORTING data = lh_line len = lh_len1 ).
CONCATENATE lh_all_file lh_line INTO lh_all_file RESPECTING BLANKS.
ENDIF.
IF sy-subrc <> 0.
EXIT.
ENDIF.
ENDDO.
FREE c_conv.
REFRESH mt_filelines.
SPLIT lh_all_file AT CL_ABAP_CHAR_UTILITIES=>CR_LF+1(1) INTO TABLE mt_filelines.
CLOSE DATASET pi_filename.
" Парсим
CALL METHOD parse( ).
IF mt_elements[] IS INITIAL.
REFRESH mt_filelines[].
RAISE ERROR_IN_PARSING.
ENDIF.
ENDMETHOD. " METHOD load_pdf_from_server.
" Загружает файл PDF в GUI
METHOD load_pdf_from_gui.
" Загружаем
REFRESH mt_filelines[].
CALL FUNCTION 'GUI_UPLOAD'
EXPORTING
filename = pi_filename
TABLES
data_tab = mt_filelines[]
EXCEPTIONS
file_open_error = 1
file_read_error = 2
no_batch = 3
gui_refuse_filetransfer = 4
invalid_type = 5
no_authority = 6
unknown_error = 7
bad_data_format = 8
header_not_allowed = 9
separator_not_allowed = 10
header_too_long = 11
unknown_dp_error = 12
access_denied = 13
dp_out_of_memory = 14
disk_full = 15
dp_timeout = 16
OTHERS = 17.
IF sy-subrc <> 0.
RAISE ERROR_IN_LOADING.
ENDIF.
" Парсим
CALL METHOD parse( ).
IF mt_elements[] IS INITIAL.
REFRESH mt_filelines[].
RAISE ERROR_IN_PARSING.
ENDIF.
ENDMETHOD. " METHOD load_pdf_from_gui.
" Загружает файл PDF, предварительно загруженный во внутреннюю таблицу
METHOD load_pdf_from_itab.
REFRESH mt_filelines[].
mt_filelines[] = pi_it_filedata[].
IF mt_filelines[] IS INITIAL.
RAISE ERROR_IN_PARSING.
ENDIF.
" Парсим
CALL METHOD parse( ).
IF mt_elements[] IS INITIAL.
REFRESH mt_filelines[].
RAISE ERROR_IN_PARSING.
ENDIF.
ENDMETHOD. " load_pdf_from_itab
" Выполняет парсинг загруженного PDF
METHOD parse.
DATA: lit_str TYPE TABLE OF STRING,
lh_str TYPE STRING,
lwa_element TYPE t_text_element,
lh_line TYPE STRING,
lh_in_obj(1) TYPE C,
lh_in_stream(1) TYPE C,
lh_in_text(1) TYPE C,
lh_font TYPE STRING,
lh_X TYPE I,
lh_Y TYPE I,
lh_len TYPE I,
lh_num TYPE I.
CHECK mt_filelines[] IS NOT INITIAL.
REFRESH mt_elements[].
" Проверяем, что это действительно PDF - это должно быть написано в начале файла
READ TABLE mt_filelines INDEX 1 INTO lh_line.
IF lh_line(4) <> '%PDF'.
RETURN.
ENDIF.
m_num_pages = 1.
LOOP AT mt_filelines INTO lh_line.
CONDENSE lh_line.
SPLIT lh_line AT SPACE INTO TABLE lit_str.
" Если мы не внутри блока текста, отслеживаем его начало
IF lh_in_obj = 'X' AND lh_in_stream = 'X' AND lh_in_text <> 'X'
AND lh_line = 'BT'.
lh_in_text = 'X'.
CLEAR: lh_font, lh_X, lh_Y.
ENDIF.
" Если мы не внутри stream, отслеживаем его начало
IF lh_in_obj = 'X' AND lh_in_stream <> 'X' AND lh_line = 'stream'.
lh_in_stream = 'X'.
ENDIF.
" Если мы не внутри объекта, отслеживаем начало объекта
IF lh_in_obj <> 'X'.
IF LINES( lit_str ) >= 3.
READ TABLE lit_str INDEX 3 INTO lh_str.
IF lh_str = 'obj'.
lh_in_obj = 'X'.
ENDIF.
ENDIF.
ENDIF.
" Отслеживаем начало новой страницы
IF lh_in_obj = 'X' AND lh_line = '<< /Type /Page'.
m_num_pages = m_num_pages + 1.
CLEAR: lh_X, lh_Y.
ENDIF.
" Отслеживаем тип строки текста по последним 2 символам и считываем
IF lh_in_obj = 'X' AND lh_in_stream = 'X' AND lh_in_text = 'X'.
lh_str = lh_line.
lh_len = strlen( lh_str ).
IF lh_len >= 2.
IF lh_len > 2.
lh_len = lh_len - 2.
SHIFT lh_str BY lh_len PLACES LEFT.
ENDIF.
CASE lh_str.
WHEN 'Tf'. " Шрифт [ /F410130 8 Tf
READ TABLE lit_str INDEX 1 INTO lh_font.
WHEN 'Td'. " Позиционирование [ -499 63 Td
" X
READ TABLE lit_str INDEX 1 INTO lh_str.
CALL METHOD try_read_number EXPORTING
pi_number = lh_str
CHANGING
pc_number = lh_num
EXCEPTIONS
INVALID_FORMAT = 1.
IF sy-subrc = 0.
lh_X = lh_X + lh_num.
ENDIF.
" Y
READ TABLE lit_str INDEX 2 INTO lh_str.
CALL METHOD try_read_number EXPORTING
pi_number = lh_str
CHANGING
pc_number = lh_num
EXCEPTIONS
INVALID_FORMAT = 1.
IF sy-subrc = 0.
lh_Y = lh_Y + lh_num.
ENDIF.
WHEN 'Tj'. " Текст [ (AGCO Parts Division) Tj
lh_str = lh_line.
SHIFT lh_str BY 1 PLACES LEFT.
lh_len = strlen( lh_str ).
lh_len = lh_len - 4.
lh_str = lh_str(lh_len).
"SHIFT lh_str RIGHT DELETING TRAILING ') Tj'.
"SHIFT lh_str BY 4 PLACES LEFT.
" Добавляем текстовый элемент
lwa_element-num_page = m_num_pages.
lwa_element-X = lh_X.
lwa_element-Y = lh_Y.
lwa_element-font = lh_font.
lwa_element-text = lh_str.
lwa_element-textUcase = lwa_element-text.
TRANSLATE lwa_element-textUcase TO UPPER CASE.
APPEND lwa_element TO mt_elements.
ENDCASE.
ENDIF.
ENDIF.
" Если мы внутри блока текста, то отслеживаем его окончание
IF lh_in_obj = 'X' AND lh_in_stream = 'X' AND lh_in_text = 'X'
AND lh_line = 'ET'.
CLEAR lh_in_text.
ENDIF.
" Если мы внутри stream, отслеживаем ее окончание
IF lh_in_obj = 'X' AND lh_in_stream = 'X' AND lh_in_text <> 'X'
AND lh_line = 'endstream'.
CLEAR lh_in_stream.
ENDIF.
" Если мы внутри объекта, отслеживаем окончание объекта
IF lh_in_obj = 'X' AND lh_in_stream <> 'X' AND lh_in_text <> 'X'
AND lh_line = 'endobj'.
CLEAR lh_in_obj.
ENDIF.
ENDLOOP.
" Сортируем, чтобы все текстовые надписи шли сверху вниз и слева направо
SORT mt_elements BY num_page y DESCENDING x.
LOOP AT mt_elements INTO lwa_element.
lwa_element-num_element = sy-tabix.
MODIFY mt_elements FROM lwa_element.
ENDLOOP.
m_num_pages = m_num_pages - 1.
IF mt_elements[] IS INITIAL.
m_num_pages = 0.
ENDIF.
ENDMETHOD. " METHOD parse
" Читает число из строки, если неверный формат, то генерит exception
METHOD try_read_number.
DATA: lh_number TYPE STRING.
lh_number = pi_number.
REPLACE ',' IN lh_number WITH '.'.
CATCH SYSTEM-EXCEPTIONS
CONVERSION_ERRORS = 1.
pc_number = lh_number.
ENDCATCH.
IF sy-subrc <> 0.
RAISE INVALID_FORMAT.
ENDIF.
ENDMETHOD. " try_read_number
" Возвращает X если PDF загружен и пусто в противном случае
METHOD is_loaded.
IF mt_elements[] IS NOT INITIAL.
pe_loaded = 'X'.
ELSE.
CLEAR pe_loaded.
ENDIF.
ENDMETHOD. " is_loaded
" Возвращает количество страниц в документе
METHOD get_num_pages.
pe_num_pages = m_num_pages.
ENDMETHOD. " get_num_pages
" Возвращает список всех элементов, отсортированный по страницам, сверху вниз и
" слева направо
METHOD get_all_elements.
pe_elements = mt_elements.
ENDMETHOD. " get_all_elements
" Возвращает элементы, содержащие определенный текст
" pi_num_page - номер страницы, если 0, то искать на всех страницах;
" pi_text - искомый текст;
" pi_match_case - учитывать ли большие-маленькие буквы или искать невзирая на них;
" pe_elements - возвращает список найденных текстовых элементов или пустую таблицу,
" если такой текст не найден.
METHOD find_text.
DATA: lr_page TYPE RANGE OF t_text_element-num_page,
lwa_r_page LIKE LINE OF lr_page,
lr_text TYPE RANGE OF t_text_element-text,
lwa_r_text LIKE LINE OF lr_text,
lr_textUcase TYPE RANGE OF t_text_element-textUcase,
lwa_r_textUcase LIKE LINE OF lr_textUcase,
lwa_element TYPE t_text_element,
lh_text TYPE STRING.
REFRESH pe_elements[].
IF pi_num_page IS NOT INITIAL.
lwa_r_page-option = 'EQ'.
lwa_r_page-sign = 'I'.
lwa_r_page-low = pi_num_page.
APPEND lwa_r_page TO lr_page.
ENDIF.
IF pi_text IS NOT INITIAL.
IF pi_match_case = 'X'.
lwa_r_text-option = 'EQ'.
lwa_r_text-sign = 'I'.
lwa_r_text-low = pi_text.
APPEND lwa_r_text TO lr_text.
ELSE.
lh_text = pi_text.
TRANSLATE lh_text TO UPPER CASE.
lwa_r_textUcase-option = 'EQ'.
lwa_r_textUcase-sign = 'I'.
lwa_r_textUcase-low = lh_text.
APPEND lwa_r_textUcase TO lr_textUcase.
ENDIF.
ENDIF.
LOOP AT mt_elements INTO lwa_element
WHERE num_page IN lr_page
AND text IN lr_text
AND textUcase IN lr_textUcase.
APPEND lwa_element TO pe_elements.
ENDLOOP.
ENDMETHOD. " find_text
" Возвращает элементы, находящиеся под данным элементом, сравнивается левый верхний угол
" элемента с левыми верхними углами других элементов на той же странице.
" pi_element - текстовый элемент, под которым нужно искать;
" pi_accuracy - диапазон поиска влево и вправо от координаты X левого верхнего угла
" элемента при сравнении с Х-координатой других элементов;
" pe_elements - найденные элементы.
METHOD find_text_below.
DATA: lr_page TYPE RANGE OF t_text_element-num_page,
lwa_r_page LIKE LINE OF lr_page,
lr_X TYPE RANGE OF t_text_element-X,
lwa_r_X LIKE LINE OF lr_X,
lr_Y TYPE RANGE OF t_text_element-Y,
lwa_r_Y LIKE LINE OF lr_Y,
lwa_element TYPE t_text_element.
REFRESH pe_elements[].
lwa_r_page-option = 'EQ'.
lwa_r_page-sign = 'I'.
lwa_r_page-low = pi_element-num_page.
APPEND lwa_r_page TO lr_page.
lwa_r_X-option = 'BT'.
lwa_r_X-sign = 'I'.
lwa_r_X-low = pi_element-X - pi_accuracy.
lwa_r_X-high = pi_element-X + pi_accuracy.
APPEND lwa_r_X TO lr_X.
lwa_r_Y-option = 'LT'.
lwa_r_Y-sign = 'I'.
lwa_r_Y-low = pi_element-Y.
APPEND lwa_r_Y TO lr_Y.
LOOP AT mt_elements INTO lwa_element
FROM pi_element-num_element
WHERE num_page IN lr_page
AND X IN lr_X
AND Y IN lr_Y.
APPEND lwa_element TO pe_elements.
ENDLOOP.
ENDMETHOD. " find_text_below
" Возвращает элементы, находящиеся справа от данного элемента,
" сравнивается левый верхний угол элемента с левыми верхними углами других
" элементов на той же странице.
" pi_element - текстовый элемент, справа от которого нужно искать;
" pi_accuracy - диапазон поиска вниз и вверх от координаты Y левого верхнего угла
" элемента при сравнении с Y-координатой других элементов;
" pe_elements - найденные элементы.
METHOD find_text_right.
DATA: lr_page TYPE RANGE OF t_text_element-num_page,
lwa_r_page LIKE LINE OF lr_page,
lr_X TYPE RANGE OF t_text_element-X,
lwa_r_X LIKE LINE OF lr_X,
lr_Y TYPE RANGE OF t_text_element-Y,
lwa_r_Y LIKE LINE OF lr_Y,
lwa_element TYPE t_text_element.
REFRESH pe_elements[].
lwa_r_page-option = 'EQ'.
lwa_r_page-sign = 'I'.
lwa_r_page-low = pi_element-num_page.
APPEND lwa_r_page TO lr_page.
lwa_r_X-option = 'GT'.
lwa_r_X-sign = 'I'.
lwa_r_X-low = pi_element-X.
APPEND lwa_r_X TO lr_X.
lwa_r_Y-option = 'BT'.
lwa_r_Y-sign = 'I'.
lwa_r_Y-low = pi_element-Y - pi_accuracy.
lwa_r_Y-high = pi_element-Y + pi_accuracy.
APPEND lwa_r_Y TO lr_Y.
LOOP AT mt_elements INTO lwa_element
WHERE num_page IN lr_page
AND X IN lr_X
AND Y IN lr_Y.
APPEND lwa_element TO pe_elements.
ENDLOOP.
ENDMETHOD. " find_text_right
" Возвращает элементы, находящиеся внутри прямоугольника на данной странице
METHOD find_text_in_box.
DATA: lr_page TYPE RANGE OF t_text_element-num_page,
lwa_r_page LIKE LINE OF lr_page,
lr_X TYPE RANGE OF t_text_element-X,
lwa_r_X LIKE LINE OF lr_X,
lr_Y TYPE RANGE OF t_text_element-Y,
lwa_r_Y LIKE LINE OF lr_Y,
lwa_element TYPE t_text_element,
lh_left TYPE I,
lh_right TYPE I,
lh_top TYPE I,
lh_bottom TYPE I,
lh_num TYPE I.
REFRESH pe_elements[].
lh_left = pi_left.
lh_top = pi_top.
lh_right = pi_right.
lh_bottom = pi_bottom.
IF lh_left > lh_right.
lh_num = lh_left.
lh_left = lh_right.
lh_right = lh_num.
ENDIF.
IF lh_top > lh_bottom.
lh_num = lh_bottom.
lh_bottom = lh_top.
lh_top = lh_num.
ENDIF.
lwa_r_page-option = 'EQ'.
lwa_r_page-sign = 'I'.
lwa_r_page-low = pi_num_page.
APPEND lwa_r_page TO lr_page.
lwa_r_X-option = 'BT'.
lwa_r_X-sign = 'I'.
lwa_r_X-low = lh_left.
lwa_r_X-high = lh_right.
APPEND lwa_r_X TO lr_X.
lwa_r_Y-option = 'BT'.
lwa_r_Y-sign = 'I'.
lwa_r_Y-low = lh_top.
lwa_r_Y-high = lh_bottom.
APPEND lwa_r_Y TO lr_Y.
LOOP AT mt_elements INTO lwa_element
WHERE num_page IN lr_page
AND X IN lr_X
AND Y IN lr_Y.
APPEND lwa_element TO pe_elements.
ENDLOOP.
ENDMETHOD. " find_text_in_box
ENDCLASS.
Теперь привожу пример чтения PDF
Предположим, у нас счет-фактура. Нужно прочитать номер этого счета-фактуры и его дату.
Номер располагается на первой странице под надписью "No de. Facture", дата располагается также на первой странице под надписью "Date emission".
Код примера:
Code:
PROGRAM ZZP_PDF_PARSER_DEMO.
PERFORM pdf_parser_demo.
INCLUDE ZMM_PDF_PARSER. " В данном инклуде сам парсер, код которого приведен выше
FORM pdf_parser_demo.
DATA: lc_pdf_parser TYPE REF TO lcl_pdf_parser,
lt_elements TYPE TABLE OF t_text_element WITH HEADER LINE,
lt_elements2 TYPE TABLE OF t_text_element WITH HEADER LINE,
l_left TYPE I,
l_top TYPE I,
l_right TYPE I,
l_bottom TYPE I,
l_invnumb TYPE STRING,
l_invdate TYPE STRING,
l_text TYPE STRING.
CREATE OBJECT lc_pdf_parser.
* lc_pdf_parser->load_pdf_from_server( EXPORTING
* pi_filename = 'D:\USR\SAP\PUT\CAT\AG\INBOX\20090619\XA0189872.pdf'
* EXCEPTIONS
* ERROR_IN_LOADING = 1
* ).
* Загрузка файла PDF с локального компьютера, есть также возможность с сервера или из внутренней таблицы
lc_pdf_parser->load_pdf_from_gui( EXPORTING
pi_filename = 'C:\_toarchive\444\sf1.pdf'
EXCEPTIONS
ERROR_IN_LOADING = 1 ).
* Поиск текста на странице. Поиск по маске пока не предусмотрен, но теоретически это несложно доработать,
* но у меня такой необходимости не было
lc_pdf_parser->find_text( EXPORTING
pi_num_page = 1
pi_text = 'No. de facture'
pi_match_case = SPACE
IMPORTING
pe_elements = lt_elements[] ).
IF lt_elements[] IS INITIAL.
MESSAGE 'Файл не является счетом-фактурой от поставщика' TYPE 'I'.
FREE lc_pdf_parser.
RETURN.
ENDIF.
READ TABLE lt_elements INDEX 1.
* Идем текст непосредственно под надписью No. de facture, координаты которой нашли раньше
* pi_accuracy - это диапазон поиска по координате X, например если текст на 2 единицы вправо, то
* визуально он все равно находится под надписью
lc_pdf_parser->find_text_below( EXPORTING
pi_element = lt_elements
pi_accuracy = 10
IMPORTING
pe_elements = lt_elements2[] ).
IF lt_elements2[] IS INITIAL.
MESSAGE 'Неверный формат файла, номер счета-фактуры не найден' TYPE 'I'.
FREE lc_pdf_parser.
RETURN.
ENDIF.
READ TABLE lt_elements2 INDEX 1.
l_invnumb = lt_elements2-text.
* Аналогично ищем другую надпись
lc_pdf_parser->find_text( EXPORTING
pi_num_page = 1
pi_text = 'Date Emission'
pi_match_case = SPACE
IMPORTING
pe_elements = lt_elements[] ).
IF lt_elements[] IS INITIAL.
MESSAGE 'Файл не является счетом-фактурой от поставщика' TYPE 'I'.
FREE lc_pdf_parser.
RETURN.
ENDIF.
* Демонстрация другой возможности - поиска всех текстов в прямоугольнике
* Координаты: слева направо идет возрастание X, а снизу вверх возрастание Y (а не наоборот),
* как в школе по математике. Точка (0,0) располагается в левом нижнем углу
READ TABLE lt_elements INDEX 1.
l_left = lt_elements-X - 10.
l_top = lt_elements-Y - 3.
l_right = lt_elements-X + 10.
l_bottom = lt_elements-Y - 30.
lc_pdf_parser->find_text_in_box( EXPORTING
pi_num_page = 1
pi_left = l_left
pi_top = l_top
pi_right = l_right
pi_bottom = l_bottom
IMPORTING
pe_elements = lt_elements2[] ).
IF lt_elements2[] IS INITIAL.
MESSAGE 'Неверный формат файла, дата счета-фактуры не найдена' TYPE 'I'.
FREE lc_pdf_parser.
RETURN.
ENDIF.
READ TABLE lt_elements2 INDEX 1.
l_invdate = lt_elements2-text.
CONCATENATE 'Файл PDF успешно прочитан, № СФ =' l_invnumb ', дата СФ =' l_invdate INTO l_text SEPARATED BY SPACE.
MESSAGE l_text TYPE 'I'.
FREE lc_pdf_parser.
ENDFORM. " pdf_parser_demo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment