Compare commits

...
Author SHA1 Message Date
deiflaender e178393c23 Fixed paragraph recognition 2020-11-16 16:00:45 +01:00
deiflaender f75aff5186 RED-692: Added matchedRule to RedactionLog 2020-11-16 11:55:58 +01:00
deiflaender 85d73dae47 RED-632: Set status DECLINED in RedactionLog for manual removals that are DECLINED 2020-11-12 16:14:45 +01:00
Dominique Eiflaender 1792eb554e Pull request #66: RED-629: Each annotation is one entry in the RedactionLog
Merge in RED/redaction-service from RED-629 to master

* commit '936683f94dd089d47b267cadb606c42b9e8b5515':
  RED-629: Each annotation is one entry in the RedactionLog
2020-11-11 15:39:08 +01:00
deiflaender 936683f94d RED-629: Each annotation is one entry in the RedactionLog 2020-11-11 15:29:28 +01:00
Dominique Eiflaender d2161e1b3f Pull request #65: RED-419: Avoid duplicate entries
Merge in RED/redaction-service from RED-419 to master

* commit 'efe49ac2c1ae8a27feb6e07c1b4614619a0b2ece':
  RED-419: Avoid duplicate entries
2020-11-05 12:34:20 +01:00
deiflaender efe49ac2c1 RED-419: Avoid duplicate entries 2020-11-05 12:27:24 +01:00
Dominique Eiflaender 61352b565d Pull request #64: RED-515: Added manualRedactionType to redactionLog, to know if it must added or not in pdftron-redaction-service if it is requested
Merge in RED/redaction-service from RED-515 to master

* commit 'c6d3e3a4cf8f34bd2f129e1ae15ce9b142fc284c':
  RED-515: Added manualRedactionType to redactionLog, to know if it must added or not in pdftron-redaction-service if it is requested
2020-11-04 15:54:20 +01:00
deiflaender c6d3e3a4cf RED-515: Added manualRedactionType to redactionLog, to know if it must added or not in pdftron-redaction-service if it is requested 2020-11-04 15:47:00 +01:00
Thierry Goeckel c650361ec2 Pull request #63: Fix sponsor companies rule and add corresponding test
Merge in RED/redaction-service from RED-473 to master

* commit '122d5a556e35b961ba2199280f6836221c6f768c':
  Bump conf service version and remove obsolete object init
  Fix test
  Fix rule in drools file, too
  Fix sponsor companies rule and add corresponding test
2020-11-04 11:59:15 +01:00
Thierry Göckel 122d5a556e Bump conf service version and remove obsolete object init 2020-11-04 11:49:13 +01:00
Thierry Göckel 355cb16679 Fix test 2020-11-03 21:10:17 +01:00
Thierry Göckel c285c384ce Fix rule in drools file, too 2020-11-03 20:48:23 +01:00
Thierry Göckel 02052fbb6a Fix sponsor companies rule and add corresponding test 2020-11-03 20:47:11 +01:00
Dominique Eiflaender 329ad98a21 Pull request #62: Temprorary exclude WebMvcMetricsAutoConfiguration till UNDERTOW-1743 is fixed
Merge in RED/redaction-service from ExcludeMetrics to master

* commit '843b32e68feae46f306c5e122ebfd98e6531fc86':
  Temprorary exclude WebMvcMetricsAutoConfiguration till UNDERTOW-1743 is fixed
2020-11-03 10:18:43 +01:00
deiflaender 843b32e68f Temprorary exclude WebMvcMetricsAutoConfiguration till UNDERTOW-1743 is fixed 2020-11-03 09:57:00 +01:00
Dominique Eiflaender 3371b825ef Pull request #61: Removed id prefix
Merge in RED/redaction-service from TimoTest to master

* commit '6e83040a50728c491667bdbe211636d245e0cf6a':
  Removed id prefix
2020-11-03 09:15:28 +01:00
deiflaender 6e83040a50 Removed id prefix 2020-10-30 16:18:44 +01:00
Dominique Eiflaender de66d2fda8 Pull request #60: RED-510: Do not add annotations for manual redactions that are approved and should be already in a dictionary
Merge in RED/redaction-service from RED-510 to master

* commit '1b2bd7fa209cba7e635bbaad7962396d931576ff':
  RED-510: Do not add annotations for manual redactions that are approved and should be already in a dictionary
2020-10-29 15:37:37 +01:00
deiflaender 1b2bd7fa20 RED-510: Do not add annotations for manual redactions that are approved and should be already in a dictionary 2020-10-29 15:16:30 +01:00
Dominique Eiflaender 013353d6ef Pull request #59: RED-499: Changed prefix for redactions, fixed legalbasis problem
Merge in RED/redaction-service from RED-499 to master

* commit '0015304eea9564d76765fcb1a76d2b6a293c1ebc':
  RED-499: Changed prefix for redactions, fixed legalbasis problem
2020-10-28 13:55:41 +01:00
deiflaender 0015304eea RED-499: Changed prefix for redactions, fixed legalbasis problem 2020-10-28 13:02:46 +01:00
Dominique Eiflaender 97257a9c08 Pull request #58: RED-427: Added possibility to add comments to all annotation
Merge in RED/redaction-service from RED-427 to master

* commit '612d67301bbf186f5144c0243f9a37fe21d3646c':
  RED-427: Added possibility to add comments to all annotation
2020-10-27 13:02:50 +01:00
deiflaender 612d67301b RED-427: Added possibility to add comments to all annotation 2020-10-27 12:32:11 +01:00
Thierry Goeckel 6986b14713 Pull request #57: RED-451 Legal Basis
Merge in RED/redaction-service from RED-451 to master

* commit '8be59e4ddf2432f47a57d50e2743886c56af3652':
  Add legal basis to redaction log entry
  Integrate legal basis to rules, redaction annotation and tests
2020-10-27 12:00:59 +01:00
Thierry Göckel 8be59e4ddf Add legal basis to redaction log entry 2020-10-26 15:03:21 +01:00
Thierry Göckel f0f748db1b Integrate legal basis to rules, redaction annotation and tests 2020-10-26 10:07:41 +01:00
Thierry Goeckel 04f0c29a49 Pull request #56: Streamline actuator settings
Merge in RED/redaction-service from actuator-settings to master

* commit 'b5192b7424745360dc2206b45ec7f7667b867e27':
  Streamline actuator settings
2020-10-22 09:43:11 +02:00
Thierry Göckel b5192b7424 Streamline actuator settings 2020-10-21 17:50:33 +02:00
Dominique Eiflaender 268dd0200e Pull request #55: RED-413: Added user to manual redactions, approved flag to Status.APPROVED
Merge in RED/redaction-service from RED-413 to master

* commit '638c4f9c65d35406bba1fd33e20c3bae8a6e6a13':
  RED-413: Added user to manual redactions, approved flag to Status.APPROVED
2020-10-12 12:32:30 +02:00
deiflaender 638c4f9c65 RED-413: Added user to manual redactions, approved flag to Status.APPROVED 2020-10-12 12:21:18 +02:00
Dominique Eiflaender 8f12695f16 Pull request #54: RED-414: Ids for manual redactions and comments must be created in file-management-service
Merge in RED/redaction-service from RED-414 to master

* commit '79db6e4bf9d821fe4716a6da31d3f6d6fcaff05b':
  RED-414: Ids for manual redactions and comments must be created in file-management-service
2020-10-09 12:17:38 +02:00
deiflaender 79db6e4bf9 RED-414: Ids for manual redactions and comments must be created in file-management-service 2020-10-09 12:06:19 +02:00
Dominique Eiflaender c8eac2ca3d Pull request #53: RED-240: Use HexColors instead of float[], RED-380: Load all system colors from dictionary-service
Merge in RED/redaction-service from RED-240 to master

* commit 'a01651b8278ed09f9409d6ec2e1615680bc846cb':
  RED-240: Use HexColors instead of float[], RED-380: Load all system colors from dictionary-service
2020-10-08 12:05:42 +02:00
deiflaender a01651b827 RED-240: Use HexColors instead of float[], RED-380: Load all system colors from dictionary-service 2020-10-08 11:51:35 +02:00
Dominique Eiflaender 943d40366e Pull request #52: RED-391: Support comments and approval of manual redactions, RED-385: Improve annotation IDs in PDFs
Merge in RED/redaction-service from RED-391 to master

* commit '365158a0084b2034aca91e6f77eb9df42bbcb246':
  RED-391: Support comments and approval of manual redactions, RED-385: Improve annotation IDs in PDFs
2020-10-05 15:36:06 +02:00
deiflaender 365158a008 RED-391: Support comments and approval of manual redactions, RED-385: Improve annotation IDs in PDFs 2020-10-05 15:14:23 +02:00
Thierry Goeckel e76b893c8d Pull request #51: RED-104
Merge in RED/redaction-service from RED-104 to master

* commit 'fa29f56d98d05fd348dd27e3bbb1098851c238bd':
  Test line break fix
2020-10-05 11:02:56 +02:00
Thierry Goeckel d91612a69a Pull request #50: Make sure section and table char indices match
Merge in RED/redaction-service from RED-381 to master

* commit '9a965dd68a72285af6bf6a28ec6aa67358c35dba':
  Make sure section and table char indices match
2020-10-05 11:02:44 +02:00
Thierry Göckel fa29f56d98 Test line break fix 2020-10-02 18:11:58 +02:00
Thierry Göckel 9a965dd68a Make sure section and table char indices match 2020-10-02 17:54:31 +02:00
Dominique Eiflaender 0f4838e60d Pull request #49: RED-373: Fixed multiline annotations view in pdftron
Merge in RED/redaction-service from RED-373-2 to master

* commit 'a4e012762e1adda234decc1137f221cfd70498d3':
  RED-373: Fixed multiline annotations view in pdftron
2020-10-01 15:31:25 +02:00
deiflaender a4e012762e RED-373: Fixed multiline annotations view in pdftron 2020-10-01 15:19:57 +02:00
Dominique Eiflaender 0c1d8ba33d Pull request #48: RED-372: Suffix cross page annotations with -1, -2, etc., to avoid duplicate annotation ids
Merge in RED/redaction-service from RED-372 to master

* commit 'ecdfd29803e09ac57cc74b19408db54d9a8247a7':
  RED-372: Suffix cross page annotations with -1, -2, etc., to avoid duplicate annotation ids
2020-10-01 13:01:24 +02:00
deiflaender ecdfd29803 RED-372: Suffix cross page annotations with -1, -2, etc., to avoid duplicate annotation ids 2020-10-01 12:48:44 +02:00
Dominique Eiflaender 107dbe1910 Pull request #47: RED-373: Use a single multiline annotation instead of seperate annotations per line for one Entity
Merge in RED/redaction-service from RED-373 to master

* commit 'dd02296face3c32bf7675301cf9969103e65f52f':
  Changed formating
  RED-373: Use a single multiline annotation instead of seperate annotations per line for one Entity
2020-10-01 12:47:58 +02:00
deiflaender dd02296fac Changed formating 2020-10-01 11:55:38 +02:00
Thierry Goeckel 589458a112 Pull request #46: Add rule redacting sponsor companies if preceded by prefix
Merge in RED/redaction-service from RED-301 to master

* commit '6a8f3665198ec2d7452ed0dac43b9a7d220c8bc5':
  Remove entries from must_redact dict, add test and refactor rules
  Add rule redacting sponsor companies if preceded by prefix
2020-10-01 11:44:41 +02:00
deiflaender ce22121802 RED-373: Use a single multiline annotation instead of seperate annotations per line for one Entity 2020-10-01 11:15:28 +02:00
Thierry Göckel 6a8f366519 Remove entries from must_redact dict, add test and refactor rules 2020-09-30 16:08:56 +02:00
Thierry Göckel cec5fd3d5e Add rule redacting sponsor companies if preceded by prefix 2020-09-30 14:13:17 +02:00
Dominique Eiflaender 99bde2956f Pull request #45: RED-277: Upgraded to pdfbox 2.0.21
Merge in RED/redaction-service from RED-277 to master

* commit 'ecc303e79e82ed698f1c8bdd37dbd521335428bc':
  RED-277: Upgraded to pdfbox 2.0.21
2020-09-29 16:17:32 +02:00
deiflaender ecc303e79e RED-277: Upgraded to pdfbox 2.0.21 2020-09-29 15:18:58 +02:00
Dominique Eiflaender 384cd1d0dc Pull request #44: Fix annotating single letter entities
Merge in RED/redaction-service from bugfix/single-letter-annotations to master

* commit '8e5ff318106e700e28abd14a0eb1337d3a44c109':
  Fix annotating single letter entities
2020-09-29 15:17:51 +02:00
Thierry Göckel 8e5ff31810 Fix annotating single letter entities 2020-09-29 15:10:52 +02:00
Dominique Eiflaender 8b1574845c Pull request #43: RED-264: Avoid phantom cells, by merging line till rounded value of biggest char height/width
Merge in RED/redaction-service from RED-264 to master

* commit 'e1adcfb02c4c7d46140884b3d38f8daf1e1aae92':
  Added missing unittest
  RED-264: Avoid phantom cells, by merging line till rounded value of biggest char height/width
2020-09-29 15:04:20 +02:00
deiflaender e1adcfb02c Added missing unittest 2020-09-29 14:54:35 +02:00
deiflaender 11164219c5 RED-264: Avoid phantom cells, by merging line till rounded value of biggest char height/width 2020-09-29 14:21:42 +02:00
Thierry Goeckel c9516205fe Pull request #42: RED-318
Merge in RED/redaction-service from RED-318 to master

* commit 'f5790bccab3fb8dce66aeb9fbe501655ae6b86c3':
  Log entries and manual redaction require one position per line
  Annotate line by line
  Refactor multiple positions to one
  Merge rectangles for single annotation/redaction log entry
2020-09-29 14:15:21 +02:00
Thierry Göckel f5790bccab Log entries and manual redaction require one position per line 2020-09-29 13:54:32 +02:00
Thierry Göckel 74e63ca292 Annotate line by line 2020-09-29 13:43:16 +02:00
Thierry Göckel 6638f0cf9e Refactor multiple positions to one 2020-09-28 17:47:55 +02:00
Thierry Göckel 5448153096 Merge rectangles for single annotation/redaction log entry 2020-09-28 17:31:40 +02:00
Dominique Eiflaender 03274ba1ab Pull request #41: RED-299: Redact complete Author(s) Field in Vertebrate study tables
Merge in RED/redaction-service from RED-299 to master

* commit '664b9b420605cb1adf1113b34c52a45e8024dbbc':
  RED-299: Redact complete Author(s) Field in Vertebrate study tables
2020-09-23 15:34:46 +02:00
deiflaender 664b9b4206 RED-299: Redact complete Author(s) Field in Vertebrate study tables 2020-09-23 15:20:52 +02:00
Thierry Goeckel 564d74e39d Pull request #40: Test modified rules 8 and 10
Merge in RED/redaction-service from RED-288 to master

* commit 'cfb7554c618b9482f529b8183bc0f1f95df7d00f':
  Test modified rules 8 and 9
2020-09-23 11:44:35 +02:00
Thierry Göckel cfb7554c61 Test modified rules 8 and 9 2020-09-22 21:36:05 +02:00
Christoph  Schabert ee40064b32 Pull request #39: RED-316: Switch to new red base image
Merge in RED/redaction-service from RED-316 to master

* commit 'f2fafc3db2ab8825eb4a2136e8eab28a12dbc2e6':
  Switch to rootless base-image
  Upgrade bamboo-specs to 7.1.2
2020-09-16 08:59:45 +02:00
Christoph  Schabert f2fafc3db2 Switch to rootless base-image 2020-09-15 16:02:08 +02:00
Christoph  Schabert d5b0638f11 Upgrade bamboo-specs to 7.1.2 2020-09-15 16:01:26 +02:00
Dominique Eiflaender 130cac0b77 Pull request #38: RED-293: Add unkown Textblocks to paragraphs
Merge in RED/redaction-service from RED-293 to master

* commit 'de310e8a654b601cb4bf258675904a25d4233783':
  RED-293: Add unkown Textblocks to paragraphs
2020-09-14 14:39:36 +02:00
deiflaender de310e8a65 RED-293: Add unkown Textblocks to paragraphs 2020-09-14 13:52:16 +02:00
Dominique Eiflaender a0a78440d8 Pull request #37: RED-305: Added ids to manual redaction entries, RED-306: set redacet to false in redactionLog for manual deletions
Merge in RED/redaction-service from RED-305 to master

* commit 'e52a54b0a3eb8491ed3e79b3f514364a5cf22f76':
  RED-305: Added ids to manual redaction entries, RED-306: set redacet to false in redactionLog for manual deletions
2020-09-11 11:18:42 +02:00
deiflaender e52a54b0a3 RED-305: Added ids to manual redaction entries, RED-306: set redacet to false in redactionLog for manual deletions 2020-09-11 11:10:10 +02:00
deiflaender 65a29fb3a5 Added new files and missing dictionary entries 2020-09-07 16:06:25 +02:00
Dominique Eiflaender ff0a73c635 Pull request #36: Added filename to redactionLog
Merge in RED/redaction-service from filename to master

* commit 'c27c9b2f5902d66f654982d7e5f005ecc176e01b':
  Added missing constuctor for RedactionLog
  Added filename to redactionLog
2020-09-02 15:22:34 +02:00
deiflaender c27c9b2f59 Added missing constuctor for RedactionLog 2020-09-02 14:56:48 +02:00
deiflaender d95ff5e5cf Added filename to redactionLog 2020-09-02 14:47:58 +02:00
Thierry Goeckel 34e058c4e4 Pull request #35: Fix redaction in single cell tables
Merge in RED/redaction-service from RED-279 to master

* commit '3d5455d7297e5805ad30a578d00d6d1737820c7d':
  Fix redaction in single cell tables
2020-09-02 14:39:08 +02:00
Thierry Göckel 3d5455d729 Fix redaction in single cell tables 2020-09-02 14:31:38 +02:00
Thierry Goeckel b07ebf78d2 Pull request #34: RED-267: Return dictionary and rules version in redactionLog
Merge in RED/redaction-service from RED-267 to master

* commit 'b5fa771c285554132edc32e1df59836fd7748b9a':
  RED-267: Return dictionary and rules version in redactionLog
2020-09-02 08:54:43 +02:00
deiflaender b5fa771c28 RED-267: Return dictionary and rules version in redactionLog 2020-09-02 08:47:05 +02:00
Thierry Goeckel 81ea2e91ef Pull request #33: Fix entity span in table rows and detection of headers in rotated tables
Merge in RED/redaction-service from bugfix/rowspan-and-header-in-rotated-table-fix to master

* commit '4954aafed78e06484531d6264bdf215176ee0ef2':
  Reduce log level.
  Remove unused import
  Adjust test to added rule and fix vertical header propagation for row > 2
  Fix entity span in table rows and detection of headers in rotated tables
2020-08-25 17:30:57 +02:00
Thierry Göckel 4954aafed7 Reduce log level. 2020-08-25 17:21:56 +02:00
Thierry Göckel 0baf48698d Remove unused import 2020-08-25 15:45:24 +02:00
Thierry Göckel 3c590dcf1d Adjust test to added rule and fix vertical header propagation for row >
2
2020-08-25 15:41:44 +02:00
Thierry Göckel 848c506c3f Fix entity span in table rows and detection of headers in rotated tables 2020-08-25 13:50:34 +02:00
Thierry Goeckel 6483e637c6 Pull request #32: Remove '-' in headernames
Merge in RED/redaction-service from tablesHeader3 to master

* commit '272c7cc2287b146664b8bb277c3731262ac78a19':
  Remove '-' in headernames
2020-08-24 16:15:44 +02:00
deiflaender 272c7cc228 Remove '-' in headernames 2020-08-24 15:39:50 +02:00
Thierry Goeckel 5586531c33 Pull request #31: Made rules for table more stabil and flexible
Merge in RED/redaction-service from tablesRules2 to master

* commit '559c42154276ea861c09cf29b71d9bb9c17e6e87':
  Made rules for table more stabil and flexible
2020-08-24 14:52:29 +02:00
deiflaender 559c421542 Made rules for table more stabil and flexible 2020-08-24 14:45:30 +02:00
Dominique Eiflaender d026f4e1db Pull request #30: Fix test and logic of same page table merge for one row tables
Merge in RED/redaction-service from bugfix/fix-test-table-merge to master

* commit '38ddb1d9c8dbf0e83d40ec916f1ebeb6d0f86f42':
  Fix test and logic of same page table merge for one row tables
2020-08-24 14:22:41 +02:00
Thierry Göckel 38ddb1d9c8 Fix test and logic of same page table merge for one row tables 2020-08-24 14:06:34 +02:00
Thierry Goeckel 92f0af9d21 Pull request #28: Highlight N in vertebrate study tables and fixed rule order
Merge in RED/redaction-service from tableRules to master

* commit 'afa26895c8e78f261ec3700c515f412d8cf605ce':
  Fixed test problem
  Fixed pmd
  Highlight N in vertebrate study tables and fixed rule order
2020-08-24 13:15:11 +02:00
Dominique Eiflaender b24922aa60 Pull request #29: Fix multi-page table merge
Merge in RED/redaction-service from bugfix/multi-page-table-merge to master

* commit 'baa703928f50e53a66f925fcf72082df188482a1':
  Fix multi-page table merge
2020-08-24 13:09:43 +02:00
deiflaender afa26895c8 Fixed test problem 2020-08-24 13:05:23 +02:00
Thierry Göckel baa703928f Fix multi-page table merge 2020-08-24 13:03:03 +02:00
deiflaender 85667c91e4 Fixed pmd 2020-08-24 12:36:49 +02:00
deiflaender 8a471d0771 Highlight N in vertebrate study tables and fixed rule order 2020-08-24 12:24:48 +02:00
Dominique Eiflaender bb1112d0d7 Pull request #27: Fix merging of tables with single header row table
Merge in RED/redaction-service from bugfix/merge-single-row-tables to master

* commit '00a960ee2308e7ed7da23ebd3e85f5ef9db14610':
  Fix merging of tables with single header row table
2020-08-24 12:10:53 +02:00
Thierry Göckel 00a960ee23 Fix merging of tables with single header row table 2020-08-24 11:43:19 +02:00
Dominique Eiflaender 8c08bb3664 Pull request #25: RED-101 & RED-102: Make table structure detection more robust
Merge in RED/redaction-service from bugfix/RED-101 to master

* commit '76369f13f8d41154f3dd3690af81b9567ca1133e':
  Remove redundant comment
  RED-101: Add features as requested in PR
  Add unit test for table structure requirements
  Make table structure detection more robust
2020-08-24 11:09:17 +02:00
Cheng Zhu 14b4f4ab8a Pull request #26: Feature/RED-116: replace gin4-commons with red-commons
Merge in RED/redaction-service from feature/RED-116 to master

* commit '4206e708a20a862083f03d27a992e8661c44abc2':
  RED-116: replace gin4-commons with red-commons
  RED-116: replace gin4-commons with red-commons
2020-08-20 17:22:36 +02:00
cheng 4206e708a2 RED-116: replace gin4-commons with red-commons 2020-08-20 16:52:56 +02:00
cheng c4164b57fb RED-116: replace gin4-commons with red-commons 2020-08-20 16:51:02 +02:00
Thierry Göckel 76369f13f8 Remove redundant comment 2020-08-18 20:53:41 +02:00
Thierry Göckel 1fff4f7eb0 RED-101: Add features as requested in PR
- Add rule for non-vertebrate studies
- Don't highlight ambiguous cell values
- Rewrite rules in more human-readable fashion
2020-08-18 20:35:40 +02:00
Thierry Göckel c7f5b4a280 Add unit test for table structure requirements 2020-08-18 20:35:17 +02:00
deiflaender 89ef8234d2 Added missing NoArgsConstructor to RedactionLogEntry, needed for Json Serialization 2020-08-14 11:21:11 +02:00
Thierry Göckel 4236fa05cc Make table structure detection more robust 2020-08-13 17:48:37 +02:00
Thierry Goeckel 031f10435d Pull request #24: RED-88: Enabled manual redaction
Merge in RED/redaction-service from RED-88 to master

* commit '5d286f58e918b22f041ae54f982f14b2d4fcefa6':
  PR Improvements
  redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactions.java online editiert mit Bitbucket
  redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactions.java online editiert mit Bitbucket
  RED-88: Enabled manual redaction
2020-08-13 16:20:53 +02:00
deiflaender 5d286f58e9 PR Improvements 2020-08-13 16:10:22 +02:00
Dominique Eiflaender 5d18a46ec3 redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactions.java online editiert mit Bitbucket 2020-08-13 16:07:11 +02:00
Dominique Eiflaender 9b61971132 redaction-service-v1/redaction-service-api-v1/src/main/java/com/iqser/red/service/redaction/v1/model/ManualRedactions.java online editiert mit Bitbucket 2020-08-13 16:07:05 +02:00
deiflaender 2b93ae57d5 RED-88: Enabled manual redaction 2020-08-13 15:34:09 +02:00
Thierry Goeckel 954765759c Pull request #23: Log warning message if tabular data mismatches
Merge in RED/redaction-service from RED-101-quickfix to master

* commit '17aabcd09c1f76bdd467f66aac0cf243c0625734':
  Fix index out of bounds exception
  Remove redundant warn message
  Fix NPE for empty cells
  Add test redacting all files and expecting no exception
  Log warning message if tabular data mismatches
