Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  font.rs   Sprache: unbekannt

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use api::{FontInstanceFlags, FontKey, FontRenderMode, FontVariation};
use api::{ColorU, GlyphDimensions, NativeFontHandle};
use crate::gamma_lut::{ColorLut, GammaLut};
use crate::rasterizer::{FontInstance, FontTransform, GlyphKey};
use crate::rasterizer::{GlyphFormat, GlyphRasterError, GlyphRasterResult, RasterizedGlyph};
use crate::rasterizer::apply_multistrike_bold;
use crate::types::{FastHashMap, FastHashSet};
use std::borrow::Borrow;
use std::collections::hash_map::Entry;
use std::hash::{Hash, Hasher};
use std::path::Path;
use std::sync::{Arc, Mutex};
use api::FontInstancePlatformOptions;
use std::mem;

lazy_static! {
    static ref DEFAULT_FONT_DESCRIPTOR: dwrote::FontDescriptor = dwrote::FontDescriptor {
        family_name: "Arial".to_owned(),
        weight: dwrote::FontWeight::Regular,
        stretch: dwrote::FontStretch::Normal,
        style: dwrote::FontStyle::Normal,
    };
}

type CachedFontKey = Arc<Path>;

// A cached dwrote font file that is shared among all faces.
// Each face holds a CachedFontKey to keep track of how many users of the font there are.
struct CachedFont {
    key: CachedFontKey,
    file: dwrote::FontFile,
}

// FontFile contains a ComPtr<IDWriteFontFile>, but DWrite font files are threadsafe.
unsafe impl Send for CachedFont {}

impl PartialEq for CachedFont {
    fn eq(&self, other: &CachedFont) -> bool {
        self.key == other.key
    }
}
impl Eq for CachedFont {}

impl Hash for CachedFont {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.key.hash(state);
    }
}

impl Borrow<Path> for CachedFont {
    fn borrow(&self) -> &Path {
        &*self.key
    }
}

lazy_static! {
    // This is effectively a weak map of dwrote FontFiles. CachedFonts are entered into the
    // cache when there are any FontFaces using them. CachedFonts are removed from the cache
    // when there are no more FontFaces using them at all.
    static ref FONT_CACHE: Mutex<FastHashSet<CachedFont>> = Mutex::new(FastHashSet::default());
}

struct FontFace {
    cached: Option<CachedFontKey>,
    file: dwrote::FontFile,
    index: u32,
    face: dwrote::FontFace,
}

pub struct FontContext {
    fonts: FastHashMap<FontKey, FontFace>,
    variations: FastHashMap<(FontKey, dwrote::DWRITE_FONT_SIMULATIONS, Vec<FontVariation>), dwrote::FontFace>,
    gamma_luts: FastHashMap<(u16, u8), GammaLut>,
}

// DirectWrite is safe to use on multiple threads and non-shareable resources are
// all hidden inside their font context.
unsafe impl Send for FontContext {}

fn dwrite_texture_type(render_mode: FontRenderMode) -> dwrote::DWRITE_TEXTURE_TYPE {
    match render_mode {
        FontRenderMode::Mono => dwrote::DWRITE_TEXTURE_ALIASED_1x1,
        FontRenderMode::Alpha |
        FontRenderMode::Subpixel => dwrote::DWRITE_TEXTURE_CLEARTYPE_3x1,
    }
}

fn dwrite_measure_mode(
    font: &FontInstance,
    bitmaps: bool,
) -> dwrote::DWRITE_MEASURING_MODE {
    if bitmaps || font.flags.contains(FontInstanceFlags::FORCE_GDI) {
        dwrote::DWRITE_MEASURING_MODE_GDI_CLASSIC
    } else {
      match font.render_mode {
          FontRenderMode::Mono => dwrote::DWRITE_MEASURING_MODE_GDI_CLASSIC,
          FontRenderMode::Alpha | FontRenderMode::Subpixel => dwrote::DWRITE_MEASURING_MODE_NATURAL,
      }
    }
}

