# Example program, i.e the public API # # When $this has camera slice /slice/ { # Wish $this displays camera slice $slice # } # Generic image class for sub-image manipulations namespace eval ::image { proc width {im} { dict get $im width } proc height {im} { dict get $im height } proc subimage {im x y subwidth subheight} { dict with im { set x [expr {int($x)}] set y [expr {int($y)}] set subdata [expr {[lindex $data 1] + ($y*$width + $x) * $components}] dict create \ width $subwidth \ height $subheight \ components $components \ bytesPerRow $bytesPerRow \ data [format "(uint8_t*) 0x%x" $subdata] } } set cc [c create] if {$tcl_platform(os) eq "Darwin"} { c loadlib "/opt/homebrew/lib/libjpeg.dylib" c loadlib "/opt/homebrew/lib/libpng.dylib" $cc cflags -I/opt/homebrew/include -L/opt/homebrew/lib } else { c loadlibLd libjpeg.so c loadlibLd libpng } $cc cflags -ljpeg -lpng defineImageType $cc $cc include $cc include if {[namespace exists ::Heap]} { $cc import ::Heap::cc folkHeapAlloc as folkHeapAlloc $cc import ::Heap::cc folkHeapFree as folkHeapFree } else { $cc code { #define folkHeapAlloc malloc } $cc code { #define folkHeapFree free } } $cc code { #undef EXTERN #include #include #include #include void jpeg(FILE* dest, uint8_t* data, uint32_t components, uint32_t bytesPerRow, uint32_t width, uint32_t height, int quality) { JSAMPARRAY image; if (components == 1) { image = calloc(height, sizeof (JSAMPROW)); for (size_t i = 0; i < height; i++) { image[i] = calloc(width * 3, sizeof (JSAMPLE)); for (size_t j = 0; j < width; j++) { image[i][j * 3 + 0] = data[(i*bytesPerRow + j)]; image[i][j * 3 + 1] = data[(i*bytesPerRow + j)]; image[i][j * 3 + 2] = data[(i*bytesPerRow + j)]; } } } else if (components == 3) { image = calloc(height, sizeof (JSAMPROW)); for (size_t i = 0; i < height; i++) { image[i] = calloc(width * 3, sizeof (JSAMPLE)); for (size_t j = 0; j < width; j++) { image[i][j * 3 + 0] = data[i*bytesPerRow + j*3]; image[i][j * 3 + 1] = data[i*bytesPerRow + j*3 + 1]; image[i][j * 3 + 2] = data[i*bytesPerRow + j*3 + 2]; } } } else { exit(1); } struct jpeg_compress_struct compress; struct jpeg_error_mgr error; compress.err = jpeg_std_error(&error); jpeg_create_compress(&compress); jpeg_stdio_dest(&compress, dest); compress.image_width = width; compress.image_height = height; compress.input_components = 3; compress.in_color_space = JCS_RGB; jpeg_set_defaults(&compress); jpeg_set_quality(&compress, quality, TRUE); jpeg_start_compress(&compress, TRUE); jpeg_write_scanlines(&compress, image, height); jpeg_finish_compress(&compress); jpeg_destroy_compress(&compress); for (size_t i = 0; i < height; i++) { free(image[i]); } free(image); } void png(FILE* dest, uint8_t* data, uint32_t components, uint32_t bytesPerRow, uint32_t width, uint32_t height) { png_structp png_w = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); png_infop info_w = png_create_info_struct(png_w); if (components == 3) png_set_IHDR(png_w, info_w, width, height, 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); else if (components == 1) png_set_IHDR(png_w, info_w, width, height, 8, PNG_COLOR_TYPE_GRAY, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); else exit(1); png_bytep* row_pointers = (png_bytep *)malloc(sizeof(png_bytep) * height); for (int i = 0; i < height; i++) { row_pointers[i] = data + i * bytesPerRow; } png_init_io(png_w, dest); png_set_rows(png_w, info_w, row_pointers); png_write_png(png_w, info_w, PNG_TRANSFORM_IDENTITY, NULL); free(row_pointers); } } $cc proc saveAsJpeg {image_t im char* filename} void { FILE* out = fopen(filename, "w"); jpeg(out, im.data, im.components, im.bytesPerRow, im.width, im.height, 100); fclose(out); } $cc proc saveAsPng {image_t im char* filename} void { FILE* out = fopen(filename, "wb"); png(out, im.data, im.components, im.bytesPerRow, im.width, im.height); fclose(out); } # Given the four corners of a region in an image, warp it to a new image of a given width and height $cc proc warp {image_t im uint32_t tl_x uint32_t tl_y uint32_t tr_x uint32_t tr_y uint32_t br_x uint32_t br_y uint32_t bl_x uint32_t bl_y uint32_t output_width uint32_t output_height} image_t { image_t ret; ret.width = output_width; ret.height = output_height; ret.components = im.components; ret.bytesPerRow = ret.width * ret.components; ret.data = folkHeapAlloc(ret.bytesPerRow * ret.height); for (int y = 0; y < output_height; y++) { for (int x = 0; x < output_width; x++) { // calculate the position in the input image float u = (float)x / (float)(output_width - 1); float v = (float)y / (float)(output_height - 1); int input_x = tl_x + u * (int)(tr_x - tl_x) + v * (int)(bl_x - tl_x); int input_y = tl_y + u * (int)(tr_y - tl_y) + v * (int)(bl_y - tl_y); if (input_x >= 0 && input_x < im.width && input_y >= 0 && input_y < im.height) { memcpy(&ret.data[y * ret.bytesPerRow + x * ret.components], &im.data[input_y * im.bytesPerRow + input_x * im.components], im.components); } } } return ret; } $cc proc loadJpeg {char* filename} image_t { FILE* file = fopen(filename, "rb"); if (!file) { fprintf(stderr, "Error opening file: %s\n", filename); exit(1); } struct jpeg_decompress_struct cinfo; struct jpeg_error_mgr jerr; cinfo.err = jpeg_std_error(&jerr); jpeg_create_decompress(&cinfo); jpeg_stdio_src(&cinfo, file); jpeg_read_header(&cinfo, TRUE); jpeg_start_decompress(&cinfo); image_t ret; ret.width = cinfo.output_width; ret.height = cinfo.output_height; ret.components = cinfo.output_components; ret.bytesPerRow = ret.width * ret.components; ret.data = folkHeapAlloc(ret.bytesPerRow * ret.height); JSAMPROW row_pointer[1]; while (cinfo.output_scanline < cinfo.output_height) { row_pointer[0] = (JSAMPLE*)ret.data + cinfo.output_scanline * ret.bytesPerRow; jpeg_read_scanlines(&cinfo, row_pointer, 1); } jpeg_finish_decompress(&cinfo); jpeg_destroy_decompress(&cinfo); fclose(file); return ret; } $cc proc loadPng {char* filename} image_t { FILE* file = fopen(filename, "rb"); if (!file) { fprintf(stderr, "Error opening file: %s\n", filename); exit(1); } png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if(!png) { fprintf(stderr, "Error reading png from file: %s it's not a png\n", filename); exit(1); } png_infop info = png_create_info_struct(png); if(!info) { fprintf(stderr, "Error reading png from file: %s no info?\n", filename); exit(1); } if(setjmp(png_jmpbuf(png))) { fprintf(stderr, "Error reading png from file: %s setjmp error?\n", filename); exit(1); } png_init_io(png, file); png_read_info(png, info); image_t ret; ret.width = png_get_image_width(png, info); ret.height = png_get_image_height(png, info); int bytes_per_pixel = png_get_channels(png, info); ret.components = png_get_channels(png, info); ret.bytesPerRow = ret.width * bytes_per_pixel; ret.data = folkHeapAlloc(ret.bytesPerRow * ret.height); // Iterate over the rows and read the image data into the buffer. for (int i = 0; i < ret.height; i++) { png_read_row(png, ret.data + (i * ret.bytesPerRow), NULL); } // Close the PNG file. png_destroy_read_struct(&png, &info, NULL); if (ret.components == 4) { // Transcode from RGBA to RGB (we don't support RGBA yet.) for(int i=0; i < ret.width*ret.height; i++) { int r = ret.data[i*4+0], g = ret.data[i*4+1], b = ret.data[i*4+2], a = ret.data[i*4+3]; ret.data[i*3+0] = r * a / 255; ret.data[i*3+1] = g * a / 255; ret.data[i*3+2] = b * a / 255; } ret.components = 3; ret.bytesPerRow = ret.width * ret.components; } return ret; } $cc proc freeImage {image_t im} void { folkHeapFree(im.data); } $cc proc freeJpeg {image_t im} void { // TODO: Free the JPEG. // ckfree(im.data); } $cc proc freePng {image_t im} void { // TODO: Free the PNG. } $cc compile variable imagesCache [dict create] # Loads a URL or file path if passed. If passed a valid image_t, # just returns that image_t. proc load {im} { variable imagesCache if {[dict exists $imagesCache $im]} { set im [dict get $imagesCache $im] } else { set impath $im if {[string match "http*://*" $impath]} { set im /tmp/[regsub -all {\W+} $impath "_"] exec -ignorestderr curl -o$im $impath } if {[string match "*jpg" $im] || [string match "*jpeg" $im] || [string match "*png" $im]} { set path [expr {[file pathtype $im] eq "relative" ? "$::env(HOME)/folk-images/$im" : $im}] if {[string match "*jpg" $im] || [string match "*jpeg" $im]} { set im [image loadJpeg $path] } else { set im [image loadPng $path] } dict set imagesCache $impath $im } } set im } namespace export * namespace ensemble create } # Callback: extract out a camera slice When /someone/ wishes /p/ has camera slice & \ the camera frame is /f/ & \ /p/ has region /r/ { # Convert region in projector coordinates to camera lassign [regionToBbox $r] minX minY maxX maxY lassign [projectorToCamera [list $minX $minY]] px0 py0 lassign [projectorToCamera [list $maxX $maxY]] px1 py1 # Clamp to image bounds set x [expr {int(max(min($px0, $px1), 0))}] set y [expr {int(max(min($py0, $py1), 0))}] set w [expr {int(min(abs($px1 - $px0), [image width $f]))}] set h [expr {int(min(abs($py1 - $py0), [image height $f]))}] # Extract and claim the image for the page set subimage [image subimage $f $x $y $w $h] Claim $p has camera slice $subimage } # Auto-trigger callback for `when has camera slice` statements When when /p/ has camera slice /slice/ /lambda/ with environment /e/ { Wish $p has camera slice } # Display a camera slice When /someone/ wishes /p/ displays camera slice /slice/ & /p/ has region /r/ { set center [region centroid $r] # set scale [expr {$Display::WIDTH / $Camera::WIDTH}] # Use 1x scale instead of $scale so the projected tag doesn't redetect. # TODO: Mask the tag out? Wish to draw an image with center $center image $slice radians 0 scale 1 } When /someone/ wishes /p/ displays image /im/ with scale /s/ { set im [image load $im] When $p has region /r/ { # Compute a scale for im that will fit in the region width/height # Draw im with scale and rotation set center [region centroid $r] # set width [region width $r] # set height [region height $r] # set scale [expr {min($width / [image width $im], # $height / [image height $im])}] # Wish $p is labelled $im Wish to draw an image with center $center image $im radians [region angle $r] scale $s } # On unmatch { # # HACK: Leaves time for the display to finish trying to display this. # after 5000 [list image freeJpeg $im] # } } When /someone/ wishes /p/ displays image /im/ { Wish $p displays image $im with scale 1 }