module fortplot_pdf_text_escape !! PDF text escaping and symbol mapping utilities use, intrinsic :: iso_fortran_env, only: wp => real64 implicit none private public :: escape_pdf_string public :: unicode_to_symbol_char public :: unicode_codepoint_to_pdf_escape public :: lookup_script_fallback public :: SCRIPT_SCALE, SUPERSCRIPT_RISE, SUBSCRIPT_DROP ! Unicode super/subscript glyphs without a WinAnsi/Symbol code are drawn ! as reduced Helvetica glyphs raised/lowered by a text rise (em fractions). real(wp), parameter :: SCRIPT_SCALE = 0.7_wp real(wp), parameter :: SUPERSCRIPT_RISE = 0.35_wp real(wp), parameter :: SUBSCRIPT_DROP = 0.15_wp contains subroutine escape_pdf_string(input, output, output_len) !! Escape special characters in PDF strings character(len=*), intent(in) :: input character(len=*), intent(out) :: output integer, intent(out) :: output_len integer :: i, j, n character :: ch n = len(input) j = 0 do i = 1, n ch = input(i:i) ! Fortran has no backslash escape: achar(92) is a literal '\'. if (ch == '(' .or. ch == ')' .or. ch == achar(92)) then j = j + 1 if (j <= len(output)) output(j:j) = achar(92) j = j + 1 if (j <= len(output)) output(j:j) = ch else j = j + 1 if (j <= len(output)) output(j:j) = ch end if end do output_len = j if (j < len(output)) output(j + 1:) = ' ' end subroutine escape_pdf_string subroutine unicode_to_symbol_char(unicode_codepoint, symbol_char) !! Convert Unicode codepoint to Symbol font character integer, intent(in) :: unicode_codepoint character(len=*), intent(out) :: symbol_char character(len=8) :: esc logical :: found symbol_char = '' esc = '' found = .false. ! Common math symbols supported by Symbol font ! U+221A (square root) maps to octal \326 in Symbol encoding if (unicode_codepoint == 8730) then symbol_char = achar(92)//'326' return end if ! U+221E (infinity) maps to octal \245 in Symbol encoding if (unicode_codepoint == 8734) then symbol_char = achar(92)//'245' return end if ! U+2202 (partial differential) maps to octal \266 in Symbol encoding if (unicode_codepoint == 8706) then symbol_char = achar(92)//'266' return end if ! Math relations and operators in Adobe Symbol encoding select case (unicode_codepoint) case (8733) ! U+221D proportional symbol_char = achar(92)//'265' return case (8776) ! U+2248 approxequal symbol_char = achar(92)//'273' return case (8764) ! U+223C similar symbol_char = achar(92)//'176' return case (8747) ! U+222B integral symbol_char = achar(92)//'362' return case (8711) ! U+2207 nabla / gradient symbol_char = achar(92)//'321' return case (8804) ! U+2264 lessequal symbol_char = achar(92)//'243' return case (8805) ! U+2265 greaterequal symbol_char = achar(92)//'263' return case (8800) ! U+2260 notequal symbol_char = achar(92)//'271' return case (8801) ! U+2261 equivalence symbol_char = achar(92)//'272' return end select call lookup_symbol_operator(unicode_codepoint, esc, found) if (found) then symbol_char = trim(esc) return end if ! Arrows in Symbol encoding select case (unicode_codepoint) case (8592) symbol_char = achar(92)//'254' return case (8593) symbol_char = achar(92)//'255' return case (8594) symbol_char = achar(92)//'256' return case (8595) symbol_char = achar(92)//'257' return case (8596) symbol_char = achar(92)//'253' return end select call lookup_lowercase_greek(unicode_codepoint, esc, found) if (.not. found) call lookup_uppercase_greek(unicode_codepoint, esc, found) if (found) symbol_char = trim(esc) end subroutine unicode_to_symbol_char subroutine unicode_codepoint_to_pdf_escape(codepoint, escape_seq) !! Convert Unicode codepoint to PDF escape sequence integer, intent(in) :: codepoint character(len=*), intent(out) :: escape_seq logical :: found escape_seq = '' found = .false. select case (codepoint) case (8722) ! U+2212 MINUS SIGN: the Helvetica font object remaps code 31 ! (octal \037) to the /minus glyph via a Differences array, so ! this renders and extracts as a true typographic minus, matching ! matplotlib's negative tick labels. escape_seq = achar(92)//'037' found = .true. case (8211) ! U+2013 EN DASH maps directly to the WinAnsi en dash. escape_seq = achar(92)//'226' found = .true. case (8212) ! U+2014 EM DASH maps to the WinAnsi em dash (octal \227). escape_seq = achar(92)//'227' found = .true. case (188) escape_seq = achar(92)//'274' found = .true. case (176) escape_seq = achar(92)//'260' found = .true. case (177) escape_seq = achar(92)//'261' found = .true. case (178) escape_seq = achar(92)//'262' found = .true. case (179) escape_seq = achar(92)//'263' found = .true. case (181) escape_seq = achar(92)//'265' found = .true. case (183) escape_seq = achar(92)//'267' found = .true. case (185) escape_seq = achar(92)//'271' found = .true. case (215) escape_seq = achar(92)//'327' found = .true. case (247) escape_seq = achar(92)//'367' found = .true. end select if (.not. found) then call lookup_lowercase_greek(codepoint, escape_seq, found) end if if (.not. found) then call lookup_uppercase_greek(codepoint, escape_seq, found) end if if (.not. found) escape_seq = '' end subroutine unicode_codepoint_to_pdf_escape subroutine lookup_lowercase_greek(codepoint, escape_seq, found) integer, intent(in) :: codepoint character(len=*), intent(out) :: escape_seq logical, intent(out) :: found found = .true. select case (codepoint) case (945) escape_seq = achar(92)//'141' case (946) escape_seq = achar(92)//'142' case (947) escape_seq = achar(92)//'147' case (948) escape_seq = achar(92)//'144' case (949) escape_seq = achar(92)//'145' case (950) escape_seq = achar(92)//'172' case (951) escape_seq = achar(92)//'150' case (952) escape_seq = achar(92)//'161' case (953) escape_seq = achar(92)//'151' case (954) escape_seq = achar(92)//'153' case (955) escape_seq = achar(92)//'154' case (956) escape_seq = achar(92)//'155' case (957) escape_seq = achar(92)//'156' case (958) escape_seq = achar(92)//'170' case (959) escape_seq = achar(92)//'157' case (960) escape_seq = achar(92)//'160' case (961) escape_seq = achar(92)//'162' case (963) escape_seq = achar(92)//'163' case (964) escape_seq = achar(92)//'164' case (965) escape_seq = achar(92)//'165' case (966) escape_seq = achar(92)//'146' case (967) escape_seq = achar(92)//'143' case (968) escape_seq = achar(92)//'171' case (969) escape_seq = achar(92)//'167' case default found = .false. end select end subroutine lookup_lowercase_greek subroutine lookup_uppercase_greek(codepoint, escape_seq, found) integer, intent(in) :: codepoint character(len=*), intent(out) :: escape_seq logical, intent(out) :: found found = .true. select case (codepoint) case (913) escape_seq = achar(92)//'101' case (914) escape_seq = achar(92)//'102' case (915) escape_seq = achar(92)//'107' case (916) escape_seq = achar(92)//'104' case (917) escape_seq = achar(92)//'105' case (918) escape_seq = achar(92)//'132' case (919) escape_seq = achar(92)//'110' case (920) escape_seq = achar(92)//'121' case (921) escape_seq = achar(92)//'111' case (922) escape_seq = achar(92)//'113' case (923) escape_seq = achar(92)//'114' case (924) escape_seq = achar(92)//'115' case (925) escape_seq = achar(92)//'116' case (926) escape_seq = achar(92)//'130' case (927) escape_seq = achar(92)//'117' case (928) escape_seq = achar(92)//'120' case (929) escape_seq = achar(92)//'122' case (931) escape_seq = achar(92)//'123' case (932) escape_seq = achar(92)//'124' case (933) escape_seq = achar(92)//'125' case (934) escape_seq = achar(92)//'106' case (935) escape_seq = achar(92)//'103' case (936) escape_seq = achar(92)//'131' case (937) escape_seq = achar(92)//'127' case default found = .false. end select end subroutine lookup_uppercase_greek subroutine lookup_symbol_operator(codepoint, escape_seq, found) !! Further operators, relations and arrows of the Adobe Symbol encoding integer, intent(in) :: codepoint character(len=*), intent(out) :: escape_seq logical, intent(out) :: found found = .true. escape_seq = '' select case (codepoint) case (8658) ! U+21D2 double arrow right escape_seq = achar(92)//'336' case (8656) ! U+21D0 double arrow left escape_seq = achar(92)//'334' case (8660) ! U+21D4 double arrow both escape_seq = achar(92)//'333' case (8657) ! U+21D1 double arrow up escape_seq = achar(92)//'335' case (8659) ! U+21D3 double arrow down escape_seq = achar(92)//'337' case (8629) ! U+21B5 carriage return escape_seq = achar(92)//'277' case (8712) ! U+2208 element of escape_seq = achar(92)//'316' case (8713) ! U+2209 not element of escape_seq = achar(92)//'317' case (8715) ! U+220B contains as member escape_seq = achar(92)//'047' case (8704) ! U+2200 for all escape_seq = achar(92)//'042' case (8707) ! U+2203 there exists escape_seq = achar(92)//'044' case (8721) ! U+2211 summation escape_seq = achar(92)//'345' case (8719) ! U+220F product escape_seq = achar(92)//'325' case (8727) ! U+2217 asterisk operator escape_seq = achar(92)//'052' case (8901) ! U+22C5 dot operator escape_seq = achar(92)//'327' case (8869) ! U+22A5 up tack escape_seq = achar(92)//'136' case (8773) ! U+2245 approximately equal escape_seq = achar(92)//'100' case (8736) ! U+2220 angle escape_seq = achar(92)//'320' case (8743) ! U+2227 logical and escape_seq = achar(92)//'331' case (8744) ! U+2228 logical or escape_seq = achar(92)//'332' case (8745) ! U+2229 intersection escape_seq = achar(92)//'307' case (8746) ! U+222A union escape_seq = achar(92)//'310' case (8834) ! U+2282 subset escape_seq = achar(92)//'314' case (8835) ! U+2283 superset escape_seq = achar(92)//'311' case (8838) ! U+2286 subset or equal escape_seq = achar(92)//'315' case (8839) ! U+2287 superset or equal escape_seq = achar(92)//'312' case (8836) ! U+2284 not subset escape_seq = achar(92)//'313' case (8709) ! U+2205 empty set escape_seq = achar(92)//'306' case (8855) ! U+2297 circled times escape_seq = achar(92)//'304' case (8853) ! U+2295 circled plus escape_seq = achar(92)//'305' case (8501) ! U+2135 alef escape_seq = achar(92)//'300' case (8472) ! U+2118 Weierstrass p escape_seq = achar(92)//'303' case (8465) ! U+2111 imaginary part escape_seq = achar(92)//'301' case (8476) ! U+211C real part escape_seq = achar(92)//'302' case (8756) ! U+2234 therefore escape_seq = achar(92)//'134' case (9001) ! U+2329 left angle bracket escape_seq = achar(92)//'341' case (10216) ! U+27E8 mathematical left angle bracket escape_seq = achar(92)//'341' case (9002) ! U+232A right angle bracket escape_seq = achar(92)//'361' case (10217) ! U+27E9 mathematical right angle bracket escape_seq = achar(92)//'361' case (8242) ! U+2032 prime escape_seq = achar(92)//'242' case (8243) ! U+2033 double prime escape_seq = achar(92)//'262' case default found = .false. end select end subroutine lookup_symbol_operator subroutine lookup_script_fallback(codepoint, base, rise) !! Unicode superscript/subscript characters absent from WinAnsi and !! Symbol: the base character and its rise (+1 super, -1 sub, 0 none). integer, intent(in) :: codepoint character(len=1), intent(out) :: base integer, intent(out) :: rise base = ' ' rise = 0 select case (codepoint) case (8304) ! U+2070 superscript zero base = '0'; rise = 1 case (8305) ! U+2071 superscript i base = 'i'; rise = 1 case (8308:8313) ! U+2074..U+2079 superscript four..nine base = achar(iachar('4') + codepoint - 8308); rise = 1 case (8314) ! U+207A superscript plus base = '+'; rise = 1 case (8315) ! U+207B superscript minus base = '-'; rise = 1 case (8316) ! U+207C superscript equals base = '='; rise = 1 case (8317) ! U+207D superscript left parenthesis base = '('; rise = 1 case (8318) ! U+207E superscript right parenthesis base = ')'; rise = 1 case (8319) ! U+207F superscript n base = 'n'; rise = 1 case (8320:8329) ! U+2080..U+2089 subscript zero..nine base = achar(iachar('0') + codepoint - 8320); rise = -1 case (8330) ! U+208A subscript plus base = '+'; rise = -1 case (8331) ! U+208B subscript minus base = '-'; rise = -1 case (8332) ! U+208C subscript equals base = '='; rise = -1 case (8333) ! U+208D subscript left parenthesis base = '('; rise = -1 case (8334) ! U+208E subscript right parenthesis base = ')'; rise = -1 case (8336) ! U+2090 subscript a base = 'a'; rise = -1 case (8337) ! U+2091 subscript e base = 'e'; rise = -1 case (8338) ! U+2092 subscript o base = 'o'; rise = -1 case (8339) ! U+2093 subscript x base = 'x'; rise = -1 end select end subroutine lookup_script_fallback end module fortplot_pdf_text_escape