2020-08-13 11:33:17 +02:00
Thierry Göckel 17aabcd09c Fix index out of bounds exception 2020-08-13 11:13:01 +02:00
Thierry Göckel 32aa500983 Remove redundant warn message 2020-08-13 11:04:56 +02:00
Thierry Göckel a151a13b4c Fix NPE for empty cells 2020-08-13 11:02:09 +02:00
Thierry Göckel 5542a97a38 Add test redacting all files and expecting no exception 2020-08-13 10:14:49 +02:00
Thierry Göckel 8edaa93bda Log warning message if tabular data mismatches 2020-08-13 10:07:53 +02:00
Dominique Eiflaender 98e0bf5606 Pull request #22: RED-242: Return sectionNumber for tests
Merge in RED/redaction-service from RED-242 to master

* commit '8344b7ccafcf98e7376c53a0a79708c51daa6d0a':
  RED-242: Return sectionNumber for tests
2020-08-12 15:51:50 +02:00
deiflaender 8344b7ccaf RED-242: Return sectionNumber for tests 2020-08-12 15:46:33 +02:00
Dominique Eiflaender 96ba93f774 Pull request #20: RED-101
Merge in RED/redaction-service from RED-101 to master

* commit 'c93ca745fc61fc2d7f1a1f474a4e3c464091e70d':
  Normalize header information
  Fix test and suppress checkstyle warnings
  Fix PMD errors
  RED-101: Detect vertebrate study row value
  RED-101: Implement table cell and row redaction
  Fix style
2020-08-11 13:00:39 +02:00
Thierry Göckel c93ca745fc Normalize header information 2020-08-11 10:24:33 +02:00
Thierry Göckel a6415363cd Fix test and suppress checkstyle warnings 2020-08-10 19:04:49 +02:00
Thierry Göckel d97a7b629a Fix PMD errors 2020-08-10 19:04:49 +02:00
Thierry Göckel 00c96c6f57 RED-101: Detect vertebrate study row value 2020-08-10 19:04:49 +02:00
Thierry Göckel 06630b09d2 RED-101: Implement table cell and row redaction 2020-08-10 19:04:49 +02:00
Thierry Göckel 695564d162 Fix style
Fix style.

Fix style.

Fix style and naming

Fix style, naming and field modifier

Fix style and remove warning suppression
2020-08-10 19:04:49 +02:00
Cheng Zhu 81048dcc9f Pull request #21: Cleaned up dictionaries
Merge in RED/redaction-service from cleanDictionaries to master

* commit 'b5412dc9590d15dcae9f87f7fc9b7ea50e4c63ae':
  Cleaned up dictionaries
2020-08-10 15:46:37 +02:00
deiflaender b5412dc959 Cleaned up dictionaries 2020-08-10 15:16:05 +02:00
Dominique Eiflaender 40e40a01ad Pull request #19: Avoid duplicate redaction if type have same entries, made Applicant and Producer rules more specific
Merge in RED/redaction-service from ApplicantRule to master

* commit '99bac4550a9cade36de313735916687ea26d7b4d':
  Use @EqualsAndHashCode(onlyExplicitlyIncluded = true) in Entity.java
  Avoid duplicate redaction if type have same entries, made Applicant and Producer rules more specific
2020-08-07 12:31:26 +02:00
deiflaender 99bac4550a Use @EqualsAndHashCode(onlyExplicitlyIncluded = true) in Entity.java 2020-08-07 12:28:21 +02:00
deiflaender 7d0b0ed3d0 Avoid duplicate redaction if type have same entries, made Applicant and Producer rules more specific 2020-08-07 12:09:37 +02:00
Cheng Zhu d465a4ba5b Pull request #18: RED-149: Added must_redact dictionary and Rule, Adjusted rules for applicant and producer to work on all documents.
Merge in RED/redaction-service from RED-149 to master

* commit 'cce8200d433ec89160af3af32f40be57c0b67678':
  redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java online editiert mit Bitbucket
  RED-149: Added must_redact dictionary and Rule, Adjusted rules for applicant and producer to work on all documents. Fixed endless loop in rules. Detect multiple occurences in rules
2020-08-05 13:21:14 +02:00
Dominique Eiflaender cce8200d43 redaction-service-v1/redaction-service-server-v1/src/main/java/com/iqser/red/service/redaction/v1/server/redaction/model/Section.java online editiert mit Bitbucket 2020-08-05 13:15:02 +02:00
deiflaender 1f8d371a82 RED-149: Added must_redact dictionary and Rule, Adjusted rules for applicant and producer to work on all documents. Fixed endless loop in rules. Detect multiple occurences in rules 2020-08-05 13:10:31 +02:00
Thierry Goeckel 70804f111d Pull request #17: Duplicates
Merge in RED/redaction-service from duplicates to master

* commit '81723ce4022e1e006054eb743f2cc2c0b9faf14f':
  Use void method type
  Use EqualsAndHashcode annotation from Lombok
  Fixed duplicated redaction/RedactionLog entries
  RED-211, RED-215 Added dictionaries and rules for testing.
2020-08-04 10:59:38 +02:00
Thierry Göckel 81723ce402 Use void method type 2020-08-04 10:34:54 +02:00
deiflaender b1a266d4d4 Use EqualsAndHashcode annotation from Lombok 2020-08-04 09:53:58 +02:00
deiflaender d2d7f8c50c Fixed duplicated redaction/RedactionLog entries 2020-07-31 16:25:10 +02:00
deiflaender e2895a1c7a RED-211, RED-215 Added dictionaries and rules for testing. 2020-07-31 16:22:47 +02:00
Lena  Maldacker cd07dc6a44 Pull request #16: Use default color from configuration-service on unknown type
Merge in RED/redaction-service from defaultColor to master

* commit '872c384dc6da60f421e7aa21f58b574c49414c81':
  Use default color from configuration-service on unknown type
