You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

2054 lines
114 KiB

# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from <pkg>-buildversion.txt
# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.3.tm
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2025
#
# @@ Meta Begin
# Application punk::pdf 999999.0a1.0
# Meta platform tcl
# Meta license MIT
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::pdf 0 999999.0a1.0]
#[copyright "2025"]
#[titledesc {Module API}] [comment {-- Name section and table of contents description --}]
#[moddesc {-}] [comment {-- Description at end of page heading --}]
#[require punk::pdf]
#[keywords module]
#[description]
#[para] -
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section Overview]
#[para] overview of punk::pdf
#[subsection Concepts]
#[para] -
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[subsection dependencies]
#[para] packages used by punk::pdf
#[list_begin itemized]
package require Tcl 8.6-
package require tclMuPDF
package require punk::args
package require punk::ansi
package require punk::lib
package require textblock
#*** !doctools
#[item] [package {Tcl 8.6}]
#*** !doctools
#[list_end]
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[section API]
# ++ +++ ++ + +++ +++ +++ +++ +++ +++ +++ +++
namespace eval ::punk::pdf::system {} ;#full definition below
namespace eval ::punk::pdf {
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Base namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[subsection {Namespace punk::pdf}]
#[para] Core API functions for punk::pdf
#[list_begin definitions]
namespace import ::punk::ansi::a
namespace path ::punk::pdf::system
variable base_x_per_c 4 ;#fixed width output font's x_per_c.
variable cfg_blocksep_default "#pdf::text:blockstart type %type% pageindex %pageindex% blockindex %blockindex% y-index %y-index% tlc {%x0% %y0%} brc {%brc%} warnings %warnings% : %marker%"
variable cfg_pagesep_default "#pdf::text:pagestart index %index% pageblocks %pageblocks% textblocks %textblocks% imageblocks %imageblocks% : [a bold green]PAGE[a]"
variable cfg_header_default
set cfg_header_default \
"#pdf::text::header doc %doc% pages %pages% size %size% sha1 %sha1% punk::pdf %parserversion%
# args
# %args%"
variable callid 0
variable results
variable test
array set results {}
punk::args::define {
@id -id ::punk::pdf::text
@cmd -name punk::pdf::text\
-summary\
"Extract text from non-image parts of a PDF"\
-help\
"Extract text lines from a pdf
(No OCR - will only retrieve actual text contents)
The text function can only operate on the text spans provided by the underlying
engine (MuPDF) - which in some cases (for example spaces adjacent to brackets in
PDF32000_2008.pdf p24 eg4) does not interpret the number of whitespace characters
as a human reader would. The text data supplied for formulas such as in 7.10.1 of
the above document can be missing symbols such as the square-root symbol. This would
have to be converted to a unicode square root symbol with additional bracketing to
maintain meaning. The positioning of subscript numerals would also need to be
processed - and without font size information from tclMuPDF - this is currently
not practical to process reasonably. Such formulas will come out mangled.
"
@leaders -min 0 -max 0
@opts -min 0 -max 2
-p|-page_indexes -parsekey -page_indexes -type indexset -default "0.." -help\
"Comma delimited list of indexes and/or ranges specifying which pages to output.
The indexes are 0-based.
Ranges must be specified with .. as the separator.
end means the last page.
end-1 means the second last page.
0.. is the same as .. and is the same as 0..end
examples:
1,3..
output the page index 1 (2nd page) followed by all from index 3 to the end.
0..2,end
output the first 3 pages, and the last page.
end-1..0
output the pages in reverse order from 2nd last page to first page.
see also 'punk::lib::resolve_indexset'"
-b|-block_indexes -parsekey -block_indexes -type indexset -default "0.." -help\
"Comma delimited list of indexes and/or ranges specifying which blocks to output.
Format is as per -page_indexes"
-merge_yblocks -default false -help\
"Whether to merge blocks vertically when they interleave in the y direction.
This usually makes the output more in line with the source document - but
in some scenarios it might be desired to keep the blocks listed vertically
even though they appear side by side in the source. Such vertical listing as
given by default when -merge_yblocks is false, does not necessarily match the
left to right order.
Note that especially when there is small-font text with other text to the right,
The small-font text that is not rightmost may be truncated (with trailing ellipsis)
to fit it into the vertically merged layout. Such truncation produces warnings on
the -errchannel and increments the warning count visible in the -sep-block line
preceding the merged blocks.
Smaller print that doesn't fit in the layout is sometimes not required to extract
the main data from a document such as an invoice - and the merged data can make
other elements more amenable to parsing. In practice, 2 passes with -merge_yblocks
configured on and off, combined with -sep_block data and a set of custom rules
based on the specific document set may be required for information extraction."
#------------------------------------------------------------------------------------
-compact -type boolean -default false -help\
"If compact is false - blank lines approximating the vertical spacing in the
source document will be emitted between each block.
If -merged_yblocks is true - there will still be potential vertical whitespace
within any merged blocks, as the point of -merge_yblocks is to align elements
closely to that within the source document, but there will be no vertical
layout spacing between such merged blocks, or other non-merging blocks.
This setting is redundant if -postcompact is true."
-postcompact -type boolean -default false -help\
"Strip out vertical spacing (empty or whitespace-only lines) from all blocks.
This sets -compact to true, and removes vertical gaps from any merged blocks.
The most vertically compact representation of the text will result from setting
-merge true, -postcompact true and -image_place_ansi to the empty string, as
well as omitting (or setting to empty string) -sep_page and -sep_block.
Note that if the source PDF actually positioned blank lines as strings of
whitespace, these too would be removed.
If -image_place_ansi is left as the default or has other ansi codes, the
image placeholders will not be stripped, even if they consist of the default
space characters.
If -sep_block or -sep_page are set to one or more spaces - these blank lines
will still be emitted."
-shrink_textfree_blocks -type boolean -default true -help\
"If a block is image only - don't output the placeholders.
These placeholders will still be emitted if -merge_yblocks is true and they
are in a merged block that has also merged text blocks. Set -image_place_ansi
to the empty string to suppress ANSI background for all image placeholders."
-image_place_ansi -type dict -defaultdisplaytype dict -default {border Term-grey78 inner Term-grey58 small Term-grey15} -help\
"Dict of ANSI colour codes for the textual image placeholders.
Colour codes are those as listed by 'punk::ansi::a?'
If the default -image_place_chars values of the space character are being used,
the colour names should begin with a capital letter (indicating background as
opposed to foreground colour).
The placeholders are by default space characters with the ANSI applied.
This means that just by stripping ANSI from the output, layout is maintained,
and the text can be processed simply, or the ANSI can be retained if your text
parser wishes to use it to make decisions.
The colour at element 'border' is for the border colour, 'inner' for the area
within the border, and the colour at element 'small' is for images too small for
border representation. The border bears no relation to the underlying image - and
is merely to aid in seeing when images overlap or are cropped etc. The dimensions
of the placeholder are approximates only to the source images, as they are in
multiples of the fixed-width output font character width and line height.
Setting -image_place_ansi to an empty value, or -image_place_chars to an empty
value will stop ANSI from being output at the image locations."
#------------------------------------------------------------------------------------
-image_place_chars -type dict -defaultdisplaytype dict -default {tlc " " hlt " " trc " " vlr " " brc " " hlb " " blc " " vll " " inner " " small " "} -help\
"Characters to use for image placeholders. The default list of empty spaces is
recommended for ease of visualisation and output parsing assuming an function
such as punk::ansi::ansistrip is available. An alternative set of characters
such as a unicode block character might be useful alongside turning ANSI off by
setting -image_place_ansi to empty; if the unicode character is known to be
outside the domain of characters in the source document, and it is desired to
use such chars to detect approximate image position in relation to text.
tlc - border top left corner
hlt - border top horizontal lines
trc - border top right corner
vlr - border right vertical line
brc - border bottom right corner
hlb - border bottom horizontal line
blc - border bottom left corner
vll - border left vertical line
inner - image area within border
small - for images too small for borders
"
-header -type string -default "" -help\
"Header line to record details of the conversion run.
Set to 'default' to use the provided default template.
Substitutions available:
%doc% - full path of source pdf
%pages% - number of pages in the pdf
%size% - size in bytes of the input pdf file
%sha1% - sha1 checksum of the input pdf file
%parserversion% - version of punk::pdf used.
%args% - arguments supplied to text command.
%default% - the default header template - for extending."
-sep_page -type string -default "" -help\
"Send a line of output to stdout at the beginning of each
page in the PDF.
If set to the value: default
a default separator with some added info will be used.
line begins with: #pdf::text:pagestart
Substitutions available:
%index% 0-based index of the page (not the page number as in the PDF)
%pageblocks% total number of blocks in the page, includes both
text and image blocks.
%textblocks% number of text blocks in the page
%imageblocks% number of image blocks in the page
%nl% newline
%default% The default page sep - to allow extending."
-sep_block -type string -default "" -help\
"Send a line of output to stdout at the beginning of each
textblock in the PDF. This can aid in parsing.
If set to the the value: default
a default separator with some added info will be used.
line begins with: #pdf::text:blockstart
Substitutions available:
%type% type of block text or image
%pageindex% 0-based index of the page (not the page number as in the PDF)
%y-index% 0-based index of block after the textblock list has
been sorted by y0
%x0% first x coordinate of the block
%y0% first y coordinate of the block
%x1% second x coordinate of the block
%y1% second y coordinate of the block
%nl% newline
%warnings% Number of warnings emitted to stderr for the block.
These are normally for text overlay attempts, which should
only affect whitespace, but should be checked.
%default% The default blocksep - to allow extending."
-outchannel -type string -default stdout -choicerestricted 0 -choices {return null stderr stdout}\
-choicelabels {
return\
"Buffer the results as written and return as the result of the command"
null\
"Discard the output. (Only useful if error output is all that's required)"
}\
-choicecolumns 1\
-help\
"Determine where the main text output goes. Aside from the listed options,
any open Tcl channel can be used."
-errchannel -type string -default stderr -choicerestricted 0 -choices {return null stderr stdout}\
-choicelabels {
return\
"Buffer the errors as written and return as the result of the command"
null\
"Discard the error output."
}\
-choicecolumns 1\
-help\
"Determine where the error output goes. Aside from the listed options,
any open Tcl channel can be used."
-debug_pageblock -default "" -type list -minsize 2 -maxsize 2 -help\
"A 2-element list of the page then block index(s) for which to output
extra information on stderr.
Supplied value if not empty, must contain 2 elements {indexset indexset}
The membership selection is performed the same way as -page_indexes.
e.g {.. end-1} means second-last block on every page."
-debug_highlight -default "red bold" -help\
"ANSI codes as understood by punk::ansi (see punk::ansi::a?)
These are used to colourise output if -debug_pageblock has been set"
-highlight_overwrites -default "bold" -help\
"Ansi codes as understood by punk::ansi (see punk::ansi::a?)
These are used to mark text elements which have been overlayed with
the same text data. This may occur for example in table rows in the source
document that have alternating colour or emphasis.
Having this ANSI applied can be useful when parsing the output to aid in
grouping lines. e.g by using [punk::ansi::ta::detect <str>] or similar.
By applying ansi, the layout is not disturbed when viewing in a terminal,
and may be more visually similar to the source document.
Saved output can always be passed through an ansistrip function if needed,
or the default -highlight_overwrites can be overridden by supplying an empty
string."
-engine -type any -default MuPDF -choices {MuPDF} -help\
"Only MuPDF is currently implemented. Be aware that while this script and tclMuPDF
are MIT/BSD licensed, the underlying MuPDF library is AGPL - which could place
some restrictions on commercial use."
-warnings_engine -default 0 -choices {0 1 2}\
-choicelabels {
0\
" Do not display"
1\
" Display at end on stderr"
2\
" Display as they occur on stderr"
}\
-help\
"Whether to display internal warnings regarding the state of the PDF document.
The underlying MuPDF library can in many cases work with bad/corrupted PDFS,
and if so, may emit warnings."
-warnings_textblock -default 2 -type integer -range {0 9} -help\
"0 to disable.
1 for least info, 9 for most.
Emits warnings to error channel"
@values -min 1 -max 1
filename -type string -help\
"Path to the .pdf file to parse"
}
proc text {args} {
variable callid
incr callid
package require tcl::chan::variable
#variable testchan
#set testchan [::tcl::chan::variable ::punk::pdf::test($callid)]
#chan configure $testchan -translation lf
variable results
variable base_x_per_c
variable cfg_blocksep_default
variable cfg_pagesep_default
variable cfg_header_default
set x_per_line [expr {$base_x_per_c * 2}] ;#hack for now
set argd [punk::args::parse $args withid ::punk::pdf::text]
lassign [dict values $argd] leaders opts values received
set opt_pagesep [dict get $opts -sep_page]
if {$opt_pagesep eq "default"} {
set opt_pagesep $cfg_pagesep_default
}
set opt_blocksep [dict get $opts -sep_block]
#structure the sep so that after the first word it can be treated as a dict (assuming no empty strings for substitutions)
if {$opt_blocksep eq "default"} {
set opt_blocksep $cfg_blocksep_default
}
set opt_header [dict get $opts -header]
if {$opt_header eq "default"} {
set opt_header $cfg_header_default
}
set debug_pageblock [dict get $opts -debug_pageblock]
set debug_highlight [string trim [dict get $opts -debug_highlight]]
set highlight_overwrites [string trim [dict get $opts -highlight_overwrites]]
set page_indexes [string trim [dict get $opts -page_indexes]]
set block_indexes [string trim [dict get $opts -block_indexes]]
set warnings_engine [dict get $opts -warnings_engine]
set opt_compact [dict get $opts -compact]
set opt_postcompact [dict get $opts -postcompact]
if {$opt_postcompact} {
set opt_compact 1 ;#don't emit inter-block spacing in the first place
#will also strip vertical space merged blocks
}
set opt_image_place_ansi [dict get $opts -image_place_ansi]
set opt_image_place_chars [dict get $opts -image_place_chars]
set opt_shrink_textfree_blocks [dict get $opts -shrink_textfree_blocks]
if {$warnings_engine == 2} {
#turn on printwarnings before we start
::mupdf::printwarnings 1
}
set warnings_textblock [dict get $opts -warnings_textblock]
set opt_merge_yblocks [dict get $opts -merge_yblocks]
set opt_outchan [dict get $opts -outchannel]
switch -- $opt_outchan {
return {
package require tcl::chan::variable
set outchan [::tcl::chan::variable ::punk::pdf::results($callid)]
#chan configure $outchan -translation binary -encoding utf-8
chan configure $outchan -translation lf -encoding utf-8
}
null {
package require tcl::chan::null
set outchan [::tcl::chan::null]
}
default {
set outchan $opt_outchan
}
}
set opt_errchan [dict get $opts -errchannel]
switch -- $opt_errchan {
return {
package require tcl::chan::variable
if {$opt_outchan eq "return"} {
set errchan $outchan
} else {
set errchan [::tcl::chan::variable ::punk::pdf::results($callid)]
}
#chan configure $errchan -buffering none -translation binary
chan configure $errchan -buffering none -translation lf -encoding utf-8
}
null {
package require tcl::chan::null
set errchan [::tcl::chan::null]
}
default {
set errchan $opt_errchan
}
}
set fname [file normalize [dict get $values filename]]
if {![file exists $fname]} {
puts $errchan "file at '$fname' not found"
if {$opt_outchan eq "return" || $opt_errchan eq "return"} {
#if both are 'return' at the same time - the same channel is used for both
flush $outchan
flush $errchan
set result [set results($callid)]
return $result
}
return
}
set d [::mupdf::open $fname]
set npages [$d npages]
if {$opt_header ne ""} {
set opt_header [string map [list %default% $cfg_header_default] $opt_header]
package require sha1
set hash [sha1::sha1 -hex -file $fname]
set map [list %nl% \n %crlf% \r\n %doc% $fname %pages% $npages %size% [file size $fname] %sha1% $hash %args% $args %parserversion% [package present punk::pdf]]
puts $outchan [string map $map $opt_header]
}
set page_index_list [punk::lib::indexset_resolve $npages $page_indexes]
set debug_pageblock_pages [punk::lib::indexset_resolve $npages [lindex $debug_pageblock 0]]
if {$warnings_textblock >= 9} {
puts $errchan "Will process page indices: $page_index_list"
}
#for {set pageindex 0} {$pageindex < $npages} {incr pageindex} {}
foreach pageindex $page_index_list {
#if {$pageindex != 0} {
# continue
#}
set p [$d getpage $pageindex]
#set rawtextlist [$p text -bbox lines]
set rawtextlist [$p text -bbox lines -mode {preserve-ligatures preserve-whitespace preserve-spans}]
#preserve-spans: spans on same line not merged - each line will be a span of text with the same font, colour and size.
set ydict [dict create]
foreach tl $rawtextlist {
if {[punk::pdf::system::is_coord_list $tl]} {continue} ;#don't know what to do with plain coord lists here
if {[llength $tl] % 2 != 0} {
puts $errchan "WARNING Unhandled textlist $tl"
continue
}
foreach {spancoords text} $tl {
lassign $spancoords sx0 sy0 sx1 sy1
dict lappend ydict [list $sy0 $sy1] [list $sx0 $sx1 $text]
}
}
set pageblocks [$p blocks]
if {$opt_pagesep ne ""} {
set textblock_indexes [lsearch -all -index 0 $pageblocks textblock] ;#for count
set imageblock_indexes [lsearch -all -index 0 $pageblocks imageblock];#for count
set opt_pagesep [string map [list %default% $cfg_pagesep_default] $opt_pagesep]
set map [list %nl% \n %crlf% \r\n %index% $pageindex %pageblocks% [llength $pageblocks] %textblocks% [llength $textblock_indexes] %imageblocks% [llength $imageblock_indexes]]
puts $outchan [string map $map $opt_pagesep]
}
if {[llength $pageblocks] == 0} {
continue
}
#review
#sort pageblocks by y0
set pageblocks [lsort -real -index 2 $pageblocks]
set block_index_list [punk::lib::indexset_resolve [llength $pageblocks] $block_indexes]
set debug_pageblock_blocks [punk::lib::indexset_resolve [llength $pageblocks] [lindex $debug_pageblock 1]]
#-----------
#todo - normalize y0 to first encountered with vertical fudge factor (but retain original y0)
# - sort within normalised ny0 by normalised x0
# - only increment y-index when ny0 changes
# - provide 0-based x-index (for blocksep) for multiple blocks with same ny0
#-----------
set fudge_block 2 ;#how to pick a suitable fudge factor when we haven't examined font-size?
set block_x0s [list [lindex $pageblocks 0 1]]
set block_y0s [list [lindex $pageblocks 0 2]]
set block_x1s [list [lindex $pageblocks 0 3]]
set block_y1s [list [lindex $pageblocks 0 4]]
#fudged_textblock has original list entries plus 4 possibly normalized coordinates
#we provide normals against all other blocks in the page (review - do per doc instead?)
set fudged_textblocks [list [list {*}[lindex $pageblocks 0] {*}[lrange [lindex $pageblocks 0] 1 end]]] ;#no vfudge on first block
foreach tb [lrange $pageblocks 1 end] {
lassign $tb _ tbx0 tby0 tbx1 tby1
set nx0 $tbx0
set ny0 $tby0
set nx1 $tbx1
set ny1 $tby1
foreach ex0 $block_x0s {
set pointdiff [expr {abs($tbx0 - $ex0)}]
if {$pointdiff < $fudge_block} {
set nx0 $ex0
break
}
}
foreach ey0 $block_y0s {
set pointdiff [expr {abs($tby0 - $ey0)}]
if {$pointdiff < $fudge_block} {
set ny0 $ey0
break
}
}
foreach ex1 $block_x1s {
set pointdiff [expr {abs($tbx1 - $ex1)}]
if {$pointdiff < $fudge_block} {
set nx1 $ex1
break
}
}
foreach ey1 $block_y1s {
set pointdiff [expr {abs($tby1 - $ey1)}]
if {$pointdiff < $fudge_block} {
set ny1 $ey1
break
}
}
lappend block_x0s $tbx0
lappend block_y0s $tby0
lappend block_x1s $tbx1
lappend block_y1s $tby1
lappend fudged_textblocks [list {*}$tb $nx0 $ny0 $nx1 $ny1]
}
#textblock x0 y0 x1 y1 nx0 ny0 nx1 ny1
set fudged_textblocks [lsort -command block_ordering $fudged_textblocks]
#package require punk::lib
#puts stderr "[punk::lib::showdict -roottype list $fudged_textblocks]"
#todo - check bounds of pageblocks for non-overlapping?
set page_seen_chunks [dict create] ;#store every chunk as a key just for lookup (with value 1 )
set yindex 0
set last_ny0 [lindex $fudged_textblocks 0 6]
set TESTBLOCK ""
set MERGE_BUFFER [list] ;#store so we can optionally vertically merge subsequent blocks with same y-index
set tbnum -1
foreach tb $fudged_textblocks {
incr tbnum
set block_warning_count 0
lassign $tb rawblocktype bx0 by0 bx1 by1 bnx0 bny0 bnx1 bny1
switch -- $rawblocktype {
textblock {set blocktype text}
imageblock {set blocktype image}
default {set blocktype ?}
}
if {$last_ny0 != $bny0} {
set is_new_yindex 1
incr yindex
set last_ny0 $bny0
} else {
set is_new_yindex 0
}
if {$is_new_yindex} {
if {![llength $MERGE_BUFFER]} {
#nothing to process
} else {
#------------
#debug
#set lev -1
#foreach level $MERGE_BUFFER {
# incr lev
# puts $outchan "MB$lev"
# foreach entry $level {
# puts $outchan " $entry"
# }
#}
#------------
#puts "MB dimensions: [punk::pdf::system::merge_buffer_dimensions MERGE_BUFFER]"
set mbdimensions [punk::pdf::system::merge_buffer_dimensions MERGE_BUFFER] ;#list x0 y0 x1 y1
#set cur_merge_base_fields [dict get [lindex $MERGE_BUFFER 0 0] header fields]
#if {$bny0 >= [dict get $cur_merge_base_fields y1]} {}
if {$bny0 >= [lindex $mbdimensions 3]} {
#this block begins below the entire MERGE_BUFFER - time to render previous block
_process_merge_buffer MERGE_BUFFER $pageindex $cfg_blocksep_default $opt_blocksep $opt_merge_yblocks $opt_shrink_textfree_blocks $opt_postcompact $warnings_textblock $outchan $errchan
if {$tbnum in $block_index_list} {
#now this block's offset from the top
if {!$opt_compact} {
#default: -compact false
#set vdist [expr {$bny0 - [dict get $cur_merge_base_fields y1]}]
set vdist [expr {$bny0 - [lindex $mbdimensions 3]}]
#set vdistlines [expr {int(ceil($vdist / $x_per_line))}]
set vdistlines [expr {int(floor($vdist / $x_per_line))}]
if {$vdistlines > 0} {
set vseparator [string repeat \n $vdistlines]
puts -nonewline $outchan $vseparator
}
}
}
} else {
if {$tbnum in $block_index_list} {
lappend MERGE_BUFFER [list]
}
}
}
set TESTBLOCK "" ;#not so efficient, but we merge the block as we go even if user didn't request merging. review
}
if {$tbnum ni $block_index_list} {
continue
}
set fields [dict create type $blocktype pageindex $pageindex blockindex $tbnum y-index $yindex x0 $bx0 y0 $by0 x1 $bx1 y1 $by1]
set BUFINFO [dict create fields $fields sep ""]
if {$opt_blocksep ne ""} {
#puts stderr "[a bold yellow]type:$blocktype tb: $tbnum tlc: $bx0 $by0 brc: $bx1 $by1[a]"
set opt_blocksep [string map [list %default% $cfg_blocksep_default] $opt_blocksep]
#$block_warning_count not available yet - placeholder <w>
set map [list %nl% "\n" %crlf% "\r\n" %type% $blocktype %pageindex% $pageindex %blockindex% $tbnum %y-index% $yindex %x0% $bx0 %y0% $by0 %x1% $bx1 %y1% $by1 %tlc% "$bx0 $by0" %brc% "$bx1 $by1" %warnings% <w>]
switch -- $blocktype {
text {dict set map %marker% "[punk::ansi::a bold yellow]TBLOCK[punk::ansi::a]"}
image {dict set map %marker% "[punk::ansi::a yellow]IBLOCK[punk::ansi::a]"}
default {dict set map %marker% "[punk::ansi::a bold red]UNKNOWN BLOCK[punk::ansi::a]"}
}
#puts stdout >>>[string map $map $opt_blocksep]
dict set BUFINFO sep [string map $map $opt_blocksep]
#lappend MERGE_BUFFER [list header [string map $map $opt_blocksep]]
}
#set bufstack [lindex $MERGE_BUFFER end]
#lappend bufstack [list header $BUFINFO]
#lset MERGE_BUFFER end $bufstack
if {![llength $MERGE_BUFFER]} {
#lset MERGE_BUFFER end+1 [list [list header $BUFINFO]]
lappend MERGE_BUFFER [list [list header $BUFINFO]]
} else {
lset MERGE_BUFFER end end+1 [list header $BUFINFO]
}
#lappend MERGE_BUFFER [list header $BUFINFO]
if {$blocktype ne "text"} {
#todo - OCR (tesseract?)
if {$pageindex in $debug_pageblock_pages && $tbnum in $debug_pageblock_blocks} {
set debuginfo "b:$tbnum yinfo: $by0 $by1 ydata: $bx0 $bx1 <img>"
if {$debug_highlight ne ""} {
set debuginfo "[punk::ansi::a {*}$debug_highlight]$debuginfo\x1b\[m"
}
puts $errchan $debuginfo
}
#Note that there can still be text elements from the line dict that are within, or partially within this block
#This can occur for example with background image blocks.
#If we process the parts of the text that are fully within this block - we'll just get duplicated text output.
#It's possible also that an image could be above some text e.g a poor attempt at redaction.
#Such text should show up in another block anyway REVIEW - consider reporting in blocksep that img overlaps with text?
set imgstart_char_pos [x_to_c $bx0]
#possible for bx0 to be negative (image offset to left)
#set img_indent [string repeat " " $imgstart_char_pos]
set imgend_char_pos [x_to_c $bx1]
set img_char_width [expr {$imgend_char_pos - $imgstart_char_pos}] ;#width in terms of fixed-width output font (close enough for this case)
set vertx [expr {$bny1 - $bny0}]
set img_char_height [expr {int(ceil($vertx / $x_per_line))}] ;#linecount
#puts "-- img_char_height:$img_char_height img_char:width: $img_char_width"
if {$img_char_height == 0} {
puts $errchan "WARNING type debug msg {zero height image}" ;#todo
incr block_warning_count ;#0 based
#nothing to render for a zero height img
#MERGE_BUFFER will have header
lset MERGE_BUFFER end end+1 [list warnings $block_warning_count]
continue
}
#put a border around the image if it's big enough
#if it can't be bordered use a different colour
#this gives us 3 greys applied to space elements, to aid in seeing image overlaps and positioning.
#all can be easily stripped out with ansistrip to leave only spaces for text processing
#lassign $opt_image_place_ansi a_small a_border a_inner
#lassign $opt_image_place_chars c_small c_border c_inner
if {$img_char_height < 3 || $img_char_width < 3} {
#can't be framed without losing inner colour - use solid block of slightly different grey instead
if {![dict size $opt_image_place_chars]} {
set b [textblock::block $img_char_width $img_char_height " "]
} else {
set c_small [dict get $opt_image_place_chars small]
set b [textblock::block $img_char_width $img_char_height $c_small]
if {[dict size $opt_image_place_ansi]} {
#set b [punk::ansi::ansiwrap Term-grey58 $b]
set b [punk::ansi::ansiwrap [dict get $opt_image_place_ansi small] $b]
}
}
} else {
if {![dict size $opt_image_place_chars]} {
set c_tlc " "
set c_hlt " "
set c_trc " "
set c_vlr " "
set c_brc " "
set c_hlb " "
set c_blc " "
set c_vll " "
set c_inner " "
} else {
set c_tlc [dict get $opt_image_place_chars tlc]
set c_hlt [dict get $opt_image_place_chars hlt]
set c_trc [dict get $opt_image_place_chars trc]
set c_vlr [dict get $opt_image_place_chars vlr]
set c_brc [dict get $opt_image_place_chars brc]
set c_hlb [dict get $opt_image_place_chars hlb]
set c_blc [dict get $opt_image_place_chars blc]
set c_vll [dict get $opt_image_place_chars vll]
set c_inner [dict get $opt_image_place_chars inner]
}
set inner [textblock::block [expr {$img_char_width -2}] [expr {$img_char_height - 2}] $c_inner]
#set b [textblock::frame -type {} -ansiborder [a Term-grey15] [punk::ansi::ansiwrap Term-grey78 $inner]]
#set b [textblock::frame -type {} -ansiborder [a $a_border] [punk::ansi::ansiwrap $a_inner $inner]]
if {![dict size $opt_image_place_ansi]} {
set b [textblock::frame -type [list tlc $c_tlc hlt $c_hlt trc $c_trc vlr $c_vlr brc $c_brc hlb $c_hlb blc $c_blc vll $c_vll] $inner]
} else {
set a_border [dict get $opt_image_place_ansi border]
set a_inner [dict get $opt_image_place_ansi inner]
set b [textblock::frame -type [list tlc $c_tlc hlt $c_hlt trc $c_trc vlr $c_vlr brc $c_brc hlb $c_hlb blc $c_blc vll $c_vll] -ansiborder [a $a_border] [punk::ansi::ansiwrap $a_inner $inner]]
}
}
#set b [textblock::block $img_char_width $img_char_height " "]
#set b [punk::ansi::ansiwrap Term-102 $b]
set img_indent [textblock::block [expr {max(0,$imgstart_char_pos)}] $img_char_height \uFFFD]
if {$imgstart_char_pos < 0} {
#img representation is effectively cropped at left
#img_indent will just be a list of newlines to match height
#even if img_indent and b have no ansi - result of renderspace will have ansi resets (todo review overtype::renderspace)
set had_ansi 0
if {[punk::ansi::ta::detect $b]} {
set had_ansi 1
}
set positioned_block [overtype::renderspace -insert_mode 0 -expand_right 1 -startcolumn $imgstart_char_pos $img_indent $b]
if {!$had_ansi} {
#if user disabled ansi on image_placeholders - we need to ensure it still has none after renderspace
set positioned_block [punk::ansi::ansistrip $positioned_block]
}
} else {
set positioned_block [textblock::join -- $img_indent $b]
}
if {![llength $MERGE_BUFFER]} {
lappend MERGE_BUFFER [list [list block $positioned_block]]
} else {
lset MERGE_BUFFER end end+1 [list block $positioned_block]
}
continue
}
#scan all y-coords for items within
#(we are assuming no text items overlap the block's x or y bounds) review
set found_lines [list]
#we aren't assuming the ydata is ordered
dict for {yinfo ydata} $ydict {
lassign $yinfo dy0 dy1
set spans_in_block [list]
if {$dy0 >= $by0 && $dy1 <= $by1} {
#lappend found_lines [list $dy0 $dy1 $ydata]
#filter out lines where NO values are in range of the block
#(common to encounter line data where only some text elements are within the block we are currently at - e.g tables)
#todo - review treatment of
#assume if it just extends out of the blocks x range - it's part of a larger block (?) review - xrangefudge?
set has_element_in_range 0
foreach xxt $ydata {
lassign $xxt dx0 dx1
set xrangefudge 0 ;#shouldn't be needed?
if {($dx0 < ($bx0 - $xrangefudge)) || ($dx1 > ($bx1 + $xrangefudge))} {
#if {$tbnum == 16} {
# puts "ydata: $ydata"
# puts "x range: $xxt out of x range: $bx0 $bx1"
#}
#ignore element for this block - not rendered here
#todo - store somewhere to verify later it ended up in some block?
} else {
lappend spans_in_block $xxt
}
}
if {[llength $debug_pageblock]} {
lassign $debug_pageblock dp db
if {$pageindex in $debug_pageblock_pages && $tbnum in $debug_pageblock_blocks} {
set debuginfo "b:$tbnum yinfo: $yinfo ydata: [punk::ansi::ansistring VIEW -lf 1 $ydata]"
if {$debug_highlight ne ""} {
set debuginfo "[punk::ansi::a {*}$debug_highlight]$debuginfo\x1b\[m"
}
puts $errchan $debuginfo
}
}
if {[llength $spans_in_block]} {
#puts stderr $spans_in_block
lappend found_lines [list $dy0 $dy1 $spans_in_block]
}
}
}
#puts "found_lines: $found_lines"
#found_lines has had text elements outside the x-range of the block removed
if {[llength $found_lines]} {
set found_lines [lsort -real -index 0 $found_lines]
#vfudge
#often elements on same line are a little off in recorded y coords
#eg 658.8599... vs 659.0999...
#normalize with some vfudge factor - (proportional to x_per_c ? e.g vdiff < x_per_c )
set vfudged_lines [list]
#normalize to first y val encountered..
lappend vfudged_lines [lindex $found_lines 0] ;#nothing to normalize against (at least within this block)
foreach fl [lrange $found_lines 1 end] {
#-------------
#we can't determine the source x_per_c based on text elements in the line without knowing exact font ans size and using a text measurement API
set first_xxt [lindex $fl 2 0]
lassign $first_xxt tx0 tx1 t
#--------------
lassign $fl line_y0 line_y1
set existing_y0s [lsearch -all -inline -index 0 -subindices $vfudged_lines *]
set existing_y1s [lsearch -all -inline -index 1 -subindices $vfudged_lines *]
set vfudged_line_added 0
#set vfidx -1
#foreach ey0 $existing_y0s {
# incr vfidx
# set vdiff [expr {abs($line_y0 - $ey0)}]
# #x units per char is normally narrower than height
# set linefudge 1.5 ;#choose linefudge less than assumed x_per_line of 2*base_x_per_c - or small font lines will not be separated #REVIEW
# if {$vdiff < ($linefudge * $base_x_per_c)} {
# #normalize to existing value
# #line_y1 unnormalised??
# #if y1 vdiff is about the same - normalise it too
# set ey1 [lindex $existing_y1s $vfidx]
# set vdiff2 [expr {abs($line_y1 -$ey1)}]
# if {abs($vdiff2 - $vdiff) <= 8} {
# set line_y1 $ey1
# }
# set vfudged_line_added 1
# lappend vfudged_lines [list $ey0 $line_y1 [lindex $fl 2]]
# break
# }
#}
#when mixing fonts in same line - aligning the bases is more common.
set vfidx -1
foreach ey1 $existing_y1s {
incr vfidx
set vdiff [expr {abs($line_y1 - $ey1)}]
#x units per char is normally narrower than height
set linefudge 1.5 ;#choose linefudge less than assumed x_per_line of 2*base_x_per_c - or small font lines will not be separated #REVIEW
if {$vdiff < ($linefudge * $base_x_per_c)} {
#normalize to existing value
#if y0 vdiff is about the same - normalise it too
set ey0 [lindex $existing_y0s $vfidx]
set vdiff2 [expr {abs($line_y0 -$ey0)}]
if {abs($vdiff2 - $vdiff) <= 8} {
set line_y0 $ey0
}
set vfudged_line_added 1
lappend vfudged_lines [list $line_y0 $ey1 [lindex $fl 2]]
break
}
}
if {!$vfudged_line_added} {
lappend vfudged_lines $fl
}
}
#now merge vfudged lines with same y0
set dict_vfudge [dict create]
foreach fl $vfudged_lines {
lassign $fl ly0 ly1 xxtext_list
dict lappend dict_vfudge [list $ly0 $ly1] {*}$xxtext_list
}
#rebuild
set vfudged_lines [list]
dict for {k v} $dict_vfudge {
lappend vfudged_lines [list [lindex $k 0] [lindex $k 1] $v]
}
#if {[llength $vfudged_lines]} {
# puts stderr "vfudged lines: $vfudged_lines"
#}
#if our vfudge factor worked - we can now treat as ordered lines
set working_lines [list]
foreach line $vfudged_lines {
lassign $line ly0 ly1 spanlist
set spans_by_x0_original [lsort -real -index 0 $spanlist]
set spans_by_x0_adjusted $spans_by_x0_original ;#copy for updated x1s due to overlap expansion as we process each line
lappend working_lines [list $ly0 $ly1 $spans_by_x0_original $spans_by_x0_adjusted]
}
set block_lines [list]
set block_linenum -1
foreach fl $working_lines {
incr block_linenum
#puts "fline: $fl"
lassign $fl fy0 fy1 spans_by_x0 ;#don't pull out spans_by_x0_adjusted - it's adjusted within this loop by referring to its index in working_lines
set line_chunks [list]
#calculating x units per char for each string is only really possible if we know the exact font and size
set xidx -1 ;#index of spans in line, which are sorted by x0 (lhs)
set thisline_spancount [llength $spans_by_x0]
foreach xxt $spans_by_x0 {
incr xidx
lassign $xxt tx0 tx1 text
#puts -nonewline $testchan $text
#any span, including first may have rhs alignments with spans from previous lines in the same block
#look at original rhs alignment of spans in previous lines that match this one's x1
#look at original lhs alignment of spans in previous lines that match this one's x0
#only treat as rhs aligned spans if the text elements being compared have no trailing whitespace
#only treat as lhs aligned spans if the text elements being comapred have no leading whitespace
# - process both types at once and use the last match in the list for alignment
set this_text_right_alignable [expr {![regexp {\s+$} $text]}] ;#right alignable if no trailing ws
set this_text_left_alignable [expr {![regexp {^\s+} $text]}] ;#left alignable if no leading ws
set earlier_aligned_spans [list]
foreach pl [lrange $working_lines 0 $block_linenum-1] {
set spanidx -1
foreach pxxt [lindex $pl 2] pxxt_adjusted [lindex $pl 3] {
incr spanidx
set canalign ""
if {$this_text_right_alignable} {
if {abs([lindex $pxxt 1] - $tx1) < 1} {
set t [lindex $pxxt 2]
if {![regexp {\s+$} $t]} {
#test if right alignment would be within block bounds
set testx0 [expr {[lindex $pxxt_adjusted 1]-[c_to_x [string length $text]]}]
if {$testx0 >= $bx0} {
#puts "bleft:$bx0 testx0:$testx0 tx0:$tx0 tx1:$tx1 t:$t"
set canalign right
}
}
}
}
if {$this_text_left_alignable} {
if {abs([lindex $pxxt 0] - $tx0) < 1} {
set t [lindex $pxxt 2]
if {![regexp {^\s+$} $t]} {
if {$canalign eq ""} {
set canalign left
} else {
set canalign both
}
}
}
}
if {$canalign ne ""} {
lappend earlier_aligned_spans [dict create align $canalign originalspan $pxxt adjustedspan $pxxt_adjusted spanindex $spanidx spancount [llength [lindex $pl 2]]]
}
}
}
if {[llength $earlier_aligned_spans]} {
if {[llength $debug_pageblock]} {
lassign $debug_pageblock dp db
if {$pageindex in $debug_pageblock_pages && $tbnum in $debug_pageblock_blocks} {
set debuginfo "-PREV alignment choices [llength $earlier_aligned_spans]"
#foreach aligninfo $earlier_aligned_spans {
# set aligntext [lindex [dict get $aligninfo originalspan] 2]
# set originalcoords [lrange [dict get $aligninfo originalspan] 0 1]
# set adjustedcoords [lrange [dict get $aligninfo adjustedspan] 0 1]
# puts stdout "-[dict get $aligninfo align] original:$originalcoords adjusted:$adjustedcoords spanindex:[dict get $aligninfo spanindex] spancount:[dict get $aligninfo spancount] text:'$aligntext' ]"
#}
if {$debug_highlight ne ""} {
set debuginfo "[punk::ansi::a {*}$debug_highlight]$debuginfo\x1b\[m"
}
puts $errchan $debuginfo
}
}
}
#1st shot - use rhs of element immediately to the left (either existing TESTBLOCK or earliear spans on same line)
if {$xidx == 0} {
set prevspan_x0 ""
set prevspan_x1 ""
set prevspan_x1_adjusted ""
if {$TESTBLOCK ne ""} {
#----------------------------
#NOTE - blocks can be listed in the doc in non-left-right order
#ie the second block might have data to the left of the 1st block.
#.eg
# 4
# 3
# 2
# 1
#----
#merging to
# 1 2 3 4
#----------------------------
#set width [textblock::widthtopline $TESTBLOCK]
#set effective_prev_x1 [c_to_x $width]
##set prevspan_x1 $effective_prev_x1
#if {$tx0 < $effective_prev_x1} {
# set start_char_pos [x_to_c $effective_prev_x1]
# set this_text_indent [string repeat " " $start_char_pos]
#} else {
# set start_char_pos [x_to_c $tx0]
# set this_text_indent [string repeat " " $start_char_pos]
#}
set start_char_pos [x_to_c $tx0]
set this_text_indent [string repeat " " $start_char_pos]
#set effective_prev_x1 $start_char_pos
} else {
set start_char_pos [x_to_c $tx0]
set this_text_indent [string repeat " " $start_char_pos]
#set effective_prev_x1 $start_char_pos
set effective_prev_x1 0
}
set effective_prev_x1 0
} else {
#set prevspan_x1_adjusted [lindex $spans_by_x0_adjusted $xidx-1 1]
set prevspan_x0 [lindex $spans_by_x0 $xidx-1 0]
set prevspan_x1_adjusted [lindex $working_lines $block_linenum 3 $xidx-1 1]
set prevspan_x1 [lindex $spans_by_x0 $xidx-1 1]
#set effective_prev_x1 [expr {max($prevspan_x1,$prevspan_x1_adjusted)}]
set effective_prev_x1 $prevspan_x1_adjusted
set start_char_pos [x_to_c $tx0]
set this_text_indent [string repeat " " $start_char_pos]
}
##override above 1st shot with closest previous-line that has aligned span if applicable
#if tx0 closely matches bx0 - leave aligned with the block
if {abs($tx0 - $bx0) > 3} {
if {[llength $earlier_aligned_spans]} {
#choose most recent aligning span from lines above, but prefer based on closest matches for spanindex and spancount
#compare spanindex with xidx and spancount with thisline_spancount
#set aligned_span_info [lindex $earlier_aligned_spans end]
set aligned_span_info [lindex $earlier_aligned_spans 0] ;#choose - unless something lower with same or better match for spanindex/spancount
set si [dict get $aligned_span_info spanindex]
set sc [dict get $aligned_span_info spancount]
#use 'have' for 'already have' this flag in a previous candidate
set have_imatch [expr {$si == $xidx}]
set have_cmatch [expr {$sc == $thisline_spancount}]
foreach espan $earlier_aligned_spans {
if {$have_imatch && $have_cmatch} {
#must also have both to become new candidate
if {[dict get $espan spancount] == $thisline_spancount && [dict get $espan spanindex] == $xidx} {
set aligned_span_info $espan
}
} elseif {$have_cmatch} {
#only both flags, or same flag let this espan be the new candidate
if {[dict get $espan spancount] == $thisline_spancount} {
#same or both
if {[dict get $espan spanindex] == $xidx} {
set have_imatch 1
}
set aligned_span_info $espan
}
} elseif {$have_imatch} {
#only both flags, or same flag let this espan be the new candidate
if {[dict get $espan spancount] == $thisline_spancount} {
if {[dict get $espan spanindex] == $xidx} {
set have_cmatch 1
set aligned_span_info $espan
}
} else {
if {[dict get $espan spanindex] == $xidx} {
#same - new candidate by virtue of lower position
set aligned_span_info $espan
}
}
} else {
#no good matches yet - anything later in list is the new candidate
set aligned_span_info $espan
set have_imatch [expr {[dict get $espan spanindex] == $xidx}]
set have_cmatch [expr {[dict get $espan spancount] == $thisline_spancount}]
}
}
if {[llength $debug_pageblock]} {
lassign $debug_pageblock dp db
if {$pageindex in $debug_pageblock_pages && $tbnum in $debug_pageblock_blocks} {
set aligninfo $aligned_span_info
set aligntext [lindex [dict get $aligninfo originalspan] 2]
set originalcoords [lrange [dict get $aligninfo originalspan] 0 1]
set adjustedcoords [lrange [dict get $aligninfo adjustedspan] 0 1]
set debuginfo "CHOSENALIGN for '$text' -[dict get $aligninfo align] original:$originalcoords adjusted:$adjustedcoords spanindex:[dict get $aligninfo spanindex] spancount:[dict get $aligninfo spancount] text:'$aligntext' ]"
if {$debug_highlight ne ""} {
set debuginfo "[punk::ansi::a {*}$debug_highlight]$debuginfo\x1b\[m"
}
puts $errchan $debuginfo
}
}
#puts "tx0:$tx0 bx0:$bx0"
set canalign [dict get $aligned_span_info align]
set pxxt_adjusted [dict get $aligned_span_info adjustedspan]
if {$canalign in {both left}} {
set px0 [lindex $pxxt_adjusted 0]
set start_char_pos [x_to_c [expr {$px0}]]
set this_text_indent [string repeat " " $start_char_pos]
} else {
#right
set px1 [lindex $pxxt_adjusted 1]
set thistext_points [c_to_x [string length $text]] ;#this text's number of points (in terms of fixed-width *output* font)
set start_char_pos [x_to_c [expr {$px1 - $thistext_points}]]
set this_text_indent [string repeat " " $start_char_pos]
}
}
}
set lastchunk [lindex $line_chunks end] ;#lastchunk includes buffering
if {[llength $debug_pageblock]} {
lassign $debug_pageblock dp db
if {$pageindex in $debug_pageblock_pages && $tbnum in $debug_pageblock_blocks} {
set debuginfo "span x0 xx1 text: [lindex $xxt 0] [lindex $xxt 1] [punk::ansi::a normal]'[lindex $xxt 2]'\x1b\[m"
if {$debug_highlight ne ""} {
set debuginfo "[punk::ansi::a overline {*}$debug_highlight]$debuginfo\x1b\[m"
}
puts $errchan $debuginfo
if {$prevspan_x1 ne $prevspan_x1_adjusted} {
set C [punk::ansi::a yellow]
set RST \x1b\[m
} else {
set C "" ; set RST ""
}
set debuginfo "prev x0 x1 : $prevspan_x0 $prevspan_x1 prevspan_x1_adjusted: $C$prevspan_x1_adjusted$RST effective_prev_x1: $effective_prev_x1"
if {$debug_highlight ne ""} {
set debuginfo "[punk::ansi::ansiwrap underline {*}$debug_highlight $debuginfo]"
}
puts $errchan $debuginfo
}
}
#the x1 values produced for a span by MuPDF represent where the possibly-variable-width font text ended.
#The x_gap between this tx0 and previous tx1 thus generally only tells us that some gap is expected between the two spans - not how much.
#i.e
# - if x_gap is zero - we should probably join the two spans - otherwise can get unintended breaks in words
# - if x_gap is > zero - we can't assume much. There might be a small x_gap only because previous span is a large font.
# This means when we use tx0 as position to start - in some cases the gap is larger than desirable
# but we need to maintain it for columnar data REVIEW.
if {$xidx == 0} {
set x_gap [expr {$tx0 - $effective_prev_x1}]
set x_gap_chars [x_to_c $x_gap]
#set x_gap 0
#set buffer [string repeat " " $start_char_pos]
#set x_gap_chars 0
} else {
set x_gap [expr {$tx0 - $prevspan_x1}] ;#usually positive but can easily be negative. e.g the word TAX in some fonts can have the left of the A slightly under the top bar of the T
#if the font is large - this negative offset could be more than an entire width of our output character size.
#we really need to know the source font to do this even close to properly - REVIEW
set x_gap_chars [x_to_c $x_gap] ;#used to expand buffer below during collision detection.
#set buffer [string repeat "^" $x_gap_chars]
}
set buffer $this_text_indent
#if {$xidx > 0} {
# set buffer [string repeat " " [expr {[x_to_c $effective_prev_x1] + $x_gap_chars}]]
#}
if {$xidx > 0 && $x_gap >= -3 && $x_gap < 0} {
#treat small negative gap (overlap) in source (prev_x1 > tx0) (typically for character overhang/underhang) as adjacency
dict set page_seen_chunks $text 1
set buffer [string repeat " " [expr {[x_to_c $effective_prev_x1]}]]
lappend line_chunks "$buffer$text"
} elseif {$xidx > 0 && $x_gap >= 0 && $x_gap < ($base_x_per_c * 0.75)} {
#treat no, or small positive gap in source (prev_x1 < tx0) as intended adjacency
#.75 is yet another fudge for lack of font info - todo - rework to another engine?
#this branch could be merged with above, but kept separate for clarity and for specific comments.
dict set page_seen_chunks $text 1
set buffer [string repeat " " [expr {[x_to_c $effective_prev_x1]}]]
lappend line_chunks "$buffer$text"
} elseif {$xidx > 0 && $effective_prev_x1 ne "" && $tx0 <= $effective_prev_x1} {
#possible overlap/overlay - left x of this data is before right x of previous
#first use output font's x_per_c and see if only overlaying whitespace
#in which case - it's probably a distinct piece of text - whether or not it matches previous value,
#whereas if it overlaps the text itself - it may be an 'overlay' of same data.
set prevtext [lindex $spans_by_x0 $xidx-1 2]
set prev_x0 [lindex $spans_by_x0 $xidx-1 0]
#set prevtrimmed [string trimright $prevtext] ;#removed this. whitespace in text data should be preserved - even if we are sometimes forced to add extra spacing, we shouldn't remove existing spacing.
set spandist [expr {[string length $prevtext] * $base_x_per_c}] ;#length of span in terms of fixed-width output font
set prevspan_to_x [expr {$prev_x0 + $spandist}] ;#rightmost x of previous output text's character data
set lastchunk_to_x [expr {[string length $lastchunk] * $base_x_per_c}]
set lastchunk_to_x $effective_prev_x1 ;#JJJ
#if {$lastchunk_to_x > $prevspan_to_x} {
# set prevdata_to_x $lastchunk_to_x
#} else {
# set prevdata_to_x $prevspan_to_x
#}
set prevdata_to_x [expr {max($prevspan_to_x,$lastchunk_to_x)}]
set x_overlap [expr {max(0,$prevdata_to_x - $tx0)}]
if {$tx0 <= $prevdata_to_x} {
#it really does overlap/collide
#not sure of all situations in which it occurs - seems to correspond to background colour in some contexts (e.g alternating highlights in table rows)
#It is known that some pdf docs overlay same text with very small x,y offsets to indicate bold/emphasis
#possibly also due to font embedding issues?
#choose to emphasize by default (or apply user-chosen ansi from -highlight_overwrites)
# - but warn if dup position with different data
if {$prevtext eq $text} {
#same text in same position.
#This can be somewhat useful in some situations to aid parsing the output.
# - It can allow grouping of related rows by whether ansi is present or not
# (occurs in some tables)
#It can also make the terminal output more visually in sync with the source document.
#It may however not always be desired (even though stripping ansi from resultant output is easy enough)
#ans so can be disabled using the -highlight_overwrites option
if {$highlight_overwrites ne ""} {
#default bolding or user-chosen ANSI
#our buffer is not right for overriding previous xchunk
#set xcursor_prev [string length [punk::ansi::ansistrip [join [lrange $line_chunks 0 end-1] ""]]]
#set buffer_prev [string repeat " " [expr {$startpos - $xcursor_prev}]]
#lset line_chunks end "$buffer_prev[punk::ansi::a {*}$highlight_overwrites]$text[punk::ansi::a]"
lset line_chunks end "$buffer[punk::ansi::a {*}$highlight_overwrites]$text[punk::ansi::a]"
}
#else: no need to overwrite same text if no ansi
} else {
#(seen in for example pdf of Erlang FTP-1.1.1 documentation.
#Documentation line where function name is fixed-width font span being inserted into pre-existing line of variable-width font
# where inserted font point-size also seems slightly larger.
#vertical alignment slightly different, and only one space either side
#)
set char_overlap [x_to_c $x_overlap]
#set expandchars [expr {max($char_overlap,$x_gap_chars)}]
set expandchars $char_overlap
#now check our expandchars isn't too short for the actual spaces and tail of prev text and start of current text
set dataspaces 0
if {[regexp {(\s+)$} $prevtext _ tailspaces]} {
if {$prevtext ne $tailspaces} {
#
incr dataspaces [string length $tailspaces]
}
}
if {[regexp {^(\s+)} $text _ leadingspaces]} {
if {$text ne $leadingspaces} {
incr dataspaces [string length $leadingspaces]
}
}
#if {$char_overlap > $x_gap_chars} {
# incr expandchars $dataspaces
#} else {
# set expandchars [expr {max($expandchars,$dataspaces)}]
#}
set expand [string repeat " " $expandchars]
append buffer $expand
# REVIEW - seems to work reasonably (e.g in FTP-1.1.1.pdf mentioned) - but we'll still output a warning
lappend line_chunks $buffer$text
if {$char_overlap > 0 && $warnings_textblock >= 4} {
puts $errchan "Warning pageindex [format %4s $pageindex] blockindex [format %4s $tbnum] blockwarning [format %4s $block_warning_count] type {text overlay}"
# "warning ---- block ---- : attempt to overlay text" ;#alignment comment
puts $errchan "existing : '$prevtext'"
puts $errchan "overlay : '$text'"
puts $errchan "calculated char overlap : $char_overlap"
puts $errchan "x-gap chars : $x_gap_chars"
#puts $errchan "data spaces : $dataspaces"
puts $errchan "expanding by : $expandchars"
#puts $errchan "end this chunk : [c_to_x [string length [lindex $line_chunks end]]]"
}
incr block_warning_count ;#0 based
#puts stderr ">>$spans_by_x0"
}
} else {
#for example?
#narrow columns where amount fills entire field
#Assume it is new data - we don't even look at whether values are the same ?
dict set page_seen_chunks $text 1
#if {$xidx > 0} {
# set buffer [string repeat " " [expr {[x_to_c $effective_prev_x1] + $x_gap_chars}]]
#}
if {$xidx > 0 && $x_gap_chars == 0} {
set buffer [string repeat " " [expr {[x_to_c $effective_prev_x1]}]]
}
lappend line_chunks "$buffer$text"
}
} else {
dict set page_seen_chunks $text 1
if {$xidx > 0} {
if {$x_gap_chars == 0} {
#set buffer [string repeat " " [expr {[x_to_c $effective_prev_x1] + $x_gap_chars}]]
set buffer [string repeat " " [expr {[x_to_c $effective_prev_x1]}]]
}
}
lappend line_chunks "$buffer$text"
}
#position 3 is spans_by_x0_adjusted
set chunk [lindex $line_chunks end]
set chunklen [string length [punk::ansi::ansistrip $chunk]]
lset working_lines $block_linenum 3 $xidx 1 [c_to_x $chunklen]
#jjj
#if {[llength $line_chunks] == 1} {
# #lset working_lines $block_linenum 3 $xidx 0 [expr {[c_to_x $chunklen]-$start_char_pos}]
# lset working_lines $block_linenum 3 $xidx 0 [c_to_x [expr {$chunklen -[string length $buffer]}]]
#} else {
# #assert 2 or more
# set prevchunk [lindex $line_chunks end-1]
# set prevchunklen [string length [punk::ansi::ansistrip $prevchunk]]
# #lset working_lines $block_linenum 3 $xidx 0 [expr {[c_to_x $chunklen]-[c_to_x $prevchunklen]}]
# lset working_lines $block_linenum 3 $xidx 0 [c_to_x [expr {$prevchunklen + [string length $buffer]}]]
#}
}
#puts stdout [join $line_chunks ""]
set line ""
foreach lc $line_chunks {
set line [overtype::renderline -insert_mode 0 -expand_right 1 -transparent 1 $line $lc]
}
#lappend block_lines [join $line_chunks ""]
lappend block_lines $line
#puts stderr ">> $spans_by_x0"
}
#lappend MERGE_BUFFER [list lines $block_lines]
lset MERGE_BUFFER end end+1 [list lines $block_lines]
#lappend MERGE_BUFFER [list warnings $block_warning_count]
lset MERGE_BUFFER end end+1 [list warnings $block_warning_count]
#TESTBLOCK
# set mergedblock [overtype::block -transparent 1 -overflow 1 $mergedblock $block]
if {$TESTBLOCK eq ""} {
package require textblock
set TESTBLOCK [textblock::join -- [join $block_lines \n]]
set x1_previous_block 0
} else {
#set TESTBLOCK [overtype::block -transparent 1 -overflow 1 $mergedblock $block]
set TESTBLOCK [overtype::block -transparent 1 -overflow 1 $TESTBLOCK [join $block_lines \n]]
#must have been a previous block
set x1_previous_block [lindex $fudged_textblocks $tbnum-1 3]
}
#REVIEW - TESTBLOCK - perhaps of little use TEST.
#puts stdout "---TESTBLOCK--"
#puts stdout $TESTBLOCK
#puts stdout "--- --"
#puts stdout "--- x1 prev block $x1_previous_block"
#puts stdout "--- x1 testblock [c_to_x [textblock::widthtopline $TESTBLOCK]]"
} else {
puts $errchan "punk::pdf::text warning - shouldn't get here"
puts $outchan "$bx0,$by0----------------------------"
puts $outchan [$p textfrombox {*}[lrange $tb 1 4]]
puts $outchan "____________________________$bx1,$by1"
}
}
if {[llength $MERGE_BUFFER]} {
_process_merge_buffer MERGE_BUFFER $pageindex $cfg_blocksep_default $opt_blocksep $opt_merge_yblocks $opt_shrink_textfree_blocks $opt_postcompact $warnings_textblock $outchan $errchan
#puts $errchan "WARNING: MERGE_BUFFER not empty, but should be empty at end of page"
#puts $outchan [punk::lib::showdict -roottype list $MERGE_BUFFER]
}
}
if {$warnings_engine == 1} {
puts $errchan "MuPDF wasrepaired: [$d wasrepaired]"
puts $errchan "MuPDF warnings : [$d warnings]"
}
flush $outchan
flush $errchan
if {$opt_outchan eq "return" || $opt_errchan eq "return"} {
#if both are 'return' at the same time - the same channel is used for both
set result [encoding convertfrom utf-8 [set results($callid)]]
#set result [set results($callid)]
return $result
}
return
}
#*** !doctools
#[list_end] [comment {--- end definitions namespace punk::pdf ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Secondary API namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
tcl::namespace::eval punk::pdf::lib {
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase
tcl::namespace::path [tcl::namespace::parent]
#*** !doctools
#[subsection {Namespace punk::pdf::lib}]
#[para] Secondary functions that are part of the API
#[list_begin definitions]
#*** !doctools
#[list_end] [comment {--- end definitions namespace punk::pdf::lib ---}]
}
::tcl::namespace::eval ::punk::pdf::system {
#for basic verifying/identifying of coord lists of form: x0 y0 x1 y1
proc is_coord_list {lst} {
if {[llength $lst] != 4} {
return 0
}
set result 1
foreach item $lst {
if {![string is double -strict $item]} {
return 0
}
}
return $result
}
#subset of possible boms - just for experimenting
proc bom_ident {binstr} {
if {[string match \uFFEF* $binstr]} {
return bom
}
set first32 [string range $binstr 0 3]
set bytes [binary scan $binstr H2H2H2H2 a b c d]
switch -- $a {
fe {
if {"$b" eq "ff"} {
return utf-16be
}
}
ef {
if {"$b$c" eq "bbbf"} {
return utf-8
}
}
ff {
if {$b eq "fe"} {
if {"$c$d" eq "0000"} {
return utf-32le
} else {
return utf-16le
}
}
}
default {
if {"$a$b$c$d" eq "0000feff"} {
return utf-32be
}
}
}
return unknown
}
#points to number of chars - in terms of fixed-width output font
proc x_to_c {x} {
upvar ::punk::pdf::base_x_per_c base_x_per_c
return [expr {round($x / $base_x_per_c)}] ;#jjj
#return [expr {int($x / $base_x_per_c)}]
#return [expr {int(ceil($x / $base_x_per_c))}]
}
#number of chars to points - in terms of fixed-width output font
proc c_to_x {charcount} {
upvar ::punk::pdf::base_x_per_c base_x_per_c
return [expr {$charcount * $base_x_per_c}]
}
proc merge_buffer_dimensions {bufname} {
upvar $bufname MB
if {![llength $MB]} {
return [list 0 0 0 0] ;#x0 y0 x1 y1
}
set firstheader [lindex $MB 0 0]
set flds [dict get [lindex $firstheader 1] fields]
set dimensions [list [dict get $flds x0] [dict get $flds y0] [dict get $flds x1] [dict get $flds y1]]
#we can get negative values e.g image offset to left or top
foreach sametopblocks $MB {
foreach B $sametopblocks {
if {[lindex $B 0] eq "header"} {
set flds [dict get [lindex $B 1] fields]
set this_x0 [dict get $flds x0]
set this_y0 [dict get $flds y0]
set this_x1 [dict get $flds x1]
set this_y1 [dict get $flds y1]
lset dimensions 0 [expr {min([lindex $dimensions 0],$this_x0)}]
lset dimensions 1 [expr {min([lindex $dimensions 1],$this_y0)}]
lset dimensions 2 [expr {max([lindex $dimensions 2],$this_x1)}]
lset dimensions 3 [expr {max([lindex $dimensions 3],$this_y1)}]
}
}
}
return $dimensions
}
#whether merging on or off - we keep a MERGE_BUFFER
#At every y-index change, and at end of page, process last entry(s)
proc _process_merge_buffer {bufname pageindex blocksep_default opt_blocksep domerge opt_shrink_textfree_blocks opt_postcompact opt_warnings_textblock outc errc} {
upvar ::punk::pdf::base_x_per_c base_x_per_c
upvar ::punk::pdf::testchan testchan
set x_per_line [expr {$base_x_per_c * 2}] ;#hack for now
upvar $bufname MB
if {!$domerge} {
set block_warnings 0
#puts "MB:'$MB'"
set warningrecords [lsearch -all -inline -index 0 $MB warnings]
foreach wr $warningrecords {
incr block_warnings [lindex $wr 1]
}
#just emit the header & line blocks we have in the merge buffer one after the other
set blockresult "" ;#needed for postcompact
set emissions [list] ;#need to enable blank sep_block even when -postcompact is true
foreach yset $MB {
foreach B $yset {
switch -- [lindex $B 0] {
header {
if {$opt_blocksep ne ""} {
set sep [dict get [lindex $B 1] sep]
set sep [string map [list <w> $block_warnings] $sep]
#puts $outc $sep
lappend emissions [list data $blockresult]
lappend emissions [list sep $sep]
set blockresult ""
#append blockresult $sep \n
}
}
lines {
set lines [lindex $B 1]
#puts $errc "--->[punk::ansi::ansistring VIEW $lines]"
set idx -1
foreach l $lines {
incr idx
if {[catch {
#puts $outc $l
append blockresult $l \n
} errMsg]} {
set prevline [lindex $lines $idx-1]
set nextline [lindex $lines $idx+1]
puts stderr "error writing output channel $outc\nerrMsg:$errMsg\n"
puts stderr " prevline:'[punk::ansi::ansistring VIEW -lf 1 $prevline]'"
puts stderr " lineview '[punk::ansi::ansistring VIEW -lf 1 $l]'"
puts stderr " nextline:'[punk::ansi::ansistring VIEW -lf 1 $nextline]'"
#puts $testchan "=----------------------------------------="
#puts $testchan $l
#puts $testchan "=----------------------------------------="
}
}
}
block {
if {!$opt_shrink_textfree_blocks} {
#puts $outc [string map [list \uFFFD " "] [lindex $B 1]]
append blockresult [string map [list \uFFFD " "] [lindex $B 1]] \n
#puts $errc "--->[punk::ansi::ansistring VIEW [lindex $B 1]]"
}
}
warnings {}
}
}
}
lappend emissions [list data $blockresult]
set output ""
if {$opt_postcompact} {
foreach e $emissions {
lassign $e etype data_or_header
switch -- $etype {
sep {
append output $data_or_header \n
}
data {
foreach ln [split $data_or_header \n] {
if {[string trim $ln] ne ""} {
append output $ln \n
}
}
}
}
}
puts -nonewline $outc $output
} else {
foreach e $emissions {
lassign $e _ data_or_header
puts -nonewline $outc $data_or_header\n
}
}
set MB [list]
return
}
set all_headers [list]
set headers_structure [list]
set mergedblock ""
set warnings [list]
if {[llength $MB]} {
#rootheader doesn't 'contain' subsequent blocks in x or y direction - it's just the y0 that triggers the new merge because it's below previous blocks
#set rootheader [lindex $MB 0 0 1]
##puts $errc "rootheader: $rootheader"
#set base_y [dict get $rootheader fields y0]
set mbdimensions [merge_buffer_dimensions MB] ;# x0 y0 x1 y1
set base_y [lindex $mbdimensions 1]
}
#loop through and determine which blocks of those to be merged are RHS e.g largest x1
set max_x1 0
#todo - further refine to find blocks in the mergeset with no textblocks to right?
set rhs_block_indexes [list]
set textblock_x0_list [list]
foreach sametopblocks $MB {
foreach B $sametopblocks {
if {[lindex $B 0] eq "header"} {
set flds [dict get [lindex $B 1] fields]
#puts "flds: $flds"
if {[dict get $flds type] eq "text"} {
set this_x1 [dict get $flds x1]
lappend textblock_x0_list [dict get $flds x0]
if {abs($this_x1 - $max_x1) < 1} {
lappend rhs_block_indexes [dict get $flds blockindex]
set max_x1 [expr {max($this_x1,$max_x1)}]
} elseif {$this_x1 > $max_x1} {
set max_x1 $this_x1
set rhs_block_indexes [list [dict get $flds blockindex]]
}
}
}
}
}
foreach sametop $MB {
set yheaders [list]
set y_index ""
if {[llength $sametop]} {
set firstheader [lindex $sametop 0 1]
set blockrow_y0 [dict get $firstheader fields y0]
set blockrow_y1 [dict get $firstheader fields y1]
set blockrow_top_offset [expr {$blockrow_y0 - $base_y}]
#set blockrow_x0 [dict get $firstheader fields x0]
#set blockrow_x1 [dict get $firstheader fields x1]
if {$opt_warnings_textblock >= 9} {
puts $errc "Warning type debug operation {merge blockrow} firstheader $firstheader"
}
}
set header_idx -1
#set leftright_max [expr {[llength $sametop] -1}]
set hdrs [lsearch -all -inline -index 0 $sametop header]
#puts "--- > $hdrs"
set header_max [expr {[llength $hdrs] -1}]
foreach B $sametop {
#puts "====>$B"
switch -- [lindex $B 0] {
header {
incr header_idx ;#not necessarily in left-right order!
set this_y_index [dict get [lindex $B 1] fields y-index]
set this_x0 [dict get [lindex $B 1] fields x0]
set this_x1 [dict get [lindex $B 1] fields x1]
set this_y0 [dict get [lindex $B 1] fields y0]
set this_y1 [dict get [lindex $B 1] fields y1]
set this_top_offset [expr {$this_y0 - $base_y}]
set this_bottom_offset [expr {$this_y1 - $base_y}]
set this_block_index [dict get [lindex $B 1] fields blockindex]
if {$y_index eq ""} {
set y_index $this_y_index
} else {
if {$this_y_index ne $y_index} {
#shouldn't happen - but be noisy and bomb out if it does rather than produce wonky text.
set errmsg "Found item in MERGE_BUFFER with non-matching y-index (this: $this_y_index vs previous: $y_index), sorry - something went wrong in [info script]"
append errmsg \n "Item being processed: $B"
append errmsg \n "remaining MERGE_BUFFER: $MB"
puts $errc $errmsg
}
}
lappend all_headers [lindex $B 1]
lappend yheaders [lindex $B 1]
}
lines {
set lines [lindex $B 1]
#review - vertical oriented text at left can stomp on data during merge
#we are only given the bbox info by tclMuPDF
if {abs($this_x1 - $this_x0) < 1} {
#non displayable
#todo - emit at document tail as a non-layed out block?
if {$opt_warnings_textblock > 0} {
#review - firstheader vs this_block_index?
#puts $errc "Warning - no x space. Undisplayed text on page:[dict get $firstheader fields pageindex] block:[dict get $firstheader fields blockindex] text:[join $lines \n]"
puts $errc "Warning pageindex [dict get $firstheader fields pageindex] blockindex [dict get $firstheader fields blockindex] type {no x space} msg {Undisplayed text} text \"[join $lines \n]\""
}
lappend warnings 1
} else {
#todo - restrict to blockrow x0 to x1 - use overtype::block to truncate with ellipsis
# - warn if ellipsis in output
#e.g overtype::textblock::ellipsis $space_available $block
set block [join $lines \n]
set numlines [llength $lines]
set bwidth [textblock::width $block] ;#assume ragged block (differing line lengths)
set margintop ""
set bottom_offsetlines [expr {int(round($this_bottom_offset / $x_per_line))}]
if {[llength $MB] > 1 && $bottom_offsetlines >= $numlines} {
#can use bottom of textblock dist from top (better line alignment for scenario: "smallprint fieldname:" "bigprint value" aligned at base ???)
set topmarginlines [expr {$bottom_offsetlines - $numlines}]
#puts "====> offset from bottom bottom_offsetlines:$bottom_offsetlines topmarginlines:$topmarginlines this_y1:$this_y1"
if {$topmarginlines > 0} {
set margintop [textblock::block $bwidth $topmarginlines " "]
}
} else {
if {$this_top_offset > 0} {
#puts "====> offset from top"
#set offsetlines [expr {int(ceil($blockrow_top_offset / $x_per_line))}]
set offsetlines [expr {int(round($blockrow_top_offset / $x_per_line))}] ;#
#set offsetlines [expr {int(floor($this_top_offset / $x_per_line))}] ;#
if {$offsetlines > 0} {
set margintop [textblock::block $bwidth $offsetlines " "]
}
}
}
if {$this_block_index ni $rhs_block_indexes} {
#truncate overwidth blocks except for rhs
#overwidth if can collide (or merge) with x0 of any other textblocks in the merge set
#only interested in blocks where x0 >= this x1
set next_x0_list [list]
foreach x0 $textblock_x0_list {
if {$x0 >= ($this_x1 - $base_x_per_c)} {
lappend next_x0_list $x0
}
}
set next_x0_list [lsort -real $next_x0_list]
set closest_x0 [lindex $next_x0_list 0]
if {$closest_x0 ne ""} {
#set maxwidthchars [x_to_c $this_x1]
set maxwidthchars [x_to_c $closest_x0]
incr maxwidthchars -1 ;#vertically merging text blocks results in unintended text adjacencies - REVIEW
if {$maxwidthchars < $bwidth} {
#warn of rhs truncation
#text will have 'ellipsis'
#review - source text could already have ellipsis - this makes detecting truncation harder
#for now we will produce a warning if ellipsis is in the result and is on the right hand side
#todo - use a function other than textblock::frame which can report truncation
set block [textblock::frame -type {} -boxlimits {} -width [expr {$maxwidthchars +2}] $block] ;# +2 for vertical frame borders - which are removed but needed for block -width
set is_truncated 0
foreach ln [split [punk::ansi::ansistrip $block] \n] {
if {[regexp {[\u2026]$} $ln]} {
set is_truncated 1
break
}
}
if {$is_truncated} {
if {$opt_warnings_textblock > 0} {
puts $errc "Warning pageindex [dict get $firstheader fields pageindex] blockindex [dict get $firstheader fields blockindex] type {truncation} msg {merged textblock truncated on rhs} text \"[join $lines \n]\""
}
lappend warnings 1
}
}
}
}
#apply margintop after possible rhs truncation above (no need for ellipsis above content)
if {$margintop ne ""} {
set movedblock $margintop\n$block
} else {
set movedblock $block
}
if {$mergedblock eq ""} {
#could be image - as we are using overtype - should result in blank area
#todo - consider option to set bg colour, then create a block of spaces of the right size - but don't add any chars other than ANSI codes which can be stripped easily.
set mergedblock $movedblock
} else {
set mergedblock [overtype::block -transparent 1 -overflow 1 $mergedblock $movedblock]
}
}
}
block {
set block [lindex $B 1]
set bwidth [textblock::widthtopline $block] ;#assume non-ragged block
set margintop ""
if {$this_top_offset > 0} {
#set offsetlines [expr {int(ceil($this_top_offset / $x_per_line))}]
set offsetlines [expr {int(round($this_top_offset / $x_per_line))}]
if {$offsetlines > 0} {
set margintop [textblock::block $bwidth $offsetlines "\uFFFD"]
}
#puts "=====> offset:$this_top_offset"
}
if {$mergedblock eq ""} {
if {$margintop ne ""} {
set mergedblock $margintop\n[string map [list \uFFFD " "] $block]
} else {
set mergedblock [string map [list \uFFFD " "] $block]
}
} else {
if {$margintop ne ""} {
set movedblock $margintop\n$block
} else {
set movedblock $block
}
#review - img placeholder can obscure text if above it - we don't have transparency info - so probably not what's wanted
set mergedblock [overtype::block -transparent \uFFFD -overflow 1 $mergedblock $movedblock]
}
}
warnings {
#count of warnings emitted to stderr for the block
lappend warnings [lindex $B 1]
}
}
}
lappend headers_structure $yheaders
}
if {[llength $all_headers] == 1} {
#puts stdout "---> [lindex $all_headers 0 1]"
#only one block was in the MERGE_BUFFER
set stored_filled_sep [dict get [lindex $all_headers 0] sep]
if {$opt_blocksep ne ""} {
#should only be one warning record - but if more, we'll sum them
set block_warnings 0
foreach w $warnings {
incr block_warnings $w
}
#all % placeholders filled, but <w> isn't
set sep [string map [list <w> $block_warnings] $stored_filled_sep]
#still want to allow emitting empty line when opt_postcompact is true, if opt_blocksep is set to space
puts $outc $sep
}
#set is_text_free 1 ;#default to disprove
#set headerinfo [lindex $all_headers 0 1]
#set list_merges [dict get $headerinfo type]
#foreach blockrow_merge $list_merges {
# foreach tp $blockrow_merge {
# if {$tp eq "text"} {
# #todo - look for whitespace only and treat as text free
# set is_text_free 0
# }
# }
#}
set blockresult ""
if {$opt_shrink_textfree_blocks} {
set teststripped [punk::ansi::ansistrip $mergedblock]
if {[string trim $teststripped] ne ""} {
set blockresult $mergedblock
}
} else {
set blockresult $mergedblock
}
set output ""
if {$opt_postcompact} {
foreach ln [split $blockresult \n] {
if {[string trim $ln] ne ""} {
append output $ln \n
}
}
puts -nonewline $outc $output
} else {
puts -nonewline $outc $blockresult\n
}
} elseif {[llength $all_headers] > 1} {
#more than one block was in the MERGE_BUFFER
set merged_minx0 Inf
set merged_maxx1 0
set merged_miny0 Inf
set merged_maxy1 0
set merged_types [list]
set block_indexes [list]
foreach yheaders $headers_structure {
set ytypes [list]
foreach h $yheaders {
#puts "===>$h"
set F [dict get $h fields]
lappend ytypes [dict get $F type]
lappend block_indexes [dict get $F blockindex]
set merged_minx0 [expr {min([dict get $F x0], $merged_minx0)}]
set merged_maxx1 [expr {max([dict get $F x1], $merged_maxx1)}]
set merged_miny0 [expr {min([dict get $F y0], $merged_miny0)}]
set merged_maxy1 [expr {max([dict get $F y1], $merged_maxy1)}]
#puts stderr $h
}
lappend merged_types $ytypes
}
if {$opt_blocksep ne ""} {
set total_warnings 0 ;#add warning counts from constituent blocks
foreach w $warnings {
incr total_warnings $w
}
set opt_blocksep [string map [list %default% $blocksep_default] $opt_blocksep]
set map [list %nl% "\n" %crlf% "\r\n" %type% "{$merged_types}" %pageindex% $pageindex %blockindex% "{$block_indexes}" %y-index% $y_index %x0% $merged_minx0 %y0% $merged_miny0 %x1% $merged_maxx1 %y1% $merged_maxy1 %tlc% "$merged_minx0 $merged_miny0" %brc% "$merged_maxx1 $merged_maxy1" %warnings% $total_warnings]
dict set map %marker% "[punk::ansi::a bold cyan]MERGEDBLOCK[punk::ansi::a]"
puts $outc [string map $map $opt_blocksep]
}
set blockresult ""
if {$opt_shrink_textfree_blocks} {
set teststripped [punk::ansi::ansistrip $mergedblock]
if {[string trim $teststripped] ne ""} {
#puts $outc $mergedblock
set blockresult $mergedblock
}
} else {
#puts $outc $mergedblock
set blockresult $mergedblock
}
set output ""
if {$opt_postcompact} {
foreach ln [split $blockresult \n] {
if {[string trim $ln] ne ""} {
append output $ln \n
}
}
puts -nonewline $outc $output
} else {
puts -nonewline $outc $blockresult\n
}
}
set MB [list]
}
#sorting helper function
proc block_ordering {a b} {
#compare by ny0 first, then nx0 (upper side of block then left side)
set s1 [expr {round([lindex $a 6] - [lindex $b 6])}]
if {$s1 == 0} {
return [expr {round([lindex $a 5] - [lindex $a 5])}]
} else {
return $s1
}
}
}
# == === === === === === === === === === === === === === ===
# Sample 'about' function with punk::args documentation
# == === === === === === === === === === === === === === ===
tcl::namespace::eval punk::pdf {
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase
variable PUNKARGS
variable PUNKARGS_aliases
lappend PUNKARGS [list {
@id -id "(package)punk::pdf"
@package -name "punk::pdf" -help\
"PDF parsing assistant"
}]
namespace eval argdoc {
#namespace for custom argument documentation
proc package_name {} {
return punk::pdf
}
proc about_topics {} {
#info commands results are returned in an arbitrary order (like array keys)
set topic_funs [info commands [namespace current]::get_topic_*]
set about_topics [list]
foreach f $topic_funs {
set tail [namespace tail $f]
lappend about_topics [string range $tail [string length get_topic_] end]
}
#Adjust this function or 'default_topics' if a different order is required
return [lsort $about_topics]
}
proc default_topics {} {return [list Description *]}
# -------------------------------------------------------------
# get_topic_ functions add more to auto-include in about topics
# -------------------------------------------------------------
proc get_topic_Description {} {
punk::args::lib::tstr [string trim {
package punk::pdf
description to come..
} \n]
}
proc get_topic_License {} {
return "MIT"
}
proc get_topic_Version {} {
return "$::punk::pdf::version"
}
proc get_topic_Contributors {} {
set authors {{<julian@precisium.com.au> "Julian Noble"}}
set contributors ""
foreach a $authors {
append contributors $a \n
}
if {[string index $contributors end] eq "\n"} {
set contributors [string range $contributors 0 end-1]
}
return $contributors
}
proc get_topic_custom-topic {} {
punk::args::lib::tstr -return string {
A custom
topic
etc
}
}
# -------------------------------------------------------------
}
# we re-use the argument definition from punk::args::standard_about and override some items
set overrides [dict create]
dict set overrides @id -id "::punk::pdf::about"
dict set overrides @cmd -name "punk::pdf::about"
dict set overrides @cmd -help [string trim [punk::args::lib::tstr {
About punk::pdf
}] \n]
dict set overrides topic -choices [list {*}[punk::pdf::argdoc::about_topics] *]
dict set overrides topic -choicerestricted 1
dict set overrides topic -default [punk::pdf::argdoc::default_topics] ;#if -default is present 'topic' will always appear in parsed 'values' dict
set newdef [punk::args::resolved_def -antiglobs -package_about_namespace -override $overrides ::punk::args::package::standard_about *]
lappend PUNKARGS [list $newdef]
proc about {args} {
package require punk::args
#standard_about accepts additional choices for topic - but we need to normalize any abbreviations to full topic name before passing on
set argd [punk::args::parse $args withid ::punk::pdf::about]
lassign [dict values $argd] _leaders opts values _received
punk::args::package::standard_about -package_about_namespace ::punk::pdf::argdoc {*}$opts {*}[dict get $values topic]
}
}
# end of sample 'about' function
# == === === === === === === === === === === === === === ===
# -----------------------------------------------------------------------------
# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked
# -----------------------------------------------------------------------------
# variable PUNKARGS
# variable PUNKARGS_aliases
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::punk::pdf
}
# -----------------------------------------------------------------------------
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide punk::pdf [tcl::namespace::eval punk::pdf {
variable pkg punk::pdf
variable version
set version 999999.0a1.0
}]
return
#*** !doctools
#[manpage_end]