{"id":29,"date":"2025-10-14T16:07:54","date_gmt":"2025-10-14T14:07:54","guid":{"rendered":"https:\/\/www2.hu-berlin.de\/insidespaetharboretum\/?page_id=29"},"modified":"2026-04-06T15:10:39","modified_gmt":"2026-04-06T13:10:39","slug":"scanner","status":"publish","type":"page","link":"https:\/\/www2.hu-berlin.de\/insidespaetharboretum\/scanner\/","title":{"rendered":"Scanner"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"29\" class=\"elementor elementor-29\">\n\t\t\t\t<div class=\"elementor-element elementor-element-f340369 e-flex e-con-boxed e-con e-parent\" data-id=\"f340369\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-83efc64 elementor-widget elementor-widget-heading\" data-id=\"83efc64\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t\t<h2 class=\"elementor-heading-title elementor-size-default\">Schilder-Scanner<\/h2>\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-802d486 e-flex e-con-boxed e-con e-parent\" data-id=\"802d486\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-5ba3306 elementor-widget elementor-widget-text-editor\" data-id=\"5ba3306\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t\t\t\t\t\t<p>Scan\u00adner-Pro\u00adto\u00adtyp \u2014 In Arbeit<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-9af712d e-flex e-con-boxed e-con e-parent\" data-id=\"9af712d\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-841651c elementor-widget elementor-widget-html\" data-id=\"841651c\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<!-- ========== [A] STYLES (Zeilen ~1\u201335) ========== -->\n<style>\n  .ocr-wrap { font-family: system-ui, sans-serif; padding: 1rem; max-width: 720px; margin: 0 auto; }\n  .ocr-wrap h1 { font-size: 1.2rem; margin-bottom: .5rem; }\n  .ocr-hint { color: #555; font-size: .95rem; }\n  .ocr-box { border: 1px solid #ddd; border-radius: .5rem; padding: 1rem; margin: 1rem 0; }\n  #ocr-preview { width: 100%; max-height: 40vh; object-fit: contain; background: #f6f6f6; }\n  #ocr-log, #ocr-err { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9rem; white-space: pre-wrap; }\n  #ocr-err { background: #fee; border: 1px solid #f99; padding: .5rem; border-radius: .35rem; display:none; }\n  .ocr-badge { display:inline-block; padding:.15rem .45rem; border-radius:.35rem; background:#eef; margin-right:.25rem; }\n  .ocr-btn { padding:.6rem .9rem; border-radius:.5rem; border:1px solid #ccc; background:#fff; cursor:pointer; }\n  .ocr-btn:disabled { opacity: .6; cursor: default; }\n<\/style>\n\n<!-- ========== [B] MARKUP (Zeilen ~36\u201386) ========== -->\n<div class=\"ocr-wrap\">\n  <h1>Baumschild lesen<\/h1>\n  <p class=\"ocr-hint\">\n    Auf dem Han\u00addy bit\u00adte \u201e<b>Foto auf\u00adneh\u00admen<\/b>\u201c w\u00e4h\u00adlen. Auf iPho\u00adnes am bes\u00adten \u00fcber <b>https<\/b> nut\u00adzen.\n  <\/p>\n\n  <div class=\"ocr-box\">\n    <input id=\"ocr-file\" type=\"file\" accept=\"image\/*;capture=camera\" capture=\"environment\">\n    <div style=\"margin:.75rem 0\">\n      <img id=\"ocr-preview\" alt=\"Vorschau\">\n    <\/div>\n    <button id=\"ocr-run\" class=\"ocr-btn\" disabled>Text erkennen<\/button>\n    <span id=\"ocr-badges\"><\/span>\n  <\/div>\n\n  <div class=\"ocr-box\">\n    <h2>Ergebnis (gefiltert)<\/h2>\n    <div id=\"ocr-result\">\u2014<\/div>\n  <\/div>\n\n  <div class=\"ocr-box\">\n    <h2>Rohtext<\/h2>\n    <div id=\"ocr-log\">\u2014<\/div>\n  <\/div>\n\n  <div class=\"ocr-box\" id=\"ocr-errBox\">\n    <h2>Fehler<\/h2>\n    <div id=\"ocr-err\">\u2014<\/div>\n  <\/div>\n<\/div>\n\n<!-- ========== [C] LIB (Zeilen ~87\u201388) ========== -->\n<script src=\"https:\/\/unpkg.com\/tesseract.js@4.0.2\/dist\/tesseract.min.js\"><\/script>\n\n<!-- ========== [D] SCRIPT (Zeilen ~89\u2013Ende) ========== -->\n<script>\n\/* ===== [D1] GRUNDGER\u00dcST \/ DOM-REFS ===== *\/\nconst $ = (id) => document.getElementById(id);\nconst fileInput = $('ocr-file');\nconst preview   = $('ocr-preview');\nconst runBtn    = $('ocr-run');\nconst logEl     = $('ocr-log');\nconst resultEl  = $('ocr-result');\nconst badges    = $('ocr-badges');\nconst errEl     = $('ocr-err');\nconst errBox    = $('ocr-errBox');\nlet imageDataUrl = null;\n\n\/* ===== [D2] FEHLERANZEIGE ===== *\/\nfunction showError(e) {\n  errEl.textContent = (e && (e.stack || e.message || e.toString())) || 'Unbekannter Fehler';\n  errBox.style.display = 'block';\n  console.error(e);\n}\nfunction clearError() { errEl.textContent = '\u2014'; errBox.style.display = 'none'; }\n\n\/* ===== [D3] DOWNSCALE-FOTO ===== *\/\nasync function downscaleToJpeg(file, maxDim = 1600, quality = 0.8) {\n  return new Promise((resolve, reject) => {\n    const img = new Image();\n    img.onload = () => {\n      const scale = Math.min(1, maxDim \/ Math.max(img.width, img.height));\n      const w = Math.max(1, Math.round(img.width * scale));\n      const h = Math.max(1, Math.round(img.height * scale));\n      const canvas = document.createElement('canvas');\n      canvas.width = w; canvas.height = h;\n      const ctx = canvas.getContext('2d');\n      ctx.drawImage(img, 0, 0, w, h);\n      try { resolve(canvas.toDataURL('image\/jpeg', quality)); }\n      catch (err) { reject(err); }\n    };\n    img.onerror = reject;\n    const r = new FileReader();\n    r.onload = () => { img.src = r.result; };\n    r.onerror = reject;\n    r.readAsDataURL(file);\n  });\n}\n\n\/* ===== [D4] SLUG-HILFEN ===== *\/\nfunction slugifyName(name){\n  return String(name || '')\n    .normalize('NFKD')\n    .replace(\/[\u00d7]\/g, 'x')              \/\/ \u00d7 \u2192 x\n    .replace(\/[^\\w\\s\\-']\/g, '')        \/\/ Sonderzeichen raus (Apostroph kurz erlauben)\n    .trim()\n    .replace(\/'+\/g, ' ')               \/\/ Apostrophe weg\n    .replace(\/\\s+\/g, '-')              \/\/ Spaces \u2192 Bindestrich\n    .toLowerCase();\n}\n\n\/\/ Aus Parts einen eindeutigen Schild-Slug bauen (Art\/Var\/Form\/Subsp\/Kultivar\/Hybrid)\nfunction makeSlugFromParts(parts, fallbackFull) {\n  if (parts && parts.genus) {\n    const tokens = [];\n    tokens.push(parts.genus);\n    if (parts.hybrid)  tokens.push('x');       \/\/ Hybridzeichen als \"x\" im Slug\n    if (parts.species) tokens.push(parts.species);\n    if (parts.rank && parts.infra) {\n      const r = parts.rank.replace(\/\\.$\/, ''); \/\/ \"subsp.\" -> \"subsp\", \"f.\" -> \"f\"\n      tokens.push(r, parts.infra);\n    }\n    if (parts.cultivar) tokens.push(parts.cultivar); \/\/ ohne Quotes\n    return slugifyName(tokens.join(' '));\n  }\n  return slugifyName(fallbackFull || '');\n}\n\n\/* ===== [D5] TEXT NORMALISIEREN ===== *\/\nfunction normalizeQuotesSpaces(s){\n  return String(s||'')\n    .replace(\/[\\u2018\\u2019\\u2032\\u00B4\u00b4]\/g, \"'\") \/\/ \u2019\u2018\u00b4 \u2192 '\n    .replace(\/[\\u201C\\u201D]\/g, '\"')               \/\/ \u201c \u201d \u2192 \"\n    .replace(\/\\s+\/g,' ')\n    .normalize('NFKC')\n    .trim();\n}\n\n\/* ===== [D6] BOTANIK-PARSER (Art\/Var\/Form\/Subsp\/Kultivar + Hybriden) =====\n   Tauschen\/\u00e4ndern nur HIER, falls ihr sp\u00e4ter Regeln anpasst. *\/\nfunction parseBotanicalFromText(raw) {\n  const text = normalizeQuotesSpaces(raw);\n\n  \/\/ optionales Hybrid-Zeichen VOR dem Genus erlauben: \u00d7Acer ...\n  const rxLeadingHybrid = \/^\\s*[x\u00d7]\\s*\/i;\n  const hasHybridBeforeGenus = rxLeadingHybrid.test(text);\n  const text2 = text.replace(rxLeadingHybrid, ''); \/\/ f\u00fcrs Matching entfernen\n\n  \/\/ Genus + (optional Hybrid) + species\n  \/\/ erlaubt: Acer \u00d7 rotundilobum | Acer rotundilobum | (durch oben auch) \u00d7Acer rotundilobum\n  const rxGS = \/\\b([A-Z\u00c4\u00d6\u00dc][a-z\u00e4\u00f6\u00fc\u00df\\-]+)\\s+(?:[x\u00d7]\\s*)?([a-z\u00e4\u00f6\u00fc\u00df\\-]+)\\b\/;\n  const mGS = rxGS.exec(text2);\n  if (!mGS) return null;\n\n  const genus   = mGS[1];\n  const species = mGS[2];\n  const hasHybridBetween = \/\\b[A-Z\u00c4\u00d6\u00dc][a-z\u00e4\u00f6\u00fc\u00df\\-]+\\s+[x\u00d7]\\s*[a-z\u00e4\u00f6\u00fc\u00df\\-]+\\b\/.test(mGS[0]);\n  const hybrid  = hasHybridBeforeGenus || hasHybridBetween;\n\n  \/\/ Rang + infra (subsp.\/ssp.\/var.\/forma\/f.)\n  let rank = '', infra = '';\n  const rest = text2.slice(mGS.index + mGS[0].length);\n  const rxRank = \/\\b(subsp\\.|ssp\\.|var\\.|forma|f\\.)\\s+([A-Za-z\u00c4\u00d6\u00dc\u00e4\u00f6\u00fc\u00df\\-]+)\\b\/i;\n  const mRank = rxRank.exec(rest);\n  if (mRank) {\n    rank  = mRank[1].toLowerCase();\n    infra = mRank[2].toLowerCase();  \n    if (rank === 'ssp.')   rank = 'subsp.'; \/\/ normalisieren\n    if (rank === 'forma')  rank = 'f.';\n  }\n\n  \/\/ Kultivar: 'Name' \/ \"Name\" oder in Verbindung mit \"Sorte\"\n  let cultivar = '';\n  const rxCvQuoted = \/['\"]\\s*([A-Z0-9][A-Za-z0-9\\-\\s]+?)\\s*['\"]\/;\n  const mCvQ = rxCvQuoted.exec(text2);\n  if (mCvQ) {\n    cultivar = mCvQ[1].trim();\n  } else {\n    const rxCvSorte1 = \/\\bSorte\\b[^A-Za-z0-9]*([A-Z][A-Za-z0-9\\-\\s]{2,})\/;\n    const rxCvSorte2 = \/([A-Z][A-Za-z0-9\\-\\s]{2,})[^A-Za-z0-9]*\\bSorte\\b\/;\n    const mCvS1 = rxCvSorte1.exec(text2);\n    const mCvS2 = rxCvSorte2.exec(text2);\n    if (mCvS1) cultivar = mCvS1[1].trim();\n    else if (mCvS2) cultivar = mCvS2[1].trim();\n  }\n  cultivar = cultivar.replace(\/\\s+\/g,' ').trim();\n\n  \/\/ Anzeige aufbauen (full), canonical bleibt Genus species\n  const canonical = genus + ' ' + species;\n  const fullBits = [genus, (hybrid ? '\u00d7' : ''), species].filter(Boolean);\n  if (rank && infra) fullBits.push(rank + ' ' + infra);\n  if (cultivar)      fullBits.push(\"'\" + cultivar + \"'\");\n  const full = fullBits.join(' ').replace(\/\\s+\/g,' ').trim();\n\n  return {\n    canonical, \/\/ nur Art, ohne \u00d7, f\u00fcr Vergleiche\n    full,      \/\/ komplette Anzeige inkl. \u00d7\/Rang\/Kultivar\n    parts: { genus, species, hybrid, rank, infra, cultivar }\n  };\n}\n\n\/* ===== [D7] KANDIDAT ERMITTELN (erst Botanik, sonst Baum-ID) ===== *\/\nfunction extractCandidate(raw) {\n  const parsed = parseBotanicalFromText(raw);\n  if (parsed) return { type: 'bot_name', value: parsed.canonical, full: parsed.full, parts: parsed.parts };\n\n  const text = String(raw||'').replace(\/\\r\/g,' ');\n  const idMatch = text.match(\/\\b(?:Baum[-\\s]?ID|ID)[:\\s]*([A-Za-z0-9\\-]{2,10})\\b\/);\n  if (idMatch) return { type: 'baum_id', value: idMatch[1] };\n\n  return null;\n}\n\n\/* ===== [D8] PROGRESS-LOGGER ===== *\/\nfunction uiLogger(m) {\n  if (!m) return;\nif (typeof m === 'string') {\n  badges.innerHTML = `<span class=\"ocr-badge\">${m}<\/span>`;\n} else if (m.status) {\n  badges.innerHTML = `<span class=\"ocr-badge\">${m.status}${m.progress != null ? ' ' + Math.round(m.progress*100) + '%' : ''}<\/span>`;\n}\n}\n\n\/* ===== [D9] DATEI-WAHL ===== *\/\nfileInput.addEventListener('change', async () => {\n  clearError();\n  const file = fileInput.files?.[0];\n  if (!file) return;\n  try {\n    badges.innerHTML = '<span class=\"ocr-badge\">Bild vorbereiten\u2026<\/span>';\n    imageDataUrl = await downscaleToJpeg(file, 1600, 0.8);\n    preview.src = imageDataUrl;\n    runBtn.disabled = false;\n    logEl.textContent = '\u2014';\n    resultEl.textContent = '\u2014';\n    badges.innerHTML = '';\n  } catch (e) { showError(e); }\n});\n\n\/* ===== [D10] OCR STARTEN ===== *\/\nasync function runOCR() {\n  clearError();\n  if (!imageDataUrl) return;\n  runBtn.disabled = true;\n  badges.innerHTML = '<span class=\"ocr-badge\">Laden\u2026<\/span>';\n\n  const start = performance.now();\n  try {\n    const worker = await Tesseract.createWorker({ logger: uiLogger });\n    await worker.loadLanguage('eng');\n    await worker.initialize('eng');\n\n    badges.innerHTML = '<span class=\"ocr-badge\">Erkennen\u2026<\/span>';\n    const { data } = await worker.recognize(imageDataUrl);\n\n    const candidate = extractCandidate(data.text || '');\n    const ms = Math.round(performance.now() - start);\n    try { await worker.terminate(); } catch(e){}\n\n    \/\/ Immer Rohtext + Status zeigen\n    logEl.textContent = (data.text || '\u2014').trim() || '\u2014';\n    badges.innerHTML = `<span class=\"ocr-badge\">Fertig in ${ms} ms<\/span>`;\n\n    if (candidate) {\n      \/\/ Slug bauen (inkl. subsp.\/var.\/f. und Hybriden\/Kultivar)\n      const slug = makeSlugFromParts(candidate.parts, candidate.full);\n      const url  = 'https:\/\/www2.hu-berlin.de\/insidespaetharboretum\/art\/' + slug + '\/';\n\n      \/\/ Anzeige + Button statt hektischem Autosprung\n     resultEl.innerHTML = `\n  <b>${candidate.full}<\/b><br>\n  <button id=\"open-art\" class=\"ocr-btn\" style=\"margin-top:.5rem\">Seite \u00f6ffnen<\/button>\n  <div style=\"margin-top:.25rem;font-size:.85rem;color:#666;\">Ziel: <code>${url}<\/code><\/div>\n`;\n\n      const openBtn = document.getElementById('open-art');\n      if (openBtn) openBtn.addEventListener('click', () => { window.location.href = url; });\n    } else {\n      resultEl.textContent = 'Kein eindeutiger Name\/ID erkannt. Bitte anderes Foto versuchen.';\n    }\n\n  } catch (e) {\n    showError(e);\n    badges.innerHTML = '';\n  } finally {\n    runBtn.disabled = false;\n  }\n}\n\nrunBtn.addEventListener('click', runOCR);\n\/* ===== [D11] ENDE SCRIPT ===== *\/\n<\/script>\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Schil\u00adder-Scan\u00adner Scan\u00ad\u00adner-Pro\u00ad\u00adto\u00ad\u00adtyp \u2014 In Arbeit Baum\u00adschild lesen Auf dem Han\u00addy bit\u00adte \u201eFoto auf\u00adneh\u00admen\u201c w\u00e4h\u00adlen. Auf iPho\u00adnes am bes\u00adten \u00fcber https nut\u00adzen. Text erken\u00adnen Ergeb\u00adnis (gefil\u00adtert) \u2014 Roh\u00adtext \u2014 Feh\u00adler \u2014<\/p>\n","protected":false},"author":3,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_acf_changed":false,"wp_typography_post_enhancements_disabled":false,"ocean_post_layout":"full-screen","ocean_both_sidebars_style":"","ocean_both_sidebars_content_width":0,"ocean_both_sidebars_sidebars_width":0,"ocean_sidebar":"","ocean_second_sidebar":"","ocean_disable_margins":"enable","ocean_add_body_class":"","ocean_shortcode_before_top_bar":"","ocean_shortcode_after_top_bar":"","ocean_shortcode_before_header":"","ocean_shortcode_after_header":"","ocean_has_shortcode":"","ocean_shortcode_after_title":"","ocean_shortcode_before_footer_widgets":"","ocean_shortcode_after_footer_widgets":"","ocean_shortcode_before_footer_bottom":"","ocean_shortcode_after_footer_bottom":"","ocean_display_top_bar":"off","ocean_display_header":"default","ocean_header_style":"","ocean_center_header_left_menu":"","ocean_custom_header_template":"","ocean_custom_logo":0,"ocean_custom_retina_logo":0,"ocean_custom_logo_max_width":0,"ocean_custom_logo_tablet_max_width":0,"ocean_custom_logo_mobile_max_width":0,"ocean_custom_logo_max_height":0,"ocean_custom_logo_tablet_max_height":0,"ocean_custom_logo_mobile_max_height":0,"ocean_header_custom_menu":"","ocean_menu_typo_font_family":"","ocean_menu_typo_font_subset":"","ocean_menu_typo_font_size":0,"ocean_menu_typo_font_size_tablet":0,"ocean_menu_typo_font_size_mobile":0,"ocean_menu_typo_font_size_unit":"px","ocean_menu_typo_font_weight":"","ocean_menu_typo_font_weight_tablet":"","ocean_menu_typo_font_weight_mobile":"","ocean_menu_typo_transform":"","ocean_menu_typo_transform_tablet":"","ocean_menu_typo_transform_mobile":"","ocean_menu_typo_line_height":0,"ocean_menu_typo_line_height_tablet":0,"ocean_menu_typo_line_height_mobile":0,"ocean_menu_typo_line_height_unit":"","ocean_menu_typo_spacing":0,"ocean_menu_typo_spacing_tablet":0,"ocean_menu_typo_spacing_mobile":0,"ocean_menu_typo_spacing_unit":"","ocean_menu_link_color":"","ocean_menu_link_color_hover":"","ocean_menu_link_color_active":"","ocean_menu_link_background":"","ocean_menu_link_hover_background":"","ocean_menu_link_active_background":"","ocean_menu_social_links_bg":"","ocean_menu_social_hover_links_bg":"","ocean_menu_social_links_color":"","ocean_menu_social_hover_links_color":"","ocean_disable_title":"on","ocean_disable_heading":"default","ocean_post_title":"","ocean_post_subheading":"","ocean_post_title_style":"","ocean_post_title_background_color":"","ocean_post_title_background":0,"ocean_post_title_bg_image_position":"","ocean_post_title_bg_image_attachment":"","ocean_post_title_bg_image_repeat":"","ocean_post_title_bg_image_size":"","ocean_post_title_height":0,"ocean_post_title_bg_overlay":0.5,"ocean_post_title_bg_overlay_color":"","ocean_disable_breadcrumbs":"default","ocean_breadcrumbs_color":"","ocean_breadcrumbs_separator_color":"","ocean_breadcrumbs_links_color":"","ocean_breadcrumbs_links_hover_color":"","ocean_display_footer_widgets":"off","ocean_display_footer_bottom":"off","ocean_custom_footer_template":"","footnotes":""},"class_list":["post-29","page","type-page","status-publish","hentry","entry"],"acf":[],"_links":{"self":[{"href":"https:\/\/www2.hu-berlin.de\/insidespaetharboretum\/wp-json\/wp\/v2\/pages\/29","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www2.hu-berlin.de\/insidespaetharboretum\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www2.hu-berlin.de\/insidespaetharboretum\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www2.hu-berlin.de\/insidespaetharboretum\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/www2.hu-berlin.de\/insidespaetharboretum\/wp-json\/wp\/v2\/comments?post=29"}],"version-history":[{"count":101,"href":"https:\/\/www2.hu-berlin.de\/insidespaetharboretum\/wp-json\/wp\/v2\/pages\/29\/revisions"}],"predecessor-version":[{"id":9874,"href":"https:\/\/www2.hu-berlin.de\/insidespaetharboretum\/wp-json\/wp\/v2\/pages\/29\/revisions\/9874"}],"wp:attachment":[{"href":"https:\/\/www2.hu-berlin.de\/insidespaetharboretum\/wp-json\/wp\/v2\/media?parent=29"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}