Listenvariablen: nur letzten Eintrag ausgeben
Zitat von Peter Quick am 27. August 2026, 11:15 UhrWarum die Umlaute kaputt gingen — das war mein Fehler: Ich habe die Zeichen per
iconvnach UTF-8 konvertiert. Der alte Code hat sie aber als ANSI/CP1252 ausgegeben (die literalen „ä“ in Ihrer alten Umlaut-Tabelle waren einzelne CP1252-Bytes, weil die PHP-Datei als ANSI gespeichert ist). Medical Office erwartet also ANSI — mein UTF-8 kam als Mojibake an.In v3 gibt es dafür oben einen Schalter:
php$ausgabeKodierung = 'CP1252'; // = ANSI, wie bisher. Alternative: 'UTF-8'Im CP1252-Modus wird das Hex-Byte aus dem RTF (
\'e4) einfach 1:1 durchgereicht — genau wie vorher, aber jetzt zusätzlich case-insensitive und für alle Sonderzeichen, nicht nur die sieben aus Ihrer Tabelle. Verifiziert: die Ausgabe enthält die Bytes0xE4,0xF6,0xDFfür ä/ö/ß — byte-identisch mit dem alten Verhalten.Zum verbliebenen Formatierungsproblem: Ich habe die Ignore-Liste deutlich erweitert (u. a.
\pn,\pntxtb,\listoverride,\shppict,\fldrslt…) und zusätzlich zwei Dinge korrigiert, die vorher durchrutschen konnten:\tab/\cellerzeugen jetzt ein Leerzeichen statt zu verschwinden (sonst klebten Wörter zusammen), und der\uc-Skip-Zähler wird an Gruppengrenzen zurückgesetzt.Aber ehrlich: ich rate hier weiterhin. Ich habe das an einem selbst nachgebauten Word-RTF getestet, nicht an Ihren echten Daten — und die Zeichenfolge „0 s22″ passt zu keinem RTF-Konstrukt, das ich sicher zuordnen kann. Solange ich das reale Feld nicht sehe, ist jede weitere Runde Stochern im Nebel.
Deshalb der Debug-Schalter oben:
php$debugRoh = true;Damit gibt das Snippet den Rohtext zwischen
ROH>>>und<<<ROHaus — vor jeder Normalisierung. Noch hilfreicher wäre der komplett unbehandelte Feldinhalt; dafür ganz kurz:php{PHP} $text = "{Freitexte:Tagzahl#3600:Datum}"; echo "<pre>" . htmlspecialchars($text) . "</pre>"; {/PHP}Wenn Sie mir davon die Ausgabe eines betroffenen Eintrags schicken (gern mit Fantasienamen statt Patientendaten), sehe ich in zwei Minuten, wo „ab“ und „0 s22″ wirklich herkommen — statt dass wir weiter probeweise Steuerwörter in die Liste aufnehmen.
Das Problem ist eben, dass wir sicherlich kein RTF-Konstrukt zum Testen haben, was man in der Rohform übergeben kann. Man könnte alternativ einen Brief oder eine Briefvorlage übergeben. Das kann ich mal testen.
{PHP} // ========================================================== // Neuesten Freitext-Eintrag aus RTF-Feld extrahieren // PHP 5.2 kompatibel (Medical Office intern) // ========================================================== // >>> Ausgabe-Kodierung: 'CP1252' = ANSI (wie bisher), oder 'UTF-8' $ausgabeKodierung = 'CP1252'; // >>> DEBUG: auf true setzen, um den unformatierten Rohtext zu sehen $debugRoh = false; $text = "{Freitexte:Tagzahl#3600:Datum}"; // ---------- Hilfsfunktionen ---------- function rtf_utf8_from_codepoint($code) { // Ersatz fuer mb_chr() (existiert erst ab PHP 7.2) if ($code < 0x80) return chr($code); if ($code < 0x800) { return chr(0xC0 | ($code >> 6)) . chr(0x80 | ($code & 0x3F)); } if ($code < 0x10000) { return chr(0xE0 | ($code >> 12)) . chr(0x80 | (($code >> 6) & 0x3F)) . chr(0x80 | ($code & 0x3F)); } return chr(0xF0 | ($code >> 18)) . chr(0x80 | (($code >> 12) & 0x3F)) . chr(0x80 | (($code >> 6) & 0x3F)) . chr(0x80 | ($code & 0x3F)); } function rtf_cp1252_from_codepoint($code) { // Unicode-Codepoint -> CP1252-Byte (soweit darstellbar) if ($code < 0x80) return chr($code); if ($code >= 0xA0 && $code <= 0xFF) return chr($code); $sonder = array( 0x20AC => 0x80, 0x201A => 0x82, 0x0192 => 0x83, 0x201E => 0x84, 0x2026 => 0x85, 0x2020 => 0x86, 0x2021 => 0x87, 0x02C6 => 0x88, 0x2030 => 0x89, 0x0160 => 0x8A, 0x2039 => 0x8B, 0x0152 => 0x8C, 0x017D => 0x8E, 0x2018 => 0x91, 0x2019 => 0x92, 0x201C => 0x93, 0x201D => 0x94, 0x2022 => 0x95, 0x2013 => 0x96, 0x2014 => 0x97, 0x02DC => 0x98, 0x2122 => 0x99, 0x0161 => 0x9A, 0x203A => 0x9B, 0x0153 => 0x9C, 0x017E => 0x9E, 0x0178 => 0x9F, ); if (isset($sonder[$code])) return chr($sonder[$code]); return '?'; } function rtf_cp1252_high_to_unicode($byte) { $map = array( 0x80=>0x20AC,0x82=>0x201A,0x83=>0x0192,0x84=>0x201E,0x85=>0x2026, 0x86=>0x2020,0x87=>0x2021,0x88=>0x02C6,0x89=>0x2030,0x8A=>0x0160, 0x8B=>0x2039,0x8C=>0x0152,0x8E=>0x017D,0x91=>0x2018,0x92=>0x2019, 0x93=>0x201C,0x94=>0x201D,0x95=>0x2022,0x96=>0x2013,0x97=>0x2014, 0x98=>0x02DC,0x99=>0x2122,0x9A=>0x0161,0x9B=>0x203A,0x9C=>0x0153, 0x9E=>0x017E,0x9F=>0x0178, ); return isset($map[$byte]) ? $map[$byte] : $byte; } // ---------- RTF -> Klartext ---------- function rtf_to_text($rtf, $zielKodierung) { // Destinations, deren INHALT kein sichtbarer Text ist -> komplett verwerfen. // Hier kamen die Fremdzeichen ("ab", "0 s22", Aufzaehlungspunkte) her. $ignoreDestinations = array( 'fonttbl','colortbl','stylesheet','info','generator','operator','company', 'title','subject','author','keywords','comment','doccomm','creatim','revtim', 'pn','pntext','pntxta','pntxtb','pnseclvl','listtext','leveltext','levelnumbers', 'list','listtable','listoverride','listoverridetable','listlevel','listname','listpicture', 'pict','nonshppict','shppict','shpinst','shptxt','object','objdata','objclass','result', 'header','footer','headerf','footerf','headerl','headerr','footerl','footerr', 'footnote','ftnsep','ftnsepc','ftncn','aftnsep','aftnsepc','aftncn', 'rsidtbl','xmlnstbl','latentstyles','themedata','colorschememapping','datastore', 'wgrffmtfilter','revtbl','filetbl','protusertbl','userprops','svb','mmath','mmathPr', 'bkmkstart','bkmkend','xe','tc','tcn','field','fldinst','fldrslt','atnid','atnauthor', 'annotation','falt','panose','template','urtf','factoidname','datafield','do', ); // Steuerwoerter, die ein sichtbares Zeichen erzeugen $symbole = array( 'bullet' => 0x2022, 'endash' => 0x2013, 'emdash' => 0x2014, 'lquote' => 0x2018, 'rquote' => 0x2019, 'ldblquote' => 0x201C, 'rdblquote' => 0x201D, 'emspace' => 0x20, 'enspace' => 0x20, 'qmspace' => 0x20, ); $len = strlen($rtf); $i = 0; $out = ''; $ignoreStack = array(false); // pro Gruppen-Ebene: Inhalt verwerfen? $unicodeSkip = 1; // Fallback-Zeichen nach \uN (per \ucN) $skipCounter = 0; while ($i < $len) { $ch = $rtf[$i]; $ignore = $ignoreStack[count($ignoreStack) - 1]; if ($ch === '{') { $ignoreStack[] = $ignore; // Kind erbt Zustand des Elternteils $skipCounter = 0; $i++; continue; } if ($ch === '}') { if (count($ignoreStack) > 1) array_pop($ignoreStack); $skipCounter = 0; $i++; continue; } if ($ch === '\\') { // --- Hex-Escape \'xx (Gross-/Kleinschreibung egal) --- if ($i + 1 < $len && $rtf[$i + 1] === "'") { $hex = substr($rtf, $i + 2, 2); if ($skipCounter > 0) { $skipCounter--; } elseif (!$ignore) { $byte = hexdec($hex); // ist bereits ein CP1252-Byte if ($zielKodierung === 'UTF-8') { $cp = ($byte >= 0x80 && $byte <= 0x9F) ? rtf_cp1252_high_to_unicode($byte) : $byte; $out .= rtf_utf8_from_codepoint($cp); } else { $out .= chr($byte); // 1:1 durchreichen = ANSI } } $i += 4; continue; } // --- \* : diese Destination ist "ignorable" --- if ($i + 1 < $len && $rtf[$i + 1] === '*') { $ignoreStack[count($ignoreStack) - 1] = true; $i += 2; continue; } // --- Steuerwort: \wort [-]zahl [Leerzeichen] --- if (preg_match('/^\\\\([a-zA-Z]+)(-?[0-9]+)?[ ]?/', substr($rtf, $i, 40), $m)) { $word = $m[1]; $param = (isset($m[2]) && $m[2] !== '') ? (int)$m[2] : null; $i += strlen($m[0]); if (in_array($word, $ignoreDestinations, true)) { $ignoreStack[count($ignoreStack) - 1] = true; } elseif ($word === 'u') { if ($param !== null && !$ignore) { $code = $param < 0 ? $param + 65536 : $param; $out .= ($zielKodierung === 'UTF-8') ? rtf_utf8_from_codepoint($code) : rtf_cp1252_from_codepoint($code); } $skipCounter = $unicodeSkip; } elseif ($word === 'uc') { $unicodeSkip = ($param !== null) ? $param : 1; } elseif (isset($symbole[$word])) { if ($skipCounter > 0) { $skipCounter--; } elseif (!$ignore) { $code = $symbole[$word]; $out .= ($zielKodierung === 'UTF-8') ? rtf_utf8_from_codepoint($code) : rtf_cp1252_from_codepoint($code); } } elseif ($word === 'par' || $word === 'line' || $word === 'sect' || $word === 'row' || $word === 'page') { if (!$ignore) $out .= "\n"; } elseif ($word === 'cell' || $word === 'tab') { if ($skipCounter > 0) { $skipCounter--; } elseif (!$ignore) $out .= ' '; } // alle uebrigen Steuerwoerter (fs22, li720, fi-360, lang7, f0, s22, ...) // sind reine Formatierung und werden verworfen continue; } // --- Kontrollsymbol: \ + genau ein Nicht-Buchstabe --- if ($i + 1 < $len) { $sym = $rtf[$i + 1]; if (!$ignore) { if ($sym === '~') $out .= ' '; elseif ($sym === '_') $out .= '-'; elseif ($sym === '\\' || $sym === '{' || $sym === '}') $out .= $sym; // \- (optionaler Trennstrich) und Rest: verwerfen } $i += 2; continue; } $i++; continue; } // --- normaler Text --- if ($ch === "\r" || $ch === "\n") { // Zeilenumbrueche im RTF-Quelltext sind bedeutungslos $i++; continue; } if ($skipCounter > 0) { $skipCounter--; } elseif (!$ignore) { $out .= $ch; } $i++; } return $out; } // ---------- Ablauf ---------- $text = rtf_to_text($text, $ausgabeKodierung); if ($debugRoh) { echo "ROH>>>" . $text . "<<<ROH"; } else { // Spaces normalisieren $text = preg_replace('/\s+/', ' ', $text); $text = trim($text); // Bloecke nach Datum aufteilen preg_match_all( '/(\d{2}\.\d{2}\.\d{2})(.*?)(?=\d{2}\.\d{2}\.\d{2}|$)/', $text, $blocks ); // Nur den aktuellsten Eintrag suchen $neusterText = ''; $neustesDatum = ''; for ($i = 0; $i < count($blocks[0]); $i++) { $datum = trim($blocks[1][$i]); $block = trim($blocks[2][$i]); // TT.MM.JJ -> JJMMTT $vergleich = substr($datum, 6, 2) . substr($datum, 3, 2) . substr($datum, 0, 2); if ($vergleich > $neustesDatum) { $neustesDatum = $vergleich; $neusterText = $block; } } // Ausgabe if ($neusterText != '') { echo $neusterText; } else { echo "nichts da aber funktioniert"; } } {/PHP}
Warum die Umlaute kaputt gingen — das war mein Fehler: Ich habe die Zeichen per iconv nach UTF-8 konvertiert. Der alte Code hat sie aber als ANSI/CP1252 ausgegeben (die literalen „ä“ in Ihrer alten Umlaut-Tabelle waren einzelne CP1252-Bytes, weil die PHP-Datei als ANSI gespeichert ist). Medical Office erwartet also ANSI — mein UTF-8 kam als Mojibake an.
In v3 gibt es dafür oben einen Schalter:
$ausgabeKodierung = 'CP1252'; // = ANSI, wie bisher. Alternative: 'UTF-8'
Im CP1252-Modus wird das Hex-Byte aus dem RTF (\'e4) einfach 1:1 durchgereicht — genau wie vorher, aber jetzt zusätzlich case-insensitive und für alle Sonderzeichen, nicht nur die sieben aus Ihrer Tabelle. Verifiziert: die Ausgabe enthält die Bytes 0xE4, 0xF6, 0xDF für ä/ö/ß — byte-identisch mit dem alten Verhalten.
Zum verbliebenen Formatierungsproblem: Ich habe die Ignore-Liste deutlich erweitert (u. a. \pn, \pntxtb, \listoverride, \shppict, \fldrslt …) und zusätzlich zwei Dinge korrigiert, die vorher durchrutschen konnten: \tab/\cell erzeugen jetzt ein Leerzeichen statt zu verschwinden (sonst klebten Wörter zusammen), und der \uc-Skip-Zähler wird an Gruppengrenzen zurückgesetzt.
Aber ehrlich: ich rate hier weiterhin. Ich habe das an einem selbst nachgebauten Word-RTF getestet, nicht an Ihren echten Daten — und die Zeichenfolge „0 s22″ passt zu keinem RTF-Konstrukt, das ich sicher zuordnen kann. Solange ich das reale Feld nicht sehe, ist jede weitere Runde Stochern im Nebel.
Deshalb der Debug-Schalter oben:
$debugRoh = true;
Damit gibt das Snippet den Rohtext zwischen ROH>>> und <<<ROH aus — vor jeder Normalisierung. Noch hilfreicher wäre der komplett unbehandelte Feldinhalt; dafür ganz kurz:
{PHP}
$text = "{Freitexte:Tagzahl#3600:Datum}";
echo "<pre>" . htmlspecialchars($text) . "</pre>";
{/PHP}
Wenn Sie mir davon die Ausgabe eines betroffenen Eintrags schicken (gern mit Fantasienamen statt Patientendaten), sehe ich in zwei Minuten, wo „ab“ und „0 s22″ wirklich herkommen — statt dass wir weiter probeweise Steuerwörter in die Liste aufnehmen.
Das Problem ist eben, dass wir sicherlich kein RTF-Konstrukt zum Testen haben, was man in der Rohform übergeben kann. Man könnte alternativ einen Brief oder eine Briefvorlage übergeben. Das kann ich mal testen.
{PHP}
// ==========================================================
// Neuesten Freitext-Eintrag aus RTF-Feld extrahieren
// PHP 5.2 kompatibel (Medical Office intern)
// ==========================================================
// >>> Ausgabe-Kodierung: 'CP1252' = ANSI (wie bisher), oder 'UTF-8'
$ausgabeKodierung = 'CP1252';
// >>> DEBUG: auf true setzen, um den unformatierten Rohtext zu sehen
$debugRoh = false;
$text = "{Freitexte:Tagzahl#3600:Datum}";
// ---------- Hilfsfunktionen ----------
function rtf_utf8_from_codepoint($code)
{
// Ersatz fuer mb_chr() (existiert erst ab PHP 7.2)
if ($code < 0x80) return chr($code);
if ($code < 0x800) {
return chr(0xC0 | ($code >> 6)) . chr(0x80 | ($code & 0x3F));
}
if ($code < 0x10000) {
return chr(0xE0 | ($code >> 12))
. chr(0x80 | (($code >> 6) & 0x3F))
. chr(0x80 | ($code & 0x3F));
}
return chr(0xF0 | ($code >> 18))
. chr(0x80 | (($code >> 12) & 0x3F))
. chr(0x80 | (($code >> 6) & 0x3F))
. chr(0x80 | ($code & 0x3F));
}
function rtf_cp1252_from_codepoint($code)
{
// Unicode-Codepoint -> CP1252-Byte (soweit darstellbar)
if ($code < 0x80) return chr($code);
if ($code >= 0xA0 && $code <= 0xFF) return chr($code);
$sonder = array(
0x20AC => 0x80, 0x201A => 0x82, 0x0192 => 0x83, 0x201E => 0x84,
0x2026 => 0x85, 0x2020 => 0x86, 0x2021 => 0x87, 0x02C6 => 0x88,
0x2030 => 0x89, 0x0160 => 0x8A, 0x2039 => 0x8B, 0x0152 => 0x8C,
0x017D => 0x8E, 0x2018 => 0x91, 0x2019 => 0x92, 0x201C => 0x93,
0x201D => 0x94, 0x2022 => 0x95, 0x2013 => 0x96, 0x2014 => 0x97,
0x02DC => 0x98, 0x2122 => 0x99, 0x0161 => 0x9A, 0x203A => 0x9B,
0x0153 => 0x9C, 0x017E => 0x9E, 0x0178 => 0x9F,
);
if (isset($sonder[$code])) return chr($sonder[$code]);
return '?';
}
function rtf_cp1252_high_to_unicode($byte)
{
$map = array(
0x80=>0x20AC,0x82=>0x201A,0x83=>0x0192,0x84=>0x201E,0x85=>0x2026,
0x86=>0x2020,0x87=>0x2021,0x88=>0x02C6,0x89=>0x2030,0x8A=>0x0160,
0x8B=>0x2039,0x8C=>0x0152,0x8E=>0x017D,0x91=>0x2018,0x92=>0x2019,
0x93=>0x201C,0x94=>0x201D,0x95=>0x2022,0x96=>0x2013,0x97=>0x2014,
0x98=>0x02DC,0x99=>0x2122,0x9A=>0x0161,0x9B=>0x203A,0x9C=>0x0153,
0x9E=>0x017E,0x9F=>0x0178,
);
return isset($map[$byte]) ? $map[$byte] : $byte;
}
// ---------- RTF -> Klartext ----------
function rtf_to_text($rtf, $zielKodierung)
{
// Destinations, deren INHALT kein sichtbarer Text ist -> komplett verwerfen.
// Hier kamen die Fremdzeichen ("ab", "0 s22", Aufzaehlungspunkte) her.
$ignoreDestinations = array(
'fonttbl','colortbl','stylesheet','info','generator','operator','company',
'title','subject','author','keywords','comment','doccomm','creatim','revtim',
'pn','pntext','pntxta','pntxtb','pnseclvl','listtext','leveltext','levelnumbers',
'list','listtable','listoverride','listoverridetable','listlevel','listname','listpicture',
'pict','nonshppict','shppict','shpinst','shptxt','object','objdata','objclass','result',
'header','footer','headerf','footerf','headerl','headerr','footerl','footerr',
'footnote','ftnsep','ftnsepc','ftncn','aftnsep','aftnsepc','aftncn',
'rsidtbl','xmlnstbl','latentstyles','themedata','colorschememapping','datastore',
'wgrffmtfilter','revtbl','filetbl','protusertbl','userprops','svb','mmath','mmathPr',
'bkmkstart','bkmkend','xe','tc','tcn','field','fldinst','fldrslt','atnid','atnauthor',
'annotation','falt','panose','template','urtf','factoidname','datafield','do',
);
// Steuerwoerter, die ein sichtbares Zeichen erzeugen
$symbole = array(
'bullet' => 0x2022, 'endash' => 0x2013, 'emdash' => 0x2014,
'lquote' => 0x2018, 'rquote' => 0x2019,
'ldblquote' => 0x201C, 'rdblquote' => 0x201D,
'emspace' => 0x20, 'enspace' => 0x20, 'qmspace' => 0x20,
);
$len = strlen($rtf);
$i = 0;
$out = '';
$ignoreStack = array(false); // pro Gruppen-Ebene: Inhalt verwerfen?
$unicodeSkip = 1; // Fallback-Zeichen nach \uN (per \ucN)
$skipCounter = 0;
while ($i < $len) {
$ch = $rtf[$i];
$ignore = $ignoreStack[count($ignoreStack) - 1];
if ($ch === '{') {
$ignoreStack[] = $ignore; // Kind erbt Zustand des Elternteils
$skipCounter = 0;
$i++;
continue;
}
if ($ch === '}') {
if (count($ignoreStack) > 1) array_pop($ignoreStack);
$skipCounter = 0;
$i++;
continue;
}
if ($ch === '\\') {
// --- Hex-Escape \'xx (Gross-/Kleinschreibung egal) ---
if ($i + 1 < $len && $rtf[$i + 1] === "'") {
$hex = substr($rtf, $i + 2, 2);
if ($skipCounter > 0) {
$skipCounter--;
} elseif (!$ignore) {
$byte = hexdec($hex); // ist bereits ein CP1252-Byte
if ($zielKodierung === 'UTF-8') {
$cp = ($byte >= 0x80 && $byte <= 0x9F)
? rtf_cp1252_high_to_unicode($byte) : $byte;
$out .= rtf_utf8_from_codepoint($cp);
} else {
$out .= chr($byte); // 1:1 durchreichen = ANSI
}
}
$i += 4;
continue;
}
// --- \* : diese Destination ist "ignorable" ---
if ($i + 1 < $len && $rtf[$i + 1] === '*') {
$ignoreStack[count($ignoreStack) - 1] = true;
$i += 2;
continue;
}
// --- Steuerwort: \wort [-]zahl [Leerzeichen] ---
if (preg_match('/^\\\\([a-zA-Z]+)(-?[0-9]+)?[ ]?/', substr($rtf, $i, 40), $m)) {
$word = $m[1];
$param = (isset($m[2]) && $m[2] !== '') ? (int)$m[2] : null;
$i += strlen($m[0]);
if (in_array($word, $ignoreDestinations, true)) {
$ignoreStack[count($ignoreStack) - 1] = true;
} elseif ($word === 'u') {
if ($param !== null && !$ignore) {
$code = $param < 0 ? $param + 65536 : $param;
$out .= ($zielKodierung === 'UTF-8')
? rtf_utf8_from_codepoint($code)
: rtf_cp1252_from_codepoint($code);
}
$skipCounter = $unicodeSkip;
} elseif ($word === 'uc') {
$unicodeSkip = ($param !== null) ? $param : 1;
} elseif (isset($symbole[$word])) {
if ($skipCounter > 0) {
$skipCounter--;
} elseif (!$ignore) {
$code = $symbole[$word];
$out .= ($zielKodierung === 'UTF-8')
? rtf_utf8_from_codepoint($code)
: rtf_cp1252_from_codepoint($code);
}
} elseif ($word === 'par' || $word === 'line' || $word === 'sect'
|| $word === 'row' || $word === 'page') {
if (!$ignore) $out .= "\n";
} elseif ($word === 'cell' || $word === 'tab') {
if ($skipCounter > 0) { $skipCounter--; }
elseif (!$ignore) $out .= ' ';
}
// alle uebrigen Steuerwoerter (fs22, li720, fi-360, lang7, f0, s22, ...)
// sind reine Formatierung und werden verworfen
continue;
}
// --- Kontrollsymbol: \ + genau ein Nicht-Buchstabe ---
if ($i + 1 < $len) {
$sym = $rtf[$i + 1];
if (!$ignore) {
if ($sym === '~') $out .= ' ';
elseif ($sym === '_') $out .= '-';
elseif ($sym === '\\' || $sym === '{' || $sym === '}') $out .= $sym;
// \- (optionaler Trennstrich) und Rest: verwerfen
}
$i += 2;
continue;
}
$i++;
continue;
}
// --- normaler Text ---
if ($ch === "\r" || $ch === "\n") { // Zeilenumbrueche im RTF-Quelltext sind bedeutungslos
$i++;
continue;
}
if ($skipCounter > 0) {
$skipCounter--;
} elseif (!$ignore) {
$out .= $ch;
}
$i++;
}
return $out;
}
// ---------- Ablauf ----------
$text = rtf_to_text($text, $ausgabeKodierung);
if ($debugRoh) {
echo "ROH>>>" . $text . "<<<ROH";
} else {
// Spaces normalisieren
$text = preg_replace('/\s+/', ' ', $text);
$text = trim($text);
// Bloecke nach Datum aufteilen
preg_match_all(
'/(\d{2}\.\d{2}\.\d{2})(.*?)(?=\d{2}\.\d{2}\.\d{2}|$)/',
$text,
$blocks
);
// Nur den aktuellsten Eintrag suchen
$neusterText = '';
$neustesDatum = '';
for ($i = 0; $i < count($blocks[0]); $i++) {
$datum = trim($blocks[1][$i]);
$block = trim($blocks[2][$i]);
// TT.MM.JJ -> JJMMTT
$vergleich =
substr($datum, 6, 2) .
substr($datum, 3, 2) .
substr($datum, 0, 2);
if ($vergleich > $neustesDatum) {
$neustesDatum = $vergleich;
$neusterText = $block;
}
}
// Ausgabe
if ($neusterText != '') {
echo $neusterText;
} else {
echo "nichts da aber funktioniert";
}
}
{/PHP}
Zitat von Peter Quick am 27. August 2026, 11:32 UhrDas könnte es sein
Danke, die Datei war der Durchbruch. Ich habe die ganze Zeit am falschen Ende gesucht — der Fehler steckt nicht im RTF-Parsing, sondern in einer einzigen Zeile ganz am Anfang:
php$text = "{Freitexte:Tagzahl#3600:Datum}"; // ^ doppelte AnführungszeichenMedical Office setzt das RTF in dieses doppelt gequotete PHP-String-Literal ein. PHP wertet darin aber seine eigenen Escape-Sequenzen aus, bevor eine einzige Zeile Ihres Codes läuft:
im RTF PHP macht daraus nach \s+-Normalisierung\tabTabulator + abab\f0Seitenvorschub + 00\fs22Seitenvorschub + s22s22\f0\fs22FF+ 0+FF+s220 s22\vertaltVertikaltab + ertaltertaltDie Steuerwörter waren also schon zerstört, bevor mein Parser sie zu Gesicht bekam — deshalb half auch keine noch so lange Ignore-Liste. Ich habe es mit Ihrer Datei nachgestellt; die Roh-Ausgabe im alten Modus beginnt mit
tf1onttbl0nilcharset1 Arial;und enthält überallab,s22,s20,ertalt.Der Fix ist eine Zeile:
php$text = '{Freitexte:Tagzahl#3600:Datum}'; // ^ einfache Anführungszeichen{PHP} // ===================================================================== // Neuesten Freitext-Eintrag aus RTF-Feld extrahieren // PHP 5.2 kompatibel (Medical Office intern) // // WICHTIG: Der Platzhalter steht in EINFACHEN Anfuehrungszeichen! // Bei doppelten Anfuehrungszeichen frisst PHP die RTF-Steuerwoerter // (\tab -> Tabulator+"ab", \fs22 -> Seitenvorschub+"s22"). // ===================================================================== $text = '{Freitexte:Tagzahl#3600:Datum}'; // >>> Ausgabe-Kodierung: 'CP1252' = ANSI (wie bisher), oder 'UTF-8' $ausgabeKodierung = 'CP1252'; // >>> DEBUG: auf true setzen, um den unformatierten Rohtext zu sehen $debugRoh = false; // ---------- Hilfsfunktionen ---------- function rtf_utf8_from_codepoint($code) { // Ersatz fuer mb_chr() (existiert erst ab PHP 7.2) if ($code < 0x80) return chr($code); if ($code < 0x800) { return chr(0xC0 | ($code >> 6)) . chr(0x80 | ($code & 0x3F)); } if ($code < 0x10000) { return chr(0xE0 | ($code >> 12)) . chr(0x80 | (($code >> 6) & 0x3F)) . chr(0x80 | ($code & 0x3F)); } return chr(0xF0 | ($code >> 18)) . chr(0x80 | (($code >> 12) & 0x3F)) . chr(0x80 | (($code >> 6) & 0x3F)) . chr(0x80 | ($code & 0x3F)); } function rtf_cp1252_from_codepoint($code) { if ($code < 0x80) return chr($code); if ($code >= 0xA0 && $code <= 0xFF) return chr($code); $sonder = array( 0x20AC => 0x80, 0x201A => 0x82, 0x0192 => 0x83, 0x201E => 0x84, 0x2026 => 0x85, 0x2020 => 0x86, 0x2021 => 0x87, 0x02C6 => 0x88, 0x2030 => 0x89, 0x0160 => 0x8A, 0x2039 => 0x8B, 0x0152 => 0x8C, 0x017D => 0x8E, 0x2018 => 0x91, 0x2019 => 0x92, 0x201C => 0x93, 0x201D => 0x94, 0x2022 => 0x95, 0x2013 => 0x96, 0x2014 => 0x97, 0x02DC => 0x98, 0x2122 => 0x99, 0x0161 => 0x9A, 0x203A => 0x9B, 0x0153 => 0x9C, 0x017E => 0x9E, 0x0178 => 0x9F, ); if (isset($sonder[$code])) return chr($sonder[$code]); return '?'; } function rtf_cp1252_high_to_unicode($byte) { $map = array( 0x80=>0x20AC,0x82=>0x201A,0x83=>0x0192,0x84=>0x201E,0x85=>0x2026, 0x86=>0x2020,0x87=>0x2021,0x88=>0x02C6,0x89=>0x2030,0x8A=>0x0160, 0x8B=>0x2039,0x8C=>0x0152,0x8E=>0x017D,0x91=>0x2018,0x92=>0x2019, 0x93=>0x201C,0x94=>0x201D,0x95=>0x2022,0x96=>0x2013,0x97=>0x2014, 0x98=>0x02DC,0x99=>0x2122,0x9A=>0x0161,0x9B=>0x203A,0x9C=>0x0153, 0x9E=>0x017E,0x9F=>0x0178, ); return isset($map[$byte]) ? $map[$byte] : $byte; } function rtf_ist_hex($c) { return ($c >= '0' && $c <= '9') || ($c >= 'a' && $c <= 'f') || ($c >= 'A' && $c <= 'F'); } // ---------- RTF -> Klartext ---------- function rtf_to_text($rtf, $zielKodierung) { // Destinations, deren INHALT kein sichtbarer Text ist -> komplett verwerfen. // (Stylesheet-Namen wie "Normal;", "Kopfzeile Zchn;", Font-/Farbtabellen, // Aufzaehlungszeichen aus \pntext / \listtext usw.) $ignoreDestinations = array( 'fonttbl','colortbl','stylesheet','info','generator','operator','company', 'title','subject','author','keywords','comment','doccomm','creatim','revtim', 'userprops','propname','staticval','proptype', 'pn','pntext','pntxta','pntxtb','pnseclvl','listtext','leveltext','levelnumbers', 'list','listtable','listoverride','listoverridetable','listlevel','listname','listpicture', 'pict','nonshppict','shppict','shpinst','shptxt','object','objdata','objclass','result', 'header','footer','headerf','footerf','headerl','headerr','footerl','footerr', 'footnote','ftnsep','ftnsepc','ftncn','aftnsep','aftnsepc','aftncn', 'rsidtbl','xmlnstbl','latentstyles','themedata','colorschememapping','datastore', 'wgrffmtfilter','revtbl','filetbl','protusertbl','svb','mmath','mmathPr', 'bkmkstart','bkmkend','xe','tc','tcn','field','fldinst','atnid','atnauthor', 'annotation','falt','panose','template','urtf','factoidname','datafield','do', ); // Steuerwoerter, die ein sichtbares Zeichen erzeugen $symbole = array( 'bullet' => 0x2022, 'endash' => 0x2013, 'emdash' => 0x2014, 'lquote' => 0x2018, 'rquote' => 0x2019, 'ldblquote' => 0x201C, 'rdblquote' => 0x201D, 'emspace' => 0x20, 'enspace' => 0x20, 'qmspace' => 0x20, ); $len = strlen($rtf); $i = 0; $out = ''; $ignoreStack = array(false); // pro Gruppen-Ebene: Inhalt verwerfen? $unicodeSkip = 1; // Fallback-Zeichen nach \uN (per \ucN) $skipCounter = 0; while ($i < $len) { $ch = $rtf[$i]; $ignore = $ignoreStack[count($ignoreStack) - 1]; // --- Hex-Escape, auch ohne Backslash --- // Einfache Anfuehrungszeichen im PHP-Literal machen aus \'FC ein 'FC. // Deshalb wird BEIDES erkannt: \'FC und 'FC $istHex = false; if ($ch === '\\' && $i + 3 <= $len - 1 && $rtf[$i + 1] === "'" && rtf_ist_hex($rtf[$i + 2]) && rtf_ist_hex($rtf[$i + 3])) { $istHex = true; $hexPos = $i + 2; $hexLen = 4; } elseif ($ch === "'" && $i + 2 <= $len - 1 && rtf_ist_hex($rtf[$i + 1]) && rtf_ist_hex($rtf[$i + 2])) { $istHex = true; $hexPos = $i + 1; $hexLen = 3; } if ($istHex) { if ($skipCounter > 0) { $skipCounter--; } elseif (!$ignore) { $byte = hexdec(substr($rtf, $hexPos, 2)); // bereits ein CP1252-Byte if ($zielKodierung === 'UTF-8') { $cp = ($byte >= 0x80 && $byte <= 0x9F) ? rtf_cp1252_high_to_unicode($byte) : $byte; $out .= rtf_utf8_from_codepoint($cp); } else { $out .= chr($byte); // 1:1 = ANSI, wie bisher } } $i += $hexLen; continue; } if ($ch === '{') { $ignoreStack[] = $ignore; // Kind erbt Zustand des Elternteils $skipCounter = 0; $i++; continue; } if ($ch === '}') { if (count($ignoreStack) > 1) array_pop($ignoreStack); $skipCounter = 0; $i++; continue; } if ($ch === '\\') { // --- \* : diese Destination ist "ignorable" --- if ($i + 1 < $len && $rtf[$i + 1] === '*') { $ignoreStack[count($ignoreStack) - 1] = true; $i += 2; continue; } // --- Steuerwort: \wort [-]zahl [Leerzeichen] --- if (preg_match('/^\\\\([a-zA-Z]+)(-?[0-9]+)?[ ]?/', substr($rtf, $i, 40), $m)) { $word = $m[1]; $param = (isset($m[2]) && $m[2] !== '') ? (int)$m[2] : null; $i += strlen($m[0]); if (in_array($word, $ignoreDestinations, true)) { $ignoreStack[count($ignoreStack) - 1] = true; } elseif ($word === 'u') { if ($param !== null && !$ignore) { $code = $param < 0 ? $param + 65536 : $param; $out .= ($zielKodierung === 'UTF-8') ? rtf_utf8_from_codepoint($code) : rtf_cp1252_from_codepoint($code); } $skipCounter = $unicodeSkip; } elseif ($word === 'uc') { $unicodeSkip = ($param !== null) ? $param : 1; } elseif (isset($symbole[$word])) { if ($skipCounter > 0) { $skipCounter--; } elseif (!$ignore) { $code = $symbole[$word]; $out .= ($zielKodierung === 'UTF-8') ? rtf_utf8_from_codepoint($code) : rtf_cp1252_from_codepoint($code); } } elseif ($word === 'par' || $word === 'line' || $word === 'sect' || $word === 'row' || $word === 'page') { if (!$ignore) $out .= "\n"; } elseif ($word === 'cell' || $word === 'tab') { if ($skipCounter > 0) { $skipCounter--; } elseif (!$ignore) $out .= ' '; } // alle uebrigen Steuerwoerter (fs22, f0, li284, tx6520, cf15, b0, // plain, pard, vertalt, ...) sind reine Formatierung -> verwerfen continue; } // --- Kontrollsymbol: \ + genau ein Nicht-Buchstabe --- // (\{ und \} sind maskierte Klammern und gehoeren als TEXT in die Ausgabe) if ($i + 1 < $len) { $sym = $rtf[$i + 1]; if (!$ignore) { if ($sym === '~') $out .= ' '; elseif ($sym === '_') $out .= '-'; elseif ($sym === '\\' || $sym === '{' || $sym === '}') $out .= $sym; // \- (optionaler Trennstrich) und Rest: verwerfen } $i += 2; continue; } $i++; continue; } // --- normaler Text --- if ($ch === "\r" || $ch === "\n") { // Zeilenumbrueche im RTF-Quelltext sind bedeutungslos $i++; continue; } if ($skipCounter > 0) { $skipCounter--; } elseif (!$ignore) { $out .= $ch; } $i++; } return $out; } // ---------- Ablauf ---------- $text = rtf_to_text($text, $ausgabeKodierung); if ($debugRoh) { echo "ROH>>>" . $text . "<<<ROH"; } else { // Spaces normalisieren $text = preg_replace('/\s+/', ' ', $text); $text = trim($text); // Bloecke nach Datum aufteilen preg_match_all( '/(\d{2}\.\d{2}\.\d{2})(.*?)(?=\d{2}\.\d{2}\.\d{2}|$)/', $text, $blocks ); // Nur den aktuellsten Eintrag suchen $neusterText = ''; $neustesDatum = ''; for ($i = 0; $i < count($blocks[0]); $i++) { $datum = trim($blocks[1][$i]); $block = trim($blocks[2][$i]); // TT.MM.JJ -> JJMMTT $vergleich = substr($datum, 6, 2) . substr($datum, 3, 2) . substr($datum, 0, 2); if ($vergleich > $neustesDatum) { $neustesDatum = $vergleich; $neusterText = $block; } } // Ausgabe if ($neusterText != '') { echo $neusterText; } else { echo "nichts da aber funktioniert"; } } {/PHP}
Das könnte es sein
Danke, die Datei war der Durchbruch. Ich habe die ganze Zeit am falschen Ende gesucht — der Fehler steckt nicht im RTF-Parsing, sondern in einer einzigen Zeile ganz am Anfang:
$text = "{Freitexte:Tagzahl#3600:Datum}";
// ^ doppelte Anführungszeichen
Medical Office setzt das RTF in dieses doppelt gequotete PHP-String-Literal ein. PHP wertet darin aber seine eigenen Escape-Sequenzen aus, bevor eine einzige Zeile Ihres Codes läuft:
| im RTF | PHP macht daraus | nach \s+-Normalisierung |
|---|---|---|
\tab |
Tabulator + ab |
ab |
\f0 |
Seitenvorschub + 0 |
0 |
\fs22 |
Seitenvorschub + s22 |
s22 |
\f0\fs22 |
FF+0+FF+s22 |
0 s22 |
\vertalt |
Vertikaltab + ertalt |
ertalt |
Die Steuerwörter waren also schon zerstört, bevor mein Parser sie zu Gesicht bekam — deshalb half auch keine noch so lange Ignore-Liste. Ich habe es mit Ihrer Datei nachgestellt; die Roh-Ausgabe im alten Modus beginnt mit tf1onttbl0nilcharset1 Arial; und enthält überall ab, s22, s20, ertalt.
Der Fix ist eine Zeile:
$text = '{Freitexte:Tagzahl#3600:Datum}'; // ^ einfache Anführungszeichen
{PHP}
// =====================================================================
// Neuesten Freitext-Eintrag aus RTF-Feld extrahieren
// PHP 5.2 kompatibel (Medical Office intern)
//
// WICHTIG: Der Platzhalter steht in EINFACHEN Anfuehrungszeichen!
// Bei doppelten Anfuehrungszeichen frisst PHP die RTF-Steuerwoerter
// (\tab -> Tabulator+"ab", \fs22 -> Seitenvorschub+"s22").
// =====================================================================
$text = '{Freitexte:Tagzahl#3600:Datum}';
// >>> Ausgabe-Kodierung: 'CP1252' = ANSI (wie bisher), oder 'UTF-8'
$ausgabeKodierung = 'CP1252';
// >>> DEBUG: auf true setzen, um den unformatierten Rohtext zu sehen
$debugRoh = false;
// ---------- Hilfsfunktionen ----------
function rtf_utf8_from_codepoint($code)
{
// Ersatz fuer mb_chr() (existiert erst ab PHP 7.2)
if ($code < 0x80) return chr($code);
if ($code < 0x800) {
return chr(0xC0 | ($code >> 6)) . chr(0x80 | ($code & 0x3F));
}
if ($code < 0x10000) {
return chr(0xE0 | ($code >> 12))
. chr(0x80 | (($code >> 6) & 0x3F))
. chr(0x80 | ($code & 0x3F));
}
return chr(0xF0 | ($code >> 18))
. chr(0x80 | (($code >> 12) & 0x3F))
. chr(0x80 | (($code >> 6) & 0x3F))
. chr(0x80 | ($code & 0x3F));
}
function rtf_cp1252_from_codepoint($code)
{
if ($code < 0x80) return chr($code);
if ($code >= 0xA0 && $code <= 0xFF) return chr($code);
$sonder = array(
0x20AC => 0x80, 0x201A => 0x82, 0x0192 => 0x83, 0x201E => 0x84,
0x2026 => 0x85, 0x2020 => 0x86, 0x2021 => 0x87, 0x02C6 => 0x88,
0x2030 => 0x89, 0x0160 => 0x8A, 0x2039 => 0x8B, 0x0152 => 0x8C,
0x017D => 0x8E, 0x2018 => 0x91, 0x2019 => 0x92, 0x201C => 0x93,
0x201D => 0x94, 0x2022 => 0x95, 0x2013 => 0x96, 0x2014 => 0x97,
0x02DC => 0x98, 0x2122 => 0x99, 0x0161 => 0x9A, 0x203A => 0x9B,
0x0153 => 0x9C, 0x017E => 0x9E, 0x0178 => 0x9F,
);
if (isset($sonder[$code])) return chr($sonder[$code]);
return '?';
}
function rtf_cp1252_high_to_unicode($byte)
{
$map = array(
0x80=>0x20AC,0x82=>0x201A,0x83=>0x0192,0x84=>0x201E,0x85=>0x2026,
0x86=>0x2020,0x87=>0x2021,0x88=>0x02C6,0x89=>0x2030,0x8A=>0x0160,
0x8B=>0x2039,0x8C=>0x0152,0x8E=>0x017D,0x91=>0x2018,0x92=>0x2019,
0x93=>0x201C,0x94=>0x201D,0x95=>0x2022,0x96=>0x2013,0x97=>0x2014,
0x98=>0x02DC,0x99=>0x2122,0x9A=>0x0161,0x9B=>0x203A,0x9C=>0x0153,
0x9E=>0x017E,0x9F=>0x0178,
);
return isset($map[$byte]) ? $map[$byte] : $byte;
}
function rtf_ist_hex($c)
{
return ($c >= '0' && $c <= '9') || ($c >= 'a' && $c <= 'f') || ($c >= 'A' && $c <= 'F');
}
// ---------- RTF -> Klartext ----------
function rtf_to_text($rtf, $zielKodierung)
{
// Destinations, deren INHALT kein sichtbarer Text ist -> komplett verwerfen.
// (Stylesheet-Namen wie "Normal;", "Kopfzeile Zchn;", Font-/Farbtabellen,
// Aufzaehlungszeichen aus \pntext / \listtext usw.)
$ignoreDestinations = array(
'fonttbl','colortbl','stylesheet','info','generator','operator','company',
'title','subject','author','keywords','comment','doccomm','creatim','revtim',
'userprops','propname','staticval','proptype',
'pn','pntext','pntxta','pntxtb','pnseclvl','listtext','leveltext','levelnumbers',
'list','listtable','listoverride','listoverridetable','listlevel','listname','listpicture',
'pict','nonshppict','shppict','shpinst','shptxt','object','objdata','objclass','result',
'header','footer','headerf','footerf','headerl','headerr','footerl','footerr',
'footnote','ftnsep','ftnsepc','ftncn','aftnsep','aftnsepc','aftncn',
'rsidtbl','xmlnstbl','latentstyles','themedata','colorschememapping','datastore',
'wgrffmtfilter','revtbl','filetbl','protusertbl','svb','mmath','mmathPr',
'bkmkstart','bkmkend','xe','tc','tcn','field','fldinst','atnid','atnauthor',
'annotation','falt','panose','template','urtf','factoidname','datafield','do',
);
// Steuerwoerter, die ein sichtbares Zeichen erzeugen
$symbole = array(
'bullet' => 0x2022, 'endash' => 0x2013, 'emdash' => 0x2014,
'lquote' => 0x2018, 'rquote' => 0x2019,
'ldblquote' => 0x201C, 'rdblquote' => 0x201D,
'emspace' => 0x20, 'enspace' => 0x20, 'qmspace' => 0x20,
);
$len = strlen($rtf);
$i = 0;
$out = '';
$ignoreStack = array(false); // pro Gruppen-Ebene: Inhalt verwerfen?
$unicodeSkip = 1; // Fallback-Zeichen nach \uN (per \ucN)
$skipCounter = 0;
while ($i < $len) {
$ch = $rtf[$i];
$ignore = $ignoreStack[count($ignoreStack) - 1];
// --- Hex-Escape, auch ohne Backslash ---
// Einfache Anfuehrungszeichen im PHP-Literal machen aus \'FC ein 'FC.
// Deshalb wird BEIDES erkannt: \'FC und 'FC
$istHex = false;
if ($ch === '\\' && $i + 3 <= $len - 1 && $rtf[$i + 1] === "'"
&& rtf_ist_hex($rtf[$i + 2]) && rtf_ist_hex($rtf[$i + 3])) {
$istHex = true; $hexPos = $i + 2; $hexLen = 4;
} elseif ($ch === "'" && $i + 2 <= $len - 1
&& rtf_ist_hex($rtf[$i + 1]) && rtf_ist_hex($rtf[$i + 2])) {
$istHex = true; $hexPos = $i + 1; $hexLen = 3;
}
if ($istHex) {
if ($skipCounter > 0) {
$skipCounter--;
} elseif (!$ignore) {
$byte = hexdec(substr($rtf, $hexPos, 2)); // bereits ein CP1252-Byte
if ($zielKodierung === 'UTF-8') {
$cp = ($byte >= 0x80 && $byte <= 0x9F)
? rtf_cp1252_high_to_unicode($byte) : $byte;
$out .= rtf_utf8_from_codepoint($cp);
} else {
$out .= chr($byte); // 1:1 = ANSI, wie bisher
}
}
$i += $hexLen;
continue;
}
if ($ch === '{') {
$ignoreStack[] = $ignore; // Kind erbt Zustand des Elternteils
$skipCounter = 0;
$i++;
continue;
}
if ($ch === '}') {
if (count($ignoreStack) > 1) array_pop($ignoreStack);
$skipCounter = 0;
$i++;
continue;
}
if ($ch === '\\') {
// --- \* : diese Destination ist "ignorable" ---
if ($i + 1 < $len && $rtf[$i + 1] === '*') {
$ignoreStack[count($ignoreStack) - 1] = true;
$i += 2;
continue;
}
// --- Steuerwort: \wort [-]zahl [Leerzeichen] ---
if (preg_match('/^\\\\([a-zA-Z]+)(-?[0-9]+)?[ ]?/', substr($rtf, $i, 40), $m)) {
$word = $m[1];
$param = (isset($m[2]) && $m[2] !== '') ? (int)$m[2] : null;
$i += strlen($m[0]);
if (in_array($word, $ignoreDestinations, true)) {
$ignoreStack[count($ignoreStack) - 1] = true;
} elseif ($word === 'u') {
if ($param !== null && !$ignore) {
$code = $param < 0 ? $param + 65536 : $param;
$out .= ($zielKodierung === 'UTF-8')
? rtf_utf8_from_codepoint($code)
: rtf_cp1252_from_codepoint($code);
}
$skipCounter = $unicodeSkip;
} elseif ($word === 'uc') {
$unicodeSkip = ($param !== null) ? $param : 1;
} elseif (isset($symbole[$word])) {
if ($skipCounter > 0) {
$skipCounter--;
} elseif (!$ignore) {
$code = $symbole[$word];
$out .= ($zielKodierung === 'UTF-8')
? rtf_utf8_from_codepoint($code)
: rtf_cp1252_from_codepoint($code);
}
} elseif ($word === 'par' || $word === 'line' || $word === 'sect'
|| $word === 'row' || $word === 'page') {
if (!$ignore) $out .= "\n";
} elseif ($word === 'cell' || $word === 'tab') {
if ($skipCounter > 0) { $skipCounter--; }
elseif (!$ignore) $out .= ' ';
}
// alle uebrigen Steuerwoerter (fs22, f0, li284, tx6520, cf15, b0,
// plain, pard, vertalt, ...) sind reine Formatierung -> verwerfen
continue;
}
// --- Kontrollsymbol: \ + genau ein Nicht-Buchstabe ---
// (\{ und \} sind maskierte Klammern und gehoeren als TEXT in die Ausgabe)
if ($i + 1 < $len) {
$sym = $rtf[$i + 1];
if (!$ignore) {
if ($sym === '~') $out .= ' ';
elseif ($sym === '_') $out .= '-';
elseif ($sym === '\\' || $sym === '{' || $sym === '}') $out .= $sym;
// \- (optionaler Trennstrich) und Rest: verwerfen
}
$i += 2;
continue;
}
$i++;
continue;
}
// --- normaler Text ---
if ($ch === "\r" || $ch === "\n") { // Zeilenumbrueche im RTF-Quelltext sind bedeutungslos
$i++;
continue;
}
if ($skipCounter > 0) {
$skipCounter--;
} elseif (!$ignore) {
$out .= $ch;
}
$i++;
}
return $out;
}
// ---------- Ablauf ----------
$text = rtf_to_text($text, $ausgabeKodierung);
if ($debugRoh) {
echo "ROH>>>" . $text . "<<<ROH";
} else {
// Spaces normalisieren
$text = preg_replace('/\s+/', ' ', $text);
$text = trim($text);
// Bloecke nach Datum aufteilen
preg_match_all(
'/(\d{2}\.\d{2}\.\d{2})(.*?)(?=\d{2}\.\d{2}\.\d{2}|$)/',
$text,
$blocks
);
// Nur den aktuellsten Eintrag suchen
$neusterText = '';
$neustesDatum = '';
for ($i = 0; $i < count($blocks[0]); $i++) {
$datum = trim($blocks[1][$i]);
$block = trim($blocks[2][$i]);
// TT.MM.JJ -> JJMMTT
$vergleich =
substr($datum, 6, 2) .
substr($datum, 3, 2) .
substr($datum, 0, 2);
if ($vergleich > $neustesDatum) {
$neustesDatum = $vergleich;
$neusterText = $block;
}
}
// Ausgabe
if ($neusterText != '') {
echo $neusterText;
} else {
echo "nichts da aber funktioniert";
}
}
{/PHP}
Zitat von Peter Quick am 27. August 2026, 12:33 UhrSo, Version 6 läuft jetzt bei mir. Umlaute stimmen, Sonderzeichen sind weg und Zeilenumbruch stimmt auch. Sie können sich gerne noch die Debug-Möglichkeiten rausnehmen, wenn es läuft. Die nehmen ja letztendlich nur Platz weg.
{PHP} // ===================================================================== // Neuesten Freitext-Eintrag aus RTF-Feld extrahieren // PHP 5.2 kompatibel (Medical Office intern) // // WICHTIG: Der Platzhalter steht in EINFACHEN Anfuehrungszeichen! // Bei doppelten Anfuehrungszeichen frisst PHP die RTF-Steuerwoerter // (\tab -> Tabulator+"ab", \fs22 -> Seitenvorschub+"s22"). // ===================================================================== $text = '{Freitexte:Tagzahl#3600:Datum}'; // >>> Ausgabe-Kodierung: 'CP1252' = ANSI (wie bisher), oder 'UTF-8' $ausgabeKodierung = 'CP1252'; // >>> Wie sollen Zeilenumbrueche AUSGEGEBEN werden? // Bewusst mit chr() geschrieben - so gibt es keine Verwechslung zwischen // einfachen und doppelten Anfuehrungszeichen ('\n' waere Backslash+n!). // // chr(13).chr(10) -> echter Umbruch CRLF <-- Standard // chr(10) -> echter Umbruch LF // chr(92)."par " -> RTF-Steuerwort \par (nur wenn MO die Ausgabe NICHT maskiert) // "<br />" -> HTML $zeilenumbruchAusgabe = chr(13) . chr(10); // >>> Leerzeilen zwischen Absaetzen erlauben? (max. 1) - sonst werden sie entfernt $leerzeilenErlauben = true; // >>> DEBUG: auf true setzen, um den unformatierten Rohtext zu sehen $debugRoh = false; // ---------- Hilfsfunktionen ---------- function rtf_utf8_from_codepoint($code) { // Ersatz fuer mb_chr() (existiert erst ab PHP 7.2) if ($code < 0x80) return chr($code); if ($code < 0x800) { return chr(0xC0 | ($code >> 6)) . chr(0x80 | ($code & 0x3F)); } if ($code < 0x10000) { return chr(0xE0 | ($code >> 12)) . chr(0x80 | (($code >> 6) & 0x3F)) . chr(0x80 | ($code & 0x3F)); } return chr(0xF0 | ($code >> 18)) . chr(0x80 | (($code >> 12) & 0x3F)) . chr(0x80 | (($code >> 6) & 0x3F)) . chr(0x80 | ($code & 0x3F)); } function rtf_cp1252_from_codepoint($code) { if ($code < 0x80) return chr($code); if ($code >= 0xA0 && $code <= 0xFF) return chr($code); $sonder = array( 0x20AC => 0x80, 0x201A => 0x82, 0x0192 => 0x83, 0x201E => 0x84, 0x2026 => 0x85, 0x2020 => 0x86, 0x2021 => 0x87, 0x02C6 => 0x88, 0x2030 => 0x89, 0x0160 => 0x8A, 0x2039 => 0x8B, 0x0152 => 0x8C, 0x017D => 0x8E, 0x2018 => 0x91, 0x2019 => 0x92, 0x201C => 0x93, 0x201D => 0x94, 0x2022 => 0x95, 0x2013 => 0x96, 0x2014 => 0x97, 0x02DC => 0x98, 0x2122 => 0x99, 0x0161 => 0x9A, 0x203A => 0x9B, 0x0153 => 0x9C, 0x017E => 0x9E, 0x0178 => 0x9F, ); if (isset($sonder[$code])) return chr($sonder[$code]); return '?'; } function rtf_cp1252_high_to_unicode($byte) { $map = array( 0x80=>0x20AC,0x82=>0x201A,0x83=>0x0192,0x84=>0x201E,0x85=>0x2026, 0x86=>0x2020,0x87=>0x2021,0x88=>0x02C6,0x89=>0x2030,0x8A=>0x0160, 0x8B=>0x2039,0x8C=>0x0152,0x8E=>0x017D,0x91=>0x2018,0x92=>0x2019, 0x93=>0x201C,0x94=>0x201D,0x95=>0x2022,0x96=>0x2013,0x97=>0x2014, 0x98=>0x02DC,0x99=>0x2122,0x9A=>0x0161,0x9B=>0x203A,0x9C=>0x0153, 0x9E=>0x017E,0x9F=>0x0178, ); return isset($map[$byte]) ? $map[$byte] : $byte; } function rtf_ist_hex($c) { return ($c >= '0' && $c <= '9') || ($c >= 'a' && $c <= 'f') || ($c >= 'A' && $c <= 'F'); } // ---------- RTF -> Klartext ---------- function rtf_to_text($rtf, $zielKodierung) { // Destinations, deren INHALT kein sichtbarer Text ist -> komplett verwerfen. // (Stylesheet-Namen wie "Normal;", "Kopfzeile Zchn;", Font-/Farbtabellen, // Aufzaehlungszeichen aus \pntext / \listtext usw.) $ignoreDestinations = array( 'fonttbl','colortbl','stylesheet','info','generator','operator','company', 'title','subject','author','keywords','comment','doccomm','creatim','revtim', 'userprops','propname','staticval','proptype', 'pn','pntext','pntxta','pntxtb','pnseclvl','listtext','leveltext','levelnumbers', 'list','listtable','listoverride','listoverridetable','listlevel','listname','listpicture', 'pict','nonshppict','shppict','shpinst','shptxt','object','objdata','objclass','result', 'header','footer','headerf','footerf','headerl','headerr','footerl','footerr', 'footnote','ftnsep','ftnsepc','ftncn','aftnsep','aftnsepc','aftncn', 'rsidtbl','xmlnstbl','latentstyles','themedata','colorschememapping','datastore', 'wgrffmtfilter','revtbl','filetbl','protusertbl','svb','mmath','mmathPr', 'bkmkstart','bkmkend','xe','tc','tcn','field','fldinst','atnid','atnauthor', 'annotation','falt','panose','template','urtf','factoidname','datafield','do', ); // Steuerwoerter, die ein sichtbares Zeichen erzeugen $symbole = array( 'bullet' => 0x2022, 'endash' => 0x2013, 'emdash' => 0x2014, 'lquote' => 0x2018, 'rquote' => 0x2019, 'ldblquote' => 0x201C, 'rdblquote' => 0x201D, 'emspace' => 0x20, 'enspace' => 0x20, 'qmspace' => 0x20, ); $len = strlen($rtf); $i = 0; $out = ''; $ignoreStack = array(false); // pro Gruppen-Ebene: Inhalt verwerfen? $unicodeSkip = 1; // Fallback-Zeichen nach \uN (per \ucN) $skipCounter = 0; while ($i < $len) { $ch = $rtf[$i]; $ignore = $ignoreStack[count($ignoreStack) - 1]; // --- Hex-Escape, auch ohne Backslash --- // Einfache Anfuehrungszeichen im PHP-Literal machen aus \'FC ein 'FC. // Deshalb wird BEIDES erkannt: \'FC und 'FC $istHex = false; if ($ch === '\\' && $i + 3 <= $len - 1 && $rtf[$i + 1] === "'" && rtf_ist_hex($rtf[$i + 2]) && rtf_ist_hex($rtf[$i + 3])) { $istHex = true; $hexPos = $i + 2; $hexLen = 4; } elseif ($ch === "'" && $i + 2 <= $len - 1 && rtf_ist_hex($rtf[$i + 1]) && rtf_ist_hex($rtf[$i + 2])) { $istHex = true; $hexPos = $i + 1; $hexLen = 3; } if ($istHex) { if ($skipCounter > 0) { $skipCounter--; } elseif (!$ignore) { $byte = hexdec(substr($rtf, $hexPos, 2)); // bereits ein CP1252-Byte if ($zielKodierung === 'UTF-8') { $cp = ($byte >= 0x80 && $byte <= 0x9F) ? rtf_cp1252_high_to_unicode($byte) : $byte; $out .= rtf_utf8_from_codepoint($cp); } else { $out .= chr($byte); // 1:1 = ANSI, wie bisher } } $i += $hexLen; continue; } if ($ch === '{') { $ignoreStack[] = $ignore; // Kind erbt Zustand des Elternteils $skipCounter = 0; $i++; continue; } if ($ch === '}') { if (count($ignoreStack) > 1) array_pop($ignoreStack); $skipCounter = 0; $i++; continue; } if ($ch === '\\') { // --- \* : diese Destination ist "ignorable" --- if ($i + 1 < $len && $rtf[$i + 1] === '*') { $ignoreStack[count($ignoreStack) - 1] = true; $i += 2; continue; } // --- Steuerwort: \wort [-]zahl [Leerzeichen] --- if (preg_match('/^\\\\([a-zA-Z]+)(-?[0-9]+)?[ ]?/', substr($rtf, $i, 40), $m)) { $word = $m[1]; $param = (isset($m[2]) && $m[2] !== '') ? (int)$m[2] : null; $i += strlen($m[0]); if (in_array($word, $ignoreDestinations, true)) { $ignoreStack[count($ignoreStack) - 1] = true; } elseif ($word === 'u') { if ($param !== null && !$ignore) { $code = $param < 0 ? $param + 65536 : $param; $out .= ($zielKodierung === 'UTF-8') ? rtf_utf8_from_codepoint($code) : rtf_cp1252_from_codepoint($code); } $skipCounter = $unicodeSkip; } elseif ($word === 'uc') { $unicodeSkip = ($param !== null) ? $param : 1; } elseif (isset($symbole[$word])) { if ($skipCounter > 0) { $skipCounter--; } elseif (!$ignore) { $code = $symbole[$word]; $out .= ($zielKodierung === 'UTF-8') ? rtf_utf8_from_codepoint($code) : rtf_cp1252_from_codepoint($code); } } elseif ($word === 'par' || $word === 'line' || $word === 'sect' || $word === 'row' || $word === 'page') { if (!$ignore) $out .= "\n"; } elseif ($word === 'cell' || $word === 'tab') { if ($skipCounter > 0) { $skipCounter--; } elseif (!$ignore) $out .= ' '; } // alle uebrigen Steuerwoerter (fs22, f0, li284, tx6520, cf15, b0, // plain, pard, vertalt, ...) sind reine Formatierung -> verwerfen continue; } // --- Kontrollsymbol: \ + genau ein Nicht-Buchstabe --- // (\{ und \} sind maskierte Klammern und gehoeren als TEXT in die Ausgabe) if ($i + 1 < $len) { $sym = $rtf[$i + 1]; if (!$ignore) { if ($sym === '~') $out .= ' '; elseif ($sym === '_') $out .= '-'; elseif ($sym === '\\' || $sym === '{' || $sym === '}') $out .= $sym; // \- (optionaler Trennstrich) und Rest: verwerfen } $i += 2; continue; } $i++; continue; } // --- normaler Text --- if ($ch === "\r" || $ch === "\n") { // Zeilenumbrueche im RTF-Quelltext sind bedeutungslos $i++; continue; } if ($skipCounter > 0) { $skipCounter--; } elseif (!$ignore) { $out .= $ch; } $i++; } return $out; } // ---------- Ablauf ---------- $text = rtf_to_text($text, $ausgabeKodierung); if ($debugRoh) { echo "ROH>>>" . $text . "<<<ROH"; } else { // Normalisieren - ABER Zeilenumbrueche erhalten! $text = str_replace("\r\n", "\n", $text); $text = str_replace("\r", "\n", $text); $text = preg_replace('/[ \t\x0B\f]+/', ' ', $text); // nur horizontale Leerzeichen $text = preg_replace('/ *\n */', "\n", $text); // Rand-Leerzeichen je Zeile weg $text = $leerzeilenErlauben ? preg_replace('/\n{3,}/', "\n\n", $text) // max. 1 Leerzeile : preg_replace('/\n{2,}/', "\n", $text); // gar keine Leerzeilen $text = trim($text); // Bloecke nach Datum aufteilen ( /s = Punkt matcht auch Zeilenumbrueche ) preg_match_all( '/(\d{2}\.\d{2}\.\d{2})(.*?)(?=\d{2}\.\d{2}\.\d{2}|$)/s', $text, $blocks ); // Nur den aktuellsten Eintrag suchen $neusterText = ''; $neustesDatum = ''; for ($i = 0; $i < count($blocks[0]); $i++) { $datum = trim($blocks[1][$i]); $block = trim($blocks[2][$i]); // TT.MM.JJ -> JJMMTT $vergleich = substr($datum, 6, 2) . substr($datum, 3, 2) . substr($datum, 0, 2); if ($vergleich > $neustesDatum) { $neustesDatum = $vergleich; $neusterText = $block; } } // Ausgabe - Zeilenumbrueche in das Zielformat uebersetzen if ($neusterText != '') { echo str_replace("\n", $zeilenumbruchAusgabe, $neusterText); } else { echo "nichts da aber funktioniert"; } } {/PHP}
So, Version 6 läuft jetzt bei mir. Umlaute stimmen, Sonderzeichen sind weg und Zeilenumbruch stimmt auch. Sie können sich gerne noch die Debug-Möglichkeiten rausnehmen, wenn es läuft. Die nehmen ja letztendlich nur Platz weg.
{PHP}
// =====================================================================
// Neuesten Freitext-Eintrag aus RTF-Feld extrahieren
// PHP 5.2 kompatibel (Medical Office intern)
//
// WICHTIG: Der Platzhalter steht in EINFACHEN Anfuehrungszeichen!
// Bei doppelten Anfuehrungszeichen frisst PHP die RTF-Steuerwoerter
// (\tab -> Tabulator+"ab", \fs22 -> Seitenvorschub+"s22").
// =====================================================================
$text = '{Freitexte:Tagzahl#3600:Datum}';
// >>> Ausgabe-Kodierung: 'CP1252' = ANSI (wie bisher), oder 'UTF-8'
$ausgabeKodierung = 'CP1252';
// >>> Wie sollen Zeilenumbrueche AUSGEGEBEN werden?
// Bewusst mit chr() geschrieben - so gibt es keine Verwechslung zwischen
// einfachen und doppelten Anfuehrungszeichen ('\n' waere Backslash+n!).
//
// chr(13).chr(10) -> echter Umbruch CRLF <-- Standard
// chr(10) -> echter Umbruch LF
// chr(92)."par " -> RTF-Steuerwort \par (nur wenn MO die Ausgabe NICHT maskiert)
// "<br />" -> HTML
$zeilenumbruchAusgabe = chr(13) . chr(10);
// >>> Leerzeilen zwischen Absaetzen erlauben? (max. 1) - sonst werden sie entfernt
$leerzeilenErlauben = true;
// >>> DEBUG: auf true setzen, um den unformatierten Rohtext zu sehen
$debugRoh = false;
// ---------- Hilfsfunktionen ----------
function rtf_utf8_from_codepoint($code)
{
// Ersatz fuer mb_chr() (existiert erst ab PHP 7.2)
if ($code < 0x80) return chr($code);
if ($code < 0x800) {
return chr(0xC0 | ($code >> 6)) . chr(0x80 | ($code & 0x3F));
}
if ($code < 0x10000) {
return chr(0xE0 | ($code >> 12))
. chr(0x80 | (($code >> 6) & 0x3F))
. chr(0x80 | ($code & 0x3F));
}
return chr(0xF0 | ($code >> 18))
. chr(0x80 | (($code >> 12) & 0x3F))
. chr(0x80 | (($code >> 6) & 0x3F))
. chr(0x80 | ($code & 0x3F));
}
function rtf_cp1252_from_codepoint($code)
{
if ($code < 0x80) return chr($code);
if ($code >= 0xA0 && $code <= 0xFF) return chr($code);
$sonder = array(
0x20AC => 0x80, 0x201A => 0x82, 0x0192 => 0x83, 0x201E => 0x84,
0x2026 => 0x85, 0x2020 => 0x86, 0x2021 => 0x87, 0x02C6 => 0x88,
0x2030 => 0x89, 0x0160 => 0x8A, 0x2039 => 0x8B, 0x0152 => 0x8C,
0x017D => 0x8E, 0x2018 => 0x91, 0x2019 => 0x92, 0x201C => 0x93,
0x201D => 0x94, 0x2022 => 0x95, 0x2013 => 0x96, 0x2014 => 0x97,
0x02DC => 0x98, 0x2122 => 0x99, 0x0161 => 0x9A, 0x203A => 0x9B,
0x0153 => 0x9C, 0x017E => 0x9E, 0x0178 => 0x9F,
);
if (isset($sonder[$code])) return chr($sonder[$code]);
return '?';
}
function rtf_cp1252_high_to_unicode($byte)
{
$map = array(
0x80=>0x20AC,0x82=>0x201A,0x83=>0x0192,0x84=>0x201E,0x85=>0x2026,
0x86=>0x2020,0x87=>0x2021,0x88=>0x02C6,0x89=>0x2030,0x8A=>0x0160,
0x8B=>0x2039,0x8C=>0x0152,0x8E=>0x017D,0x91=>0x2018,0x92=>0x2019,
0x93=>0x201C,0x94=>0x201D,0x95=>0x2022,0x96=>0x2013,0x97=>0x2014,
0x98=>0x02DC,0x99=>0x2122,0x9A=>0x0161,0x9B=>0x203A,0x9C=>0x0153,
0x9E=>0x017E,0x9F=>0x0178,
);
return isset($map[$byte]) ? $map[$byte] : $byte;
}
function rtf_ist_hex($c)
{
return ($c >= '0' && $c <= '9') || ($c >= 'a' && $c <= 'f') || ($c >= 'A' && $c <= 'F');
}
// ---------- RTF -> Klartext ----------
function rtf_to_text($rtf, $zielKodierung)
{
// Destinations, deren INHALT kein sichtbarer Text ist -> komplett verwerfen.
// (Stylesheet-Namen wie "Normal;", "Kopfzeile Zchn;", Font-/Farbtabellen,
// Aufzaehlungszeichen aus \pntext / \listtext usw.)
$ignoreDestinations = array(
'fonttbl','colortbl','stylesheet','info','generator','operator','company',
'title','subject','author','keywords','comment','doccomm','creatim','revtim',
'userprops','propname','staticval','proptype',
'pn','pntext','pntxta','pntxtb','pnseclvl','listtext','leveltext','levelnumbers',
'list','listtable','listoverride','listoverridetable','listlevel','listname','listpicture',
'pict','nonshppict','shppict','shpinst','shptxt','object','objdata','objclass','result',
'header','footer','headerf','footerf','headerl','headerr','footerl','footerr',
'footnote','ftnsep','ftnsepc','ftncn','aftnsep','aftnsepc','aftncn',
'rsidtbl','xmlnstbl','latentstyles','themedata','colorschememapping','datastore',
'wgrffmtfilter','revtbl','filetbl','protusertbl','svb','mmath','mmathPr',
'bkmkstart','bkmkend','xe','tc','tcn','field','fldinst','atnid','atnauthor',
'annotation','falt','panose','template','urtf','factoidname','datafield','do',
);
// Steuerwoerter, die ein sichtbares Zeichen erzeugen
$symbole = array(
'bullet' => 0x2022, 'endash' => 0x2013, 'emdash' => 0x2014,
'lquote' => 0x2018, 'rquote' => 0x2019,
'ldblquote' => 0x201C, 'rdblquote' => 0x201D,
'emspace' => 0x20, 'enspace' => 0x20, 'qmspace' => 0x20,
);
$len = strlen($rtf);
$i = 0;
$out = '';
$ignoreStack = array(false); // pro Gruppen-Ebene: Inhalt verwerfen?
$unicodeSkip = 1; // Fallback-Zeichen nach \uN (per \ucN)
$skipCounter = 0;
while ($i < $len) {
$ch = $rtf[$i];
$ignore = $ignoreStack[count($ignoreStack) - 1];
// --- Hex-Escape, auch ohne Backslash ---
// Einfache Anfuehrungszeichen im PHP-Literal machen aus \'FC ein 'FC.
// Deshalb wird BEIDES erkannt: \'FC und 'FC
$istHex = false;
if ($ch === '\\' && $i + 3 <= $len - 1 && $rtf[$i + 1] === "'"
&& rtf_ist_hex($rtf[$i + 2]) && rtf_ist_hex($rtf[$i + 3])) {
$istHex = true; $hexPos = $i + 2; $hexLen = 4;
} elseif ($ch === "'" && $i + 2 <= $len - 1
&& rtf_ist_hex($rtf[$i + 1]) && rtf_ist_hex($rtf[$i + 2])) {
$istHex = true; $hexPos = $i + 1; $hexLen = 3;
}
if ($istHex) {
if ($skipCounter > 0) {
$skipCounter--;
} elseif (!$ignore) {
$byte = hexdec(substr($rtf, $hexPos, 2)); // bereits ein CP1252-Byte
if ($zielKodierung === 'UTF-8') {
$cp = ($byte >= 0x80 && $byte <= 0x9F)
? rtf_cp1252_high_to_unicode($byte) : $byte;
$out .= rtf_utf8_from_codepoint($cp);
} else {
$out .= chr($byte); // 1:1 = ANSI, wie bisher
}
}
$i += $hexLen;
continue;
}
if ($ch === '{') {
$ignoreStack[] = $ignore; // Kind erbt Zustand des Elternteils
$skipCounter = 0;
$i++;
continue;
}
if ($ch === '}') {
if (count($ignoreStack) > 1) array_pop($ignoreStack);
$skipCounter = 0;
$i++;
continue;
}
if ($ch === '\\') {
// --- \* : diese Destination ist "ignorable" ---
if ($i + 1 < $len && $rtf[$i + 1] === '*') {
$ignoreStack[count($ignoreStack) - 1] = true;
$i += 2;
continue;
}
// --- Steuerwort: \wort [-]zahl [Leerzeichen] ---
if (preg_match('/^\\\\([a-zA-Z]+)(-?[0-9]+)?[ ]?/', substr($rtf, $i, 40), $m)) {
$word = $m[1];
$param = (isset($m[2]) && $m[2] !== '') ? (int)$m[2] : null;
$i += strlen($m[0]);
if (in_array($word, $ignoreDestinations, true)) {
$ignoreStack[count($ignoreStack) - 1] = true;
} elseif ($word === 'u') {
if ($param !== null && !$ignore) {
$code = $param < 0 ? $param + 65536 : $param;
$out .= ($zielKodierung === 'UTF-8')
? rtf_utf8_from_codepoint($code)
: rtf_cp1252_from_codepoint($code);
}
$skipCounter = $unicodeSkip;
} elseif ($word === 'uc') {
$unicodeSkip = ($param !== null) ? $param : 1;
} elseif (isset($symbole[$word])) {
if ($skipCounter > 0) {
$skipCounter--;
} elseif (!$ignore) {
$code = $symbole[$word];
$out .= ($zielKodierung === 'UTF-8')
? rtf_utf8_from_codepoint($code)
: rtf_cp1252_from_codepoint($code);
}
} elseif ($word === 'par' || $word === 'line' || $word === 'sect'
|| $word === 'row' || $word === 'page') {
if (!$ignore) $out .= "\n";
} elseif ($word === 'cell' || $word === 'tab') {
if ($skipCounter > 0) { $skipCounter--; }
elseif (!$ignore) $out .= ' ';
}
// alle uebrigen Steuerwoerter (fs22, f0, li284, tx6520, cf15, b0,
// plain, pard, vertalt, ...) sind reine Formatierung -> verwerfen
continue;
}
// --- Kontrollsymbol: \ + genau ein Nicht-Buchstabe ---
// (\{ und \} sind maskierte Klammern und gehoeren als TEXT in die Ausgabe)
if ($i + 1 < $len) {
$sym = $rtf[$i + 1];
if (!$ignore) {
if ($sym === '~') $out .= ' ';
elseif ($sym === '_') $out .= '-';
elseif ($sym === '\\' || $sym === '{' || $sym === '}') $out .= $sym;
// \- (optionaler Trennstrich) und Rest: verwerfen
}
$i += 2;
continue;
}
$i++;
continue;
}
// --- normaler Text ---
if ($ch === "\r" || $ch === "\n") { // Zeilenumbrueche im RTF-Quelltext sind bedeutungslos
$i++;
continue;
}
if ($skipCounter > 0) {
$skipCounter--;
} elseif (!$ignore) {
$out .= $ch;
}
$i++;
}
return $out;
}
// ---------- Ablauf ----------
$text = rtf_to_text($text, $ausgabeKodierung);
if ($debugRoh) {
echo "ROH>>>" . $text . "<<<ROH";
} else {
// Normalisieren - ABER Zeilenumbrueche erhalten!
$text = str_replace("\r\n", "\n", $text);
$text = str_replace("\r", "\n", $text);
$text = preg_replace('/[ \t\x0B\f]+/', ' ', $text); // nur horizontale Leerzeichen
$text = preg_replace('/ *\n */', "\n", $text); // Rand-Leerzeichen je Zeile weg
$text = $leerzeilenErlauben
? preg_replace('/\n{3,}/', "\n\n", $text) // max. 1 Leerzeile
: preg_replace('/\n{2,}/', "\n", $text); // gar keine Leerzeilen
$text = trim($text);
// Bloecke nach Datum aufteilen ( /s = Punkt matcht auch Zeilenumbrueche )
preg_match_all(
'/(\d{2}\.\d{2}\.\d{2})(.*?)(?=\d{2}\.\d{2}\.\d{2}|$)/s',
$text,
$blocks
);
// Nur den aktuellsten Eintrag suchen
$neusterText = '';
$neustesDatum = '';
for ($i = 0; $i < count($blocks[0]); $i++) {
$datum = trim($blocks[1][$i]);
$block = trim($blocks[2][$i]);
// TT.MM.JJ -> JJMMTT
$vergleich =
substr($datum, 6, 2) .
substr($datum, 3, 2) .
substr($datum, 0, 2);
if ($vergleich > $neustesDatum) {
$neustesDatum = $vergleich;
$neusterText = $block;
}
}
// Ausgabe - Zeilenumbrueche in das Zielformat uebersetzen
if ($neusterText != '') {
echo str_replace("\n", $zeilenumbruchAusgabe, $neusterText);
} else {
echo "nichts da aber funktioniert";
}
}
{/PHP}
Zitat von Leon Rosen am 28. August 2026, 7:49 UhrSchon eine Menge Code, aber funktioniert. Auch als Autotext oder Textbaustein. Chapeau an Claude, und Sie natürlich! : )
Schon eine Menge Code, aber funktioniert. Auch als Autotext oder Textbaustein. Chapeau an Claude, und Sie natürlich! : )
Zitat von Peter Quick am 28. August 2026, 11:20 UhrMan könnte noch mal versuchen, das Ganze zu verkürzen.
Ich wollte jetzt aber auch nicht zu viel Zeit dort rein investieren, ich fand das Thema nur spannend und hab das neben der Sprechstunde mit Claude zusammen getestet. Würde ich mir für mich auch mal ablegen, kann ich vielleicht noch mal später gebrauchen…
mit dem Ansatz könnte man wahrscheinlich auch das eigentliche Problem des letzten Labor Eintrags lösen….
wenn MedicalOffice mal endlich PHP 8 könnte, wäre schon vieles besser. PHP 5 kann viele Dinge nicht, und man braucht dann wirklich viele Umgehungen
ich habe noch mal über die ganze Sache nachgedacht. Sie könnten auch mal ihren originalen Code versuchen. Das Problem lag primär daran, dass sie das Argument an PHP mit normalen Anführungszeichen übergeben haben. Hier müssten Sie es mal mit einfachen Anführungszeichen versuchen und schauen, ob nicht vielleicht sogar schon der originale Code läuft.
Man könnte noch mal versuchen, das Ganze zu verkürzen.
Ich wollte jetzt aber auch nicht zu viel Zeit dort rein investieren, ich fand das Thema nur spannend und hab das neben der Sprechstunde mit Claude zusammen getestet. Würde ich mir für mich auch mal ablegen, kann ich vielleicht noch mal später gebrauchen…
mit dem Ansatz könnte man wahrscheinlich auch das eigentliche Problem des letzten Labor Eintrags lösen….
wenn MedicalOffice mal endlich PHP 8 könnte, wäre schon vieles besser. PHP 5 kann viele Dinge nicht, und man braucht dann wirklich viele Umgehungen
ich habe noch mal über die ganze Sache nachgedacht. Sie könnten auch mal ihren originalen Code versuchen. Das Problem lag primär daran, dass sie das Argument an PHP mit normalen Anführungszeichen übergeben haben. Hier müssten Sie es mal mit einfachen Anführungszeichen versuchen und schauen, ob nicht vielleicht sogar schon der originale Code läuft.