fn dwrite_render_mode(
    font_face: &dwrote::FontFace,
    font: &FontInstance,
    em_size: f32,
    measure_mode: dwrote::DWRITE_MEASURING_MODE,
    bitmaps: bool,
) -> dwrote::DWRITE_RENDERING_MODE {
    let dwrite_render_mode = match font.render_mode {
        FontRenderMode::Mono => dwrote::DWRITE_RENDERING_MODE_ALIASED,
        FontRenderMode::Alpha | FontRenderMode::Subpixel => {
            if bitmaps || font.flags.contains(FontInstanceFlags::FORCE_GDI) {
                dwrote::DWRITE_RENDERING_MODE_GDI_CLASSIC
            } else if font.flags.contains(FontInstanceFlags::FORCE_SYMMETRIC) {
                dwrote::DWRITE_RENDERING_MODE_CLEARTYPE_NATURAL_SYMMETRIC
            } else if font.flags.contains(FontInstanceFlags::NO_SYMMETRIC) {
                dwrote::DWRITE_RENDERING_MODE_CLEARTYPE_NATURAL
            } else {
                font_face.get_recommended_rendering_mode_default_params(em_size, 1.0, measure_mode)
            }
        }
    };

    if dwrite_render_mode == dwrote::DWRITE_RENDERING_MODE_OUTLINE {
        // Outline mode is not supported
        return dwrote::DWRITE_RENDERING_MODE_CLEARTYPE_NATURAL_SYMMETRIC;
    }

    dwrite_render_mode
}

fn is_bitmap_font(font: &FontInstance) -> bool {
    // If bitmaps are requested, then treat as a bitmap font to disable transforms.
    // If mono AA is requested, let that take priority over using bitmaps.
    font.render_mode != FontRenderMode::Mono &&
        font.flags.contains(FontInstanceFlags::EMBEDDED_BITMAPS)
}

impl FontContext {
    pub fn distribute_across_threads() -> bool {
        true
    }

    pub fn new() -> FontContext {
        FontContext {
            fonts: FastHashMap::default(),
            variations: FastHashMap::default(),
            gamma_luts: FastHashMap::default(),
        }
    }

    fn add_font_descriptor(&mut self, font_key: &FontKey, desc: &dwrote::FontDescriptor) {
        let system_fc = dwrote::FontCollection::get_system(false);
        if let Some(font) = system_fc.get_font_from_descriptor(desc) {
            let face = font.create_font_face();
            let file = face.get_files().pop().unwrap();
            let index = face.get_index();
            self.fonts.insert(*font_key, FontFace { cached: None, file, index, face });
        }
    }

    pub fn add_raw_font(&mut self, font_key: &FontKey, data: Arc<Vec<u8>>, index: u32) {
        if self.fonts.contains_key(font_key) {
            return;
        }

        if let Some(file) = dwrote::FontFile::new_from_data(data) {
            if let Ok(face) = file.create_face(index, dwrote::DWRITE_FONT_SIMULATIONS_NONE) {
                self.fonts.insert(*font_key, FontFace { cached: None, file, index, face });
                return;
            }
        }
        // XXX add_raw_font needs to have a way to return an error
        debug!("DWrite WR failed to load font from data, using Arial instead");
        self.add_font_descriptor(font_key, &DEFAULT_FONT_DESCRIPTOR);
    }

    pub fn add_native_font(&mut self, font_key: &FontKey, font_handle: NativeFontHandle) {
        if self.fonts.contains_key(font_key) {
            return;
        }

        let index = font_handle.index;
        let mut cache = FONT_CACHE.lock().unwrap();
        // Check to see if the font is already in the cache. If so, reuse it.
        if let Some(font) = cache.get(font_handle.path.as_path()) {
            if let Ok(face) = font.file.create_face(index, dwrote::DWRITE_FONT_SIMULATIONS_NONE) {
                self.fonts.insert(
                    *font_key,
                    FontFace { cached: Some(font.key.clone()), file: font.file.clone(), index, face },
                );
                return;
            }
        }
        if let Some(file) = dwrote::FontFile::new_from_path(&font_handle.path) {
            // The font is not in the cache yet, so try to create the font and insert it in the cache.
            if let Ok(face) = file.create_face(index, dwrote::DWRITE_FONT_SIMULATIONS_NONE) {
                let key: CachedFontKey = font_handle.path.into();
                self.fonts.insert(
                    *font_key,
                    FontFace { cached: Some(key.clone()), file: file.clone(), index, face },
                );
                cache.insert(CachedFont { key, file });
                return;
            }
        }

        // XXX add_native_font needs to have a way to return an error
        debug!("DWrite WR failed to load font from path, using Arial instead");
        self.add_font_descriptor(font_key, &DEFAULT_FONT_DESCRIPTOR);
    }

