Comments (3)
https://github.com/Lokathor/imagine/blob/da17bf5e0bdde3b274bff4799a8e5eb35d516cc9/src/bmp/mod.rs
from imagine.
Lines 374 to 881 in da17bf5
fn parse_me_a_bmp_yo(bmp: &[u8]) -> Result<(Vec<RGBA8>, u32, u32), BmpError> { | |
println!("== Parsing a BMP..."); | |
let (file_header, rest) = BmpFileHeader::try_from_bytes(bmp)?; | |
println!("file_header: {:?}", file_header); | |
if file_header.total_file_size as usize != bmp.len() | |
|| !(COMMON_BMP_TAGS.contains(&file_header.tag)) | |
{ | |
println!("actual size: {}", bmp.len()); | |
return Err(BmpError::ThisIsProbablyNotABmpFile); | |
} | |
let (info_header, mut rest) = BmpInfoHeader::try_from_bytes(rest)?; | |
println!("info_header: {info_header:?}", info_header = info_header); | |
let compression = info_header.compression(); | |
let bits_per_pixel = info_header.bits_per_pixel() as usize; | |
let width: usize = info_header.width().unsigned_abs() as usize; | |
let height: usize = info_header.height().unsigned_abs() as usize; | |
let pixel_count: usize = width.saturating_mul(height); | |
let [r_mask, g_mask, b_mask, a_mask] = match compression { | |
BmpCompression::Bitfields => { | |
let (a, new_rest) = try_split_off_byte_array::<{ size_of::<u32>() * 3 }>(rest) | |
.ok_or(BmpError::InsufficientBytes)?; | |
rest = new_rest; | |
[ | |
u32::from_le_bytes(a[0..4].try_into().unwrap()), | |
u32::from_le_bytes(a[4..8].try_into().unwrap()), | |
u32::from_le_bytes(a[8..12].try_into().unwrap()), | |
0, | |
] | |
} | |
BmpCompression::AlphaBitfields => { | |
let (a, new_rest) = try_split_off_byte_array::<{ size_of::<u32>() * 4 }>(rest) | |
.ok_or(BmpError::InsufficientBytes)?; | |
rest = new_rest; | |
[ | |
u32::from_le_bytes(a[0..4].try_into().unwrap()), | |
u32::from_le_bytes(a[4..8].try_into().unwrap()), | |
u32::from_le_bytes(a[8..12].try_into().unwrap()), | |
u32::from_le_bytes(a[12..16].try_into().unwrap()), | |
] | |
} | |
// When bitmasks aren't specified, there's default RGB mask values based on | |
// the bit depth, either 555 (16-bit) or 888 (32-bit). | |
_ => match bits_per_pixel { | |
16 => [0b11111 << 10, 0b11111 << 5, 0b11111, 0], | |
32 => [0b11111111 << 16, 0b11111111 << 8, 0b11111111, 0], | |
_ => [0, 0, 0, 0], | |
}, | |
}; | |
println!( | |
"bitmasks: [\n r:{r_mask:032b}\n g:{g_mask:032b}\n b:{b_mask:032b}\n a:{a_mask:032b}\n]", | |
r_mask = r_mask, | |
g_mask = g_mask, | |
b_mask = b_mask, | |
a_mask = a_mask | |
); | |
// we make our final storage once we know about the masks. if there is a | |
// non-zero alpha mask then we assume the image is alpha aware and pixels | |
// default to transparent black. otherwise we assume that the image doesn't | |
// know about alpha and use opaque black as the default. This is significant | |
// because the RLE compresions can skip touching some pixels entirely and just | |
// leave the default color in place. | |
let mut final_storage: Vec<RGBA8> = Vec::new(); | |
final_storage.try_reserve(pixel_count).map_err(|_| BmpError::AllocError)?; | |
final_storage.resize( | |
pixel_count, | |
if a_mask != 0 { RGBA8::default() } else { RGBA8 { r: 0, g: 0, b: 0, a: 0xFF } }, | |
); | |
#[allow(unused_assignments)] | |
let palette: Vec<RGBA8> = match info_header.palette_len() { | |
0 => Vec::new(), | |
count => { | |
let mut v = Vec::new(); | |
v.try_reserve(count).map_err(|_| BmpError::AllocError)?; | |
match info_header { | |
BmpInfoHeader::Core(_) => { | |
let bytes_needed = count * size_of::<[u8; 3]>(); | |
let (pal_slice, new_rest) = if rest.len() < bytes_needed { | |
return Err(BmpError::InsufficientBytes); | |
} else { | |
rest.split_at(bytes_needed) | |
}; | |
rest = new_rest; | |
let pal_slice: &[[u8; 3]] = cast_slice(pal_slice); | |
for [r, g, b] in pal_slice.iter().copied() { | |
v.push(RGBA8 { r, g, b, a: 0xFF }); | |
} | |
} | |
_ => { | |
let bytes_needed = count * size_of::<[u8; 4]>(); | |
let (pal_slice, new_rest) = if rest.len() < bytes_needed { | |
return Err(BmpError::InsufficientBytes); | |
} else { | |
rest.split_at(bytes_needed) | |
}; | |
rest = new_rest; | |
let pal_slice: &[[u8; 4]] = cast_slice(pal_slice); | |
for [r, g, b, a] in pal_slice.iter().copied() { | |
v.push(RGBA8 { r, g, b, a }); | |
} | |
if v.iter().copied().all(|c| c.a == 0) { | |
v.iter_mut().for_each(|c| c.a = 0xFF); | |
} | |
} | |
} | |
v | |
} | |
}; | |
println!("palette: {palette:?}", palette = palette); | |
let pixel_data_start_index: usize = file_header.pixel_data_offset as usize; | |
let pixel_data_end_index: usize = pixel_data_start_index + info_header.pixel_data_len(); | |
let pixel_data = if bmp.len() < pixel_data_end_index { | |
return Err(BmpError::InsufficientBytes); | |
} else { | |
&bmp[pixel_data_start_index..pixel_data_end_index] | |
}; | |
match compression { | |
BmpCompression::RgbNoCompression | |
| BmpCompression::Bitfields | |
| BmpCompression::AlphaBitfields => { | |
let bits_per_line: usize = | |
bits_per_pixel.saturating_mul(info_header.width().unsigned_abs() as usize); | |
let no_padding_bytes_per_line: usize = | |
(bits_per_line / 8) + (((bits_per_line % 8) != 0) as usize); | |
let bytes_per_line: usize = | |
((no_padding_bytes_per_line / 4) + ((no_padding_bytes_per_line % 4) != 0) as usize) * 4; | |
debug_assert!(no_padding_bytes_per_line <= bytes_per_line); | |
debug_assert_eq!(bytes_per_line % 4, 0); | |
if (pixel_data.len() % bytes_per_line) != 0 | |
|| (pixel_data.len() / bytes_per_line) != (info_header.height().unsigned_abs() as usize) | |
{ | |
return Err(BmpError::PixelDataIllegalLength); | |
} | |
// | |
match bits_per_pixel { | |
1 | 2 | 4 => { | |
let (base_mask, base_down_shift) = match bits_per_pixel { | |
1 => (0b1000_0000, 7), | |
2 => (0b1100_0000, 6), | |
4 => (0b1111_0000, 4), | |
_ => unreachable!(), | |
}; | |
let mut per_row_op = |i: &mut dyn Iterator<Item = (usize, &[u8])>| { | |
while let Some((y, data_row)) = i.next() { | |
let mut x = 0; | |
for byte in data_row.iter().copied() { | |
let mut mask: u8 = base_mask; | |
let mut down_shift: usize = base_down_shift; | |
while mask != 0 && x < width { | |
let pal_index = (byte & mask) >> down_shift; | |
let rgba8 = palette.get(pal_index as usize).copied().unwrap_or_default(); | |
final_storage[(y * width + x) as usize] = rgba8; | |
// | |
mask >>= bits_per_pixel; | |
down_shift = down_shift.wrapping_sub(bits_per_pixel); | |
x += 1; | |
} | |
} | |
} | |
}; | |
if info_header.height() < 0 { | |
per_row_op(&mut pixel_data.chunks_exact(bytes_per_line).enumerate()); | |
} else { | |
per_row_op(&mut pixel_data.rchunks_exact(bytes_per_line).enumerate()); | |
} | |
} | |
8 => { | |
let mut per_row_op = |i: &mut dyn Iterator<Item = (usize, &[u8])>| { | |
while let Some((y, data_row)) = i.next() { | |
for (x, pal_index) in | |
data_row[..no_padding_bytes_per_line].iter().copied().enumerate() | |
{ | |
let rgba8 = palette.get(pal_index as usize).copied().unwrap_or_default(); | |
final_storage[(y * width + x) as usize] = rgba8; | |
} | |
} | |
}; | |
if info_header.height() < 0 { | |
per_row_op(&mut pixel_data.chunks_exact(bytes_per_line).enumerate()); | |
} else { | |
per_row_op(&mut pixel_data.rchunks_exact(bytes_per_line).enumerate()); | |
} | |
} | |
24 => { | |
let mut per_row_op = |i: &mut dyn Iterator<Item = (usize, &[u8])>| { | |
while let Some((y, data_row)) = i.next() { | |
for (x, [r, g, b]) in | |
cast_slice::<u8, [u8; 3]>(&data_row[..no_padding_bytes_per_line]) | |
.iter() | |
.copied() | |
.enumerate() | |
{ | |
let rgba8 = RGBA8 { r, g, b, a: 0xFF }; | |
final_storage[y * width + x] = rgba8; | |
} | |
} | |
}; | |
if info_header.height() < 0 { | |
per_row_op(&mut pixel_data.chunks_exact(bytes_per_line).enumerate()) | |
} else { | |
per_row_op(&mut pixel_data.rchunks_exact(bytes_per_line).enumerate()) | |
} | |
} | |
16 => { | |
let r_shift: u32 = if r_mask != 0 { r_mask.trailing_zeros() } else { 0 }; | |
let g_shift: u32 = if g_mask != 0 { g_mask.trailing_zeros() } else { 0 }; | |
let b_shift: u32 = if b_mask != 0 { b_mask.trailing_zeros() } else { 0 }; | |
let a_shift: u32 = if a_mask != 0 { a_mask.trailing_zeros() } else { 0 }; | |
let r_max: f32 = (r_mask >> r_shift) as f32; | |
let g_max: f32 = (g_mask >> g_shift) as f32; | |
let b_max: f32 = (b_mask >> b_shift) as f32; | |
let a_max: f32 = (a_mask >> a_shift) as f32; | |
// | |
#[rustfmt::skip] | |
let mut per_row_op = |i: &mut dyn Iterator<Item = (usize, &[u8])>| { | |
while let Some((y, data_row)) = i.next() { | |
for (x, data) in cast_slice::<u8, [u8; 2]>(&data_row[..no_padding_bytes_per_line]) | |
.iter() | |
.copied() | |
.enumerate() | |
{ | |
// TODO: look at how SIMD this could be. | |
let u = u16::from_le_bytes(data) as u32; | |
let r: u8 = if r_mask != 0 { ((((u & r_mask) >> r_shift) as f32 / r_max) * 255.0) as u8 } else { 0 }; | |
let g: u8 = if g_mask != 0 { ((((u & g_mask) >> g_shift) as f32 / g_max) * 255.0) as u8 } else { 0 }; | |
let b: u8 = if b_mask != 0 { ((((u & b_mask) >> b_shift) as f32 / b_max) * 255.0) as u8 } else { 0 }; | |
let a: u8 = if a_mask != 0 { ((((u & a_mask) >> a_shift) as f32 / a_max) * 255.0) as u8 } else { 0xFF }; | |
let rgba8 = RGBA8 { r, g, b, a }; | |
final_storage[(y * width + x) as usize] = rgba8; | |
} | |
} | |
}; | |
if info_header.height() < 0 { | |
per_row_op(&mut pixel_data.chunks_exact(bytes_per_line).enumerate()) | |
} else { | |
per_row_op(&mut pixel_data.rchunks_exact(bytes_per_line).enumerate()) | |
} | |
} | |
32 => { | |
let r_shift: u32 = if r_mask != 0 { r_mask.trailing_zeros() } else { 0 }; | |
let g_shift: u32 = if g_mask != 0 { g_mask.trailing_zeros() } else { 0 }; | |
let b_shift: u32 = if b_mask != 0 { b_mask.trailing_zeros() } else { 0 }; | |
let a_shift: u32 = if a_mask != 0 { a_mask.trailing_zeros() } else { 0 }; | |
let r_max: f32 = (r_mask >> r_shift) as f32; | |
let g_max: f32 = (g_mask >> g_shift) as f32; | |
let b_max: f32 = (b_mask >> b_shift) as f32; | |
let a_max: f32 = (a_mask >> a_shift) as f32; | |
// | |
#[rustfmt::skip] | |
let mut per_row_op = |i: &mut dyn Iterator<Item = (usize, &[u8])>| { | |
while let Some((y, data_row)) = i.next() { | |
for (x, data) in cast_slice::<u8, [u8; 4]>(&data_row[..no_padding_bytes_per_line]) | |
.iter() | |
.copied() | |
.enumerate() | |
{ | |
// TODO: look at how SIMD this could be. | |
let u = u32::from_le_bytes(data); | |
let r: u8 = if r_mask != 0 { ((((u & r_mask) >> r_shift) as f32 / r_max) * 255.0) as u8 } else { 0 }; | |
let g: u8 = if g_mask != 0 { ((((u & g_mask) >> g_shift) as f32 / g_max) * 255.0) as u8 } else { 0 }; | |
let b: u8 = if b_mask != 0 { ((((u & b_mask) >> b_shift) as f32 / b_max) * 255.0) as u8 } else { 0 }; | |
let a: u8 = if a_mask != 0 { ((((u & a_mask) >> a_shift) as f32 / a_max) * 255.0) as u8 } else { 0xFF }; | |
let rgba8 = RGBA8 { r, g, b, a }; | |
final_storage[(y * width + x) as usize] = rgba8; | |
} | |
} | |
}; | |
if info_header.height() < 0 { | |
per_row_op(&mut pixel_data.chunks_exact(bytes_per_line).enumerate()) | |
} else { | |
per_row_op(&mut pixel_data.rchunks_exact(bytes_per_line).enumerate()) | |
} | |
} | |
_ => return Err(BmpError::IllegalBitDepth), | |
} | |
} | |
BmpCompression::RgbRLE8 => { | |
// For the RLE encodings, there's either "encoded" pairs of bytes, or | |
// "absolute" runs of bytes that are also always even in length (with a | |
// padding byte if necessary). Thus, no matter what, the number of bytes | |
// in the pixel data should always be even when we're processing RLE data. | |
if pixel_data.len() % 2 != 0 { | |
return Err(BmpError::PixelDataIllegalLength); | |
} | |
let mut it = cast_slice::<u8, [u8; 2]>(pixel_data).iter().copied(); | |
// Now the MSDN docs get kinda terrible. They talk about "encoded" and | |
// "absolute" mode, but whoever wrote that is bad at writing docs. What | |
// we're doing is we'll pull off two bytes at a time from the pixel data. | |
// Then we look at the first byte in a pair and see if it's zero or not. | |
// | |
// * If the first byte is **non-zero** it's the number of times that the second | |
// byte appears in the output. The second byte is an index into the palette, | |
// and you just put out that color and output it into the bitmap however many | |
// times. | |
// * If the first byte is **zero**, it signals an "escape sequence" sort of | |
// situation. The second byte will give us the details: | |
// * 0: end of line | |
// * 1: end of bitmap | |
// * 2: "Delta", the *next* two bytes after this are unsigned offsets to the | |
// right and up of where the output should move to (remember that this mode | |
// always describes the BMP with a bottom-left origin). | |
// * 3+: "Absolute", The second byte gives a count of how many bytes follow | |
// that we'll output without repetition. The absolute output sequences | |
// always have a padding byte on the ending if the sequence count is odd, so | |
// we can keep pulling `[u8;2]` at a time from our data and it all works. | |
let mut x = 0; | |
let mut y = height - 1; | |
'iter_pull_rle8: while let Some([count, pal_index]) = it.next() { | |
if count > 0 { | |
// data run of the `pal_index` value's color. | |
let rgba8 = palette.get(pal_index as usize).copied().unwrap_or_default(); | |
let count = count as usize; | |
let i = y * width + x; | |
let target_slice_mut: &mut [RGBA8] = if i + count <= final_storage.len() { | |
&mut final_storage[i..(i + count)] | |
} else { | |
// this probably means the data was encoded wrong? We'll still fill | |
// in some of the pixels at least, and if people can find a | |
// counter-example we can fix this. | |
&mut final_storage[i..] | |
}; | |
target_slice_mut.iter_mut().for_each(|c| *c = rgba8); | |
x += count; | |
} else { | |
match pal_index { | |
0 => { | |
// end of line. | |
x = 0; | |
y = y.saturating_sub(1); | |
} | |
1 => { | |
// end of bitmap | |
break 'iter_pull_rle8; | |
} | |
2 => { | |
// position delta | |
if let Some([d_right, d_up]) = it.next() { | |
x += d_right as usize; | |
y = y.saturating_sub(d_up as usize); | |
} else { | |
return Err(BmpError::PixelDataIllegalRLEContent); | |
} | |
} | |
mut raw_count => { | |
while raw_count > 0 { | |
// process two bytes at a time, which we'll call `q` and `w` for | |
// lack of better names. | |
if let Some([q, w]) = it.next() { | |
// q byte | |
let rgba8 = palette.get(q as usize).copied().unwrap_or_default(); | |
let i = y * width + x; | |
// If this goes OOB then that's the fault of the encoder and | |
// it's better to just drop some data than to panic. | |
final_storage.get_mut(i).map(|c| *c = rgba8); | |
x += 1; | |
// If `raw_count` is only 1 then we don't output the `w` byte. | |
if raw_count >= 2 { | |
// w byte | |
let rgba8 = palette.get(w as usize).copied().unwrap_or_default(); | |
let i = y * width + x; | |
final_storage.get_mut(i).map(|c| *c = rgba8); | |
x += 1; | |
} | |
} else { | |
return Err(BmpError::PixelDataIllegalRLEContent); | |
} | |
// | |
raw_count = raw_count.saturating_sub(2); | |
} | |
} | |
} | |
} | |
} | |
} | |
BmpCompression::RgbRLE4 => { | |
// RLE4 works *basically* how RLE8 does, except that every time we | |
// process a byte as a color to output then it's actually two outputs | |
// instead (upper bits then lower bits). The stuff about the escape | |
// sequences and all that is still the same sort of thing. | |
if pixel_data.len() % 2 != 0 { | |
return Err(BmpError::PixelDataIllegalLength); | |
} | |
let mut it = cast_slice::<u8, [u8; 2]>(pixel_data).iter().copied(); | |
// | |
let mut x = 0; | |
let mut y = height - 1; | |
// | |
'iter_pull_rle4: while let Some([count, pal_index]) = it.next() { | |
if count > 0 { | |
// in this case, `count` is the number of indexes to output, and | |
// `pal_index` is *two* pixel indexes (high bits then low bits). We'll | |
// write the pair of them over and over in a loop however many times. | |
if (pal_index >> 4) as usize >= palette.len() { | |
println!("pal_index high bits oob: {} of {}", (pal_index >> 4), palette.len()); | |
} | |
if (pal_index & 0b1111) as usize >= palette.len() { | |
println!( | |
"pal_index high bits oob: {} of {}", | |
(pal_index & 0b1111) as usize, | |
palette.len() | |
); | |
} | |
let rgba8_h = palette.get((pal_index >> 4) as usize).copied().unwrap_or_default(); | |
let rgba8_l = palette.get((pal_index & 0b1111) as usize).copied().unwrap_or_default(); | |
let count = count as usize; | |
debug_assert!(x < width, "x:{}, width:{}", x, width); | |
debug_assert!(y < height, "y:{}, height:{}", y, height); | |
let i = y * width + x; | |
let target_slice_mut: &mut [RGBA8] = if i + count < final_storage.len() { | |
&mut final_storage[i..(i + count)] | |
} else { | |
// this probably means the data was encoded wrong? We'll still fill | |
// in some of the pixels at least, and if people can find a | |
// counter-example we can fix this. | |
&mut final_storage[i..] | |
}; | |
let mut chunks_exact_mut = target_slice_mut.chunks_exact_mut(2); | |
while let Some(chunk) = chunks_exact_mut.next() { | |
chunk[0] = rgba8_h; | |
chunk[1] = rgba8_l; | |
} | |
chunks_exact_mut.into_remainder().iter_mut().for_each(|c| { | |
*c = rgba8_h; | |
}); | |
x += count; | |
} else { | |
// If the count is zero then we use the same escape sequence scheme as | |
// with the RLE8 format. | |
match pal_index { | |
0 => { | |
// end of line. | |
//print!("RLE4: == END OF LINE: before (x: {}, y: {})", x, y); | |
x = 0; | |
y = y.saturating_sub(1); | |
//println!(" after (x: {}, y: {})", x, y); | |
} | |
1 => { | |
// end of bitmap | |
//println!("RLE4: ==>> END OF THE BITMAP"); | |
break 'iter_pull_rle4; | |
} | |
2 => { | |
// delta | |
if let Some([d_right, d_up]) = it.next() { | |
x += d_right as usize; | |
y = y.saturating_sub(d_up as usize); | |
} else { | |
return Err(BmpError::PixelDataIllegalRLEContent); | |
} | |
} | |
mut raw_count => { | |
// in this case, we'll still have raw outputs for a sequence, but | |
// `raw_count` is the number of indexes, and each 2 bytes in the | |
// sequence is **four** output indexes. The only complication is | |
// that we need to be sure we stop *as soon* as `raw_count` hits 0 | |
// because otherwise we'll mess up our `x` position (ruining all | |
// future outputs on this scanline, and possibly ruining other | |
// scanlines or something). | |
while raw_count > 0 { | |
if let Some([q, w]) = it.next() { | |
for pal_index in [ | |
((q >> 4) as usize), | |
((q & 0b1111) as usize), | |
((w >> 4) as usize), | |
((w & 0b1111) as usize), | |
] { | |
let i = y * width + x; | |
if pal_index >= palette.len() { | |
println!("oob pal_index: {} of {}", pal_index, palette.len()); | |
} | |
let rgba8_h = palette.get(pal_index).copied().unwrap_or_default(); | |
final_storage.get_mut(i).map(|c| *c = rgba8_h); | |
x += 1; | |
raw_count = raw_count.saturating_sub(1); | |
if raw_count == 0 { | |
break; | |
} | |
} | |
} else { | |
return Err(BmpError::PixelDataIllegalRLEContent); | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
// Note(Lokathor): Uh, I guess "the entire file is inside the 'pixel_array' | |
// data" or whatever? We need example files that use this compression before | |
// we can begin to check out what's going on here. | |
BmpCompression::Jpeg => return Err(BmpError::ParserIncomplete), | |
BmpCompression::Png => return Err(BmpError::ParserIncomplete), | |
// Note(Lokathor): probably we never need to support this until someone asks? | |
BmpCompression::CmykNoCompression => return Err(BmpError::ParserIncomplete), | |
BmpCompression::CmykRLE4 => return Err(BmpError::ParserIncomplete), | |
BmpCompression::CmykRLE8 => return Err(BmpError::ParserIncomplete), | |
} | |
Ok((final_storage, width as u32, height as u32)) | |
} |
from imagine.
added in 0.3
from imagine.
Related Issues (20)
- format: XPM / XBM HOT 1
- tag things that require features in their rustdoc
- [bmp] Converting bitmasked data into RGBA8 should use SIMD
- format: XWD
- format: PICT
- [bmp] Header reading/writing functions should probably use references rather than owned arrays
- Clean up the pixel formats module.
- [png] The chrm and gama chunks should have a constant with their srgb equivalent value
- [png] Allocating "automatic" parser for PNG files HOT 1
- Decoders should return iterators
- format: JPEG XL
- png: transparency HOT 1
- netpbm: restore HOT 2
- png: background color HOT 1
- Auto-decoder size limit HOT 2
- pixel format: argb8888 HOT 1
- `Image::vertical_flip` method
- qoi format support
- make bmp parsing be simd
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from imagine.