vCard

vCard Reader
vCard Reader
zoom

vCard Reader

Testet doch einfach mal eine vCard Datei (.vcf), dieser Reader unterstützt ein Großteil der Eigenschaften über alle bis jetzt erschienen Versionen hinweg.
Alles ist komplett über JavaScript geregelt, es erfolgt keine Übertragung der Daten!

Für interessierte Entwickler steht der Quellcode natürlich wieder auf der Seite zur Verfügung.


                  

                
HTML:
vCard.htm HTML (3,04 kByte) 25.03.2026 22:03
<!DOCTYPE html >
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>vCard</title>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width, minimum-scale=1, maximum-scale=1" />
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:" />
    <style>
      html, body {height: 100%; margin: 0; font-family: Arial, Helvetica, sans-serif; background-color: #f6f6f6;}
    #header, #content, #footer {position: absolute; left: 0; right:0;}
    #header {height: 50px; top: 0;}
    fieldset {margin: 0; background-color: #fff;}
    #content {top: 51px; bottom: 51px; margin:5px;}
    #footer {height: 50px; bottom: 0;}

    pre {position: absolute; top: 0; bottom: 0; width: 50%; outline: 1px solid #ccc; margin: 0; overflow: scroll; background-color: #fff;}
    #vcard {left: 0;}
    #json {right: 0;}

    #json .string {color: brown;}
    #json .number {color: green;}
    #json .boolean {color: blue;}
    #json .null {color: red;}
    #json .key {color: navy;}

    .overlay {position:fixed; top:0; right:0; bottom:0; left:0; z-index:9200; background-color:#222;}
    .popup-content {position:absolute; top:50%; left:50%; max-width:100%; transform:translate(-50%, -50%);}
    .popup-close {position:absolute; cursor:pointer; background:none; border:none; color:#fff;}
    .popup-close::before {display:inline-block; text-shadow:1px 1px 2px black, 0 0 1em black, 0 0 0.2em black;}
    .popup-close {top:0; right:20px;}
    .popup-close::before {content:"\00d7"; font-size:50px; font-weight:normal;}
    .popup-content .card {margin-bottom: 5px; padding: 5px; background-color: #fff;}
    .popup-content .card-content {display: flex; padding-top: 5px;}
    .popup-content div.wrapper {display: inline-block; margin-right: 10px;}
    .popup-content div.wrapper.photo {margin-right:80px;}
    .popup-content div.wrapper.name {display: block;}
    .popup-content div.wrapper.home:before {content: "\1F3E0";}
    .popup-content div.wrapper.work:before {content: "\1F3E2";}
    .popup-content p {margin: 0;}
    .popup-content p:before {padding-right: 5px;}
    .popup-content p.tel:before,
    .popup-content p.voice:before {content: "\1F4DE";}
    .popup-content p.fax:before {content: "\1F5B7";}
    .popup-content p.cell:before {content: "\1F4F1";}
    .popup-content p.mailto:before {content: "\1F582";}
    .popup-content p.https:before {content: "\1F310";}
    </style>
  </head>
  <body>
    <div id="header">
      <form id="vcardFile" name="vcardFile">
        <fieldset><label for="fileinput">vCard File (.vcf):</label><input type="file" id="fileinput" /><!--input type="button" id="btnLoad" value="add" /--><input type="button" id="btnPreview" value="preview" /><input type="button" id="btnCleanUp" value="reset" /></fieldset>
      </form>
    </div>
    <div id="content">
      <pre id="vcard"></pre>
      <pre id="json"></pre>
    </div>
    <div id="footer">
      <p>© Udo Schmal</p>
    </div>
    <script src="vCard.js"></script>
  </body>
</html>
JavaScript:
vCard.js JavaScript (19,7 kByte) 19.08.2025 18:17
// coding: utf-8
/** Created by: Udo Schmal | https://www.gocher.me/ */
(function () {
  'use strict';

  let vCardFields = {
    "ADR": {
      "method": addressValue,
      "property": "address"
    },
    "BDAY": {
      "method": dateValue,
      "property": "birthday"
    },
    "BEGIN": {
      "method": noValue,
      "property": "begin" // not used
    },
    "CATEGORIES": {
      "method": listValue,
      "property": "categories"
    },
    "EMAIL": {
      "method": typedValue,
      "property": "email"
    },
    "END": {
      "method": endCard,
      "property": "begin" // not used
    },
    "FN": {
      "method": stringValue,
      "property": "displayName"
    },
    "GENDER": {
      "method": genderValue,
      "property": "gender"
    },
    "KIND": {
      "method": stringValue,
      "property": "kind"
    },
    "LABEL": {
      "method": labelValue,
      "property": "label"
    },
    "LANG": {
      "method": stringValue,
      "property": "language"
    },
    "N": {
      "method": structuredValue(['surname', 'name', 'additionalName', 'prefix', 'suffix']),
      "property": "name"
    },
    "NICKNAME": {
      "method": stringValue,
      "property": "nickname"
    },
    "NOTE": {
      "method": stringValue,
      "property": "notes"
    },
    "ORG": {
      "method": stringValue,
      "property": "organization"
    },
    "PHOTO": {
      "method": mediaValue,
      "property": "photo"
    },
    "REV": {
      "method": dateValue,
      "property": "revision"
    },
    "ROLE": {
      "method": stringValue,
      "property": "role"
    },
    "SOUND": {
      "method": mediaValue,
      "property": "sound"
    },
    "SOURCE": {
      "method": stringValue,
      "property": "source"
    },
    "TEL": {
      "method": typedValue,
      "property": "telephone"
    },
    "TITLE": {
      "method": stringValue,
      "property": "title"
    },
    "TZ": {
      "method": stringValue,
      "property": "timezone"
    },
    "UID": {
      "method": stringValue,
      "property": "uid"
    },
    "URL": {
      "method": typedValue,
      "property": "url"
    },
    "VERSION": {
      "method": stringValue,
      "property": "version"
    }
  },
  vCardData = '',
  vCard = {},
  vCards = [];

  function decodeQuotedPrintable(str) {
    str = (str || '')
      .toString()
      // remove invalid whitespace from the end of lines
      .replace(/[\t ]+$/gm, '')
      // remove soft line breaks
      .replace(/\=(?:\r?\n|$)/g, '');
    let decoded = '';
    for (let i = 0, len = str.length; i < len; i++) {
      let chr = str.charAt(i), hex;
      if (chr === '=' && (hex = str.substr(i + 1, 2)) && /[\da-fA-F]{2}/.test(hex)) {
        decoded += String.fromCharCode(parseInt(hex, 16));
        i += 2;
      } else {
        decoded += chr;
      }
    }
    return decoded;
  }

  function noValue() {
  }

  function stringValue(fieldValue, fieldName) {
    // convert escaped new lines to real new lines.
    fieldValue = fieldValue.replace(/\\n/g, '\n');
    fieldValue = fieldValue.replace(/\\,/g, ',');
    fieldValue = fieldValue.replace(/\\;/g, ';');
    // convert quoted-printable encoded data
    fieldValue = decodeQuotedPrintable(fieldValue);
    fieldValue = fieldValue.replaceAll('\r\n', '\n');
    // append value if previously specified
    if (vCard[fieldName]) {
      vCard[fieldName] += '\n' + fieldValue;
    } else {
      vCard[fieldName] = fieldValue;
    }
  }

  function genderValue(fieldValue, fieldName) {
    switch (fieldValue.toUpperCase()) {
      case 'F': vCard[fieldName] = 'female'; break;
      case 'M': vCard[fieldName] = 'male'; break;
      case 'D': vCard[fieldName] = 'diverse'; break;
    }
  }

  function listValue(fieldValue, fieldName) {
    vCard[fieldName] = fieldValue.split(',');
  }

  function dateValue(fieldValue, fieldName) {
    let dateValue;
    if (fieldValue.length === 16) {
      // long format "19680628T105900Z"
      dateValue = new Date(fieldValue.substr(0, 4) + '-' + fieldValue.substr(4, 2) + '-' + fieldValue.substr(6, 2) +
                           'T' + fieldValue.substr(9, 2) + ':' + fieldValue.substr(11, 2) + ':' + fieldValue.substr(13, 2));
    } else if (fieldValue.length === 8) {
      // short format "19680628"
      dateValue = new Date(fieldValue.substr(0, 4) + '-' + fieldValue.substr(4, 2) + '-' + fieldValue.substr(6, 2));
    } else {
      // date format "1968-06-28" / "2025-08-17T18:25:00.000Z"
      dateValue = new Date(fieldValue);
    }
    if (!dateValue || isNaN(dateValue.getDate())) {
      dateValue = null;
      alert('invalid date format ' + fieldValue);
    }
    vCard[fieldName] = dateValue && dateValue.toJSON(); // ISO date format
  }

  function structuredValue(fields) {
    return function (fieldValue, fieldName) {
      var values = fieldValue.split(';');
      vCard[fieldName] = fields.reduce(function (p, c, i) {
        p[c] = values[i] || '';
          return p;
        }, {});
    }
  }

  function typedValue(fieldValue, fieldName, typeInfo, valueFormatter) {
    var isDefault = false, fieldSubpart = '';
    // find out if it is that preferred value and prepare type info
    typeInfo = typeInfo.reduce(function (p, c) {
      if (c.value === undefined) {
        c.value = c.name;
        c.name = 'type';
      }
      if (c.name === 'type') {
        let values = c.value.toLowerCase().split(',');
        if (values.indexOf('pref') > -1) {
          values = values.filter(e => e !== 'pref');
          isDefault = true;
        }
        c.value = values.join(',');
      }
      if (c.value !== '') {
        if (p[c.name]) {
          p[c.name] += ',' + c.value;
        } else {
          p[c.name] = c.value;
        }
      }
      return p;
    }, {});

    let backupFieldName = '';
    if (fieldName === 'label') {
      backupFieldName = 'label';
      fieldName = 'address';
    }
    vCard[fieldName] = vCard[fieldName] || [];
    // merge label and adddress
    if (fieldName === 'address') {
      // find match
      let match = vCard[fieldName]?.find((element) => element.valueInfo.type == typeInfo.type);
      if (match) {
        if (backupFieldName === 'label') {
          // store lable value into valueInfo.label
          match.valueInfo.label = valueFormatter ? valueFormatter(fieldValue, typeInfo) : fieldValue;
        } else {
          match.isDefault = isDefault;
          // store old value into typeinfo.label
          match.valueInfo['label'] = match.value;
          for (let key in typeInfo) {
            match.valueInfo[key] = typeInfo[key];
          }
          match.value = valueFormatter ? valueFormatter(fieldValue, typeInfo) : fieldValue;
        }
        return ;
      }
    }
    vCard[fieldName].push({
      isDefault: isDefault,
      valueInfo: typeInfo,
      value: valueFormatter ? valueFormatter(fieldValue, typeInfo) : fieldValue
    });
  }

  function addressValue(fieldValue, fieldName, typeInfo) {
    typedValue(fieldValue, fieldName, typeInfo, function (value) {
      var names = value.split(';');
      return {
        // ADR field sequence
        postOfficeBox: names[0],
        street: names[2] || '',
        number: names[1],
        postalCode: names[5] || '',
        city: names[3] || '',
        region: names[4] || '',
        country: names[6] || ''
      };
    });
  }

  function mediaValue(fieldValue, fieldName, typeInfo) {
    typedValue(fieldValue, fieldName, typeInfo, function (value, typeInfo) {
      let prefix = '';
      if (typeInfo && typeInfo.encoding) {
        if (typeInfo.type) {
          prefix += (fieldName === 'sound' ? 'data:audio/' : 'data:image/') + typeInfo.type + ';';
        }
        if ((typeInfo.encoding == 'b') || (typeInfo.encoding.toLowerCase() == 'base64')) {
          prefix += 'base64,';
        }
      }
      return prefix + value;
    });
  }

  function labelValue(fieldValue, fieldName, typeInfo) {
    typedValue(fieldValue, fieldName, typeInfo, function (value, typeInfo) {
      // convert escaped new lines to real new lines.
      let str = value.replaceAll('\\n', '\n');
      str = str.replace(/\\n/g, '\n');
      if (typeInfo && typeInfo.encoding && typeInfo.encoding.toLowerCase() == 'quoted-printable') {
        // convert quoted-printable encoded data
        str = decodeQuotedPrintable(str);
        str = str.replaceAll('\r\n', '\n');
      }
      return str;
    });
  }

  // store vCard in vCards and reset vCard.
  function endCard() {
    vCards.push(vCard);
    vCard = {};
  }

  // parse data, add to vCards
  function parse(data) {
    if (!data) {
      return ;
    }
    vCardData += '\n' + data;
    let lines = data
      // replace escaped new lines
      .replace(/\r\n\s{1}/g, '')
      // split if a character is directly after a newline
      .split(/\r\n(?=\S)|\r(?=\S)|\n(?=\S)/);

    for (var i = 0; i < lines.length; i++) {
      let line = lines[i];

      // sometimes lines are prefixed by "item" keyword like "item1.ADR;type=WORK:....."
      if (line.substring(0, 4) === "item") {
        line = line.match(/item\d\.(.*)/)[1];
      }

      if ((line.substring(0, 5) === "home.") || (line.substring(0, 5) === "work.")) {
        if (line.substring(5, 11) === "label:") {
          line = line.substring(5).replace(":", ";TYPE=" + line.substring(0, 4) + ":");
        } else {
          line = line.substring(5).replace("type=", "TYPE=" + line.substring(0, 4) + ",");
        }
        console.log(line);
      }

      var pairs = line.split(':'),
        fieldName = pairs[0],
        fieldTypeInfo = [],
        fieldValue = pairs.slice(1).join(':');

      // is additional type info provided ?
      if (fieldName.indexOf(';') >= 0 && line.indexOf(';') < line.indexOf(':')) {
        var typeInfo = fieldName.split(';');
        fieldName = typeInfo[0];
        fieldTypeInfo = typeInfo.slice(1).map(function (type) {
          let info = type.split('=');
          return {
            name: info[0]?.toLowerCase(),
            value: info[1]?.replace(/"(.*)"/, '$1')
          }
        });
      }

      // ensure fieldType is in upper case
      let ucFieldName = fieldName.toUpperCase();
      if (ucFieldName.substring(0, 2) === 'X-') {
        // ignore X- prefixed extension fields.
        alert('ignore field "' + fieldName + '" with value "' + fieldValue + '"');
      } else {
        if (vCardFields[ucFieldName]) {
          vCardFields[ucFieldName].method(fieldValue, vCardFields[ucFieldName].property, fieldTypeInfo);
        } else {
          alert('unknown field "' + fieldName + '" with value "' + fieldValue + '"');
        }
      }
    }
    return vCards;
  }

  // convert object to json
  function toJson(cards) {
    if (!cards) {
      cards = vCards;
    }
    return JSON.stringify(cards, null, 2); // spacing level = 2
  }

  // reset object
  function reset() {
    vCardData = '';
    vCard = {};
    vCards = [];
  }

  // if form is present
  function syntaxHighlight(json) {
    if (typeof json != 'string') {
      json = JSON.stringify(json, undefined, 2);
    }
    json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    let syn = /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g;
    return json.replace(syn, function (match) {
      var cls = 'number';
      if (/^"/.test(match)) {
        if (/:$/.test(match)) {
          cls = 'key';
        } else {
          cls = 'string';
        }
      } else if (/true|false/.test(match)) {
        cls = 'boolean';
      } else if (/null/.test(match)) {
        cls = 'null';
      }
      return '<span class="' + cls + '">' + match + '</span>';
    });
  }

  var input = document.getElementById('fileinput');
  if (input) {
    var btnLoad = document.getElementById('btnLoad');
    var btnPreview = document.getElementById('btnPreview');
    var btnCleanUp = document.getElementById('btnCleanUp');
    if (typeof window.FileReader !== 'function') {
      alert("The file API isn't supported on this browser yet.");
    }
    if (!input.files) {
      alert("This browser doesn't seem to support the `files` property of file inputs.");
    }

    function checkStatus() {
      if (btnLoad) {
        if (!input.files[0]) {
          btnLoad.disabled = true;
        } else {
          btnLoad.disabled = false;
        }
      } else {
        if (input.files[0]) {
          loadFile();
        }
      }
      if (vCards.length > 0) {
        if (btnCleanUp) {
          btnCleanUp.disabled = false;
        }
        if (btnPreview) {
          btnPreview.disabled = false;
        }
      } else {
        if (btnCleanUp) {
          btnCleanUp.disabled = true;
        }
        if (btnPreview) {
          btnPreview.disabled = true;
        }
      }
    }
    checkStatus();
    input.addEventListener('change', checkStatus);

    function loadFile() {
      var file, fr;
      if (input.files[0]) {
        file = input.files[0];
        fr = new FileReader();
        fr.addEventListener('load', receivedText);
        fr.readAsText(file);
      }

      function receivedText(e) {
        let data = e.target.result;

        // convert vCard to object
        let cards = parse(data);

        // show data vCard
        let preVcard = document.getElementById('vcard');
        if (preVcard) {
          preVcard.innerHTML = vCardData;
        }

        // convert object to json
        let jsonCards = toJson(cards);

        // show json with syntax highlight
        let json = syntaxHighlight(jsonCards);
        let preJson = document.getElementById('json');
        if (preJson) {
          preJson.innerHTML = json;
        }
        input.value = null;
        checkStatus();
      }
    }

    if (btnLoad) {
      btnLoad.addEventListener('click', loadFile);
    }

    if (btnPreview) {
      function preview() {
        var overlay = document.createElement('div');
        overlay.className = 'overlay';
        document.body.appendChild(overlay);
        var content = document.createElement('div');
        content.className = 'popup-content';
        overlay.appendChild(content);
        var close = document.createElement('span');
        close.className = 'popup-close';
        overlay.appendChild(close);
        close.addEventListener('click', function (event) {
          document.body.removeChild(overlay);
        });
        let divCard = null, divCardContent = null, home = null, work = null;
        function getElement(el) {
          switch (el) {
            case 'home':
              if (!home) {
                home = document.createElement('div');
                home.className = 'wrapper home';
                divCardContent.appendChild(home);
              }
              return home;
              break;
            case 'work':
              if (!work) {
                work = document.createElement('div');
                work.className = 'wrapper work';
                divCardContent.appendChild(work);
              }
              return work;
              break;
          }
        }
        function name(name, displayName) {
          let wrapper = document.createElement('div');
          wrapper.className = 'wrapper name';
          divCard.appendChild(wrapper);
          let p = document.createElement('p');
          wrapper.appendChild(p);
          let strong = document.createElement('strong');
          p.appendChild(strong);
          if (name) {
            strong.appendChild(document.createTextNode(name.prefix + ' ' + name.name + ' ' + name.additionalName + ' ' + name.surname));
            p.appendChild(document.createElement('br'));
            p.appendChild(document.createTextNode(name.suffix));
          } else if (vCard.displayName) {
            strong.appendChild(document.createTextNode(displayName));
          }
        }
        function photo(arr) {
          if (arr && arr[0]) {
            let wrapper = document.createElement('div');
            wrapper.className = 'wrapper photo';
            divCardContent.appendChild(wrapper);
            let img = document.createElement('img');
            img.src = arr[0].value;
            img.className = 'photo';
            wrapper.appendChild(img);
          }
        }
        function addresses(arr) {
          if (arr) {
            for (let i=0; i<arr.length; i++) {
              let item = arr[i];
              let value = item.value;
              if (value) {
                let parent = item.valueInfo.type?.indexOf('work') > -1 ? getElement('work') : getElement('home');
                let p = document.createElement('p');
                p.className = 'address ' + item.valueInfo.type;
                parent.appendChild(p);
                if (value instanceof Object) {
                  p.appendChild(document.createTextNode(value.street + ' ' + value.number));
                  p.appendChild(document.createElement('br'));
                  p.appendChild(document.createTextNode(value.postalCode + ' ' + value.city));
                  p.appendChild(document.createElement('br'));
                  p.appendChild(document.createTextNode(value.region));
                  p.appendChild(document.createElement('br'));
                  p.appendChild(document.createTextNode(value.country));
                } else {
                  value = value.split('\n');
                  for(let u=0; u<value.length; u++) {
                    p.appendChild(document.createTextNode(value[u]));
                    if (u < value.length-1) {
                      p.appendChild(document.createElement('br'));
                    }
                  }
                }
              }
            }
          }
        }
        function communications(name, arr) {
          if (arr) {
            for (let i=0; i<arr.length; i++) {
              let item = arr[i];
              let parent = item.valueInfo.type?.indexOf('work') > -1 ? getElement('work') : getElement('home');
              let p = document.createElement('p');
              p.className = name + ' ' + item.valueInfo.type?.replaceAll(',', ' ');
              parent.appendChild(p);
              let a = document.createElement('a');
              let url = item.value;
              if (!item.valueInfo.value || item.valueInfo.value !== 'uri') {
                url = name + ':' + (name === 'https' ? '////' : '') + url;
              }
              a.href = url;
              a.target = '_blank';
              let caption = item.value;
              if (item.valueInfo.value && item.valueInfo.value === 'uri') {
                caption = caption.replace(/^\/\/|^.*?:(\/\/)?/, '');
              }
              a.appendChild(document.createTextNode(caption));
              p.appendChild(a);
            }
          }
        }

        for (let o=0; o<vCards.length; o++) {
          let card = vCards[o];

          divCard = document.createElement('div');
          divCard.className = 'card';
          content.appendChild(divCard);
          name(card.name, card.displayName);
          divCardContent = document.createElement('div');
          divCardContent.className = 'card-content';
          divCard.appendChild(divCardContent);
          photo(card.photo);
          addresses(card.address);
          communications('tel', card.telephone);
          communications('mailto', card.email);
          communications('https', card.url);

          // next card ?
          divCard = null;
          divCardContent = null;
          home = null;
          work = null;
        }
      }
      btnPreview.addEventListener('click', preview);
    }

    if (btnCleanUp) {
      function cleanUp() {
        let preVcard = document.getElementById('vcard');
        if (preVcard) {
          preVcard.innerHTML = '';
        }
        let preJson = document.getElementById('json');
        if (preJson) {
          preJson.innerHTML = '';
        }
        reset();
        checkStatus();
      }
      btnCleanUp.addEventListener('click', cleanUp);
    }
  }
  // expose globally
  window.vCards = {
    parse: parse,
    toJson: toJson,
    reset: reset
  };
})();

Kontakt

Udo Schmal
Udo Schmal

Udo Schmal
Softwareentwickler
Ellerndiek 26
24837 Schleswig
Schleswig-Holstein
Germany




+49 4621 9785538
+49 1575 0663676
+49 4621 9785539
SMS
WhatsApp

Google Maps Profile
Instagram Profile
vCard 2.1, vCard 3.0, vCard 4.0

Service Infos

CMS Info

Product Name:
UDOs Webserver
Version:
0.5.2.228
Description:
All in one Webserver
Copyright:
Udo Schmal
Compilation:
Tue, 17. Feb 2026 21:08:36

Development Info

Compiler:
Free Pascal FPC 3.3.1
compiled for:
OS:Linux, CPU:x86_64

System Info

OS:
Ubuntu 24.04.4 LTS (Noble Numbat)

Hardware Info

Model:
Hewlett-Packard HP Pavilion dm4 Notebook PC
CPU Name:
Intel(R) Core(TM) i5-2430M CPU @ 2.40GHz
CPU Type:
x86_64, 1 physical CPU(s), 2 Core(s), 4 logical CPU(s), max 3000.0000 MHz