    pub fn delete_font(&mut self, font_key: &FontKey) {
        if let Some(face) = self.fonts.remove(font_key) {
            self.variations.retain(|k, _| k.0 != *font_key);
            // Check if this was a cached font.
            if let Some(key) = face.cached {
                let mut cache = FONT_CACHE.lock().unwrap();
                // If there are only two references left, that means only this face and
                // the cache are using the font. So remove it from the cache.
                if Arc::strong_count(&key) == 2 {
                    cache.remove(&*key);
                }
            }
        }
    }

    pub fn delete_font_instance(&mut self, instance: &FontInstance) {
        // Ensure we don't keep around excessive amounts of stale variations.
        if !instance.variations.is_empty() {
            let sims = if instance.flags.contains(FontInstanceFlags::SYNTHETIC_BOLD) {
                dwrote::DWRITE_FONT_SIMULATIONS_BOLD
            } else {
                dwrote::DWRITE_FONT_SIMULATIONS_NONE
            };
            self.variations.remove(&(instance.font_key, sims, instance.variations.clone()));
        }
    }

    // Assumes RGB format from dwrite, which is 3 bytes per pixel as dwrite
    // doesn't output an alpha value via GlyphRunAnalysis::CreateAlphaTexture
    #[allow(dead_code)]
    fn print_glyph_data(&self, data: &[u8], width: usize, height: usize) {
        // Rust doesn't have step_by support on stable :(
        for i in 0 .. height {
            let current_height = i * width * 3;

            for pixel in data[current_height .. current_height + (width * 3)].chunks(3) {
                let r = pixel[0];
                let g = pixel[1];
                let b = pixel[2];
                debug!("({}, {}, {}) ", r, g, b,);
            }
        }
    }

    fn get_font_face(
        &mut self,
        font: &FontInstance,
    ) -> &dwrote::FontFace {
        if !font.flags.contains(FontInstanceFlags::SYNTHETIC_BOLD) &&
           font.variations.is_empty() {
            return &self.fonts.get(&font.font_key).unwrap().face;
        }
        let sims = if font.flags.contains(FontInstanceFlags::SYNTHETIC_BOLD) {
            dwrote::DWRITE_FONT_SIMULATIONS_BOLD
        } else {
            dwrote::DWRITE_FONT_SIMULATIONS_NONE
        };
        match self.variations.entry((font.font_key, sims, font.variations.clone())) {
            Entry::Occupied(entry) => entry.into_mut(),
            Entry::Vacant(entry) => {
                let normal_face = self.fonts.get(&font.font_key).unwrap();
                if !font.variations.is_empty() {
                    if let Some(var_face) = normal_face.face.create_font_face_with_variations(
                        sims,
                        &font.variations.iter().map(|var| {
                            dwrote::DWRITE_FONT_AXIS_VALUE {
                                // OpenType tags are big-endian, but DWrite wants little-endian.
                                axisTag: var.tag.swap_bytes(),
                                value: var.value,
                            }
                        }).collect::<Vec<_>>(),
                    ) {
                        return entry.insert(var_face);
                    }
                }
                let var_face = normal_face.file
                    .create_face(normal_face.index, sims)
                    .unwrap_or_else(|_| normal_face.face.clone());
                entry.insert(var_face)
            }
        }
    }