2020-07-28 12:41:35 +02:00
86 changed files with 27601 additions and 4184 deletions
+2 -2
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>com.atlassian.bamboo</groupId>
<artifactId>bamboo-specs-parent</artifactId>
<version>7.0.4</version>
<version>7.1.2</version>
<relativePath/>
</parent>
@@ -34,4 +34,4 @@
<!-- run 'mvn test' to perform offline validation of the plan -->
<!-- run 'mvn -Ppublish-specs' to upload the plan to your Bamboo server -->
</project>
</project>
@@ -1,4 +1,4 @@
FROM gin5/platform-base:5.2.0
FROM red/base-image:1.0.0
ARG PLATFORM_JAR
@@ -6,4 +6,4 @@ ENV PLATFORM_JAR ${PLATFORM_JAR}
ENV USES_ELASTICSEARCH false
COPY ["${PLATFORM_JAR}", "/"]
COPY ["${PLATFORM_JAR}", "/"]
+13 -13
View File
@@ -5,7 +5,7 @@
<parent>
<artifactId>platform-dependency</artifactId>
<groupId>com.iqser.red</groupId>
<version>1.0.1</version>
<version>1.0.2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -22,7 +22,7 @@
</modules>
<properties>
<pdfbox.version>2.0.16</pdfbox.version>
<pdfbox.version>2.0.21</pdfbox.version>
</properties>
@@ -32,21 +32,21 @@
<dependency>
<groupId>com.iqser.red</groupId>
<artifactId>platform-commons-dependency</artifactId>
<version>1.0.0</version>
<version>1.1.0</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>${pdfbox.version}</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox-tools</artifactId>
<version>${pdfbox.version}</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>${pdfbox.version}</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox-tools</artifactId>
<version>${pdfbox.version}</version>
</dependency>
</dependencies>
@@ -0,0 +1,21 @@
package com.iqser.red.service.redaction.v1.model;
import java.time.OffsetDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Comment {
private String id;
private OffsetDateTime date;
private String text;
private String user;
}
@@ -0,0 +1,19 @@
package com.iqser.red.service.redaction.v1.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class IdRemoval {
private String id;
private String user;
private Status status;
private boolean removeFromDictionary;
}
@@ -0,0 +1,30 @@
package com.iqser.red.service.redaction.v1.model;
import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ManualRedactionEntry {
private String id;
private String user;
private String type;
private String value;
private String reason;
private String legalBasis;
private List<Rectangle> positions = new ArrayList<>();
private Status status;
private boolean addToDictionary;
private String section;
private int sectionNumber;
}
@@ -0,0 +1,5 @@
package com.iqser.red.service.redaction.v1.model;
public enum ManualRedactionType {
ADD, REMOVE
}
@@ -0,0 +1,29 @@
package com.iqser.red.service.redaction.v1.model;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ManualRedactions {
@Builder.Default
private Set<IdRemoval> idsToRemove = new HashSet<>();
@Builder.Default
private Set<ManualRedactionEntry> entriesToAdd = new HashSet<>();
@Builder.Default
private Map<String, List<Comment>> comments = new HashMap<>();
}
@@ -11,6 +11,19 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
public class RedactionLog {
public RedactionLog(List<RedactionLogEntry> redactionLogEntry, long dictionaryVersion, long rulesVersion) {
this.redactionLogEntry = redactionLogEntry;
this.dictionaryVersion = dictionaryVersion;
this.rulesVersion = rulesVersion;
}
private List<RedactionLogEntry> redactionLogEntry;
private long dictionaryVersion = -1;
private long rulesVersion = -1;
private String filename;
}
@@ -3,19 +3,33 @@ package com.iqser.red.service.redaction.v1.model;
import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RedactionLogEntry {
private String id;
private String type;
private String value;
private String reason;
private int matchedRule;
private String legalBasis;
private boolean redacted;
private boolean isHint;
private String section;
private float[] color;
@Builder.Default
private List<Rectangle> positions = new ArrayList<>();
private int sectionNumber;
private boolean manual;
private Status status;
private ManualRedactionType manualRedactionType;
}
@@ -13,4 +13,5 @@ public class RedactionRequest {
private byte[] document;
private boolean flatRedaction;
private ManualRedactions manualRedactions;
}
@@ -0,0 +1,5 @@
package com.iqser.red.service.redaction.v1.model;
public enum Status {
REQUESTED, APPROVED, DECLINED
}
@@ -11,25 +11,6 @@
<artifactId>redaction-service-server-v1</artifactId>
<properties>
<pdfbox.version>2.0.20</pdfbox.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>${pdfbox.version}</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox-tools</artifactId>
<version>${pdfbox.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.iqser.red.service</groupId>
@@ -39,7 +20,7 @@
<dependency>
<groupId>com.iqser.red.service</groupId>
<artifactId>configuration-service-api-v1</artifactId>
<version>1.0.12</version>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
@@ -56,17 +37,22 @@
<artifactId>jts-core</artifactId>
<version>1.16.1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
<!-- commons -->
<dependency>
<groupId>com.iqser.gin4.commons</groupId>
<groupId>com.iqser.red.commons</groupId>
<artifactId>spring-commons</artifactId>
</dependency>
<dependency>
<groupId>com.iqser.gin4.commons</groupId>
<groupId>com.iqser.red.commons</groupId>
<artifactId>logging-commons</artifactId>
</dependency>
<dependency>
<groupId>com.iqser.gin4.commons</groupId>
<groupId>com.iqser.red.commons</groupId>
<artifactId>metric-commons</artifactId>
</dependency>
<!-- other external -->
@@ -99,7 +85,7 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.iqser.gin4.commons</groupId>
<groupId>com.iqser.red.commons</groupId>
<artifactId>test-commons</artifactId>
<scope>test</scope>
</dependency>
@@ -12,6 +12,7 @@ import org.kie.api.builder.KieModule;
import org.kie.api.runtime.KieContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.autoconfigure.metrics.web.servlet.WebMvcMetricsAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
@@ -20,7 +21,7 @@ import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import com.iqser.gin4.commons.spring.DefaultWebMvcConfiguration;
import com.iqser.red.commons.spring.DefaultWebMvcConfiguration;
import com.iqser.red.service.configuration.v1.api.model.RulesResponse;
import com.iqser.red.service.redaction.v1.server.client.RulesClient;
import com.iqser.red.service.redaction.v1.server.exception.RulesValidationException;
@@ -29,7 +30,7 @@ import com.iqser.red.service.redaction.v1.server.settings.RedactionServiceSettin
@Import({DefaultWebMvcConfiguration.class})
@EnableFeignClients(basePackageClasses = RulesClient.class)
@EnableConfigurationProperties(RedactionServiceSettings.class)
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class})
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, WebMvcMetricsAutoConfiguration.class})
public class Application {
@Autowired
@@ -4,7 +4,6 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.iqser.red.service.redaction.v1.model.RedactionLogEntry;
import com.iqser.red.service.redaction.v1.server.redaction.model.Entity;
@@ -18,7 +17,7 @@ public class Document {
private List<Page> pages = new ArrayList<>();
private List<Paragraph> paragraphs = new ArrayList<>();
private Map<Integer, Set<Entity>> entities = new HashMap<>();
private Map<Integer, List<Entity>> entities = new HashMap<>();
private FloatFrequencyCounter textHeightCounter = new FloatFrequencyCounter();
private FloatFrequencyCounter fontSizeCounter= new FloatFrequencyCounter();
private StringFrequencyCounter fontCounter= new StringFrequencyCounter();
@@ -10,7 +10,6 @@ import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class Paragraph {
@@ -18,10 +17,12 @@ public class Paragraph {
private List<AbstractTextContainer> pageBlocks = new ArrayList<>();
private String headline;
public SearchableText getSearchableText(){
public SearchableText getSearchableText() {
SearchableText searchableText = new SearchableText();
pageBlocks.forEach(block -> {
if(block instanceof TextBlock){
if (block instanceof TextBlock) {
searchableText.addAll(((TextBlock) block).getSequences());
}
});
@@ -29,14 +30,27 @@ public class Paragraph {
}
public List<Table> getTables(){
public List<Table> getTables() {
List<Table> tables = new ArrayList<>();
pageBlocks.forEach(block -> {
if(block instanceof Table){
if (block instanceof Table) {
tables.add((Table) block);
}
});
return tables;
}
}
public List<TextBlock> getTextBlocks() {
List<TextBlock> textBlocks = new ArrayList<>();
pageBlocks.forEach(block -> {
if (block instanceof TextBlock) {
textBlocks.add((TextBlock) block);
}
});
return textBlocks;
}
}
@@ -5,43 +5,45 @@ import java.util.Map;
import lombok.Getter;
/**
*
*/
public class StringFrequencyCounter {
@Getter
Map<String, Integer> countPerValue = new HashMap<>();
private final Map<String, Integer> countPerValue = new HashMap<>();
public void add(String value){
if(!countPerValue.containsKey(value)){
public void add(String value) {
if (!countPerValue.containsKey(value)) {
countPerValue.put(value, 1);
} else {
countPerValue.put(value, countPerValue.get(value) + 1);
}
}
public void addAll(Map<String, Integer> otherCounter){
for(Map.Entry<String, Integer> entry: otherCounter.entrySet()){
if(countPerValue.containsKey(entry.getKey())){
countPerValue.put(entry.getKey(), countPerValue.get(entry.getKey())+ entry.getValue());
public void addAll(Map<String, Integer> otherCounter) {
for (Map.Entry<String, Integer> entry : otherCounter.entrySet()) {
if (countPerValue.containsKey(entry.getKey())) {
countPerValue.put(entry.getKey(), countPerValue.get(entry.getKey()) + entry.getValue());
} else {
countPerValue.put(entry.getKey(), entry.getValue());
}
}
}
public String getMostPopular(){
public String getMostPopular() {
Map.Entry<String, Integer> mostPopular = null;
for(Map.Entry<String, Integer> entry: countPerValue.entrySet()){
if(mostPopular == null){
for (Map.Entry<String, Integer> entry : countPerValue.entrySet()) {
if (mostPopular == null) {
mostPopular = entry;
} else if(entry.getValue() > mostPopular.getValue()){
} else if (entry.getValue() > mostPopular.getValue()) {
mostPopular = entry;
}
}
return mostPopular != null ? mostPopular.getKey() : null;
}
}
}
@@ -131,7 +131,7 @@ public class TextBlock extends AbstractTextContainer {
TextPositionSequence previous = null;
for (TextPositionSequence word : sequences) {
if (previous != null) {
if (Math.abs(previous.getY1() - word.getY1()) > word.getTextHeight()) {
if (Math.abs(previous.getRotationAdjustedY() - word.getRotationAdjustedY()) > word.getTextHeight()) {
sb.append('\n');
} else {
sb.append(' ');
@@ -29,20 +29,16 @@ public class BlockificationService {
float minX = 1000, maxX = 0, minY = 1000, maxY = 0;
TextPositionSequence prev = null;
for (TextPositionSequence word : textPositions) {
boolean lineSeparation = minY - word.getY2() > word.getHeight() * 1.25;
boolean startFromTop = word.getY1() > maxY + word.getHeight();
if (prev != null &&
(lineSeparation
|| startFromTop
|| word.getRotation() == 0 && isSplittedByRuling(maxX, minY, word.getX1(), word.getY1(), verticalRulingLines)
|| word.getRotation() == 0 && isSplittedByRuling(minX, minY, word.getX1(), word.getY2(), horizontalRulingLines)
|| word.getRotation() == 90 && isSplittedByRuling(maxX, minY, word.getX1(), word.getY1(), horizontalRulingLines)
|| word.getRotation() == 90 && isSplittedByRuling(minX, minY, word.getX1(), word.getY2(), verticalRulingLines)
)) {
if (prev != null && (lineSeparation || startFromTop || word.getRotation() == 0 && isSplittedByRuling(maxX, minY, word
.getX1(), word.getY1(), verticalRulingLines) || word.getRotation() == 0 && isSplittedByRuling(minX, minY, word
.getX1(), word.getY2(), horizontalRulingLines) || word.getRotation() == 90 && isSplittedByRuling(maxX, minY, word
.getX1(), word.getY1(), horizontalRulingLines) || word.getRotation() == 90 && isSplittedByRuling(minX, minY, word
.getX1(), word.getY2(), verticalRulingLines))) {
TextBlock cb1 = buildTextBlock(chunkWords);
chunkBlockList1.add(cb1);
@@ -100,11 +96,12 @@ public class BlockificationService {
styleFrequencyCounter.add(wordBlock.getFontStyle());
if (textBlock == null) {
textBlock = new TextBlock(wordBlock.getX1(), wordBlock.getX2(), wordBlock.getY1(), wordBlock.getY2(), wordBlockList, wordBlock.getRotation());
textBlock = new TextBlock(wordBlock.getX1(), wordBlock.getX2(), wordBlock.getY1(), wordBlock.getY2(), wordBlockList, wordBlock
.getRotation());
} else {
TextBlock spatialEntity = textBlock.union(wordBlock);
textBlock.resize(spatialEntity.getMinX(), spatialEntity.getMinY(),
spatialEntity.getWidth(), spatialEntity.getHeight());
textBlock.resize(spatialEntity.getMinX(), spatialEntity.getMinY(), spatialEntity.getWidth(), spatialEntity
.getHeight());
}
}
@@ -122,6 +119,7 @@ public class BlockificationService {
private boolean isSplittedByRuling(float previousX2, float previousY1, float currentX1, float currentY1, List<Ruling> rulingLines) {
for (Ruling ruling : rulingLines) {
if (ruling.intersectsLine(previousX2, previousY1, currentX1, currentY1)) {
return true;
@@ -133,7 +131,6 @@ public class BlockificationService {
public Rectangle calculateBodyTextFrame(List<Page> pages, FloatFrequencyCounter documentFontSizeCounter, boolean landscape) {
float minX = 10000;
float maxX = -100;
float minY = 10000;
@@ -147,7 +144,6 @@ public class BlockificationService {
for (AbstractTextContainer container : page.getTextBlocks()) {
if (container instanceof TextBlock) {
TextBlock textBlock = (TextBlock) container;
if (textBlock.getMostPopularWordFont() == null || textBlock.getMostPopularWordStyle() == null) {
@@ -179,16 +175,15 @@ public class BlockificationService {
}
}
if (container instanceof Table) {
Table table = (Table) container;
for (List<Cell> row : table.getRows()) {
for (Cell column : row) {
for (Cell cell : row) {
if (column == null || column.getTextBlocks() == null) {
if (cell == null || cell.getTextBlocks() == null) {
continue;
}
for (TextBlock textBlock : column.getTextBlocks()) {
for (TextBlock textBlock : cell.getTextBlocks()) {
if (textBlock.getMinX() < minX) {
minX = textBlock.getMinX();
}
@@ -211,5 +206,4 @@ public class BlockificationService {
return new Rectangle(minY, minX, maxX - minX, maxY - minY);
}
}
@@ -84,7 +84,7 @@ public class ClassificationService {
}
else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock) && textBlock.getMostPopularWordFontSize() == document.getFontSizeCounter().getMostPopular() && textBlock.getMostPopularWordStyle().equals("italic") && !document.getFontStyleCounter().getMostPopular().equals("italic") && PositionUtils.getApproxLineCount(textBlock) < 2.9) {
textBlock.setClassification("TextBlock Italic");
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock) && textBlock.getSequences().size() > 3){
} else if (PositionUtils.isWithinBodyTextFrame(bodyTextFrame, textBlock)){
textBlock.setClassification("TextBlock Unknown");
}
}
@@ -2,13 +2,13 @@ package com.iqser.red.service.redaction.v1.server.controller;
import java.time.OffsetDateTime;
import com.iqser.red.commons.spring.ErrorMessage;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.iqser.gin4.commons.api.errorhandling.ErrorMessage;
import com.iqser.red.service.redaction.v1.server.exception.RulesValidationException;
import lombok.extern.slf4j.Slf4j;
@@ -3,18 +3,21 @@ package com.iqser.red.service.redaction.v1.server.controller;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.iqser.red.service.redaction.v1.model.RedactionLog;
import com.iqser.red.service.redaction.v1.model.RedactionLogEntry;
import com.iqser.red.service.redaction.v1.model.RedactionRequest;
import com.iqser.red.service.redaction.v1.model.RedactionResult;
import com.iqser.red.service.redaction.v1.resources.RedactionResource;
import com.iqser.red.service.redaction.v1.server.classification.model.Document;
import com.iqser.red.service.redaction.v1.server.classification.model.Page;
import com.iqser.red.service.redaction.v1.server.exception.RedactionException;
import com.iqser.red.service.redaction.v1.server.redaction.service.DictionaryService;
import com.iqser.red.service.redaction.v1.server.redaction.service.DroolsExecutionService;
import com.iqser.red.service.redaction.v1.server.redaction.service.EntityRedactionService;
import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService;
@@ -36,6 +39,7 @@ public class RedactionController implements RedactionResource {
private final EntityRedactionService entityRedactionService;
private final PdfFlattenService pdfFlattenService;
private final DroolsExecutionService droolsExecutionService;
private final DictionaryService dictionaryService;
@Override
public RedactionResult redact(@RequestBody RedactionRequest redactionRequest) {
@@ -44,15 +48,15 @@ public class RedactionController implements RedactionResource {
pdDocument.setAllSecurityToBeRemoved(true);
Document classifiedDoc = pdfSegmentationService.parseDocument(pdDocument);
entityRedactionService.processDocument(classifiedDoc);
annotationHighlightService.highlight(pdDocument, classifiedDoc, redactionRequest.isFlatRedaction());
entityRedactionService.processDocument(classifiedDoc, redactionRequest.getManualRedactions());
annotationHighlightService.highlight(pdDocument, classifiedDoc, redactionRequest.isFlatRedaction(), redactionRequest.getManualRedactions());
if (redactionRequest.isFlatRedaction()) {
PDDocument flatDocument = pdfFlattenService.flattenPDF(pdDocument);
return convert(flatDocument, classifiedDoc.getPages().size(), new RedactionLog(classifiedDoc.getRedactionLogEntities()));
return convert(flatDocument, classifiedDoc.getPages().size(), classifiedDoc.getRedactionLogEntities());
}
return convert(pdDocument, classifiedDoc.getPages().size(), new RedactionLog(classifiedDoc.getRedactionLogEntities()));
return convert(pdDocument, classifiedDoc.getPages().size(), classifiedDoc.getRedactionLogEntities());
} catch (IOException e) {
throw new RedactionException(e);
@@ -130,14 +134,14 @@ public class RedactionController implements RedactionResource {
return convert(document, numberOfPages, null);
}
private RedactionResult convert(PDDocument document, int numberOfPages, RedactionLog redactionLog) throws IOException {
private RedactionResult convert(PDDocument document, int numberOfPages, List<RedactionLogEntry> redactionLogEntities) throws IOException {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
document.save(byteArrayOutputStream);
return RedactionResult.builder()
.document(byteArrayOutputStream.toByteArray())
.numberOfPages(numberOfPages)
.redactionLog(redactionLog)
.redactionLog(new RedactionLog(redactionLogEntities, dictionaryService.getDictionaryVersion(), droolsExecutionService.getRulesVersion()))
.build();
}
@@ -44,10 +44,10 @@ import lombok.extern.slf4j.Slf4j;
public class PDFLinesTextStripper extends PDFTextStripper {
@Getter
private float minCharWidth = Float.MAX_VALUE;
private int maxCharWidths;
@Getter
private float minCharHeight = Float.MAX_VALUE;
private int maxCharHeight;
@Getter
private final List<TextPositionSequence> textPositionSequences = new ArrayList<>();
@@ -201,8 +201,16 @@ public class PDFLinesTextStripper extends PDFTextStripper {
int startIndex = 0;
for (int i = 0; i <= textPositions.size() - 1; i++) {
minCharWidth = Math.min(minCharWidth, textPositions.get(i).getWidthDirAdj());
minCharHeight = Math.min(minCharHeight, textPositions.get(i).getHeightDir());
int charHeight = (int) textPositions.get(i).getHeightDir();
if(charHeight > maxCharHeight){
maxCharHeight = charHeight;
}
int charWidth = (int) textPositions.get(i).getWidthDirAdj();
if(charWidth > maxCharWidths){
maxCharWidths = charWidth;
}
if (i == 0 && textPositions.get(i).getUnicode().equals(" ")) {
startIndex++;
@@ -241,8 +249,8 @@ public class PDFLinesTextStripper extends PDFTextStripper {
@Override
public String getText(PDDocument doc) throws IOException {
minCharWidth = Float.MAX_VALUE;
minCharHeight = Float.MAX_VALUE;
maxCharWidths = 0;
maxCharWidths = 0;
textPositionSequences.clear();
rulings.clear();
graphicsPath.clear();
@@ -17,6 +17,6 @@ public class ParsedElements {
private boolean landscape;
private boolean rotated;
private float minCharWidth;
private float minCharHeight;
private float maxCharWidth;
private float maxCharHeight;
}
@@ -5,10 +5,11 @@ import java.util.List;
import org.apache.pdfbox.text.TextPosition;
import com.iqser.red.service.redaction.v1.model.Point;
import com.iqser.red.service.redaction.v1.model.Rectangle;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
@Data
@RequiredArgsConstructor
@@ -16,42 +17,50 @@ public class TextPositionSequence implements CharSequence {
private List<TextPosition> textPositions = new ArrayList<>();
@Getter
@Setter
private float[] annotationColor;
private final int page;
public TextPositionSequence(List<TextPosition> textPositions, int page){
public TextPositionSequence(List<TextPosition> textPositions, int page) {
this.textPositions = textPositions;
this.page = page;
}
@Override
public int length() {
return textPositions.size();
}
@Override
public char charAt(int index) {
TextPosition textPosition = textPositionAt(index);
String text = textPosition.getUnicode();
return text.charAt(0);
}
public char charAt(int index, boolean caseInSensitive) {
TextPosition textPosition = textPositionAt(index);
String text = textPosition.getUnicode();
return caseInSensitive ? text.toLowerCase().charAt(0) : text.charAt(0);
}
@Override
public TextPositionSequence subSequence(int start, int end) {
return new TextPositionSequence(textPositions.subList(start, end), page);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder(length());
for (int i = 0; i < length(); i++) {
builder.append(charAt(i));
@@ -59,15 +68,21 @@ public class TextPositionSequence implements CharSequence {
return builder.toString();
}
public TextPosition textPositionAt(int index) {
return textPositions.get(index);
}
public void add(TextPosition textPosition) {
this.textPositions.add(textPosition);
}
public float getX1() {
if (textPositions.get(0).getRotation() == 90) {
return textPositions.get(0).getYDirAdj() - getTextHeight();
} else {
@@ -75,15 +90,23 @@ public class TextPositionSequence implements CharSequence {
}
}
public float getX2() {
if (textPositions.get(0).getRotation() == 90) {
return textPositions.get(0).getYDirAdj();
} else {
return textPositions.get(textPositions.size() - 1).getXDirAdj() + textPositions.get(textPositions.size() - 1).getWidth() + 1;
return textPositions.get(textPositions.size() - 1)
.getXDirAdj() + textPositions.get(textPositions.size() - 1).getWidth() + 1;
}
}
public float getRotationAdjustedY() {
return textPositions.get(0).getY();
}
public float getY1() {
if (textPositions.get(0).getRotation() == 90) {
return textPositions.get(0).getXDirAdj();
} else {
@@ -91,30 +114,46 @@ public class TextPositionSequence implements CharSequence {
}
}
public float getY2() {
if (textPositions.get(0).getRotation() == 90) {
return textPositions.get(textPositions.size() - 1).getXDirAdj() + getTextHeight() -2 ;
return textPositions.get(textPositions.size() - 1).getXDirAdj() + getTextHeight() - 2;
} else {
return textPositions.get(0).getPageHeight() - textPositions.get(0).getYDirAdj() + getTextHeight();
}
}
public float getTextHeight() {
return textPositions.get(0).getHeightDir() + 2;
}
public float getHeight() {
return getY2() - getY1();
}
public float getWidth() {
return getX2() - getX1();
}
public String getFont() {
return textPositions.get(0).getFont().toString().toLowerCase().replaceAll(",bold", "").replaceAll(",italic", "");
return textPositions.get(0)
.getFont()
.toString()
.toLowerCase()
.replaceAll(",bold", "")
.replaceAll(",italic", "");
}
public String getFontStyle() {
String lowercaseFontName = textPositions.get(0).getFont().toString().toLowerCase();
@@ -131,16 +170,48 @@ public class TextPositionSequence implements CharSequence {
}
public float getFontSize() {
return textPositions.get(0).getFontSizeInPt();
}
public float getSpaceWidth() {
return textPositions.get(0).getWidthOfSpace();
}
public int getRotation() {
return textPositions.get(0).getRotation();
}
public Rectangle getRectangle() {
float height = getTextHeight();
float posXInit = getX1();
float posXEnd;
float posYInit;
float posYEnd;
if (textPositions.get(0).getRotation() == 90) {
posXEnd = textPositions.get(0).getYDirAdj() + 2;
posYInit = getY1();
posYEnd = textPositions.get(textPositions.size() - 1).getXDirAdj() - height + 4;
} else {
posXEnd = textPositions.get(textPositions.size() - 1)
.getXDirAdj() + textPositions.get(textPositions.size() - 1).getWidth() + 1;
posYInit = textPositions.get(0).getPageHeight() - textPositions.get(0).getYDirAdj() - 2;
posYEnd = textPositions.get(0).getPageHeight() - textPositions.get(textPositions.size() - 1)
.getYDirAdj() + 2;
}
return new Rectangle(new Point(posXInit, posYInit), posXEnd - posXInit, posYEnd - posYInit + height, page);
}
}
@@ -0,0 +1,40 @@
package com.iqser.red.service.redaction.v1.server.redaction.model;
import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock;
import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSequence;
import com.iqser.red.service.redaction.v1.server.redaction.utils.TextNormalizationUtilities;
import lombok.Value;
@Value
public class CellValue {
TextBlock textBlock;
int rowSpanStart;
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
TextPositionSequence previous = null;
for (TextPositionSequence word : textBlock.getSequences()) {
if (previous != null) {
if (Math.abs(previous.getRotationAdjustedY() - word.getRotationAdjustedY()) > word.getTextHeight()) {
sb.append('\n');
} else {
sb.append(' ');
}
}
sb.append(word.toString());
previous = word;
}
return TextNormalizationUtilities.removeHyphenLineBreaks(sb.toString())
.replaceAll("\n", " ")
.replaceAll(" {2}", " ");
}
}
@@ -1,25 +1,38 @@
package com.iqser.red.service.redaction.v1.server.redaction.model;
import java.util.ArrayList;
import java.util.List;
import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSequence;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Entity {
@EqualsAndHashCode.Include
private final String word;
private final String type;
private boolean redaction;
private String redactionReason;
private String legalBasis;
private List<EntityPositionSequence> positionSequences = new ArrayList<>();
private List<TextPositionSequence> targetSequences;
private Integer start;
private Integer end;
@EqualsAndHashCode.Include
private String headline;
private int matchedRule;
public Entity(String word, String type, boolean redaction, String redactionReason, List<EntityPositionSequence> positionSequences, String headline, int matchedRule) {
@EqualsAndHashCode.Include
private int sectionNumber;
public Entity(String word, String type, boolean redaction, String redactionReason, List<EntityPositionSequence> positionSequences, String headline, int matchedRule, int sectionNumber, String legalBasis) {
this.word = word;
this.type = type;
this.redaction = redaction;
@@ -27,13 +40,19 @@ public class Entity {
this.positionSequences = positionSequences;
this.headline = headline;
this.matchedRule = matchedRule;
this.sectionNumber = sectionNumber;
this.legalBasis = legalBasis;
}
public Entity(String word, String type, Integer start, Integer end, String headline) {
public Entity(String word, String type, Integer start, Integer end, String headline, int sectionNumber) {
this.word = word;
this.type = type;
this.start = start;
this.end = end;
this.headline = headline;
this.sectionNumber = sectionNumber;
}
}
@@ -2,19 +2,23 @@ package com.iqser.red.service.redaction.v1.server.redaction.model;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSequence;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
@Data
@RequiredArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class EntityPositionSequence {
@EqualsAndHashCode.Exclude
private List<TextPositionSequence> sequences = new ArrayList<>();
private int pageNumber;
private final UUID id;
private final String id;
}
@@ -1,17 +1,17 @@
package com.iqser.red.service.redaction.v1.server.redaction.model;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;
import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSequence;
import com.iqser.red.service.redaction.v1.server.redaction.utils.IdBuilder;
import com.iqser.red.service.redaction.v1.server.redaction.utils.TextNormalizationUtilities;
@SuppressWarnings("all")
public class SearchableText {
private List<TextPositionSequence> sequences = new ArrayList<>();
private final List<TextPositionSequence> sequences = new ArrayList<>();
public void add(TextPositionSequence textPositionSequence) {
@@ -28,6 +28,14 @@ public class SearchableText {
public List<EntityPositionSequence> getSequences(String searchString, boolean caseInsensitive) {
return getSequences(searchString, caseInsensitive, null);
}
@SuppressWarnings("checkstyle:ModifiedControlVariable")
public List<EntityPositionSequence> getSequences(String searchString, boolean caseInsensitive,
List<TextPositionSequence> sequencesSubList) {
String normalizedSearchString;
if (caseInsensitive) {
normalizedSearchString = searchString.toLowerCase();
@@ -40,37 +48,50 @@ public class SearchableText {
List<TextPositionSequence> crossSequenceParts = new ArrayList<>();
List<EntityPositionSequence> finalMatches = new ArrayList<>();
for (int i = 0; i < sequences.size(); i++) {
TextPositionSequence partMatch = new TextPositionSequence(sequences.get(i).getPage());
for (int j = 0; j < sequences.get(i).length(); j++) {
if (i > 0 && j == 0 && sequences.get(i).charAt(0, caseInsensitive) == ' ' && sequences.get(i - 1)
.charAt(sequences.get(i - 1).length() - 1, caseInsensitive) == ' ' || j > 0 && sequences.get(i)
.charAt(j, caseInsensitive) == ' ' && sequences.get(i).charAt(j - 1, caseInsensitive) == ' ') {
if (j == sequences.get(i).length() - 1 && counter != 0 && !partMatch.getTextPositions().isEmpty()) {
List<TextPositionSequence> searchSpace;
if (sequencesSubList != null) {
int subListIndex = Collections.indexOfSubList(sequences, sequencesSubList);
if (subListIndex != -1) {
searchSpace = sequences.subList(subListIndex, subListIndex + sequencesSubList.size());
} else {
searchSpace = sequences;
}
} else {
searchSpace = sequences;
}
for (int i = 0; i < searchSpace.size(); i++) {
TextPositionSequence partMatch = new TextPositionSequence(searchSpace.get(i).getPage());
for (int j = 0; j < searchSpace.get(i).length(); j++) {
if (i > 0 && j == 0 && searchSpace.get(i).charAt(0, caseInsensitive) == ' ' && searchSpace.get(i - 1)
.charAt(searchSpace.get(i - 1).length() - 1, caseInsensitive) == ' ' || j > 0 && searchSpace.get(i)
.charAt(j, caseInsensitive) == ' ' && searchSpace.get(i).charAt(j - 1, caseInsensitive) == ' ') {
if (j == searchSpace.get(i).length() - 1 && counter != 0 && !partMatch.getTextPositions().isEmpty()) {
crossSequenceParts.add(partMatch);
}
continue;
}
if (j == 0 && sequences.get(i).charAt(j, caseInsensitive) != ' ' && i != 0 && sequences.get(i - 1)
.charAt(sequences.get(i - 1)
if (j == 0 && searchSpace.get(i).charAt(j, caseInsensitive) != ' ' && i != 0 && searchSpace.get(i - 1)
.charAt(searchSpace.get(i - 1)
.length() - 1, caseInsensitive) != ' ' && searchChars[counter] == ' ') {
counter++;
}
if (sequences.get(i)
.charAt(j, caseInsensitive) == searchChars[counter] || counter != 0 && sequences.get(i)
if (searchSpace.get(i)
.charAt(j, caseInsensitive) == searchChars[counter] || counter != 0 && searchSpace.get(i)
.charAt(j, caseInsensitive) == '-') {
if (counter != 0 || i == 0 && j == 0 || j != 0 && isSeparator(sequences.get(i)
.charAt(j - 1, caseInsensitive)) || j == 0 && i != 0 && isSeparator(sequences.get(i - 1)
.charAt(sequences.get(i - 1)
.length() - 1, caseInsensitive)) || j == 0 && i != 0 && sequences.get(i - 1)
.charAt(sequences.get(i - 1).length() - 1, caseInsensitive) != ' ' && sequences.get(i)
if (counter != 0 || i == 0 && j == 0 || j != 0 && isSeparator(searchSpace.get(i)
.charAt(j - 1, caseInsensitive)) || j == 0 && i != 0 && isSeparator(searchSpace.get(i - 1)
.charAt(searchSpace.get(i - 1)
.length() - 1, caseInsensitive)) || j == 0 && i != 0 && searchSpace.get(i - 1)
.charAt(searchSpace.get(i - 1).length() - 1, caseInsensitive) != ' ' && searchSpace.get(i)
.charAt(j, caseInsensitive) != ' ') {
partMatch.add(sequences.get(i).textPositionAt(j));
if (!(j == sequences.get(i).length() - 1 && sequences.get(i)
partMatch.add(searchSpace.get(i).textPositionAt(j));
if (!(j == searchSpace.get(i).length() - 1 && searchSpace.get(i)
.charAt(j, caseInsensitive) == '-' && searchChars[counter] != '-')) {
counter++;
}
@@ -79,19 +100,19 @@ public class SearchableText {
if (counter == searchString.length()) {
crossSequenceParts.add(partMatch);
if (i == sequences.size() - 1 && j == sequences.get(i).length() - 1 || j != sequences.get(i)
.length() - 1 && isSeparator(sequences.get(i)
.charAt(j + 1, caseInsensitive)) || j == sequences.get(i)
.length() - 1 && isSeparator(sequences.get(i + 1)
.charAt(0, caseInsensitive)) || j == sequences.get(i).length() - 1 && sequences.get(i)
.charAt(j, caseInsensitive) != ' ' && sequences.get(i + 1)
if (i == searchSpace.size() - 1 && j == searchSpace.get(i).length() - 1 || j != searchSpace.get(i)
.length() - 1 && isSeparator(searchSpace.get(i)
.charAt(j + 1, caseInsensitive)) || j == searchSpace.get(i)
.length() - 1 && isSeparator(searchSpace.get(i + 1)
.charAt(0, caseInsensitive)) || j == searchSpace.get(i).length() - 1 && searchSpace.get(i)
.charAt(j, caseInsensitive) != ' ' && searchSpace.get(i + 1)
.charAt(0, caseInsensitive) != ' ') {
finalMatches.addAll(buildEntityPositionSequence(crossSequenceParts));
}
counter = 0;
crossSequenceParts = new ArrayList<>();
partMatch = new TextPositionSequence(sequences.get(i).getPage());
partMatch = new TextPositionSequence(searchSpace.get(i).getPage());
}
} else {
counter = 0;
@@ -99,24 +120,27 @@ public class SearchableText {
j--;
}
crossSequenceParts = new ArrayList<>();
partMatch = new TextPositionSequence(sequences.get(i).getPage());
partMatch = new TextPositionSequence(searchSpace.get(i).getPage());
}
if (j == sequences.get(i).length() - 1 && counter != 0) {
if (j == searchSpace.get(i).length() - 1 && counter != 0) {
crossSequenceParts.add(partMatch);
}
}
}
return finalMatches;
}
private List<EntityPositionSequence> buildEntityPositionSequence(List<TextPositionSequence> crossSequenceParts) {
UUID id = UUID.randomUUID();
String plainId = IdBuilder.buildId(crossSequenceParts);
String id = plainId;
List<EntityPositionSequence> result = new ArrayList<>();
int currentPage = -1;
int idDiffentPageSuffix = 1;
EntityPositionSequence entityPositionSequence = new EntityPositionSequence(id);
for (TextPositionSequence textPositionSequence : crossSequenceParts) {
if (currentPage == -1) {
@@ -126,9 +150,13 @@ public class SearchableText {
} else if (currentPage == textPositionSequence.getPage()) {
entityPositionSequence.getSequences().add(textPositionSequence);
} else {
id = plainId + "-" + idDiffentPageSuffix;
idDiffentPageSuffix++;
result.add(entityPositionSequence);
entityPositionSequence = new EntityPositionSequence(id);
entityPositionSequence.setPageNumber(textPositionSequence.getPage());
entityPositionSequence.getSequences().add(textPositionSequence);
currentPage = textPositionSequence.getPage();
}
}
result.add(entityPositionSequence);
@@ -151,7 +179,7 @@ public class SearchableText {
for (TextPositionSequence word : sequences) {
if (previous != null) {
if (Math.abs(previous.getY1() - word.getY1()) > word.getTextHeight()) {
if (Math.abs(previous.getRotationAdjustedY() - word.getRotationAdjustedY()) > word.getTextHeight()) {
sb.append('\n');
} else {
sb.append(' ');
@@ -163,7 +191,7 @@ public class SearchableText {
return TextNormalizationUtilities.removeHyphenLineBreaks(sb.toString())
.replaceAll("\n", " ")
.replaceAll(" ", " ");
.replaceAll(" {2}", " ");
}
@@ -175,7 +203,7 @@ public class SearchableText {
for (TextPositionSequence word : sequences) {
if (previous != null) {
if (Math.abs(previous.getY1() - word.getY1()) > word.getTextHeight()) {
if (Math.abs(previous.getRotationAdjustedY() - word.getRotationAdjustedY()) > word.getTextHeight()) {
sb.append('\n');
} else {
sb.append(' ');
@@ -187,4 +215,4 @@ public class SearchableText {
return sb.append("\n").toString();
}
}
}
@@ -3,6 +3,7 @@ package com.iqser.red.service.redaction.v1.server.redaction.model;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
@@ -10,8 +11,10 @@ import org.apache.commons.lang3.StringUtils;
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@Data
@Slf4j
@Builder
public class Section {
@@ -25,20 +28,42 @@ public class Section {
private String headline;
private int sectionNumber;
public boolean contains(String type) {
private Map<String, CellValue> tabularData;
public boolean rowEquals(String headerName, String value) {
String cleanHeaderName = headerName.replaceAll("\n", "").replaceAll(" ", "").replaceAll("-", "");
return tabularData != null && tabularData.containsKey(cleanHeaderName) && tabularData.get(cleanHeaderName)
.getTextBlock()
.getText()
.equals(value);
}
public boolean matchesType(String type) {
return entities.stream().anyMatch(entity -> entity.getType().equals(type));
}
public void redact(String type, int ruleNumber, String reason) {
public boolean headlineContainsWord(String word) {
return StringUtils.containsIgnoreCase(headline, word);
}
public void redact(String type, int ruleNumber, String reason, String legalBasis) {
entities.forEach(entity -> {
if (entity.getType().equals(type)) {
entity.setRedaction(true);
entity.setMatchedRule(ruleNumber);
entity.setRedactionReason(reason);
entity.setLegalBasis(legalBasis);
}
});
}
@@ -56,13 +81,30 @@ public class Section {
}
public void redactLineAfter(String start, String asType, int ruleNumber, String reason) {
public void redactIfPrecededBy(String prefix, String type, int ruleNumber, String reason, String legalBasis) {
String value = StringUtils.substringBetween(text, start, "\n");
entities.forEach(entity -> {
if (entity.getType().equals(type) && searchText.indexOf(prefix + entity.getWord()) != 1) {
entity.setRedaction(true);
entity.setMatchedRule(ruleNumber);
entity.setRedactionReason(reason);
entity.setLegalBasis(legalBasis);
}
});
}
if (value != null) {
Set<Entity> found = findEntity(value.trim(), asType);
entities.addAll(found);
public void redactLineAfter(String start, String asType, int ruleNumber, String reason, String legalBasis) {
String[] values = StringUtils.substringsBetween(text, start, "\n");
if (values != null) {
for (String value : values) {
if (StringUtils.isNotBlank(value)) {
Set<Entity> found = findEntities(value.trim(), asType);
entities.addAll(found);
}
}
}
// TODO No need to iterate
@@ -71,19 +113,24 @@ public class Section {
entity.setRedaction(true);
entity.setMatchedRule(ruleNumber);
entity.setRedactionReason(reason);
entity.setLegalBasis(legalBasis);
}
});
}
public void redactBetween(String start, String stop, String asType, int ruleNumber, String reason) {
public void redactBetween(String start, String stop, String asType, int ruleNumber, String reason, String legalBasis) {
String value = StringUtils.substringBetween(searchText, start, stop);
String[] values = StringUtils.substringsBetween(searchText, start, stop);
if (value != null) {
Set<Entity> found = findEntity(value.trim(), asType);
entities.addAll(found);
if (values != null) {
for (String value : values) {
if (StringUtils.isNotBlank(value)) {
Set<Entity> found = findEntities(value.trim(), asType);
entities.addAll(found);
}
}
}
// TODO No need to iterate
@@ -92,12 +139,13 @@ public class Section {
entity.setRedaction(true);
entity.setMatchedRule(ruleNumber);
entity.setRedactionReason(reason);
entity.setLegalBasis(legalBasis);
}
});
}
private Set<Entity> findEntity(String value, String asType) {
private Set<Entity> findEntities(String value, String asType) {
Set<Entity> found = new HashSet<>();
@@ -107,25 +155,30 @@ public class Section {
startIndex = searchText.indexOf(value, stopIndex);
stopIndex = startIndex + value.length();
if (startIndex > -1 && (startIndex == 0 || Character.isWhitespace(searchText.charAt(startIndex - 1)) || isSeparator(searchText
.charAt(startIndex - 1))) && (stopIndex == searchText.length() || isSeparator(searchText.charAt(stopIndex)))) {
found.add(new Entity(searchText.substring(startIndex, stopIndex), asType, startIndex, stopIndex, headline));
if (startIndex > -1 && (startIndex == 0 || Character.isWhitespace(searchText.charAt(startIndex - 1)) || isSeparator(
searchText.charAt(startIndex - 1))) && (stopIndex == searchText.length() || isSeparator(searchText.charAt(
stopIndex)))) {
found.add(new Entity(searchText.substring(startIndex, stopIndex),
asType,
startIndex,
stopIndex,
headline,
sectionNumber));
}
} while (startIndex > -1);
removeEntitiesContainedInLarger(found);
return found;
return removeEntitiesContainedInLarger(found);
}
private boolean isSeparator(char c) {
return Character.isWhitespace(c) || Pattern.matches("\\p{Punct}", String.valueOf(c)) || c == '\"' || c == '‘' || c == '’';
return Character.isWhitespace(c) || Pattern.matches("\\p{Punct}",
String.valueOf(c)) || c == '\"' || c == '‘' || c == '’';
}
public void removeEntitiesContainedInLarger(Set<Entity> entities) {
public Set<Entity> removeEntitiesContainedInLarger(Set<Entity> entities) {
List<Entity> wordsToRemove = new ArrayList<>();
for (Entity word : entities) {
@@ -137,6 +190,56 @@ public class Section {
}
}
entities.removeAll(wordsToRemove);
return entities;
}
public void highlightCell(String cellHeader, int ruleNumber, String type) {
annotateCell(cellHeader, ruleNumber, type, false, null, null);
}
public void redactCell(String cellHeader, int ruleNumber, String type, String reason, String legalBasis) {
annotateCell(cellHeader, ruleNumber, type, true, reason, legalBasis);
}
public void redactNotCell(String cellHeader, int ruleNumber, String type, String reason) {
annotateCell(cellHeader, ruleNumber, type, false, reason, null);
}
private void annotateCell(String cellHeader, int ruleNumber, String type, boolean redact, String reason, String legalBasis) {
String cleanHeaderName = cellHeader.replaceAll("\n", "").replaceAll(" ", "").replaceAll("-", "");
CellValue value = tabularData.get(cleanHeaderName);
if (value == null) {
log.warn("Could not find any data for {}.", cellHeader);
} else {
String word = value.toString();
Entity entity = new Entity(word,
type,
value.getRowSpanStart(),
value.getRowSpanStart() + word.length(),
headline,
sectionNumber);
entity.setRedaction(redact);
entity.setMatchedRule(ruleNumber);
entity.setRedactionReason(reason);
entity.setTargetSequences(value.getTextBlock()
.getSequences()); // Make sure no other cells with same content are highlighted
entity.setLegalBasis(legalBasis);
// HashSet keeps the older value, but we want the new only.
entities.remove(entity);
entities.add(entity);
entities = removeEntitiesContainedInLarger(entities);
}
}
}
@@ -1,5 +1,6 @@
package com.iqser.red.service.redaction.v1.server.redaction.service;
import java.awt.Color;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@@ -11,6 +12,7 @@ import java.util.stream.Collectors;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;
import com.iqser.red.service.configuration.v1.api.model.Colors;
import com.iqser.red.service.configuration.v1.api.model.TypeResponse;
import com.iqser.red.service.configuration.v1.api.model.TypeResult;
import com.iqser.red.service.redaction.v1.server.client.DictionaryClient;
@@ -20,13 +22,14 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
@Slf4j
public class DictionaryService {
private final DictionaryClient dictionaryClient;
@Getter
private long dictionaryVersion = -1;
@Getter
@@ -44,6 +47,15 @@ public class DictionaryService {
@Getter
private float[] defaultColor;
@Getter
private float[] requestAddColor;
@Getter
private float[] requestRemoveColor;
@Getter
private float[] notRedactedColor;
public void updateDictionary() {
@@ -62,7 +74,7 @@ public class DictionaryService {
if (typeResponse != null && CollectionUtils.isNotEmpty(typeResponse.getTypes())) {
entryColors = typeResponse.getTypes()
.stream()
.collect(Collectors.toMap(TypeResult::getType, TypeResult::getColor));
.collect(Collectors.toMap(TypeResult::getType, t -> convertColor(t.getHexColor())));
hintTypes = typeResponse.getTypes()
.stream()
.filter(TypeResult::isHint)
@@ -73,8 +85,16 @@ public class DictionaryService {
.filter(TypeResult::isCaseInsensitive)
.map(TypeResult::getType)
.collect(Collectors.toList());
dictionary = entryColors.keySet().stream().collect(Collectors.toMap(type -> type, s -> convertEntries(s)));
defaultColor = dictionaryClient.getDefaultColor().getColor();
dictionary = entryColors.keySet()
.stream()
.collect(Collectors.toMap(type -> type, this::convertEntries));
Colors colors = dictionaryClient.getColors();
defaultColor = convertColor(colors.getDefaultColor());
requestAddColor = convertColor(colors.getRequestAdd());
requestRemoveColor = convertColor(colors.getRequestRemove());
notRedactedColor = convertColor(colors.getNotRedacted());
}
} catch (FeignException e) {
log.warn("Got some unknown feignException", e);
@@ -84,6 +104,7 @@ public class DictionaryService {
private Set<String> convertEntries(String s) {
if (caseInsensitiveTypes.contains(s)) {
return dictionaryClient.getDictionaryForType(s)
.getEntries()
@@ -94,4 +115,11 @@ public class DictionaryService {
return new HashSet<>(dictionaryClient.getDictionaryForType(s).getEntries());
}
private float[] convertColor(String hex) {
Color color = Color.decode(hex);
return new float[]{color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f};
}
}
@@ -18,6 +18,7 @@ import com.iqser.red.service.redaction.v1.server.client.RulesClient;
import com.iqser.red.service.redaction.v1.server.exception.RulesValidationException;
import com.iqser.red.service.redaction.v1.server.redaction.model.Section;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Service
@@ -29,6 +30,7 @@ public class DroolsExecutionService {
@Autowired
private KieContainer kieContainer;
@Getter
private long rulesVersion = -1;
public Section executeRules(Section section) {
@@ -1,18 +1,26 @@
package com.iqser.red.service.redaction.v1.server.redaction.service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import com.iqser.red.service.redaction.v1.model.ManualRedactionEntry;
import com.iqser.red.service.redaction.v1.model.ManualRedactions;
import com.iqser.red.service.redaction.v1.model.Rectangle;
import com.iqser.red.service.redaction.v1.server.classification.model.Document;
import com.iqser.red.service.redaction.v1.server.classification.model.Paragraph;
import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock;
import com.iqser.red.service.redaction.v1.server.redaction.model.CellValue;
import com.iqser.red.service.redaction.v1.server.redaction.model.Entity;
import com.iqser.red.service.redaction.v1.server.redaction.model.EntityPositionSequence;
import com.iqser.red.service.redaction.v1.server.redaction.model.SearchableText;
import com.iqser.red.service.redaction.v1.server.redaction.model.Section;
import com.iqser.red.service.redaction.v1.server.tableextraction.model.Cell;
@@ -28,12 +36,13 @@ public class EntityRedactionService {
private final DroolsExecutionService droolsExecutionService;
public void processDocument(Document classifiedDoc) {
public void processDocument(Document classifiedDoc, ManualRedactions manualRedactions) {
dictionaryService.updateDictionary();
droolsExecutionService.updateRules();
Set<Entity> documentEntities = new HashSet<>();
int sectionNumber = 1;
for (Paragraph paragraph : classifiedDoc.getParagraphs()) {
SearchableText searchableText = paragraph.getSearchableText();
@@ -41,88 +50,121 @@ public class EntityRedactionService {
List<Table> tables = paragraph.getTables();
for (Table table : tables) {
boolean singleCellTable = table.getRowCount() == 1 && table.getColCount() == 1;
for (List<Cell> row : table.getRows()) {
SearchableText searchableRow = new SearchableText();
for (Cell column : row) {
if (column == null || column.getTextBlocks() == null) {
Map<String, CellValue> tabularData = new HashMap<>();
int start = 0;
for (Cell cell : row) {
if (!singleCellTable && cell.isHeaderCell() || CollectionUtils.isEmpty(cell.getTextBlocks())) {
continue;
}
for (TextBlock textBlock : column.getTextBlocks()) {
addSectionToManualRedactions(cell.getTextBlocks(), manualRedactions, table.getHeadline(), sectionNumber);
int cellStart = start;
cell.getHeaderCells().forEach(headerCell -> {
StringBuilder headerBuilder = new StringBuilder();
headerCell.getTextBlocks().forEach(textBlock -> headerBuilder.append(textBlock.getText()));
String headerName = headerBuilder.toString()
.replaceAll("\n", "")
.replaceAll(" ", "")
.replaceAll("-", "");
tabularData.put(headerName, new CellValue(cell.getTextBlocks().get(0), cellStart));
});
start = start + cell.toString().length() + 1; // include automatically appended white space
for (TextBlock textBlock : cell.getTextBlocks()) {
searchableRow.addAll(textBlock.getSequences());
}
}
Set<Entity> rowEntities = findEntities(searchableRow, table.getHeadline());
Set<Entity> rowEntities = findEntities(searchableRow, table.getHeadline(), sectionNumber);
Section analysedRowSection = droolsExecutionService.executeRules(Section.builder()
.entities(rowEntities)
.text(searchableRow.getAsStringWithLinebreaks())
.searchText(searchableRow.toString())
.headline(table.getHeadline())
.sectionNumber(sectionNumber)
.tabularData(tabularData)
.build());
for (Entity entity : analysedRowSection.getEntities()) {
if (dictionaryService.getCaseInsensitiveTypes().contains(entity.getType())) {
entity.setPositionSequences(searchableRow.getSequences(entity.getWord(), true));
} else {
entity.setPositionSequences(searchableRow.getSequences(entity.getWord(), false));
}
}
documentEntities.addAll(analysedRowSection.getEntities());
documentEntities.addAll(clearAndFindPositions(analysedRowSection.getEntities(), searchableRow));
sectionNumber++;
}
sectionNumber++;
}
Set<Entity> entities = findEntities(searchableText, paragraph.getHeadline());
addSectionToManualRedactions(paragraph.getTextBlocks(), manualRedactions, paragraph.getHeadline(), sectionNumber);
Set<Entity> entities = findEntities(searchableText, paragraph.getHeadline(), sectionNumber);
Section analysedSection = droolsExecutionService.executeRules(Section.builder()
.entities(entities)
.text(searchableText.getAsStringWithLinebreaks())
.searchText(searchableText.toString())
.headline(paragraph.getHeadline())
.sectionNumber(sectionNumber)
.build());
for (Entity entity : analysedSection.getEntities()) {
if (dictionaryService.getCaseInsensitiveTypes().contains(entity.getType())) {
entity.setPositionSequences(searchableText.getSequences(entity.getWord(), true));
} else {
entity.setPositionSequences(searchableText.getSequences(entity.getWord(), false));
}
}
documentEntities.addAll(analysedSection.getEntities());
documentEntities.addAll(clearAndFindPositions(analysedSection.getEntities(), searchableText));
sectionNumber++;
}
documentEntities.forEach(entity -> {
entity.getPositionSequences().forEach(sequence -> {
for (Entity entity : documentEntities) {
Map<Integer, List<EntityPositionSequence>> sequenceOnPage = new HashMap<>();
for (EntityPositionSequence entityPositionSequence : entity.getPositionSequences()) {
sequenceOnPage.computeIfAbsent(entityPositionSequence.getPageNumber(), (x) -> new ArrayList<>())
.add(entityPositionSequence);
}
for (Map.Entry<Integer, List<EntityPositionSequence>> entry : sequenceOnPage.entrySet()) {
classifiedDoc.getEntities()
.computeIfAbsent(sequence.getPageNumber(), (x) -> new HashSet<>())
.add(new Entity(entity.getWord(), entity.getType(), entity.isRedaction(), entity.getRedactionReason(), List
.of(sequence), entity.getHeadline(), entity.getMatchedRule()));
});
});
.computeIfAbsent(entry.getKey(), (x) -> new ArrayList<>())
.add(new Entity(entity.getWord(), entity.getType(), entity.isRedaction(), entity.getRedactionReason(), entry
.getValue(), entity.getHeadline(), entity.getMatchedRule(), entity.getSectionNumber(), entity.getLegalBasis()));
}
}
}
private Set<Entity> findEntities(SearchableText searchableText, String headline) {
private Set<Entity> clearAndFindPositions(Set<Entity> entities, SearchableText text) {
String inputString = searchableText.toString();
String lowercaseInputString = inputString.toLowerCase();
removeEntitiesContainedInLarger(entities);
for (Entity entity : entities) {
if (dictionaryService.getCaseInsensitiveTypes().contains(entity.getType())) {
entity.setPositionSequences(text.getSequences(entity.getWord(), true, entity.getTargetSequences()));
} else {
entity.setPositionSequences(text.getSequences(entity.getWord(), false, entity.getTargetSequences()));
}
}
return entities;
}
private Set<Entity> findEntities(SearchableText searchableText, String headline, int sectionNumber) {
Set<Entity> found = new HashSet<>();
for (Map.Entry<String, Set<String>> entry : dictionaryService.getDictionary().entrySet()) {
String searchableString = searchableText.toString();
if (StringUtils.isEmpty(searchableString)) {
return found;
}
String lowercaseInputString = searchableString.toLowerCase();
for (Map.Entry<String, Set<String>> entry : dictionaryService.getDictionary().entrySet()) {
if (dictionaryService.getCaseInsensitiveTypes().contains(entry.getKey())) {
found.addAll(find(lowercaseInputString, entry.getValue(), entry.getKey(), headline));
found.addAll(find(lowercaseInputString, entry.getValue(), entry.getKey(), headline, sectionNumber));
} else {
found.addAll(find(inputString, entry.getValue(), entry.getKey(), headline));
found.addAll(find(searchableString, entry.getValue(), entry.getKey(), headline, sectionNumber));
}
}
removeEntitiesContainedInLarger(found);
return found;
}
private Set<Entity> find(String inputString, Set<String> values, String type, String headline) {
private Set<Entity> find(String inputString, Set<String> values, String type, String headline, int sectionNumber) {
Set<Entity> found = new HashSet<>();
for (String value : values) {
@@ -134,7 +176,7 @@ public class EntityRedactionService {
if (startIndex > -1 && (startIndex == 0 || Character.isWhitespace(inputString.charAt(startIndex - 1)) || isSeparator(inputString
.charAt(startIndex - 1))) && (stopIndex == inputString.length() || isSeparator(inputString.charAt(stopIndex)))) {
found.add(new Entity(inputString.substring(startIndex, stopIndex), type, startIndex, stopIndex, headline));
found.add(new Entity(inputString.substring(startIndex, stopIndex), type, startIndex, stopIndex, headline, sectionNumber));
}
} while (startIndex > -1);
}
@@ -162,4 +204,23 @@ public class EntityRedactionService {
entities.removeAll(wordsToRemove);
}
private void addSectionToManualRedactions(List<TextBlock> textBlocks, ManualRedactions manualRedactions, String section, int sectionNumber) {
if (manualRedactions == null || manualRedactions.getEntriesToAdd().isEmpty()) {
return;
}
for (TextBlock textBlock : textBlocks) {
for (ManualRedactionEntry manualRedactionEntry : manualRedactions.getEntriesToAdd()) {
for (Rectangle rectangle : manualRedactionEntry.getPositions()) {
if (textBlock.contains(rectangle)) {
manualRedactionEntry.setSection(section);
manualRedactionEntry.setSectionNumber(sectionNumber);
}
}
}
}
}
}
@@ -0,0 +1,26 @@
package com.iqser.red.service.redaction.v1.server.redaction.utils;
import java.nio.charset.StandardCharsets;
import java.util.List;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSequence;
import lombok.experimental.UtilityClass;
@UtilityClass
public class IdBuilder {
private final HashFunction hashFunction = Hashing.murmur3_128();
public String buildId(List<TextPositionSequence> crossSequenceParts) {
StringBuilder sb = new StringBuilder();
crossSequenceParts.forEach(sequencePart -> sequencePart.getTextPositions().forEach(textPosition -> {
sb.append(textPosition.getTextMatrix()).append(sequencePart.getPage());
}));
return hashFunction.hashString(sb.toString(), StandardCharsets.UTF_8).toString();
}
}
@@ -21,7 +21,6 @@ import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractT
import com.iqser.red.service.redaction.v1.server.tableextraction.model.CleanRulings;
import com.iqser.red.service.redaction.v1.server.tableextraction.service.RulingCleaningService;
import com.iqser.red.service.redaction.v1.server.tableextraction.service.TableExtractionService;
import com.iqser.red.service.redaction.v1.server.tableextraction.utils.Utils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -29,7 +28,6 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
@SuppressWarnings("PMD")
public class PdfSegmentationService {
private final RulingCleaningService rulingCleaningService;
@@ -38,6 +36,7 @@ public class PdfSegmentationService {
private final ClassificationService classificationService;
private final SectionsBuilderService sectionsBuilderService;
public Document parseDocument(PDDocument pdDocument) throws IOException {
Document document = new Document();
@@ -57,19 +56,21 @@ public class PdfSegmentationService {
int rotation = pdPage.getRotation();
boolean isRotated = rotation != 0 && rotation != 360;
ParsedElements parsedElements = ParsedElements
.builder()
ParsedElements parsedElements = ParsedElements.builder()
.rulings(stripper.getRulings())
.sequences(stripper.getTextPositionSequences())
.minCharWidth(Utils.round(stripper.getMinCharWidth(), 2))
.minCharHeight(Utils.round(stripper.getMinCharHeight(), 2))
.maxCharWidth(stripper.getMaxCharWidths())
.maxCharHeight(stripper.getMaxCharWidths())
.landscape(isLandscape)
.rotated(isRotated)
.build();
CleanRulings cleanRulings = rulingCleaningService.getCleanRulings(parsedElements.getRulings(), parsedElements.getMinCharWidth(), parsedElements.getMinCharHeight());
CleanRulings cleanRulings = rulingCleaningService.getCleanRulings(parsedElements.getRulings(), parsedElements
.getMaxCharWidth(), parsedElements.getMaxCharHeight());
Page page = blockificationService.blockify(parsedElements.getSequences(), cleanRulings.getHorizontal(), cleanRulings.getVertical());
Page page = blockificationService.blockify(parsedElements.getSequences(), cleanRulings.getHorizontal(), cleanRulings
.getVertical());
page.setRotation(rotation);
tableExtractionService.extractTables(cleanRulings, page);
@@ -92,7 +93,10 @@ public class PdfSegmentationService {
}
private void increaseDocumentStatistics(Page page, Document document) {
if (!page.isLandscape()) {
document.getFontSizeCounter().addAll(page.getFontSizeCounter().getCountPerValue());
}
@@ -101,6 +105,7 @@ public class PdfSegmentationService {
document.getFontStyleCounter().addAll(page.getFontStyleCounter().getCountPerValue());
}
private void buildPageStatistics(Page page) {
// Collect all statistics for the page, except from blocks inside tables, as tables will always be added to BodyTextFrame.
@@ -1,9 +1,12 @@
package com.iqser.red.service.redaction.v1.server.segmentation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;
import com.iqser.red.service.redaction.v1.server.classification.model.Document;
@@ -11,10 +14,10 @@ import com.iqser.red.service.redaction.v1.server.classification.model.Page;
import com.iqser.red.service.redaction.v1.server.classification.model.Paragraph;
import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock;
import com.iqser.red.service.redaction.v1.server.tableextraction.model.AbstractTextContainer;
import com.iqser.red.service.redaction.v1.server.tableextraction.model.Cell;
import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table;
@Service
@SuppressWarnings("all")
public class SectionsBuilderService {
public void buildSections(Document document) {
@@ -25,6 +28,7 @@ public class SectionsBuilderService {
AbstractTextContainer prev = null;
String lastHeadline = "";
Table previousTable = null;
for (Page page : document.getPages()) {
for (AbstractTextContainer current : page.getTextBlocks()) {
@@ -35,32 +39,68 @@ public class SectionsBuilderService {
current.setPage(page.getPageNumber());
if (prev != null && current.getClassification().startsWith("H ") || !document.isHeadlines()) {
if (prev != null && current.getClassification().startsWith("H ") && !prev.getClassification().startsWith("H ") || !document.isHeadlines()) {
Paragraph chunkBlock = buildTextBlock(chunkWords, lastHeadline);
chunkBlock.setHeadline(lastHeadline);
lastHeadline = current.getText();
if(document.isHeadlines()) {
lastHeadline = current.getText();
}
chunkBlockList.add(chunkBlock);
chunkWords = new ArrayList<>();
if (CollectionUtils.isNotEmpty(chunkBlock.getTables())) {
previousTable = chunkBlock.getTables().get(chunkBlock.getTables().size() - 1);
}
}
if (current instanceof Table) {
Table table = (Table) current;
// Distribute header information for subsequent tables
mergeTableMetadata(table, previousTable);
previousTable = table;
}
chunkWords.add(current);
prev = current;
}
}
Paragraph chunkBlock = buildTextBlock(chunkWords, lastHeadline);
if (chunkBlock != null) {
chunkBlockList.add(chunkBlock);
chunkBlock.setHeadline(lastHeadline);
}
chunkBlock.setHeadline(lastHeadline);
chunkBlockList.add(chunkBlock);
document.setParagraphs(chunkBlockList);
}
private void mergeTableMetadata(Table currentTable, Table previousTable) {
// Distribute header information for subsequent tables
if (previousTable != null && hasInvalidHeaderInformation(currentTable) && hasValidHeaderInformation(previousTable)) {
List<Cell> previousTableNonHeaderRow = getRowWithNonHeaderCells(previousTable);
List<Cell> tableNonHeaderRow = getRowWithNonHeaderCells(currentTable);
// Allow merging of tables if header row is separated from first logical non-header row
if (previousTableNonHeaderRow.isEmpty() && previousTable.getRowCount() == 1 && previousTable.getRows()
.get(0)
.size() == tableNonHeaderRow.size()) {
previousTableNonHeaderRow = previousTable.getRows().get(0).stream().map(cell -> {
Cell fakeCell = new Cell(cell.getPoints()[0], cell.getPoints()[2]);
fakeCell.setHeaderCells(Collections.singletonList(cell));
return fakeCell;
}).collect(Collectors.toList());
}
if (previousTableNonHeaderRow.size() == tableNonHeaderRow.size()) {
for (int i = currentTable.getRowCount() - 1; i >= 0; i--) { // Non header rows are most likely at bottom of table
List<Cell> row = currentTable.getRows().get(i);
if (row.size() == tableNonHeaderRow.size() && row.stream()
.allMatch(cell -> cell.getHeaderCells().isEmpty())) {
for (int j = 0; j < row.size(); j++) {
row.get(j).setHeaderCells(previousTableNonHeaderRow.get(j).getHeaderCells());
}
}
}
}
}
}
private Paragraph buildTextBlock(List<AbstractTextContainer> wordBlockList, String lastHeadline) {
Paragraph paragraph = new Paragraph();
@@ -76,19 +116,20 @@ public class SectionsBuilderService {
AbstractTextContainer container = itty.next();
if (container instanceof Table) {
Table table = (Table) container;
splitByTable = true;
if (previous != null && previous instanceof TextBlock && previous.getText().startsWith("Table ")) {
((Table) container).setHeadline(previous.getText());
if (previous != null && previous.getText().startsWith("Table ")) {
table.setHeadline(previous.getText());
} else {
((Table) container).setHeadline("Table in: " + lastHeadline);
table.setHeadline("Table in: " + lastHeadline);
}
if (textBlock != null && !alreadyAdded) {
paragraph.getPageBlocks().add(textBlock);
alreadyAdded = true;
}
paragraph.getPageBlocks().add(container);
paragraph.getPageBlocks().add(table);
continue;
}
@@ -125,4 +166,42 @@ public class SectionsBuilderService {
return paragraph;
}
}
private boolean hasValidHeaderInformation(Table table) {
return !hasInvalidHeaderInformation(table);
}
private boolean hasInvalidHeaderInformation(Table table) {
return table.getRows().stream()
.flatMap(row -> row.stream()
.filter(cell -> CollectionUtils.isNotEmpty(cell.getHeaderCells())))
.findAny()
.isEmpty();
}
private List<Cell> getRowWithNonHeaderCells(Table table) {
for (int i = table.getRowCount() - 1; i >= 0; i--) { // Non header rows are most likely at bottom of table
List<Cell> row = table.getRows().get(i);
boolean allNonHeader = true;
for (Cell cell : row) {
if (cell.isHeaderCell()) {
allNonHeader = false;
break;
}
}
if (allNonHeader) {
return row;
}
}
return Collections.emptyList();
}
}
@@ -1,5 +1,7 @@
package com.iqser.red.service.redaction.v1.server.tableextraction.model;
import com.iqser.red.service.redaction.v1.model.Rectangle;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -22,4 +24,8 @@ public abstract class AbstractTextContainer {
return this.minX <= other.minX && this.maxX >= other.maxX && this.minY >= other.minY && this.maxY <= other.maxY;
}
public boolean contains(Rectangle other) {
return page == other.getPage() && this.minX <= other.getTopLeft().getX() && this.maxX >= other.getTopLeft().getX() + other.getWidth() && this.minY <= other.getTopLeft().getY() && this.maxY >= other.getTopLeft().getY() + other.getHeight();
}
}
@@ -5,6 +5,8 @@ import java.util.ArrayList;
import java.util.List;
import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock;
import com.iqser.red.service.redaction.v1.server.parsing.model.TextPositionSequence;
import com.iqser.red.service.redaction.v1.server.redaction.utils.TextNormalizationUtilities;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -16,11 +18,47 @@ public class Cell extends Rectangle {
private List<TextBlock> textBlocks = new ArrayList<>();
private List<Cell> headerCells = new ArrayList<>();
private boolean isHeaderCell;
public Cell(Point2D topLeft, Point2D bottomRight) {
super((float) topLeft.getY(), (float) topLeft.getX(), (float) (bottomRight.getX() - topLeft.getX()), (float) (bottomRight.getY() - topLeft.getY()));
super((float) topLeft.getY(), (float) topLeft.getX(), (float) (bottomRight.getX() - topLeft.getX()), (float) (bottomRight
.getY() - topLeft.getY()));
}
public void addTextBlock(TextBlock textBlock) {
textBlocks.add(textBlock);
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (TextBlock textBlock : textBlocks) {
TextPositionSequence previous = null;
for (TextPositionSequence word : textBlock.getSequences()) {
if (previous != null) {
if (Math.abs(previous.getRotationAdjustedY() - word.getRotationAdjustedY()) > word.getTextHeight()) {
sb.append('\n');
} else {
sb.append(' ');
}
}
sb.append(word.toString());
previous = word;
}
}
return TextNormalizationUtilities.removeHyphenLineBreaks(sb.toString())
.replaceAll("\n", " ")
.replaceAll(" {2}", " ");
}
}
@@ -0,0 +1,22 @@
package com.iqser.red.service.redaction.v1.server.tableextraction.model;
import lombok.RequiredArgsConstructor;
import lombok.Value;
@Value
@RequiredArgsConstructor
public class CellPosition implements Comparable<CellPosition> {
int row;
int col;
@Override
public int compareTo(CellPosition other) {
int rowDiff = row - other.row;
return rowDiff != 0 ? rowDiff : col - other.col;
}
}
@@ -8,25 +8,28 @@ import org.locationtech.jts.index.strtree.STRtree;
import com.iqser.red.service.redaction.v1.server.tableextraction.utils.Utils;
@SuppressWarnings("all")
public class RectangleSpatialIndex<T extends Rectangle> {
private final STRtree si = new STRtree();
private final List<T> rectangles = new ArrayList<>();
public void add(T te) {
rectangles.add(te);
si.insert(new Envelope(te.getLeft(), te.getRight(), te.getBottom(), te.getTop()), te);
}
public List<T> contains(Rectangle r) {
List<T> intersection = si.query(new Envelope(r.getLeft(), r.getRight(), r.getTop(), r.getBottom()));
public List<T> contains(Rectangle rectangle) {
List<T> intersection = si.query(new Envelope(rectangle.getLeft(), rectangle.getRight(), rectangle.getTop(), rectangle
.getBottom()));
List<T> rv = new ArrayList<T>();
for (T ir: intersection) {
if (r.contains(ir)) {
for (T ir : intersection) {
if (rectangle.contains(ir)) {
rv.add(ir);
}
}
@@ -34,18 +37,22 @@ public class RectangleSpatialIndex<T extends Rectangle> {
Utils.sort(rv, Rectangle.ILL_DEFINED_ORDER);
return rv;
}
public List<T> intersects(Rectangle r) {
List rv = si.query(new Envelope(r.getLeft(), r.getRight(), r.getTop(), r.getBottom()));
return rv;
}
/**
* Minimum bounding box of all the Rectangles contained on this RectangleSpatialIndex
*
*
* @return a Rectangle
*/
public Rectangle getBounds() {
return Rectangle.boundingBoxOf(rectangles);
}
@@ -9,31 +9,38 @@ import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.apache.commons.collections4.CollectionUtils;
import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock;
import com.iqser.red.service.redaction.v1.server.tableextraction.utils.Utils;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@SuppressWarnings("all")
@Slf4j
public class Table extends AbstractTextContainer {
private final TreeMap<CellPosition, Cell> cells = new TreeMap<>();
private RectangleSpatialIndex<Cell> si = new RectangleSpatialIndex<>();
private final RectangleSpatialIndex<Cell> si = new RectangleSpatialIndex<>();
@Getter
@Setter
private String headline;
@Getter
private int rowCount = 0;
@Getter
private int colCount = 0;
private int unrotatedRowCount;
private int rotation = 0;
private int unrotatedColCount;
private int rowCount = -1;
private int colCount = -1;
private final int rotation;
private List<List<Cell>> rows;
private List<List<Cell>> memoizedRows = null;
public Table(List<Cell> cells, Rectangle area, int rotation) {
@@ -47,43 +54,129 @@ public class Table extends AbstractTextContainer {
}
public List<List<Cell>> getRows() {
if (memoizedRows == null) {
memoizedRows = computeRows();
if (rows == null) {
rows = computeRows();
computeHeaders();
}
return memoizedRows;
return rows;
}
public int getRowCount() {
if (rowCount == -1) {
rowCount = getRows().size();
}
return rowCount;
}
public int getColCount() {
if (colCount == -1) {
colCount = getRows().stream().mapToInt(List::size).max().orElse(0);
}
return colCount;
}
/**
* Detect header cells (either first row or first column):
* Column is marked as header if cell text is bold and row cell text is not bold.
* Defaults to row.
*/
private void computeHeaders() {
if (rows == null) {
rows = computeRows();
}
// A bold cell is a header cell as long as every cell to the left/top is bold, too
// we move from left to right and top to bottom
for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
List<Cell> rowCells = rows.get(rowIndex);
for (int colIndex = 0; colIndex < rowCells.size(); colIndex++) {
Cell cell = rowCells.get(colIndex);
List<Cell> cellsToTheLeft = rowCells.subList(0, colIndex);
Cell lastHeaderCell = null;
for (Cell leftCell : cellsToTheLeft) {
if (leftCell.isHeaderCell()) {
lastHeaderCell = leftCell;
} else {
break;
}
}
if (lastHeaderCell != null) {
cell.getHeaderCells().add(lastHeaderCell);
}
List<Cell> cellsToTheTop = new ArrayList<>();
for (int i = 0; i < rowIndex; i++) {
try {
cellsToTheTop.add(rows.get(i).get(colIndex));
} catch (IndexOutOfBoundsException e) {
log.debug("No cell {} in row {}, ignoring.", colIndex, rowIndex);
}
}
for (Cell topCell : cellsToTheTop) {
if (topCell.isHeaderCell()) {
lastHeaderCell = topCell;
} else {
break;
}
}
if (lastHeaderCell != null) {
cell.getHeaderCells().add(lastHeaderCell);
}
if (CollectionUtils.isNotEmpty(cell.getTextBlocks()) && cell.getTextBlocks()
.get(0)
.getMostPopularWordStyle()
.equals("bold")) {
cell.setHeaderCell(true);
}
}
}
}
private List<List<Cell>> computeRows() {
List<List<Cell>> rows = new ArrayList<>();
if (rotation == 90) {
for (int i = 0; i < colCount; i++) { // rows
for (int i = 0; i < unrotatedColCount; i++) { // rows
List<Cell> lastRow = new ArrayList<>();
for (int j = rowCount - 1; j >= 0; j--) { // cols
for (int j = unrotatedRowCount - 1; j >= 0; j--) { // cols
Cell cell = cells.get(new CellPosition(j, i));
lastRow.add(cell);
if (cell != null) {
lastRow.add(cell);
}
}
rows.add(lastRow);
}
} else if (rotation == 270) {
for (int i = colCount - 1; i >= 0; i--) { // rows
for (int i = unrotatedColCount - 1; i >= 0; i--) { // rows
List<Cell> lastRow = new ArrayList<>();
for (int j = 0; j < rowCount; j++) { // cols
for (int j = 0; j < unrotatedRowCount; j++) { // cols
Cell cell = cells.get(new CellPosition(i, j));
lastRow.add(cell);
if (cell != null) {
lastRow.add(cell);
}
}
rows.add(lastRow);
}
} else {
for (int i = 0; i < rowCount; i++) {
for (int i = 0; i < unrotatedRowCount; i++) {
List<Cell> lastRow = new ArrayList<>();
for (int j = 0; j < colCount; j++) {
for (int j = 0; j < unrotatedColCount; j++) {
Cell cell = cells.get(new CellPosition(i, j)); // JAVA_8 use getOrDefault()
lastRow.add(cell);
if (cell != null) {
lastRow.add(cell);
}
}
rows.add(lastRow);
}
@@ -93,16 +186,18 @@ public class Table extends AbstractTextContainer {
}
public void add(Cell chunk, int row, int col) {
rowCount = Math.max(rowCount, row + 1);
colCount = Math.max(colCount, col + 1);
private void add(Cell chunk, int row, int col) {
unrotatedRowCount = Math.max(unrotatedRowCount, row + 1);
unrotatedColCount = Math.max(unrotatedColCount, col + 1);
CellPosition cp = new CellPosition(row, col);
cells.put(cp, chunk);
}
private void addCells(List<Cell> cells) {
if (cells.isEmpty()) {
@@ -131,25 +226,21 @@ public class Table extends AbstractTextContainer {
while (rowCells.hasNext()) {
Cell cell = rowCells.next();
if (i > 0) {
List<List<Cell>> others = rowsOfCells(
si.contains(
new Rectangle(cell.getBottom(),
si.getBounds().getLeft(),
cell.getLeft() - si.getBounds().getLeft() + 1,
si.getBounds().getBottom() - cell.getBottom()
)
));
Rectangle rectangle = new Rectangle(cell.getBottom(),
si.getBounds().getLeft(),
cell.getLeft() - si.getBounds().getLeft() + 1,
si.getBounds().getBottom() - cell.getBottom());
List<List<Cell>> others = rowsOfCells(si.contains(rectangle));
for (List<Cell> r : others) {
jumpToColumn = Math.max(jumpToColumn, r.size());
}
}
while (startColumn != jumpToColumn) {
add(previousNonNullCellForColumnIndex.get(startColumn), i, startColumn);
startColumn++;
while (startColumn != jumpToColumn) {
add(previousNonNullCellForColumnIndex.get(startColumn), i, startColumn);
startColumn++;
}
}
add(cell, i, startColumn);
previousNonNullCellForColumnIndex.put(startColumn, cell);
startColumn++;
@@ -158,34 +249,24 @@ public class Table extends AbstractTextContainer {
}
}
private static List<List<Cell>> rowsOfCells(List<Cell> cells) {
Cell c;
float lastTop;
private List<List<Cell>> rowsOfCells(List<Cell> cells) {
List<List<Cell>> rv = new ArrayList<>();
List<Cell> lastRow;
if (cells.isEmpty()) {
return rv;
}
cells.sort(Comparator.comparingDouble(Rectangle::getLeft));
Collections.sort(cells, new Comparator<Cell>() {
@Override
public int compare(Cell arg0, Cell arg1) {
return Double.compare(arg0.getLeft(), arg1.getLeft());
}
});
Collections.sort(cells, Collections.reverseOrder(new Comparator<Cell>() {
@Override
public int compare(Cell arg0, Cell arg1) {
return Float.compare(Utils.round(arg0.getBottom(), 2), Utils.round(arg1.getBottom(),2));
}
}));
cells.sort(Collections.reverseOrder((arg0, arg1) -> Float.compare(Utils.round(arg0.getBottom(), 2),
Utils.round(arg1
.getBottom(), 2))));
Iterator<Cell> iter = cells.iterator();
c = iter.next();
lastTop = c.getBottom();
lastRow = new ArrayList<>();
Cell c = iter.next();
float lastTop = c.getBottom();
List<Cell> lastRow = new ArrayList<>();
lastRow.add(c);
rv.add(lastRow);
@@ -201,6 +282,7 @@ public class Table extends AbstractTextContainer {
return rv;
}
@Override
public String getText() {
@@ -237,6 +319,7 @@ public class Table extends AbstractTextContainer {
return sb.toString();
}
public String getTextAsHtml() {
StringBuilder sb = new StringBuilder();
@@ -270,41 +353,4 @@ public class Table extends AbstractTextContainer {
return sb.toString();
}
class CellPosition implements Comparable<CellPosition> {
CellPosition(int row, int col) {
this.row = row;
this.col = col;
}
final int row, col;
@Override
public int hashCode() {
return row + 101 * col;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
CellPosition other = (CellPosition) obj;
return row == other.row && col == other.col;
}
@Override
public int compareTo(CellPosition other) {
int rowdiff = row - other.row;
return rowdiff != 0 ? rowdiff : col - other.col;
}
}
}
@@ -18,9 +18,9 @@ import com.iqser.red.service.redaction.v1.server.tableextraction.utils.Utils;
@Service
public class RulingCleaningService {
public CleanRulings getCleanRulings(List<Ruling> rulings, float minCharWidth, float minCharHeight){
public CleanRulings getCleanRulings(List<Ruling> rulings, float maxCharWidth, float maxCharHeight){
if (!rulings.isEmpty()) {
snapPoints(rulings, minCharWidth , minCharHeight);
snapPoints(rulings, maxCharWidth , maxCharHeight);
}
List<Ruling> vrs = new ArrayList<>();
@@ -2,7 +2,6 @@ package com.iqser.red.service.redaction.v1.server.tableextraction.service;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
@@ -25,26 +24,28 @@ import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table;
import com.iqser.red.service.redaction.v1.server.tableextraction.utils.Utils;
@Service
@SuppressWarnings("all")
public class TableExtractionService {
public void extractTables(CleanRulings cleanRulings, Page page){
public void extractTables(CleanRulings cleanRulings, Page page) {
List<Cell> cells = findCells(cleanRulings.getHorizontal(), cleanRulings.getVertical());
Iterator<AbstractTextContainer> itty = page.getTextBlocks().iterator();
while (itty.hasNext()) {
TextBlock textBlock = (TextBlock) itty.next();
for (AbstractTextContainer abstractTextContainer : page.getTextBlocks()) {
TextBlock textBlock = (TextBlock) abstractTextContainer;
for (Cell cell : cells) {
if (cell.intersects(textBlock.getMinX(), textBlock.getMinY(), textBlock.getWidth(), textBlock.getHeight())) {
if (cell.intersects(textBlock.getMinX(), textBlock.getMinY(), textBlock.getWidth(),
textBlock.getHeight())) {
cell.addTextBlock(textBlock);
break;
}
}
}
List<Rectangle> spreadsheetAreas = findSpreadsheetsFromCells(cells)
.stream()
cells = new ArrayList<>(new HashSet<>(cells));
Utils.sort(cells, Rectangle.ILL_DEFINED_ORDER);
List<Rectangle> spreadsheetAreas = findSpreadsheetsFromCells(cells).stream()
.filter(r -> r.getWidth() > 0f && r.getHeight() > 0f)
.collect(Collectors.toList());
@@ -63,9 +64,9 @@ public class TableExtractionService {
for (Table table : tables) {
int position = -1;
itty = page.getTextBlocks().iterator();
Iterator<AbstractTextContainer> itty = page.getTextBlocks().iterator();
while (itty.hasNext()) {
AbstractTextContainer textBlock = (AbstractTextContainer) itty.next();
AbstractTextContainer textBlock = itty.next();
if (table.contains(textBlock)) {
if (position == -1) {
position = page.getTextBlocks().indexOf(textBlock);
@@ -79,17 +80,18 @@ public class TableExtractionService {
}
}
public List<Cell> findCells(List<Ruling> horizontalRulingLines, List<Ruling> verticalRulingLines) {
List<Cell> cellsFound = new ArrayList<>();
Map<Point2D, Ruling[]> intersectionPoints = Ruling.findIntersections(horizontalRulingLines, verticalRulingLines);
Map<Point2D, Ruling[]> intersectionPoints = Ruling.findIntersections(horizontalRulingLines,
verticalRulingLines);
List<Point2D> intersectionPointsList = new ArrayList<>(intersectionPoints.keySet());
Collections.sort(intersectionPointsList, POINT_COMPARATOR);
boolean doBreak;
intersectionPointsList.sort(POINT_COMPARATOR);
for (int i = 0; i < intersectionPointsList.size(); i++) {
Point2D topLeft = intersectionPointsList.get(i);
Ruling[] hv = intersectionPoints.get(topLeft);
doBreak = false;
// CrossingPointsDirectlyBelow( topLeft );
List<Point2D> xPoints = new ArrayList<>();
@@ -106,10 +108,6 @@ public class TableExtractionService {
}
outer:
for (Point2D xPoint : xPoints) {
if (doBreak) {
break;
}
// is there a vertical edge b/w topLeft and xPoint?
if (!hv[1].equals(intersectionPoints.get(xPoint)[1])) {
continue;
@@ -120,11 +118,9 @@ public class TableExtractionService {
continue;
}
Point2D btmRight = new Point2D.Float((float) yPoint.getX(), (float) xPoint.getY());
if (intersectionPoints.containsKey(btmRight)
&& intersectionPoints.get(btmRight)[0].equals(intersectionPoints.get(xPoint)[0])
&& intersectionPoints.get(btmRight)[1].equals(intersectionPoints.get(yPoint)[1])) {
if (intersectionPoints.containsKey(btmRight) && intersectionPoints.get(btmRight)[0].equals(intersectionPoints
.get(xPoint)[0]) && intersectionPoints.get(btmRight)[1].equals(intersectionPoints.get(yPoint)[1])) {
cellsFound.add(new Cell(topLeft, btmRight));
doBreak = true;
break outer;
}
}
@@ -139,7 +135,7 @@ public class TableExtractionService {
}
public List<Rectangle> findSpreadsheetsFromCells(List<? extends Rectangle> cells) {
private List<Rectangle> findSpreadsheetsFromCells(List<? extends Rectangle> cells) {
// via: http://stackoverflow.com/questions/13746284/merging-multiple-adjacent-rectangles-into-one-polygon
List<Rectangle> rectangles = new ArrayList<>();
Set<Point2D> pointSet = new HashSet<>();
@@ -147,10 +143,6 @@ public class TableExtractionService {
Map<Point2D, Point2D> edgesV = new HashMap<>();
int i = 0;
cells = new ArrayList<>(new HashSet<>(cells));
Utils.sort(cells, Rectangle.ILL_DEFINED_ORDER);
for (Rectangle cell : cells) {
for (Point2D pt : cell.getPoints()) {
if (pointSet.contains(pt)) { // shared vertex, remove it
@@ -163,10 +155,10 @@ public class TableExtractionService {
// X first sort
List<Point2D> pointsSortX = new ArrayList<>(pointSet);
Collections.sort(pointsSortX, X_FIRST_POINT_COMPARATOR);
pointsSortX.sort(X_FIRST_POINT_COMPARATOR);
// Y first sort
List<Point2D> pointsSortY = new ArrayList<>(pointSet);
Collections.sort(pointsSortY, POINT_COMPARATOR);
pointsSortY.sort(POINT_COMPARATOR);
while (i < pointSet.size()) {
float currY = (float) pointsSortY.get(i).getY();
@@ -203,13 +195,12 @@ public class TableExtractionService {
nextVertex = edgesV.get(curr.point);
edgesV.remove(curr.point);
lastAddedVertex = new PolygonVertex(nextVertex, Direction.VERTICAL);
polygon.add(lastAddedVertex);
} else {
nextVertex = edgesH.get(curr.point);
edgesH.remove(curr.point);
lastAddedVertex = new PolygonVertex(nextVertex, Direction.HORIZONTAL);
polygon.add(lastAddedVertex);
}
polygon.add(lastAddedVertex);
if (lastAddedVertex.equals(polygon.get(0))) {
// closed polygon
@@ -227,10 +218,10 @@ public class TableExtractionService {
// calculate grid-aligned minimum area rectangles for each found polygon
for (List<PolygonVertex> poly : polygons) {
float top = java.lang.Float.MAX_VALUE;
float left = java.lang.Float.MAX_VALUE;
float bottom = java.lang.Float.MIN_VALUE;
float right = java.lang.Float.MIN_VALUE;
float top = Float.MAX_VALUE;
float left = Float.MAX_VALUE;
float bottom = Float.MIN_VALUE;
float right = Float.MIN_VALUE;
for (PolygonVertex pt : poly) {
top = (float) Math.min(top, pt.point.getY());
left = (float) Math.min(left, pt.point.getX());
@@ -244,69 +235,66 @@ public class TableExtractionService {
}
private static final Comparator<Point2D> X_FIRST_POINT_COMPARATOR = new Comparator<Point2D>() {
@Override
public int compare(Point2D arg0, Point2D arg1) {
int rv = 0;
float arg0X = Utils.round(arg0.getX(), 2);
float arg0Y = Utils.round(arg0.getY(), 2);
float arg1X = Utils.round(arg1.getX(), 2);
float arg1Y = Utils.round(arg1.getY(), 2);
private static final Comparator<Point2D> X_FIRST_POINT_COMPARATOR = (arg0, arg1) -> {
if (arg0X > arg1X) {
rv = 1;
} else if (arg0X < arg1X) {
rv = -1;
} else if (arg0Y > arg1Y) {
rv = 1;
} else if (arg0Y < arg1Y) {
rv = -1;
}
return rv;
int rv = 0;
float arg0X = Utils.round(arg0.getX(), 2);
float arg0Y = Utils.round(arg0.getY(), 2);
float arg1X = Utils.round(arg1.getX(), 2);
float arg1Y = Utils.round(arg1.getY(), 2);
if (arg0X > arg1X) {
rv = 1;
} else if (arg0X < arg1X) {
rv = -1;
} else if (arg0Y > arg1Y) {
rv = 1;
} else if (arg0Y < arg1Y) {
rv = -1;
}
return rv;
};
private static final Comparator<Point2D> POINT_COMPARATOR = (arg0, arg1) -> {
private static final Comparator<Point2D> POINT_COMPARATOR = new Comparator<Point2D>() {
@Override
public int compare(Point2D arg0, Point2D arg1) {
int rv = 0;
float arg0X = Utils.round(arg0.getX(), 2);
float arg0Y = Utils.round(arg0.getY(), 2);
float arg1X = Utils.round(arg1.getX(), 2);
float arg1Y = Utils.round(arg1.getY(), 2);
int rv = 0;
float arg0X = Utils.round(arg0.getX(), 2);
float arg0Y = Utils.round(arg0.getY(), 2);
float arg1X = Utils.round(arg1.getX(), 2);
float arg1Y = Utils.round(arg1.getY(), 2);
if (arg0Y > arg1Y) {
rv = 1;
} else if (arg0Y < arg1Y) {
rv = -1;
} else if (arg0X > arg1X) {
rv = 1;
} else if (arg0X < arg1X) {
rv = -1;
}
return rv;
if (arg0Y > arg1Y) {
rv = 1;
} else if (arg0Y < arg1Y) {
rv = -1;
} else if (arg0X > arg1X) {
rv = 1;
} else if (arg0X < arg1X) {
rv = -1;
}
return rv;
};
private enum Direction {
HORIZONTAL,
VERTICAL
HORIZONTAL, VERTICAL
}
static class PolygonVertex {
Point2D point;
Direction direction;
public PolygonVertex(Point2D point, Direction direction) {
PolygonVertex(Point2D point, Direction direction) {
this.direction = direction;
this.point = point;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
@@ -316,15 +304,21 @@ public class TableExtractionService {
return this.point.equals(((PolygonVertex) other).point);
}
@Override
public int hashCode() {
return this.point.hashCode();
}
@Override
public String toString() {
return String.format("%s[point=%s,direction=%s]", this.getClass().getName(), this.point.toString(), this.direction.toString());
return String.format("%s[point=%s,direction=%s]", this.getClass()
.getName(), this.point.toString(), this.direction.toString());
}
}
}
@@ -2,7 +2,12 @@ package com.iqser.red.service.redaction.v1.server.visualization.service;
import java.awt.Color;
import java.io.IOException;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
@@ -13,12 +18,19 @@ import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationText;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationTextMarkup;
import org.apache.pdfbox.text.TextPosition;
import org.springframework.stereotype.Service;
import com.iqser.red.service.redaction.v1.model.Point;
import com.iqser.red.service.redaction.v1.model.Comment;
import com.iqser.red.service.redaction.v1.model.IdRemoval;
import com.iqser.red.service.redaction.v1.model.ManualRedactionEntry;
import com.iqser.red.service.redaction.v1.model.ManualRedactionType;
import com.iqser.red.service.redaction.v1.model.ManualRedactions;
import com.iqser.red.service.redaction.v1.model.Rectangle;
import com.iqser.red.service.redaction.v1.model.RedactionLogEntry;
import com.iqser.red.service.redaction.v1.model.Status;
import com.iqser.red.service.redaction.v1.server.classification.model.Document;
import com.iqser.red.service.redaction.v1.server.classification.model.Paragraph;
import com.iqser.red.service.redaction.v1.server.classification.model.TextBlock;
@@ -31,9 +43,7 @@ import com.iqser.red.service.redaction.v1.server.tableextraction.model.Cell;
import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class AnnotationHighlightService {
@@ -41,139 +51,328 @@ public class AnnotationHighlightService {
private final DictionaryService dictionaryService;
public void highlight(PDDocument document, Document classifiedDoc, boolean flatRedaction) throws IOException {
public void highlight(PDDocument document, Document classifiedDoc, boolean flatRedaction,
ManualRedactions manualRedactions) throws IOException {
Set<Integer> manualRedactionPages = getManualRedactionPages(manualRedactions);
for (int page = 1; page <= document.getNumberOfPages(); page++) {
PDPage pdPage = document.getPage(page - 1);
if (!flatRedaction) {
PDPageContentStream contentStream = new PDPageContentStream(document, pdPage, PDPageContentStream.AppendMode.APPEND, true);
drawSectionFrames(document, classifiedDoc, flatRedaction, pdPage, page);
for (Paragraph paragraph : classifiedDoc.getParagraphs()) {
for (int i = 0; i <= paragraph.getPageBlocks().size() - 1; i++) {
AbstractTextContainer textBlock = paragraph.getPageBlocks().get(i);
if (textBlock.getPage() != page) {
continue;
}
if (textBlock instanceof TextBlock) {
textBlock.setClassification((i + 1) + "/" + paragraph.getPageBlocks().size());
visualizeTextBlock((TextBlock) textBlock, contentStream);
} else if (textBlock instanceof Table) {
textBlock.setClassification((i + 1) + "/" + paragraph.getPageBlocks().size());
visualizeTable((Table) textBlock, contentStream);
}
}
}
contentStream.close();
if (classifiedDoc.getEntities().get(page) != null) {
addAnnotations(pdPage, classifiedDoc, flatRedaction, manualRedactions, page);
}
if (classifiedDoc.getEntities().get(page) == null) {
continue;
if (manualRedactionPages.contains(page)) {
addManualAnnotations(pdPage, classifiedDoc, manualRedactions, page);
}
for (Entity entity : classifiedDoc.getEntities().get(page)) {
RedactionLogEntry redactionLogEntry = new RedactionLogEntry();
for (EntityPositionSequence entityPositionSequence : entity.getPositionSequences()) {
if (flatRedaction && !isRedactionType(entity)) {
continue;
}
for (TextPositionSequence textPositions : entityPositionSequence.getSequences()) {
float height = textPositions.getTextPositions().get(0).getHeightDir() + 2;
float posXInit;
float posXEnd;
float posYInit;
float posYEnd;
if (textPositions.getTextPositions().get(0).getRotation() == 90) {
posXEnd = textPositions.getTextPositions().get(0).getYDirAdj() + 2;
posXInit = textPositions.getTextPositions().get(0).getYDirAdj() - height;
posYInit = textPositions.getTextPositions().get(0).getXDirAdj();
posYEnd = textPositions.getTextPositions()
.get(textPositions.getTextPositions().size() - 1)
.getXDirAdj() - height + 4;
} else {
posXInit = textPositions.getTextPositions().get(0).getXDirAdj();
posXEnd = textPositions.getTextPositions()
.get(textPositions.getTextPositions().size() - 1)
.getXDirAdj() + textPositions.getTextPositions()
.get(textPositions.getTextPositions().size() - 1)
.getWidth() + 1;
posYInit = textPositions.getTextPositions()
.get(0)
.getPageHeight() - textPositions.getTextPositions().get(0).getYDirAdj() - 2;
posYEnd = textPositions.getTextPositions()
.get(0)
.getPageHeight() - textPositions.getTextPositions()
.get(textPositions.getTextPositions().size() - 1)
.getYDirAdj() + 2;
}
Rectangle textHighlightRectangle = new Rectangle(new Point(posXInit, posYInit), posXEnd - posXInit, posYEnd - posYInit + height, page);
List<PDAnnotation> annotations = pdPage.getAnnotations();
PDAnnotationTextMarkup highlight = new PDAnnotationTextMarkup(PDAnnotationTextMarkup.SUB_TYPE_HIGHLIGHT);
highlight.constructAppearances();
PDRectangle annotationPosition = new PDRectangle();
annotationPosition.setLowerLeftX(posXInit);
annotationPosition.setLowerLeftY(posYEnd);
annotationPosition.setUpperRightX(posXEnd);
annotationPosition.setUpperRightY(posYEnd + height);
highlight.setRectangle(annotationPosition);
if (!flatRedaction && !isHint(entity)) {
highlight.setAnnotationName(entityPositionSequence.getId().toString());
highlight.setTitlePopup(entityPositionSequence.getId().toString());
highlight.setContents("\nRule " + entity.getMatchedRule() + " matched\n\n" + entity.getRedactionReason() + "\n\n" + "In Section : \"" + entity
.getHeadline() + "\"");
}
highlight.setQuadPoints(toQuadPoints(textHighlightRectangle));
PDColor color;
if (flatRedaction) {
color = new PDColor(new float[]{0, 0, 0}, PDDeviceRGB.INSTANCE);
} else {
color = new PDColor(getColor(entity), PDDeviceRGB.INSTANCE);
}
highlight.setColor(color);
annotations.add(highlight);
redactionLogEntry.getPositions().add(textHighlightRectangle);
}
redactionLogEntry.setId(entityPositionSequence.getId().toString());
}
redactionLogEntry.setColor(getColor(entity));
redactionLogEntry.setReason(entity.getRedactionReason());
redactionLogEntry.setValue(entity.getWord());
redactionLogEntry.setType(entity.getType());
redactionLogEntry.setRedacted(entity.isRedaction());
redactionLogEntry.setSection(entity.getHeadline());
redactionLogEntry.setHint(isHint(entity));
classifiedDoc.getRedactionLogEntities().add(redactionLogEntry);
}
}
}
private float[] toQuadPoints(Rectangle rectangle) {
private Set<Integer> getManualRedactionPages(ManualRedactions manualRedactions) {
Set<Integer> manualRedactionPages = new HashSet<>();
if (manualRedactions == null) {
return manualRedactionPages;
}
manualRedactions.getEntriesToAdd().forEach(entry -> {
entry.getPositions().forEach(pos -> {
manualRedactionPages.add(pos.getPage());
});
});
return manualRedactionPages;
}
private void addAnnotations(PDPage pdPage, Document classifiedDoc, boolean flatRedaction,
ManualRedactions manualRedactions, int page) throws IOException {
List<PDAnnotation> annotations = pdPage.getAnnotations();
// Duplicates can exist due table extraction colums over multiple rows.
Set<String> processedIds = new HashSet<>();
entityLoop:
for (Entity entity : classifiedDoc.getEntities().get(page)) {
if (flatRedaction && !isRedactionType(entity)) {
continue;
}
boolean requestedToRemove = false;
List<Comment> comments = null;
for (EntityPositionSequence entityPositionSequence : entity.getPositionSequences()) {
RedactionLogEntry redactionLogEntry = createRedactionLogEntry(entity);
if (processedIds.contains(entityPositionSequence.getId())) {
// TODO refactor this outer loop jump as soon as we have the time.
continue entityLoop;
} else {
processedIds.add(entityPositionSequence.getId());
}
if (manualRedactions != null && !manualRedactions.getIdsToRemove().isEmpty()) {
for (IdRemoval manualRemoval : manualRedactions.getIdsToRemove()) {
if (manualRemoval.getId().equals(entityPositionSequence.getId())) {
comments = manualRedactions.getComments().get(manualRemoval.getId());
String manualOverrideReason = null;
if (manualRemoval.getStatus().equals(Status.APPROVED)) {
entity.setRedaction(false);
redactionLogEntry.setRedacted(false);
redactionLogEntry.setStatus(Status.APPROVED);
manualOverrideReason = entity.getRedactionReason() + ", removed by manual override";
} else if (manualRemoval.getStatus().equals(Status.REQUESTED)) {
requestedToRemove = true;
manualOverrideReason = entity.getRedactionReason() + ", requested to remove";
redactionLogEntry.setStatus(Status.REQUESTED);
} else {
redactionLogEntry.setStatus(Status.DECLINED);
}
entity.setRedactionReason(manualOverrideReason != null ? manualOverrideReason : entity.getRedactionReason());
redactionLogEntry.setReason(manualOverrideReason);
redactionLogEntry.setManual(true);
redactionLogEntry.setManualRedactionType(ManualRedactionType.REMOVE);
}
}
}
if (CollectionUtils.isNotEmpty(entityPositionSequence.getSequences())) {
List<Rectangle> rectanglesPerLine = getRectanglesPerLine(entityPositionSequence.getSequences()
.stream()
.flatMap(seq -> seq.getTextPositions().stream())
.collect(Collectors.toList()), page);
if (manualRedactions != null) {
comments = manualRedactions.getComments().get(entityPositionSequence.getId());
}
redactionLogEntry.getPositions().addAll(rectanglesPerLine);
annotations.addAll(createAnnotation(rectanglesPerLine, entityPositionSequence.getId(), createAnnotationContent(entity), getColor(entity, requestedToRemove), comments, !isHint(entity)));
}
redactionLogEntry.setId(entityPositionSequence.getId());
// FIXME ids should never be null. Figure out why this happens.
if (redactionLogEntry.getId() != null) {
classifiedDoc.getRedactionLogEntities().add(redactionLogEntry);
}
}
}
}
private List<Rectangle> getRectanglesPerLine(List<TextPosition> textPositions, int page) {
List<Rectangle> rectangles = new ArrayList<>();
if (textPositions.size() == 1) {
rectangles.add(new TextPositionSequence(textPositions, page).getRectangle());
} else {
float y = textPositions.get(0).getYDirAdj();
int startIndex = 0;
for (int i = 1; i < textPositions.size(); i++) {
float yDirAdj = textPositions.get(i).getYDirAdj();
if (yDirAdj != y) {
rectangles.add(new TextPositionSequence(textPositions.subList(startIndex, i), page).getRectangle());
y = yDirAdj;
startIndex = i;
}
}
if (startIndex != textPositions.size() - 1) {
rectangles.add(new TextPositionSequence(textPositions.subList(startIndex, textPositions.size()), page).getRectangle());
}
}
return rectangles;
}
private void addManualAnnotations(PDPage pdPage, Document classifiedDoc, ManualRedactions manualRedactions,
int page) throws IOException {
if (manualRedactions == null) {
return;
}
List<PDAnnotation> annotations = pdPage.getAnnotations();
for (ManualRedactionEntry manualRedactionEntry : manualRedactions.getEntriesToAdd()) {
String id = manualRedactionEntry.getId();
RedactionLogEntry redactionLogEntry = createRedactionLogEntry(manualRedactionEntry, id);
List<Rectangle> rectanglesOnPage = new ArrayList<>();
for (Rectangle rectangle : manualRedactionEntry.getPositions()) {
if (page == rectangle.getPage()) {
rectanglesOnPage.add(rectangle);
redactionLogEntry.getPositions().add(rectangle);
}
}
if (!rectanglesOnPage.isEmpty() && !approvedAndShouldBeInDictionary(manualRedactionEntry)) {
annotations.addAll(createAnnotation(rectanglesOnPage, id, createAnnotationContent(manualRedactionEntry), getColorForManualAdd(manualRedactionEntry
.getType(), manualRedactionEntry.getStatus()), manualRedactions.getComments().get(id), true));
classifiedDoc.getRedactionLogEntities().add(redactionLogEntry);
}
}
}
private boolean approvedAndShouldBeInDictionary(ManualRedactionEntry manualRedactionEntry) {
return manualRedactionEntry.getStatus().equals(Status.APPROVED) && manualRedactionEntry.isAddToDictionary();
}
private RedactionLogEntry createRedactionLogEntry(ManualRedactionEntry manualRedactionEntry, String id) {
return RedactionLogEntry.builder()
.id(id)
.color(getColor(manualRedactionEntry.getType()))
.reason(manualRedactionEntry.getReason())
.legalBasis(manualRedactionEntry.getLegalBasis())
.value(manualRedactionEntry.getValue())
.type(manualRedactionEntry.getType())
.redacted(true)
.isHint(false)
.section(manualRedactionEntry.getSection())
.sectionNumber(manualRedactionEntry.getSectionNumber())
.manual(true)
.status(manualRedactionEntry.getStatus())
.manualRedactionType(ManualRedactionType.ADD)
.build();
}
private RedactionLogEntry createRedactionLogEntry(Entity entity) {
return RedactionLogEntry.builder()
.color(getColor(entity, false))
.reason(entity.getRedactionReason())
.legalBasis(entity.getLegalBasis())
.value(entity.getWord())
.type(entity.getType())
.redacted(entity.isRedaction())
.isHint(isHint(entity))
.section(entity.getHeadline())
.sectionNumber(entity.getSectionNumber())
.matchedRule(entity.getMatchedRule())
.build();
}
private List<PDAnnotation> createAnnotation(List<Rectangle> rectangles, String id, String content, float[] color,
List<Comment> comments, boolean popup) {
List<PDAnnotation> annotations = new ArrayList<>();
PDAnnotationTextMarkup annotation = new PDAnnotationTextMarkup(PDAnnotationTextMarkup.SUB_TYPE_HIGHLIGHT);
annotation.constructAppearances();
PDRectangle pdRectangle = toPDRectangle(rectangles);
annotation.setRectangle(pdRectangle);
annotation.setQuadPoints(toQuadPoints(rectangles));
if (popup) {
annotation.setContents(content);
}
annotation.setTitlePopup(id);
annotation.setAnnotationName(id);
annotation.setColor(new PDColor(color, PDDeviceRGB.INSTANCE));
annotations.add(annotation);
if (comments != null) {
for (Comment comment : comments) {
PDAnnotationText txtAnnot = new PDAnnotationText();
txtAnnot.setAnnotationName(comment.getId());
txtAnnot.setInReplyTo(annotation); // Reference to highlight annotation
txtAnnot.setName(PDAnnotationText.NAME_COMMENT);
txtAnnot.setCreationDate(GregorianCalendar.from(comment.getDate().toZonedDateTime()));
txtAnnot.setTitlePopup(comment.getUser());
txtAnnot.setContents(comment.getText());
txtAnnot.setRectangle(pdRectangle);
annotations.add(txtAnnot);
}
}
return annotations;
}
private String createAnnotationContent(Entity entity) {
return "\nRule " + entity.getMatchedRule() + " matched\n\n" + entity.getRedactionReason() + "\n\nLegal basis:" + entity
.getLegalBasis() + "\n\nIn section: \"" + entity.getHeadline() + "\"";
}
private String createAnnotationContent(ManualRedactionEntry entry) {
return "\nManual Redaction\n\nIn Section : \"" + entry.getSection() + "\"";
}
private PDRectangle toPDRectangle(List<Rectangle> rectangles) {
float lowerLeftX = Float.MAX_VALUE;
float upperRightX = 0;
float lowerLeftY = 0;
float upperRightY = Float.MAX_VALUE;
for (Rectangle rectangle : rectangles) {
if (rectangle.getTopLeft().getX() < lowerLeftX) {
lowerLeftX = rectangle.getTopLeft().getX();
}
if (rectangle.getTopLeft().getX() + rectangle.getWidth() > upperRightX) {
upperRightX = rectangle.getTopLeft().getX() + rectangle.getWidth();
}
if (rectangle.getTopLeft().getY() + rectangle.getHeight() > lowerLeftY) {
lowerLeftY = rectangle.getTopLeft().getY() + rectangle.getHeight();
}
if (rectangle.getTopLeft().getY() < upperRightY) {
upperRightY = rectangle.getTopLeft().getY();
}
}
PDRectangle annotationPosition = new PDRectangle();
annotationPosition.setLowerLeftX(lowerLeftX);
annotationPosition.setLowerLeftY(lowerLeftY);
annotationPosition.setUpperRightX(upperRightX);
annotationPosition.setUpperRightY(upperRightY);
return annotationPosition;
}
private float[] toQuadPoints(List<Rectangle> rectangles) {
float[] quadPoints = new float[rectangles.size() * 8];
int i = 0;
for (Rectangle rectangle : rectangles) {
float[] quadPoint = toQuadPoint(rectangle);
for (int j = 0; j <= 7; j++) {
quadPoints[i + j] = quadPoint[j];
}
i += 8;
}
return quadPoints;
}
private float[] toQuadPoint(Rectangle rectangle) {
// quadPoints is array of x,y coordinates in Z-like order (top-left, top-right, bottom-left,bottom-right)
// of the area to be highlighted
@@ -189,52 +388,95 @@ public class AnnotationHighlightService {
if (!entity.isRedaction()) {
return false;
}
if (isHint(entity)) {
return false;
}
return true;
return !isHint(entity);
}
private float[] getColor(Entity entity) {
private float[] getColor(Entity entity, boolean requestedToRemove) {
if (requestedToRemove) {
return dictionaryService.getRequestRemoveColor();
}
if (!entity.isRedaction() && !isHint(entity)) {
return new float[]{0.627f, 0.627f, 0.627f};
return dictionaryService.getNotRedactedColor();
}
if (!dictionaryService.getEntryColors().containsKey(entity.getType())) {
return dictionaryService.getDefaultColor();
}
return dictionaryService.getEntryColors().get(entity.getType());
}
private boolean isHint(Entity entity) {
List<String> hintTypes = dictionaryService.getHintTypes();
if (CollectionUtils.isNotEmpty(hintTypes) && hintTypes.contains(entity.getType())) {
return true;
private float[] getColorForManualAdd(String type, Status status) {
if (status.equals(Status.REQUESTED)) {
return dictionaryService.getRequestAddColor();
} else if (status.equals(Status.DECLINED)) {
return dictionaryService.getNotRedactedColor();
}
return false;
return getColor(type);
}
private float[] getColor(String type) {
if (!dictionaryService.getEntryColors().containsKey(type)) {
return dictionaryService.getDefaultColor();
}
return dictionaryService.getEntryColors().get(type);
}
private boolean isHint(Entity entity) {
List<String> hintTypes = dictionaryService.getHintTypes();
return CollectionUtils.isNotEmpty(hintTypes) && hintTypes.contains(entity.getType());
}
private void drawSectionFrames(PDDocument document, Document classifiedDoc, boolean flatRedaction, PDPage pdPage,
int page) throws IOException {
if (flatRedaction) {
return;
}
PDPageContentStream contentStream = new PDPageContentStream(document, pdPage, PDPageContentStream.AppendMode.APPEND, true);
for (Paragraph paragraph : classifiedDoc.getParagraphs()) {
for (int i = 0; i <= paragraph.getPageBlocks().size() - 1; i++) {
AbstractTextContainer textBlock = paragraph.getPageBlocks().get(i);
if (textBlock.getPage() != page) {
continue;
}
if (textBlock instanceof TextBlock) {
textBlock.setClassification((i + 1) + "/" + paragraph.getPageBlocks().size());
visualizeTextBlock((TextBlock) textBlock, contentStream);
} else if (textBlock instanceof Table) {
textBlock.setClassification((i + 1) + "/" + paragraph.getPageBlocks().size());
visualizeTable((Table) textBlock, contentStream);
}
}
}
contentStream.close();
}
private void visualizeTextBlock(TextBlock textBlock, PDPageContentStream contentStream) throws IOException {
contentStream.setStrokingColor(Color.LIGHT_GRAY);
contentStream.setLineWidth(0.5f);
contentStream.addRect(textBlock.getMinX(), textBlock.getMinY(), textBlock.getWidth(), textBlock.getHeight());
contentStream.stroke();
if (textBlock.getClassification() != null) {
contentStream.beginText();
contentStream.setNonStrokingColor(Color.DARK_GRAY);
contentStream.setFont(PDType1Font.TIMES_ROMAN, 8f);
contentStream.newLineAtOffset(textBlock.getMinX(), textBlock.getMaxY());
contentStream.showText(textBlock.getClassification());
contentStream.endText();
}
}
@@ -251,28 +493,18 @@ public class AnnotationHighlightService {
contentStream.addRect((float) cell.getX(), (float) cell.getY(), (float) cell.getWidth(), (float) cell
.getHeight());
contentStream.stroke();
// contentStream.setStrokingColor(Color.GREEN);
// for (TextBlock textBlock : cell.getTextBlocks()) {
// contentStream.addRect(textBlock.getMinX(), textBlock.getMinY(), textBlock.getWidth(), textBlock.getHeight());
// contentStream.stroke();
// }
}
}
}
if (table.getClassification() != null) {
contentStream.beginText();
contentStream.setNonStrokingColor(Color.DARK_GRAY);
contentStream.setFont(PDType1Font.TIMES_ROMAN, 8f);
contentStream.newLineAtOffset(table.getMinX(), table.getMinY());
contentStream.showText(table.getClassification());
contentStream.endText();
}
}
}
}
@@ -8,15 +8,10 @@ spring:
profiles:
active: kubernetes
platform.multi-tenancy:
enabled: ${multitenancy.enabled:false}
tenantFilter:
urlPatterns: /redact
urlPatternsToIgnore:
management:
endpoint:
metrics.enabled: ${monitoring.enabled:false}
prometheus.enabled: ${monitoring.enabled:false}
health.enabled: true
endpoints.web.exposure.include: prometheus, health
metrics.export.prometheus.enabled: ${monitoring.enabled:false}
@@ -2,10 +2,6 @@ spring:
application:
name: redaction-service-v1
management:
endpoints:
web:
base-path: /
path-mapping:
health: "health"
management.endpoints:
web.base-path: /
enabled-by-default: false
@@ -1,25 +1,30 @@
package com.iqser.red.service.redaction.v1.server;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.io.IOUtils;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.kie.api.KieServices;
@@ -35,20 +40,27 @@ import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.test.context.junit4.SpringRunner;
import com.iqser.red.service.configuration.v1.api.model.DefaultColor;
import com.iqser.red.service.configuration.v1.api.model.Colors;
import com.iqser.red.service.configuration.v1.api.model.DictionaryResponse;
import com.iqser.red.service.configuration.v1.api.model.RulesResponse;
import com.iqser.red.service.configuration.v1.api.model.TypeResponse;
import com.iqser.red.service.configuration.v1.api.model.TypeResult;
import com.iqser.red.service.redaction.v1.model.Comment;
import com.iqser.red.service.redaction.v1.model.IdRemoval;
import com.iqser.red.service.redaction.v1.model.ManualRedactionEntry;
import com.iqser.red.service.redaction.v1.model.ManualRedactions;
import com.iqser.red.service.redaction.v1.model.Point;
import com.iqser.red.service.redaction.v1.model.Rectangle;
import com.iqser.red.service.redaction.v1.model.RedactionLogEntry;
import com.iqser.red.service.redaction.v1.model.RedactionRequest;
import com.iqser.red.service.redaction.v1.model.RedactionResult;
import com.iqser.red.service.redaction.v1.model.Status;
import com.iqser.red.service.redaction.v1.server.client.DictionaryClient;
import com.iqser.red.service.redaction.v1.server.client.RulesClient;
import com.iqser.red.service.redaction.v1.server.controller.RedactionController;
import com.iqser.red.service.redaction.v1.server.redaction.utils.ResourceLoader;
import com.iqser.red.service.redaction.v1.server.redaction.utils.TextNormalizationUtilities;
@Ignore
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = DEFINED_PORT)
public class RedactionIntegrationTest {
@@ -57,7 +69,11 @@ public class RedactionIntegrationTest {
private static final String VERTEBRATES_CODE = "vertebrate";
private static final String ADDRESS_CODE = "address";
private static final String NAME_CODE = "name";
private static final String SPONSOR = "sponsor";
private static final String NO_REDACTION_INDICATOR = "no_redaction_indicator";
private static final String REDACTION_INDICATOR = "redaction_indicator";
private static final String HINT_ONLY = "hint_only";
private static final String MUST_REDACT = "must_redact";
@Autowired
private RedactionController redactionController;
@@ -69,9 +85,10 @@ public class RedactionIntegrationTest {
private DictionaryClient dictionaryClient;
private final Map<String, List<String>> dictionary = new HashMap<>();
private final Map<String, float[]> typeColorMap = new HashMap<>();
private final Map<String, String> typeColorMap = new HashMap<>();
private final Map<String, Boolean> hintTypeMap = new HashMap<>();
private final Map<String, Boolean> caseInSensitiveMap = new HashMap<>();
private final Colors colors = new Colors();
@TestConfiguration
public static class RedactionIntegrationTestConfiguration {
@@ -96,7 +113,7 @@ public class RedactionIntegrationTest {
@Before
public void stubRulesClient() {
public void stubClients() {
when(rulesClient.getVersion()).thenReturn(0L);
when(rulesClient.getRules()).thenReturn(new RulesResponse(RULES));
@@ -108,8 +125,12 @@ public class RedactionIntegrationTest {
when(dictionaryClient.getDictionaryForType(VERTEBRATES_CODE)).thenReturn(getDictionaryResponse(VERTEBRATES_CODE));
when(dictionaryClient.getDictionaryForType(ADDRESS_CODE)).thenReturn(getDictionaryResponse(ADDRESS_CODE));
when(dictionaryClient.getDictionaryForType(NAME_CODE)).thenReturn(getDictionaryResponse(NAME_CODE));
when(dictionaryClient.getDictionaryForType(SPONSOR)).thenReturn(getDictionaryResponse(SPONSOR));
when(dictionaryClient.getDictionaryForType(NO_REDACTION_INDICATOR)).thenReturn(getDictionaryResponse(NO_REDACTION_INDICATOR));
when(dictionaryClient.getDefaultColor()).thenReturn(new DefaultColor(new float[]{1f, 0.502f, 0f}));
when(dictionaryClient.getDictionaryForType(REDACTION_INDICATOR)).thenReturn(getDictionaryResponse(REDACTION_INDICATOR));
when(dictionaryClient.getDictionaryForType(HINT_ONLY)).thenReturn(getDictionaryResponse(HINT_ONLY));
when(dictionaryClient.getDictionaryForType(MUST_REDACT)).thenReturn(getDictionaryResponse(MUST_REDACT));
when(dictionaryClient.getColors()).thenReturn(colors);
}
@@ -120,6 +141,11 @@ public class RedactionIntegrationTest {
.stream()
.map(this::cleanDictionaryEntry)
.collect(Collectors.toSet()));
dictionary.computeIfAbsent(SPONSOR, v -> new ArrayList<>())
.addAll(ResourceLoader.load("dictionaries/sponsor_companies.txt")
.stream()
.map(this::cleanDictionaryEntry)
.collect(Collectors.toSet()));
dictionary.computeIfAbsent(VERTEBRATES_CODE, v -> new ArrayList<>())
.addAll(ResourceLoader.load("dictionaries/vertebrates.txt")
.stream()
@@ -131,7 +157,22 @@ public class RedactionIntegrationTest {
.map(this::cleanDictionaryEntry)
.collect(Collectors.toSet()));
dictionary.computeIfAbsent(NO_REDACTION_INDICATOR, v -> new ArrayList<>())
.addAll(ResourceLoader.load("dictionaries/NoRedactionIndicator.txt")
.addAll(ResourceLoader.load("dictionaries/no_redaction_indicator.txt")
.stream()
.map(this::cleanDictionaryEntry)
.collect(Collectors.toSet()));
dictionary.computeIfAbsent(REDACTION_INDICATOR, v -> new ArrayList<>())
.addAll(ResourceLoader.load("dictionaries/redaction_indicator.txt")
.stream()
.map(this::cleanDictionaryEntry)
.collect(Collectors.toSet()));
dictionary.computeIfAbsent(HINT_ONLY, v -> new ArrayList<>())
.addAll(ResourceLoader.load("dictionaries/hint_only.txt")
.stream()
.map(this::cleanDictionaryEntry)
.collect(Collectors.toSet()));
dictionary.computeIfAbsent(MUST_REDACT, v -> new ArrayList<>())
.addAll(ResourceLoader.load("dictionaries/must_redact.txt")
.stream()
.map(this::cleanDictionaryEntry)
.collect(Collectors.toSet()));
@@ -146,20 +187,37 @@ public class RedactionIntegrationTest {
private void loadTypeForTest() {
typeColorMap.put(VERTEBRATES_CODE, new float[]{0, 1, 0});
typeColorMap.put(ADDRESS_CODE, new float[]{0, 1, 1});
typeColorMap.put(NAME_CODE, new float[]{1, 1, 0});
typeColorMap.put(NO_REDACTION_INDICATOR, new float[]{1, 0.502f, 0});
typeColorMap.put(VERTEBRATES_CODE, "#00ff00");
typeColorMap.put(ADDRESS_CODE, "#00ffff");
typeColorMap.put(NAME_CODE, "#ffff00");
typeColorMap.put(SPONSOR, "#2c21fc");
typeColorMap.put(NO_REDACTION_INDICATOR, "#e600ff");
typeColorMap.put(REDACTION_INDICATOR, "#ff7700");
typeColorMap.put(HINT_ONLY, "#00fcb1");
typeColorMap.put(MUST_REDACT, "#ff0000");
hintTypeMap.put(VERTEBRATES_CODE, true);
hintTypeMap.put(ADDRESS_CODE, false);
hintTypeMap.put(NAME_CODE, false);
hintTypeMap.put(SPONSOR, false);
hintTypeMap.put(NO_REDACTION_INDICATOR, true);
hintTypeMap.put(REDACTION_INDICATOR, true);
hintTypeMap.put(HINT_ONLY, true);
hintTypeMap.put(MUST_REDACT, true);
caseInSensitiveMap.put(VERTEBRATES_CODE, true);
caseInSensitiveMap.put(ADDRESS_CODE, false);
caseInSensitiveMap.put(NAME_CODE, false);
caseInSensitiveMap.put(SPONSOR, false);
caseInSensitiveMap.put(NO_REDACTION_INDICATOR, true);
caseInSensitiveMap.put(REDACTION_INDICATOR, true);
caseInSensitiveMap.put(HINT_ONLY, true);
caseInSensitiveMap.put(MUST_REDACT, true);
colors.setDefaultColor("#acfc00");
colors.setNotRedacted("#cccccc");
colors.setRequestAdd("#04b093");
colors.setRequestRemove("#04b093");
}
@@ -169,7 +227,7 @@ public class RedactionIntegrationTest {
.stream()
.map(typeColor -> TypeResult.builder()
.type(typeColor.getKey())
.color(typeColor.getValue())
.hexColor(typeColor.getValue())
.isHint(hintTypeMap.get(typeColor.getKey()))
.isCaseInsensitive(caseInSensitiveMap.get(typeColor.getKey()))
.build())
@@ -181,7 +239,7 @@ public class RedactionIntegrationTest {
private DictionaryResponse getDictionaryResponse(String type) {
return DictionaryResponse.builder()
.color(typeColorMap.get(type))
.hexColor(typeColorMap.get(type))
.entries(dictionary.get(type))
.isHint(hintTypeMap.get(type))
.isCaseInsensitive(caseInSensitiveMap.get(type))
@@ -189,9 +247,59 @@ public class RedactionIntegrationTest {
}
@Test
public void noExceptionShouldBeThrownForAnyFiles() throws IOException {
System.out.println("noExceptionShouldBeThrownForAnyFiles");
ClassLoader loader = getClass().getClassLoader();
URL url = loader.getResource("files");
File[] files = new File(url.getPath()).listFiles();
List<File> input = new ArrayList<>();
for (File file : files) {
input.addAll(getPathsRecursively(file));
}
for (File path : input) {
RedactionRequest request = RedactionRequest.builder()
.document(IOUtils.toByteArray(new FileInputStream(path)))
.build();
System.out.println("Redacting file : " + path.getName());
RedactionResult result = redactionController.redact(request);
Map<String, List<RedactionLogEntry>> duplicates = new HashMap<>();
result.getRedactionLog().getRedactionLogEntry().forEach(entry -> {
duplicates.computeIfAbsent(entry.getId(), v -> new ArrayList<>()).add(entry);
});
duplicates.entrySet().forEach(entry -> {
assertThat(entry.getValue().size()).isEqualTo(1);
});
}
}
private List<File> getPathsRecursively(File path) {
List<File> result = new ArrayList<>();
if (path == null || path.listFiles() == null) {
return result;
}
for (File f : path.listFiles()) {
if (f.isFile()) {
result.add(f);
} else {
result.addAll(getPathsRecursively(f));
}
}
return result;
}
@Test
public void redactionTest() throws IOException {
System.out.println("redactionTest");
long start = System.currentTimeMillis();
ClassPathResource pdfFileResource = new ClassPathResource("files/Metolachlor/S-Metolachlor_RAR_01_Volume_1_2018-09-06.pdf");
@@ -212,9 +320,85 @@ public class RedactionIntegrationTest {
}
@Test
public void testTableRedaction() throws IOException {
System.out.println("testTableRedaction");
long start = System.currentTimeMillis();
ClassPathResource pdfFileResource = new ClassPathResource("files/Metolachlor/S-Metolachlor_RAR_02_Volume_2_2018-09-06.pdf");
RedactionRequest request = RedactionRequest.builder()
.document(IOUtils.toByteArray(pdfFileResource.getInputStream()))
.build();
RedactionResult result = redactionController.redact(request);
try (FileOutputStream fileOutputStream = new FileOutputStream("/tmp/Redacted.pdf")) {
fileOutputStream.write(result.getDocument());
}
long end = System.currentTimeMillis();
System.out.println("duration: " + (end - start));
System.out.println("numberOfPages: " + result.getNumberOfPages());
}
@Test
public void testManualRedaction() throws IOException {
System.out.println("testManualRedaction");
long start = System.currentTimeMillis();
ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/Single Table.pdf");
ManualRedactions manualRedactions = new ManualRedactions();
String manualAddId = UUID.randomUUID().toString();
Comment comment = Comment.builder()
.date(OffsetDateTime.now())
.user("TEST_USER")
.text("This is a comment test")
.build();
manualRedactions.setIdsToRemove(Set.of(IdRemoval.builder()
.id("0836727c3508a0b2ea271da69c04cc2f")
.status(Status.REQUESTED)
.build()));
manualRedactions.getComments().put("e5be0f1d941bbb92a068e198648d06c4", List.of(comment));
manualRedactions.getComments().put("0836727c3508a0b2ea271da69c04cc2f", List.of(comment));
manualRedactions.getComments().put(manualAddId, List.of(comment));
ManualRedactionEntry manualRedactionEntry = new ManualRedactionEntry();
manualRedactionEntry.setId(manualAddId);
manualRedactionEntry.setStatus(Status.REQUESTED);
manualRedactionEntry.setType("name");
manualRedactionEntry.setValue("O'Loughlin C.K.");
manualRedactionEntry.setReason("Manual Redaction");
manualRedactionEntry.setPositions(List.of(new Rectangle(new Point(375.61096f, 241.282f), 7.648041f, 43.72262f, 1), new Rectangle(new Point(384.83517f, 241.282f), 7.648041f, 17.043358f, 1)));
manualRedactions.getEntriesToAdd().add(manualRedactionEntry);
RedactionRequest request = RedactionRequest.builder()
.document(IOUtils.toByteArray(pdfFileResource.getInputStream()))
.manualRedactions(manualRedactions)
.build();
RedactionResult result = redactionController.redact(request);
try (FileOutputStream fileOutputStream = new FileOutputStream("/tmp/Redacted.pdf")) {
fileOutputStream.write(result.getDocument());
}
long end = System.currentTimeMillis();
System.out.println("duration: " + (end - start));
System.out.println("numberOfPages: " + result.getNumberOfPages());
}
@Test
public void classificationTest() throws IOException {
System.out.println("classificationTest");
ClassPathResource pdfFileResource = new ClassPathResource("files/Fludioxonil/51 Fludioxonil_RAR_02_Volume_2_2018-02-21.pdf");
RedactionRequest request = RedactionRequest.builder()
@@ -232,7 +416,8 @@ public class RedactionIntegrationTest {
@Test
public void sectionsTest() throws IOException {
ClassPathResource pdfFileResource = new ClassPathResource("files/Fludioxonil/51 Fludioxonil_RAR_02_Volume_2_2018-02-21.pdf");
System.out.println("sectionsTest");
ClassPathResource pdfFileResource = new ClassPathResource("files/Fludioxonil/51 " + "Fludioxonil_RAR_02_Volume_2_2018-02-21.pdf");
RedactionRequest request = RedactionRequest.builder()
.document(IOUtils.toByteArray(pdfFileResource.getInputStream()))
@@ -249,7 +434,8 @@ public class RedactionIntegrationTest {
@Test
public void htmlTablesTest() throws IOException {
ClassPathResource pdfFileResource = new ClassPathResource("files/Fludioxonil/51 Fludioxonil_RAR_02_Volume_2_2018-02-21.pdf");
System.out.println("htmlTablesTest");
ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/line_breaks.pdf");
RedactionRequest request = RedactionRequest.builder()
.document(IOUtils.toByteArray(pdfFileResource.getInputStream()))
@@ -266,6 +452,7 @@ public class RedactionIntegrationTest {
@Test
public void htmlTableRotationTest() throws IOException {
System.out.println("htmlTableRotationTest");
ClassPathResource pdfFileResource = new ClassPathResource("files/Metolachlor/S-Metolachlor_RAR_02_Volume_2_2018-09-06.pdf");
RedactionRequest request = RedactionRequest.builder()
@@ -280,6 +467,49 @@ public class RedactionIntegrationTest {
}
@Test
public void phantomCellsDocumentTest() throws IOException {
ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/Phantom Cells.pdf");
RedactionRequest request = RedactionRequest.builder()
.document(IOUtils.toByteArray(pdfFileResource.getInputStream()))
.build();
request.setFlatRedaction(false);
RedactionResult result = redactionController.redact(request);
result.getRedactionLog().getRedactionLogEntry().forEach(entry -> {
if (!entry.isHint()) {
assertThat(entry.getReason()).isEqualTo("Not redacted because row is not a vertebrate study");
}
});
}
@Test
public void sponsorCompanyTest() throws IOException {
long start = System.currentTimeMillis();
ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/sponsor_companies.pdf");
RedactionRequest request = RedactionRequest.builder()
.document(IOUtils.toByteArray(pdfFileResource.getInputStream()))
.build();
request.setFlatRedaction(false);
RedactionResult result = redactionController.redact(request);
try (FileOutputStream fileOutputStream = new FileOutputStream("/tmp/Redacted.pdf")) {
fileOutputStream.write(result.getDocument());
}
long end = System.currentTimeMillis();
System.out.println("duration: " + (end - start));
System.out.println("numberOfPages: " + result.getNumberOfPages());
}
private static String loadFromClassPath(String path) {
URL resource = ResourceLoader.class.getClassLoader().getResource(path);
@@ -0,0 +1,476 @@
package com.iqser.red.service.redaction.v1.server.redaction.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.kie.api.KieServices;
import org.kie.api.builder.KieBuilder;
import org.kie.api.builder.KieFileSystem;
import org.kie.api.builder.KieModule;
import org.kie.api.runtime.KieContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.test.context.junit4.SpringRunner;
import com.iqser.red.service.configuration.v1.api.model.Colors;
import com.iqser.red.service.configuration.v1.api.model.DictionaryResponse;
import com.iqser.red.service.configuration.v1.api.model.RulesResponse;
import com.iqser.red.service.configuration.v1.api.model.TypeResponse;
import com.iqser.red.service.configuration.v1.api.model.TypeResult;
import com.iqser.red.service.redaction.v1.model.RedactionRequest;
import com.iqser.red.service.redaction.v1.server.classification.model.Document;
import com.iqser.red.service.redaction.v1.server.client.DictionaryClient;
import com.iqser.red.service.redaction.v1.server.client.RulesClient;
import com.iqser.red.service.redaction.v1.server.redaction.model.Entity;
import com.iqser.red.service.redaction.v1.server.redaction.utils.ResourceLoader;
import com.iqser.red.service.redaction.v1.server.segmentation.PdfSegmentationService;
@SpringBootTest
@RunWith(SpringRunner.class)
public class EntityRedactionServiceTest {
private static final String DEFAULT_RULES = loadFromClassPath("drools/rules.drl");
private static final String NAME_CODE = "name";
private static final String ADDRESS_CODE = "address";
private static final String SPONSOR_CODE = "sponsor";
private static final AtomicLong DICTIONARY_VERSION = new AtomicLong();
private static final AtomicLong RULES_VERSION = new AtomicLong();
@MockBean
private DictionaryClient dictionaryClient;
@MockBean
private RulesClient rulesClient;
@Autowired
private EntityRedactionService entityRedactionService;
@Autowired
private PdfSegmentationService pdfSegmentationService;
@Autowired
private DroolsExecutionService droolsExecutionService;
@TestConfiguration
public static class RedactionIntegrationTestConfiguration {
@Bean
public KieContainer kieContainer() {
KieServices kieServices = KieServices.Factory.get();
KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
InputStream input = new ByteArrayInputStream(DEFAULT_RULES.getBytes(StandardCharsets.UTF_8));
kieFileSystem.write("src/test/resources/drools/rules.drl", kieServices.getResources()
.newInputStreamResource(input));
KieBuilder kieBuilder = kieServices.newKieBuilder(kieFileSystem);
kieBuilder.buildAll();
KieModule kieModule = kieBuilder.getKieModule();
return kieServices.newKieContainer(kieModule.getReleaseId());
}
}
@Test
public void testNestedEntitiesRemoval() {
Set<Entity> entities = new HashSet<>();
Entity nested = new Entity("nested", "fake type", 10, 16, "fake headline", 0);
Entity nesting = new Entity("nesting nested", "fake type", 2, 16, "fake headline", 0);
entities.add(nested);
entities.add(nesting);
entityRedactionService.removeEntitiesContainedInLarger(entities);
assertThat(entities.size()).isEqualTo(1);
assertThat(entities).contains(nesting);
}
@Test
public void testTableRedaction() throws IOException {
ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/Single Table.pdf");
RedactionRequest redactionRequest = RedactionRequest.builder()
.document(IOUtils.toByteArray(pdfFileResource.getInputStream()))
.build();
DictionaryResponse dictionaryResponse = DictionaryResponse.builder()
.entries(Arrays.asList("Casey, H.W.", "O’Loughlin, C.K.", "Salamon, C.M.", "Smith, S.H."))
.build();
when(dictionaryClient.getVersion()).thenReturn(DICTIONARY_VERSION.incrementAndGet());
when(dictionaryClient.getDictionaryForType(NAME_CODE)).thenReturn(dictionaryResponse);
DictionaryResponse addressResponse = DictionaryResponse.builder()
.entries(Collections.singletonList("Toxigenics, Inc., Decatur, IL 62526, USA"))
.build();
when(dictionaryClient.getDictionaryForType(ADDRESS_CODE)).thenReturn(addressResponse);
try (PDDocument pdDocument = PDDocument.load(new ByteArrayInputStream(redactionRequest.getDocument()))) {
Document classifiedDoc = pdfSegmentationService.parseDocument(pdDocument);
entityRedactionService.processDocument(classifiedDoc, null);
assertThat(classifiedDoc.getEntities()).hasSize(1); // one page
assertThat(classifiedDoc.getEntities().get(1)).hasSize(7);// 3 author cells, 1 address, 1 Y and 2 N entities
}
}
@Test
public void testNestedRedaction() throws IOException {
ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/nested_redaction.pdf");
RedactionRequest redactionRequest = RedactionRequest.builder()
.document(IOUtils.toByteArray(pdfFileResource.getInputStream()))
.build();
DictionaryResponse dictionaryResponse = DictionaryResponse.builder()
.entries(Arrays.asList("Casey, H.W.", "O’Loughlin, C.K.", "Salamon, C.M.", "Smith, S.H."))
.build();
when(dictionaryClient.getVersion()).thenReturn(DICTIONARY_VERSION.incrementAndGet());
when(dictionaryClient.getDictionaryForType(NAME_CODE)).thenReturn(dictionaryResponse);
DictionaryResponse addressResponse = DictionaryResponse.builder()
.entries(Collections.singletonList("Toxigenics, Inc., Decatur, IL 62526, USA"))
.build();
when(dictionaryClient.getDictionaryForType(ADDRESS_CODE)).thenReturn(addressResponse);
try (PDDocument pdDocument = PDDocument.load(new ByteArrayInputStream(redactionRequest.getDocument()))) {
Document classifiedDoc = pdfSegmentationService.parseDocument(pdDocument);
entityRedactionService.processDocument(classifiedDoc, null);
assertThat(classifiedDoc.getEntities()).hasSize(1); // one page
assertThat(classifiedDoc.getEntities().get(1)).hasSize(7);// 3 author cells, 1 address, 1 Y and 2 N entities
}
}
@Test
public void testTrueNegativesInTable() throws IOException {
ClassPathResource pdfFileResource = new ClassPathResource("files/Cyprodinil/40 Cyprodinil - EU AIR3 - LCA Section 1" +
" Supplement - Identity of the active substance - Reference list.pdf");
when(dictionaryClient.getVersion()).thenReturn(DICTIONARY_VERSION.incrementAndGet());
DictionaryResponse dictionaryResponse = DictionaryResponse.builder()
.entries(new ArrayList<>(ResourceLoader.load("dictionaries/names.txt")))
.build();
when(dictionaryClient.getDictionaryForType(NAME_CODE)).thenReturn(dictionaryResponse);
DictionaryResponse addressResponse = DictionaryResponse.builder()
.entries(new ArrayList<>(ResourceLoader.load("dictionaries/addresses.txt")))
.build();
when(dictionaryClient.getDictionaryForType(ADDRESS_CODE)).thenReturn(addressResponse);
try (PDDocument pdDocument = PDDocument.load(pdfFileResource.getInputStream())) {
Document classifiedDoc = pdfSegmentationService.parseDocument(pdDocument);
entityRedactionService.processDocument(classifiedDoc, null);
assertThat(classifiedDoc.getEntities()
.entrySet()
.stream()
.noneMatch(entry -> entry.getValue().stream().anyMatch(e -> e.getMatchedRule() == 9))).isTrue();
}
pdfFileResource = new ClassPathResource("files/Compounds/27 A8637C - EU AIR3 - MCP Section 1 - Identity of " +
"the plant protection product.pdf");
try (PDDocument pdDocument = PDDocument.load(pdfFileResource.getInputStream())) {
Document classifiedDoc = pdfSegmentationService.parseDocument(pdDocument);
entityRedactionService.processDocument(classifiedDoc, null);
assertThat(classifiedDoc.getEntities()
.entrySet()
.stream()
.noneMatch(entry -> entry.getValue().stream().anyMatch(e -> e.getMatchedRule() == 9))).isTrue();
}
}
@Test
public void testFalsePositiveInWrongCell() throws IOException {
ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/Row With Ambiguous Redaction.pdf");
when(dictionaryClient.getVersion()).thenReturn(DICTIONARY_VERSION.incrementAndGet());
DictionaryResponse dictionaryResponse = DictionaryResponse.builder()
.entries(new ArrayList<>(ResourceLoader.load("dictionaries/names.txt")))
.build();
when(dictionaryClient.getDictionaryForType(NAME_CODE)).thenReturn(dictionaryResponse);
DictionaryResponse addressResponse = DictionaryResponse.builder()
.entries(new ArrayList<>(ResourceLoader.load("dictionaries/addresses.txt")))
.build();
when(dictionaryClient.getDictionaryForType(ADDRESS_CODE)).thenReturn(addressResponse);
try (PDDocument pdDocument = PDDocument.load(pdfFileResource.getInputStream())) {
Document classifiedDoc = pdfSegmentationService.parseDocument(pdDocument);
entityRedactionService.processDocument(classifiedDoc, null);
assertThat(classifiedDoc.getEntities()).hasSize(1); // one page
assertThat(classifiedDoc.getEntities().get(1).stream()
.filter(entity -> entity.getMatchedRule() == 9)
.count()).isEqualTo(10);
}
}
@Test
public void testApplicantInTableRedaction() throws IOException {
String tableRules = "package drools\n" +
"\n" +
"import com.iqser.red.service.redaction.v1.server.redaction.model.Section\n" +
"\n" +
"global Section section\n" +
"rule \"6: Redact contact information if applicant is found\"\n" +
" when\n" +
" eval(section.headlineContainsWord(\"applicant\") || section.getText().contains(\"Applicant\"));\n" +
" then\n" +
" section.redactLineAfter(\"Name:\", \"address\", 6, \"Applicant information was found\", \"Reg" +
" (EC) No 1107/2009 Art. 63 (2g)\");\n" +
" section.redactBetween(\"Address:\", \"Contact\", \"address\", 6, \"Applicant information was found\", \"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" +
" section.redactLineAfter(\"Contact point:\", \"address\", 6, \"Applicant information was found\", \"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" +
" section.redactLineAfter(\"Phone:\", \"address\", 6, \"Applicant information was found\", " +
"\"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" +
" section.redactLineAfter(\"Fax:\", \"address\", 6, \"Applicant information was found\", \"Reg " +
"(EC) No 1107/2009 Art. 63 (2g)\");\n" +
" section.redactLineAfter(\"Tel.:\", \"address\", 6, \"Applicant information was found\", \"Reg" +
" (EC) No 1107/2009 Art. 63 (2g)\");\n" +
" section.redactLineAfter(\"Tel:\", \"address\", 6, \"Applicant information was found\", \"Reg " +
"(EC) No 1107/2009 Art. 63 (2g)\");\n" +
" section.redactLineAfter(\"E-mail:\", \"address\", 6, \"Applicant information was found\", " +
"\"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" +
" section.redactLineAfter(\"Email:\", \"address\", 6, \"Applicant information was found\", " +
"\"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" +
" section.redactLineAfter(\"Contact:\", \"address\", 6, \"Applicant information was found\", " +
"\"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" +
" section.redactLineAfter(\"Telephone number:\", \"address\", 6, \"Applicant information was found\", \"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" +
" section.redactLineAfter(\"Fax number:\", \"address\", 6, \"Applicant information was found\"," +
" \"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" +
" section.redactLineAfter(\"Telephone:\", \"address\", 6, \"Applicant information was found\", " +
"\"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" +
" section.redactBetween(\"No:\", \"Fax\", \"address\", 6, \"Applicant information was found\", " +
"\"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" +
" section.redactBetween(\"Contact:\", \"Tel.:\", \"address\", 6, \"Applicant information was found\", \"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" +
" end";
when(rulesClient.getVersion()).thenReturn(RULES_VERSION.incrementAndGet());
when(rulesClient.getRules()).thenReturn(new RulesResponse(tableRules));
droolsExecutionService.updateRules();
ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/Applicant Producer Table.pdf");
when(dictionaryClient.getVersion()).thenReturn(DICTIONARY_VERSION.incrementAndGet());
DictionaryResponse dictionaryResponse = DictionaryResponse.builder()
.entries(new ArrayList<>(ResourceLoader.load("dictionaries/names.txt")))
.build();
when(dictionaryClient.getDictionaryForType(NAME_CODE)).thenReturn(dictionaryResponse);
DictionaryResponse addressResponse = DictionaryResponse.builder()
.entries(new ArrayList<>(ResourceLoader.load("dictionaries/addresses.txt")))
.build();
when(dictionaryClient.getDictionaryForType(ADDRESS_CODE)).thenReturn(addressResponse);
try (PDDocument pdDocument = PDDocument.load(pdfFileResource.getInputStream())) {
Document classifiedDoc = pdfSegmentationService.parseDocument(pdDocument);
entityRedactionService.processDocument(classifiedDoc, null);
assertThat(classifiedDoc.getEntities()).hasSize(1); // one page
assertThat(classifiedDoc.getEntities().get(1).stream()
.filter(entity -> entity.getMatchedRule() == 6)
.count()).isEqualTo(18);
}
}
@Test
public void testSponsorInCell() throws IOException {
String tableRules = "package drools\n" +
"\n" +
"import com.iqser.red.service.redaction.v1.server.redaction.model.Section\n" +
"\n" +
"global Section section\n" + "rule \"11: Redact sponsor company\"\n" + " when\n" + " " +
"Section(searchText.toLowerCase().contains(\"batches produced at\"))\n" + " then\n" + " section" +
".redactIfPrecededBy(\"batches produced at\", \"sponsor\", 11, \"Redacted because it represents a " +
"sponsor company\", \"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" + " end";
when(rulesClient.getVersion()).thenReturn(RULES_VERSION.incrementAndGet());
when(rulesClient.getRules()).thenReturn(new RulesResponse(tableRules));
droolsExecutionService.updateRules();
ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/batches_new_line.pdf");
when(dictionaryClient.getVersion()).thenReturn(DICTIONARY_VERSION.incrementAndGet());
DictionaryResponse dictionaryResponse = DictionaryResponse.builder()
.entries(new ArrayList<>(ResourceLoader.load("dictionaries/sponsor_companies.txt")))
.build();
when(dictionaryClient.getDictionaryForType(SPONSOR_CODE)).thenReturn(dictionaryResponse);
try (PDDocument pdDocument = PDDocument.load(pdfFileResource.getInputStream())) {
Document classifiedDoc = pdfSegmentationService.parseDocument(pdDocument);
entityRedactionService.processDocument(classifiedDoc, null);
assertThat(classifiedDoc.getEntities()).hasSize(1); // one page
assertThat(classifiedDoc.getEntities().get(1).stream()
.filter(entity -> entity.getMatchedRule() == 11)
.count()).isEqualTo(1);
}
}
@Test
public void headerPropagation() throws IOException {
ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/Header Propagation.pdf");
DictionaryResponse dictionaryResponse = DictionaryResponse.builder()
.entries(Arrays.asList("Bissig R.", "Thanei P."))
.build();
when(dictionaryClient.getVersion()).thenReturn(DICTIONARY_VERSION.incrementAndGet());
when(dictionaryClient.getDictionaryForType(NAME_CODE)).thenReturn(dictionaryResponse);
DictionaryResponse addressResponse = DictionaryResponse.builder()
.entries(Collections.singletonList("Novartis Crop Protection AG, Basel, Switzerland"))
.build();
when(dictionaryClient.getDictionaryForType(ADDRESS_CODE)).thenReturn(addressResponse);
try (PDDocument pdDocument = PDDocument.load(pdfFileResource.getInputStream())) {
Document classifiedDoc = pdfSegmentationService.parseDocument(pdDocument);
entityRedactionService.processDocument(classifiedDoc, null);
assertThat(classifiedDoc.getEntities()).hasSize(2); // two pages
assertThat(classifiedDoc.getEntities().get(1).stream().filter(entity -> entity.getMatchedRule() == 9).count()).isEqualTo(8);
assertThat(classifiedDoc.getEntities().get(2).stream().filter(entity -> entity.getMatchedRule() == 9).count()).isEqualTo(5); // 2 names, 1 address, 2 Y
}
pdfFileResource = new ClassPathResource("files/Minimal Examples/Header Propagation2.pdf");
dictionaryResponse = DictionaryResponse.builder()
.entries(Arrays.asList("Tribolet, R.", "Muir, G.", "Kühne-Thu, H.", "Close, C."))
.build();
when(dictionaryClient.getVersion()).thenReturn(DICTIONARY_VERSION.incrementAndGet());
when(dictionaryClient.getDictionaryForType(NAME_CODE)).thenReturn(dictionaryResponse);
addressResponse = DictionaryResponse.builder()
.entries(Collections.singletonList("Novartis Crop Protection AG, Basel, Switzerland"))
.build();
when(dictionaryClient.getDictionaryForType(ADDRESS_CODE)).thenReturn(addressResponse);
try (PDDocument pdDocument = PDDocument.load(pdfFileResource.getInputStream())) {
Document classifiedDoc = pdfSegmentationService.parseDocument(pdDocument);
entityRedactionService.processDocument(classifiedDoc, null);
assertThat(classifiedDoc.getEntities()).hasSize(1); // one page
assertThat(classifiedDoc.getEntities().get(1).stream().filter(entity -> entity.getMatchedRule() == 9).count()).isEqualTo(3);
assertThat(classifiedDoc.getEntities().get(1).stream().filter(entity -> entity.getMatchedRule() == 8).count()).isEqualTo(8);
}
}
@Test
public void testNGuideline() throws IOException {
ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/Empty Tabular Data.pdf");
DictionaryResponse dictionaryResponse = DictionaryResponse.builder()
.entries(Collections.singletonList("Aldershof S."))
.build();
when(dictionaryClient.getVersion()).thenReturn(DICTIONARY_VERSION.incrementAndGet());
when(dictionaryClient.getDictionaryForType(NAME_CODE)).thenReturn(dictionaryResponse);
DictionaryResponse addressResponse = DictionaryResponse.builder()
.entries(Collections.singletonList("Novartis Crop Protection AG, Basel, Switzerland"))
.build();
when(dictionaryClient.getDictionaryForType(ADDRESS_CODE)).thenReturn(addressResponse);
try (PDDocument pdDocument = PDDocument.load(pdfFileResource.getInputStream())) {
Document classifiedDoc = pdfSegmentationService.parseDocument(pdDocument);
entityRedactionService.processDocument(classifiedDoc, null);
assertThat(classifiedDoc.getEntities()).hasSize(1); // one page
assertThat(classifiedDoc.getEntities().get(1).stream().filter(entity -> entity.getMatchedRule() == 8).count()).isEqualTo(6);
}
}
@Before
public void stubRedaction() {
String tableRules = "package drools\n" +
"\n" +
"import com.iqser.red.service.redaction.v1.server.redaction.model.Section\n" +
"\n" +
"global Section section\n" +
"rule \"8: Not redacted because Vertebrate Study = N\"\n" +
" when\n" +
" Section(rowEquals(\"Vertebrate study Y/N\", \"N\") || rowEquals(\"Vertebrate study Y/N\", \"No\"))\n" +
" then\n" +
" section.redactNotCell(\"Author(s)\", 8, \"name\", \"Not redacted because row is not a vertebrate study\");\n" +
" section.redactNot(\"address\", 8, \"Not redacted because row is not a vertebrate study\");\n" +
" section.highlightCell(\"Vertebrate study Y/N\", 8, \"hint_only\");\n" +
" end\n" +
"rule \"9: Redact Authors and Addresses in Reference Table, if it is a Vertebrate study\"\n" +
" when\n" +
" Section(rowEquals(\"Vertebrate study Y/N\", \"Y\") || rowEquals(\"Vertebrate study Y/N\", " +
"\"Yes\"))\n" +
" then\n" +
" section.redactCell(\"Author(s)\", 9, \"name\", \"Redacted because row is a vertebrate study\", \"Reg (EC) No 1107/2009 Art. 63 (2g)\");\n" +
" section.redact(\"address\", 9, \"Redacted because row is a vertebrate study\", \"Reg (EC) No" +
" 1107/2009 Art. 63 (2g)\");\n" +
" section.highlightCell(\"Vertebrate study Y/N\", 9, \"must_redact\");\n" +
" end";
when(rulesClient.getVersion()).thenReturn(RULES_VERSION.incrementAndGet());
when(rulesClient.getRules()).thenReturn(new RulesResponse(tableRules));
TypeResponse typeResponse = TypeResponse.builder()
.types(Arrays.asList(
TypeResult.builder().type(NAME_CODE).hexColor("#ffff00").build(),
TypeResult.builder().type(ADDRESS_CODE).hexColor("#ff00ff").build(),
TypeResult.builder().type(SPONSOR_CODE).hexColor("#00ffff").build()))
.build();
when(dictionaryClient.getVersion()).thenReturn(DICTIONARY_VERSION.incrementAndGet());
when(dictionaryClient.getAllTypes()).thenReturn(typeResponse);
// Default empty return to prevent NPEs
DictionaryResponse dictionaryResponse = DictionaryResponse.builder()
.build();
when(dictionaryClient.getDictionaryForType(NAME_CODE)).thenReturn(dictionaryResponse);
DictionaryResponse addressResponse = DictionaryResponse.builder()
.build();
when(dictionaryClient.getDictionaryForType(ADDRESS_CODE)).thenReturn(addressResponse);
DictionaryResponse sponsorResponse = DictionaryResponse.builder()
.build();
when(dictionaryClient.getDictionaryForType(SPONSOR_CODE)).thenReturn(sponsorResponse);
Colors colors = new Colors();
colors.setDefaultColor("#acfc00");
colors.setNotRedacted("#cccccc");
colors.setRequestAdd("#04b093");
colors.setRequestRemove("#04b093");
when(dictionaryClient.getColors()).thenReturn(colors);
}
private static String loadFromClassPath(String path) {
URL resource = ResourceLoader.class.getClassLoader().getResource(path);
if (resource == null) {
throw new IllegalArgumentException("could not load classpath resource: drools/rules.drl");
}
try (BufferedReader br = new BufferedReader(new InputStreamReader(resource.openStream(), StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String str;
while ((str = br.readLine()) != null) {
sb.append(str).append("\n");
}
return sb.toString();
} catch (IOException e) {
throw new IllegalArgumentException("could not load classpath resource: " + path, e);
}
}
}
@@ -0,0 +1,189 @@
package com.iqser.red.service.redaction.v1.server.segmentation;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.kie.api.runtime.KieContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.test.context.junit4.SpringRunner;
import com.iqser.red.service.redaction.v1.server.classification.model.Document;
import com.iqser.red.service.redaction.v1.server.classification.service.BlockificationService;
import com.iqser.red.service.redaction.v1.server.tableextraction.model.Cell;
import com.iqser.red.service.redaction.v1.server.tableextraction.model.Table;
import com.iqser.red.service.redaction.v1.server.tableextraction.service.RulingCleaningService;
import com.iqser.red.service.redaction.v1.server.tableextraction.service.TableExtractionService;
@SpringBootTest
@RunWith(SpringRunner.class)
public class PdfSegmentationServiceTest {
@Autowired
private PdfSegmentationService pdfSegmentationService;
@Autowired
private RulingCleaningService rulingCleaningService;
@Autowired
private TableExtractionService tableExtractionService;
@Autowired
private BlockificationService blockificationService;
@MockBean
private KieContainer kieContainer;
@Test
public void testPDFSegmentationWithComplexTable() throws IOException {
ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/Spanning Cells.pdf");
try (PDDocument pdDocument = PDDocument.load(pdfFileResource.getInputStream())) {
Document document = pdfSegmentationService.parseDocument(pdDocument);
assertThat(document.getParagraphs()
.stream()
.flatMap(paragraph -> paragraph.getTables().stream())
.collect(Collectors.toList())).isNotEmpty();
Table table = document.getParagraphs()
.stream()
.flatMap(paragraph -> paragraph.getTables().stream())
.collect(Collectors.toList())
.get(0);
assertThat(table.getColCount()).isEqualTo(6);
assertThat(table.getRowCount()).isEqualTo(13);
assertThat(table.getRows().stream().mapToInt(List::size).sum()).isEqualTo(6 * 13);
}
}
@Test
public void testTableExtraction() throws IOException {
ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/Merge Table.pdf");
try (PDDocument pdDocument = PDDocument.load(pdfFileResource.getInputStream())) {
Document document = pdfSegmentationService.parseDocument(pdDocument);
assertThat(document.getParagraphs()
.stream()
.flatMap(paragraph -> paragraph.getTables().stream())
.collect(Collectors.toList())).isNotEmpty();
Table firstTable = document.getParagraphs()
.stream()
.flatMap(paragraph -> paragraph.getTables().stream())
.collect(Collectors.toList())
.get(0);
assertThat(firstTable.getColCount()).isEqualTo(8);
assertThat(firstTable.getRowCount()).isEqualTo(1);
Table secondTable = document.getParagraphs()
.stream()
.flatMap(paragraph -> paragraph.getTables().stream())
.collect(Collectors.toList())
.get(1);
assertThat(secondTable.getColCount()).isEqualTo(8);
assertThat(secondTable.getRowCount()).isEqualTo(2);
List<List<Cell>> firstTableHeaderCells = firstTable.getRows()
.get(0)
.stream()
.map(Collections::singletonList)
.collect(Collectors.toList());
assertThat(secondTable.getRows().stream()
.allMatch(row -> row.stream()
.map(Cell::getHeaderCells)
.collect(Collectors.toList())
.equals(firstTableHeaderCells)))
.isTrue();
}
}
@Test
public void testMultiPageMetadataPropagation() throws IOException {
ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/Merge Multi Page Table.pdf");
try (PDDocument pdDocument = PDDocument.load(pdfFileResource.getInputStream())) {
Document document = pdfSegmentationService.parseDocument(pdDocument);
assertThat(document.getParagraphs()
.stream()
.flatMap(paragraph -> paragraph.getTables().stream())
.collect(Collectors.toList())).isNotEmpty();
Table firstTable = document.getParagraphs()
.stream()
.flatMap(paragraph -> paragraph.getTables().stream())
.collect(Collectors.toList())
.get(0);
assertThat(firstTable.getColCount()).isEqualTo(9);
assertThat(firstTable.getRowCount()).isEqualTo(5);
Table secondTable = document.getParagraphs()
.stream()
.flatMap(paragraph -> paragraph.getTables().stream())
.collect(Collectors.toList())
.get(1);
assertThat(secondTable.getColCount()).isEqualTo(9);
assertThat(secondTable.getRowCount()).isEqualTo(6);
List<List<Cell>> firstTableHeaderCells = firstTable.getRows()
.get(firstTable.getRowCount() - 1)
.stream()
.map(Cell::getHeaderCells)
.collect(Collectors.toList());
assertThat(secondTable.getRows().stream()
.allMatch(row -> row.stream()
.map(Cell::getHeaderCells)
.collect(Collectors.toList())
.equals(firstTableHeaderCells)))
.isTrue();
}
}
@Test
public void testHeaderCellsForRotatedTable() throws IOException {
ClassPathResource pdfFileResource = new ClassPathResource("files/Minimal Examples/Rotated Table Headers.pdf");
try (PDDocument pdDocument = PDDocument.load(pdfFileResource.getInputStream())) {
Document document = pdfSegmentationService.parseDocument(pdDocument);
assertThat(document.getParagraphs()
.stream()
.flatMap(paragraph -> paragraph.getTables().stream())
.collect(Collectors.toList())).isNotEmpty();
Table firstTable = document.getParagraphs()
.stream()
.flatMap(paragraph -> paragraph.getTables().stream())
.collect(Collectors.toList())
.get(0);
assertThat(firstTable.getColCount()).isEqualTo(8);
assertThat(firstTable.getRowCount()).isEqualTo(1);
Table secondTable = document.getParagraphs()
.stream()
.flatMap(paragraph -> paragraph.getTables().stream())
.collect(Collectors.toList())
.get(1);
assertThat(secondTable.getColCount()).isEqualTo(8);
assertThat(secondTable.getRowCount()).isEqualTo(6);
List<List<Cell>> firstTableHeaderCells = firstTable.getRows()
.get(0)
.stream()
.map(Collections::singletonList)
.collect(Collectors.toList());
assertThat(secondTable.getRows().stream()
.allMatch(row -> row.stream()
.map(Cell::getHeaderCells)
.collect(Collectors.toList())
.equals(firstTableHeaderCells)))
.isTrue();
}
}
}
@@ -0,0 +1,2 @@
guideline
unpublished
@@ -0,0 +1,2 @@
CTL
determination of residues
@@ -0,0 +1,3 @@
published paper
in vitro
in-vitro
@@ -0,0 +1,9 @@
in vivo
in-vivo
dermal penetration
oral toxicity
oral-toxicity
acute toxicity
acute-toxicity
eco toxicity
eco-toxicity
@@ -0,0 +1,7 @@
Monthey Syngenta Crop Protection AG, Basel, Switzerland
Syngenta Crop Protection, Monthey, Switzerland
Fine Organics Limited, Middlesbrough, United Kingdom
Syngenta Monthey Switzerland
Hunan Haili Chemical Industry Co., Ltd., Hunan, China
Syngenta, Switzerland
Syngenta Nantong, China
@@ -1,48 +1,63 @@
Vulpes vulpes
a. sylvaticus
african clawed frog
agalychnis callidryas
albino rat
american bullfrog tadpole
american toad
amphibian
amphibians
American bullfrog tadpole
american toad
anad platyrhynchos
Anas platyrhynchos
anas platyrhynchos
anuran
anurans
apodemus
apodemus flavicollis
apodemus syl vaticus
apodemus sylvaticus
arvicola terrestris
avian
bank vole
bird
birds
bluegill
bluegill sunfish
bobwhite
bobwhite quail
bullfrog
Bufo americanus
brachydanio rerio
brown hare
bufo americanus
bullfrog
canary
carassius carassius
carp
catesbeiana
catfish
cattle
cattles
channel catfish
Chinook
chicken
Colinus virginianus
chinese hamster
chinese hamsters
chinook
coho salmon
colinus virginianus
Common carp
columba palumbus
columbidae
common carp
common vole
coturnix japonica
Coturnix japonica
cow
cows
Crucian carp
crocidura russula
crucian carp
cyprinodon variegatus
cyprinus carpio
dog
dogs
duck
ducks
european brown hare
european rabbit
fathead minnow
fish
fishes
@@ -56,174 +71,121 @@ galaxias truttaceus
gasterosteus aculeatus
goat
goats
greater white-toothed shrew
guinea
guinea pig
guinea pigs
Guppy
guinea-pigs
guppy
hamster
hamsters
hen
hens
Hyla versicolor
house mouse
hyla versicolor
ictalurus melas
ictalurus punctatus
japanese quail
japonica
kisutch
lagomorph
lebistes reticulatus
leiostomus xanthurus
leisostomus xanthurus
lepomis macrochirus
lepus europaeus
limnocharis
limnodynastes
limnodynastes tasmaniensis
livestock
livestocks
mallard
mallard duck
mammal
mammalian
mammals
Mammalian
marten
martes
mice
microtus
microtus agrestis
microtus arvalis
microtus subterraneus
midwestern anurans
minnow
minnows
monkey
mouse
mus musculus
myodes glareolus
northern bobwhite
o. cuniculus
o. mykiss
Oncorhynchus mykiss
Oncorhynchus
O. mykiss
o. tshawytscha
oncorhynchus
oncorhynchus mykiss
oncorhynchus tshawytscha
oryctolagus cuniculus
oryzias melastigma
oryzias melastigma larvae
p. promelas
pagrus major
palumbus
pig
pigeon
pigeons
pigs
pimephales promela
pimephales promelas
Pseudacris triseriata
poecilia reticulata
poultry
pseudacris
pseudacris triseriata
quail
r. catesbeiana
rabbit
rabbits
rainbow trout
Rana limnocharis
rana
limnocharis
rana catesbeiana
rana limnocharis
rana pipiens
rat
rats
reptile
reptiles
ricefish
ruminant
ruminants
salmo gairdneri
salmon
serinus canaria
sheepshead minnow
sheepshead minnows
spea multiplicata
Salmo gairdneri
salmon
spotted march frog
tadpoles
treefrog
toad
terrestrial vertrebrates
Limnodynastes tasmaniensis
trout
Vulpes vulpes
wistar
xenopus laevis
xenpous leavis
zebra fish
zebrafish
Salmo gairdneri
minnow
minnows
Pimephales promela
Cyprinodon variegatus
limnodynastes
Rana catesbeiana
R. catesbeiana
coho salmon
Oncorhynchus tshawytscha
O. tshawytscha
tshawytscha
catesbeiana
kisutch
Pseudacris triseriata
Pseudacris
triseriata
Wood pigeon
Columba palumbus
palumbus
Columbidae
shrew
shrews
bank vole
common vole
sorex araneus
spea multiplicata
spotted march frog
tadpoles
terrestrial vertrebrates
toad
treefrog
triseriata
trout
tshawytscha
vole
voles
lagomorph
Wood mouse
Apodemus sylvaticus
A. sylvaticus
Apodemus flavicollis
Apodemus
mus musculus
Microtus arvalis
Microtus agrestis
Microtus
Arvicola terrestris
Sorex araneus
Myodes glareolus
yellow-necked mouse
house mouse
Oryctolagus cuniculus
marten
martes
vulpes vulpes
white rabbits
white-toothed shrew
greater white-toothed shrew
Lepus europaeus
brown hare
European brown hare
European rabbit
O. cuniculus
Crocidura russula
Chinese Hamster
Rat
Rats
Dog
Chinese hamsters
Chinese hamster
Mouse
Guinea pig
Wistar rats
Rabbit
mammalian
Japanese quail
Microtus subterraneus
Lepomis macrochirus
P. promelas
Cyprinus carpio
Fish
Ictalurus punctatus
Carassius carassius
Lepomis macrochirus
Poecilia reticulata
Lebistes reticulatus
Lepomis macrochirus
Leiostomus xanthurus
Pimephales promelas
Lepomis macrochirus
Albino rat
Hen
Goat
Livestock
Guinea Pigs
Hamster
wistar
wistar rats
wood mice
wood mouse
Rabbits
Mice
Rainbow trout
Canary
Serinus canaria
Guinea Pig
Cow
Pigs
Poultry
Guinea-pigs
White rabbits
Birds
Wood mice
wood pigeon
xenopus laevis
xenpous leavis
yellow-necked mouse
zebra fish
zebrafish
@@ -7,16 +7,16 @@ global Section section
rule "1: Redacted because Section contains Vertebrate"
when
eval(section.contains("vertebrate")==true);
Section(matchesType("vertebrate"))
then
section.redact("name", 1, "Redacted because Section contains Vertebrate");
section.redact("address", 1, "Redacted because Section contains Vertebrate");
section.redact("name", 1, "Redacted because Section contains Vertebrate", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redact("address", 1, "Redacted because Section contains Vertebrate", "Reg (EC) No 1107/2009 Art. 63 (2g)");
end
rule "2: Not Redacted because Section contains no Vertebrate"
when
eval(section.contains("vertebrate")==false);
Section(!matchesType("vertebrate"))
then
section.redactNot("name", 2, "Not Redacted because Section contains no Vertebrate");
section.redactNot("address", 2, "Not Redacted because Section contains no Vertebrate");
@@ -25,34 +25,111 @@ rule "2: Not Redacted because Section contains no Vertebrate"
rule "3: Do not redact Names and Addresses if no redaction Indicator is contained"
when
eval(section.contains("vertebrate")==true && section.contains("no_redaction_indicator")==true);
Section(matchesType("vertebrate"), matchesType("no_redaction_indicator"))
then
section.redactNot("name", 3, "Vertebrate was found, but also a no redaction indicator");
section.redactNot("address", 3, "Vertebrate was found, but also a no redaction indicator");
end
rule "4: Redact contact information, if applicant is found"
rule "4: Redact Names and Addresses if no_redaction_indicator and redaction_indicator is contained"
when
eval(section.getText().toLowerCase().contains("applicant"));
Section(matchesType("vertebrate"), matchesType("no_redaction_indicator"), matchesType("redaction_indicator"))
then
section.redactLineAfter("Name:", "address", 4, "Redacted because of Rule 4");
section.redactBetween("Address:", "Contact", "address", 4, "Redacted because of Rule 4");
section.redactLineAfter("Contact point:", "address", 4, "Redacted because of Rule 4");
section.redactLineAfter("Phone:", "address", 4, "Redacted because of Rule 4");
section.redactLineAfter("Fax:", "address", 4, "Redacted because of Rule 4");
section.redactLineAfter("E-mail:", "address", 4, "Redacted because of Rule 4");
section.redact("name", 4, "Vertebrate was found and no_redaction_indicator and redaction_indicator", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redact("address", 4, "Vertebrate was found and no_redaction_indicator and redaction_indicator", "Reg (EC) No 1107/2009 Art. 63 (2g)");
end
rule "5: Redact contact information, if 'Producer of the plant protection product' is found"
rule "5: Do not redact in guideline sections"
when
eval(section.getText().contains("Producer of the plant protection product"));
Section(headlineContainsWord("guideline") || headlineContainsWord("Guidance"))
then
section.redactLineAfter("Name:", "address", 5, "xxxx");
section.redactBetween("Address:", "Contact", "address", 5, "xxxx");
section.redactBetween("Contact:", "Phone", "address", 5, "xxxx");
section.redactLineAfter("Phone:", "address", 5, "xxxx");
section.redactLineAfter("Fax:", "address", 5, "xxxx");
section.redactLineAfter("E-mail:", "address", 5, "xxxx");
end
section.redactNot("name", 5, "Section is a guideline section.");
section.redactNot("address", 5, "Section is a guideline section.");
end
rule "6: Redact contact information if applicant is found"
when
Section(headlineContainsWord("applicant") || text.contains("Applicant") || headlineContainsWord("Primary contact") || headlineContainsWord("Alternative contact"))
then
section.redactLineAfter("Name:", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactBetween("Address:", "Contact", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Contact point:", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Phone:", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Fax:", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Tel.:", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Tel:", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("E-mail:", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Email:", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("e-mail:", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("E-mail address:", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Contact:", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Alternative contact:", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Telephone number:", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Telephone No:", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Fax number:", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Telephone:", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Company:", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactBetween("No:", "Fax", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactBetween("Contact:", "Tel.:", "address", 6, "Applicant information was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
end
rule "7: Redact contact information if Producer is found"
when
Section(text.toLowerCase().contains("producer of the plant protection") || text.toLowerCase().contains("producer of the active substance") || text.contains("Manufacturer of the active substance") || text.contains("Manufacturer:") || text.contains("Producer or producers of the active substance"))
then
section.redactLineAfter("Name:", "address", 7, "Producer was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactBetween("Address:", "Contact", "address", 7, "Producer was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactBetween("Contact:", "Phone", "address", 7, "Producer was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactBetween("Contact:", "Telephone number:", "address", 7, "Producer was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactBetween("Address:", "Manufacturing", "address", 7, "Producer was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Telephone:", "address", 7, "Producer was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Phone:", "address", 7, "Producer was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Fax:", "address", 7, "Producer was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("E-mail:", "address", 7, "Producer was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Contact:", "address", 7, "Producer was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Fax number:", "address", 7, "Producer was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Telephone number:", "address", 7, "Producer was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactLineAfter("Tel:", "address", 7, "Producer was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redactBetween("No:", "Fax", "address", 7, "Producer was found", "Reg (EC) No 1107/2009 Art. 63 (2g)");
end
rule "8: Not redacted because Vertebrate Study = N"
when
Section(rowEquals("Vertebrate study Y/N", "N") || rowEquals("Vertebrate study Y/N", "No"))
then
section.redactNotCell("Author(s)", 8, "name", "Not redacted because row is not a vertebrate study");
section.redactNot("address", 8, "Not redacted because row is not a vertebrate study");
section.highlightCell("Vertebrate study Y/N", 8, "hint_only");
end
rule "9: Redact if must redact entry is found"
when
Section(matchesType("must_redact"))
then
section.redact("name", 9, "must_redact entry was found.", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redact("address", 9, "must_redact entry was found.", "Reg (EC) No 1107/2009 Art. 63 (2g)");
end
rule "10: Redact Authors and Addresses in Reference Table if it is a Vertebrate study"
when
Section(rowEquals("Vertebrate study Y/N", "Y") || rowEquals("Vertebrate study Y/N", "Yes"))
then
section.redactCell("Author(s)", 10, "name", "Redacted because row is a vertebrate study", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.redact("address", 10, "Redacted because row is a vertebrate study", "Reg (EC) No 1107/2009 Art. 63 (2g)");
section.highlightCell("Vertebrate study Y/N", 10, "must_redact");
end
rule "11: Redact sponsor company"
when
Section(searchText.toLowerCase().contains("batches produced at"))
then
section.redactIfPrecededBy("batches produced at", "sponsor", 11, "Redacted because it represents a sponsor company", "Reg (EC) No 1107/2009 Art. 63 (2g)");
end
File diff suppressed because one or more lines are too long