    fn create_glyph_analysis(
        &mut self,
        font: &FontInstance,
        key: &GlyphKey,
        size: f32,
        transform: Option<dwrote::DWRITE_MATRIX>,
        bitmaps: bool,
    ) -> Result<(dwrote::GlyphRunAnalysis, dwrote::DWRITE_TEXTURE_TYPE, dwrote::RECT), dwrote::HRESULT> {
        let face = self.get_font_face(font);
        let glyph = key.index() as u16;
        let advance = 0.0f32;
        let offset = dwrote::GlyphOffset {
            advanceOffset: 0.0,
            ascenderOffset: 0.0,
        };

        let glyph_run = dwrote::DWRITE_GLYPH_RUN {
            fontFace: unsafe { face.as_ptr() },
            fontEmSize: size, // size in DIPs (1/96", same as CSS pixels)
            glyphCount: 1,
            glyphIndices: &glyph,
            glyphAdvances: &advance,
            glyphOffsets: &offset,
            isSideways: 0,
            bidiLevel: 0,
        };

        let dwrite_measure_mode = dwrite_measure_mode(font, bitmaps);
        let dwrite_render_mode = dwrite_render_mode(
            face,
            font,
            size,
            dwrite_measure_mode,
            bitmaps,
        );

        let analysis = dwrote::GlyphRunAnalysis::create(
            &glyph_run,
            1.0,
            transform,
            dwrite_render_mode,
            dwrite_measure_mode,
            0.0,
            0.0,
        )?;
        let texture_type = dwrite_texture_type(font.render_mode);
        let bounds = analysis.get_alpha_texture_bounds(texture_type)?;
        // If the bounds are empty, then we might not be able to render the glyph with cleartype.
        // Try again with aliased rendering to check if that works instead.
        if font.render_mode != FontRenderMode::Mono &&
           (bounds.left == bounds.right || bounds.top == bounds.bottom) {
            let analysis2 = dwrote::GlyphRunAnalysis::create(
                &glyph_run,
                1.0,
                transform,
                dwrote::DWRITE_RENDERING_MODE_ALIASED,
                dwrite_measure_mode,
                0.0,
                0.0,
            )?;
            let bounds2 = analysis2.get_alpha_texture_bounds(dwrote::DWRITE_TEXTURE_ALIASED_1x1)?;
            if bounds2.left != bounds2.right && bounds2.top != bounds2.bottom {
                return Ok((analysis2, dwrote::DWRITE_TEXTURE_ALIASED_1x1, bounds2));
            }
        }
        Ok((analysis, texture_type, bounds))
    }

    pub fn get_glyph_index(&mut self, font_key: FontKey, ch: char) -> Option<u32> {
        let face = &self.fonts.get(&font_key).unwrap().face;
        let indices = face.get_glyph_indices(&[ch as u32]);
        indices.first().map(|idx| *idx as u32)
    }

    pub fn get_glyph_dimensions(
        &mut self,
        font: &FontInstance,
        key: &GlyphKey,
    ) -> Option<GlyphDimensions> {
        let (size, x_scale, y_scale, bitmaps, transform) = Self::get_glyph_parameters(font, key);
        let (_, _, bounds) = self.create_glyph_analysis(font, key, size, transform, bitmaps).ok()?;

        let width = (bounds.right - bounds.left) as i32;
        let height = (bounds.bottom - bounds.top) as i32;

        // Alpha texture bounds can sometimes return an empty rect
        // Such as for spaces
        if width == 0 || height == 0 {
            return None;
        }

        let (strike_scale, pixel_step) = if bitmaps {
            (y_scale, 1.0)
        } else {
            (x_scale, y_scale / x_scale)
        };
        let extra_strikes = font.get_extra_strikes(FontInstanceFlags::MULTISTRIKE_BOLD, strike_scale);
        let extra_width = extra_strikes as f64 * pixel_step;

        let face = self.get_font_face(font);
        face.get_design_glyph_metrics(&[key.index() as u16], false)
            .first()
            .map(|metrics| {
                let em_size = size / 16.;
                let design_units_per_pixel = face.metrics().metrics0().designUnitsPerEm as f32 / 16. as f32;
                let scaled_design_units_to_pixels = em_size / design_units_per_pixel;
                let advance = metrics.advanceWidth as f32 * scaled_design_units_to_pixels;

                GlyphDimensions {
                    left: bounds.left,
                    top: -bounds.top,
                    width: width + extra_width.ceil() as i32,
                    height,
                    advance: advance + extra_width as f32,
                }
            })
    }

    // DWrite ClearType gives us values in RGB, but WR expects BGRA.
    fn convert_to_bgra(
        &self,
        pixels: &[u8],
        width: usize,
        height: usize,
        texture_type: dwrote::DWRITE_TEXTURE_TYPE,
        render_mode: FontRenderMode,
        bitmaps: bool,
        subpixel_bgr: bool,
        padding: usize,
    ) -> (Vec<u8>, bool) {
        let (buffer_width, buffer_height) = (width + padding * 2, height + padding * 2);
        let buffer_length = buffer_width * buffer_height * 4;
        let mut bgra_pixels: Vec<u8> = vec![0; buffer_length];

        match (texture_type, render_mode, bitmaps) {
            (dwrote::DWRITE_TEXTURE_ALIASED_1x1, _, _) => {
                assert!(width * height == pixels.len());
                let mut i = 0;
                for row in padding .. height + padding {
                    let row_offset = row * buffer_width;
                    for col in padding .. width + padding {
                        let offset = (row_offset + col) * 4;
                        let alpha = pixels[i];
                        i += 1;
                        bgra_pixels[offset + 0] = alpha;
                        bgra_pixels[offset + 1] = alpha;
                        bgra_pixels[offset + 2] = alpha;
                        bgra_pixels[offset + 3] = alpha;
                    }
                }
                (bgra_pixels, false)
            }
            (_, FontRenderMode::Subpixel, false) => {
                assert!(width * height * 3 == pixels.len());
                let mut i = 0;
                for row in padding .. height + padding {
                    let row_offset = row * buffer_width;
                    for col in padding .. width + padding {
                        let offset = (row_offset + col) * 4;
                        let (mut r, g, mut b) = (pixels[i + 0], pixels[i + 1], pixels[i + 2]);
                        if subpixel_bgr {
                            mem::swap(&mut r, &mut b);
                        }
                        i += 3;
                        bgra_pixels[offset + 0] = b;
                        bgra_pixels[offset + 1] = g;
                        bgra_pixels[offset + 2] = r;
                        bgra_pixels[offset + 3] = 0xff;
                    }
                }
                (bgra_pixels, true)
            }
            _ => {
                assert!(width * height * 3 == pixels.len());
                let mut i = 0;
                for row in padding .. height + padding {
                    let row_offset = row * buffer_width;
                    for col in padding .. width + padding {
                        let offset = (row_offset + col) * 4;
                        // Only take the G channel, as its closest to D2D
                        let alpha = pixels[i + 1] as u8;
                        i += 3;
                        bgra_pixels[offset + 0] = alpha;
                        bgra_pixels[offset + 1] = alpha;
                        bgra_pixels[offset + 2] = alpha;
                        bgra_pixels[offset + 3] = alpha;
                    }
                }
                (bgra_pixels, false)
            }
        }
    }

    pub fn prepare_font(font: &mut FontInstance) {
        match font.render_mode {
            FontRenderMode::Mono => {
                // In mono mode the color of the font is irrelevant.
                font.color = ColorU::new(255, 255, 255, 255);
                // Subpixel positioning is disabled in mono mode.
                font.disable_subpixel_position();
            }
            FontRenderMode::Alpha => {
                font.color = font.color.luminance_color().quantize();
            }
            FontRenderMode::Subpixel => {
                font.color = font.color.quantize();
            }
        }
    }

    fn get_glyph_parameters(font: &FontInstance, key: &GlyphKey)
                            -> (f32, f64, f64, bool, Option<dwrote::DWRITE_MATRIX>) {
        let (x_scale, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0));
        let scaled_size = font.size.to_f64_px() * y_scale;
        let bitmaps = is_bitmap_font(font);
        let (mut shape, (mut x_offset, mut y_offset)) = if bitmaps {
            (FontTransform::identity(), (0.0, 0.0))
        } else {
            (font.transform.invert_scale(y_scale, y_scale), font.get_subpx_offset(key))
        };
        if font.flags.contains(FontInstanceFlags::FLIP_X) {
            shape = shape.flip_x();
        }
        if font.flags.contains(FontInstanceFlags::FLIP_Y) {
            shape = shape.flip_y();
        }
        if font.flags.contains(FontInstanceFlags::TRANSPOSE) {
            shape = shape.swap_xy();
        }
        let (mut tx, mut ty) = (0.0, 0.0);
        if font.synthetic_italics.is_enabled() {
            let (shape_, (tx_, ty_)) = font.synthesize_italics(shape, scaled_size);
            shape = shape_;
            tx = tx_;
            ty = ty_;
        };
        x_offset += tx;
        y_offset += ty;
        let transform = if !shape.is_identity() || (x_offset, y_offset) != (0.0, 0.0) {
            Some(dwrote::DWRITE_MATRIX {
                m11: shape.scale_x,
                m12: shape.skew_y,
                m21: shape.skew_x,
                m22: shape.scale_y,
                dx: x_offset as f32,
                dy: y_offset as f32,
            })
        } else {
            None
        };
        (scaled_size as f32, x_scale, y_scale, bitmaps, transform)
    }

    pub fn begin_rasterize(_font: &FontInstance) {
    }

    pub fn end_rasterize(_font: &FontInstance) {
    }

    pub fn rasterize_glyph(&mut self, font: &FontInstance, key: &GlyphKey) -> GlyphRasterResult {
        let (size, x_scale, y_scale, bitmaps, transform) = Self::get_glyph_parameters(font, key);
        let (analysis, texture_type, bounds) = self.create_glyph_analysis(font, key, size, transform, bitmaps)
                                                   .or(Err(GlyphRasterError::LoadFailed))?;
        let mut width = (bounds.right - bounds.left) as i32;
        let height = (bounds.bottom - bounds.top) as i32;
        // Alpha texture bounds can sometimes return an empty rect
        // Such as for spaces
        if width == 0 || height == 0 {
            return Err(GlyphRasterError::LoadFailed);
        }

        let pixels = analysis.create_alpha_texture(texture_type, bounds).or(Err(GlyphRasterError::LoadFailed))?;
        let padding = if font.use_texture_padding() { 1 } else { 0 };
        let (mut bgra_pixels, is_subpixel) = self.convert_to_bgra(
            &pixels,
            width as usize,
            height as usize,
            texture_type,
            font.render_mode,
            bitmaps,
            font.flags.contains(FontInstanceFlags::SUBPIXEL_BGR),
            padding as usize,
        );

        // Apply multistrike bold, if necessary, and replace the current pixels with it.
        let (strike_scale, pixel_step) = if bitmaps {
            (y_scale, 1.0)
        } else {
            (x_scale, y_scale / x_scale)
        };
        let extra_strikes = font.get_extra_strikes(FontInstanceFlags::MULTISTRIKE_BOLD, strike_scale);
        if extra_strikes > 0 {
            let (bold_pixels, bold_width) = apply_multistrike_bold(
                &bgra_pixels,
                (width + padding * 2) as usize,
                (height + padding * 2) as usize,
                is_subpixel,
                extra_strikes,
                pixel_step,
            );
            width = bold_width as i32 - padding * 2;
            bgra_pixels = bold_pixels;
        }

        let FontInstancePlatformOptions { gamma, contrast, cleartype_level, .. } =
            font.platform_options.unwrap_or_default();
        let gamma_lut = self.gamma_luts
            .entry((gamma, contrast))
            .or_insert_with(||
                GammaLut::new(
                    contrast as f32 / 100.0,
                    gamma as f32 / 100.0,
                    gamma as f32 / 100.0,
                ));
        if is_subpixel {
            gamma_lut.preblend_scaled(&mut bgra_pixels, font.color, cleartype_level);
        } else {
            gamma_lut.preblend(&mut bgra_pixels, font.color);
        }

        let format = if bitmaps {
            GlyphFormat::Bitmap
        } else if texture_type == dwrote::DWRITE_TEXTURE_ALIASED_1x1 {
            font.get_alpha_glyph_format()
        } else {
            font.get_glyph_format()
        };

        Ok(RasterizedGlyph {
            left: (bounds.left - padding) as f32,
            top: (-bounds.top + padding) as f32,
            width: width + padding * 2,
            height: height + padding * 2,
            scale: (if bitmaps { y_scale.recip() } else { 1.0 }) as f32,
            format,
            bytes: bgra_pixels,
        })
    }
}

[ Dauer der Verarbeitung: 0.34 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge