// Screen capture, as Snagit does it: PrintScreen on Windows, Cmd+Shift+2 on macOS (the capture_region // global hotkey, from any app, even with mcap hidden in the tray) freezes every screen, and a rectangle // dragged over them is taken (app/screen_capture_win32.jai, app/screen_capture_macos.jai). What is taken goes onto the clipboard at once, // and into the library as an .mcap in the folder SCREEN_CAPTURE_FOLDER: the pixels as a PNG, with // the window it was taken from in metadata.json, its title as WindowName and its program as AppName, // as Snagit records them, so searches find it the same way. mcap then comes to the front showing // it in the Capture window, and OCR and labels read it once they are free. // // The PNG is made and the file written on a thread of its own, as a large capture takes a second or // so to compress. A screen recording (app/screen_record.jai) goes into the library through the same // jobs. SCREEN_CAPTURE_FOLDER :: "Screen Captures"; // in the library Screen_Capture_Job :: struct { shot: Screen_Shot; date: string; // CaptureDate; heap // A recording (app/screen_record.jai), not a still; its strings are heap. video: bool; recording: string; // the MP4 recorded, deleted once in the library encoder: string; // "h264_nvenc" source: string; // how the screen was taken: Record_Stats.source dropped: s64; // frames the encoder fell too far behind for paused_seconds: float64; // left out of it audio: string; // the sound's sources, "Speakers (...); Microphone (...)", or "" thread: Thread; busy: bool; // cleared by the thread as it ends path: string; // what it wrote; heap error: string; // or why it could not; heap } screen_capture: struct { jobs: [..] *Screen_Capture_Job; scan_pending: [..] s32; // captures OCR and labels have yet to read segment_pending: [..] s32; // and then segmentation, when the folder is set to segment } // From app_frame when the hotkey fired, outside the ImGui frame: lets a region be chosen, copies it, // and starts writing it into the library. screen_capture_take :: () { if app_exit_shutdown_started() return; date := import_calendar_text(to_calendar(current_time_consensus(), .LOCAL)); start := current_time_monotonic(); // While recording, the overlay's frozen screen would be in the recording: the screen under the // cursor is taken at once instead. shot, ok, error := platform_screen_capture_select(immediate = screen_record.recording); if !ok { if error { log_error("[capture] %.", error); toast_show(.Error, "Screen capture: %", error); } else log("[capture] Cancelled."); return; } log("[capture] % x % at %, % (%) from \"%\" (%), chosen in % s.", shot.width, shot.height, shot.left, shot.top, ifx shot.whole_screen then "the whole screen" else "a region", shot.window_title, shot.app_name, FF(to_float64_seconds(current_time_monotonic() - start), 1)); if !platform_copy_image_to_clipboard(shot.pixels, shot.width, shot.height) log_error("[capture] Could not put the capture on the clipboard."); job := New(Screen_Capture_Job,, context.default_allocator); job.shot = shot; job.date = copy_string(date,, context.default_allocator); screen_capture_job_start(job); } // Writes the job into the library on a thread of its own; screen_capture_frame takes it from there. // The job is freed when it cannot start. screen_capture_job_start :: (job: *Screen_Capture_Job) -> bool { job.busy = true; if !thread_init(*job.thread, screen_capture_save_proc, temporary_storage_size = 1024 * 1024, name = "screen capture") { what := ifx job.video then "recording" else "capture"; log_error("[capture] Could not start the thread to save the %.", what); toast_show(.Error, "The screen % could not be saved", what); screen_capture_job_free(job); return false; } thread_start(*job.thread, job); screen_capture.jobs.allocator = context.default_allocator; array_add(*screen_capture.jobs, job); return true; } // Every frame, even while hidden in the tray: the captures written, into the grid and the Capture // window, and then read. screen_capture_frame :: () { if app_exit_shutdown_started() return; screen_record_frame(); for job: screen_capture.jobs { if atomic_read(*job.busy) continue; thread_is_done(*job.thread, -1); thread_deinit(*job.thread); what := ifx job.video then "recording" else "capture"; if job.video screen_record_saved(job.path, job.error); if job.error { log_error("[capture] Could not save the %: %", what, job.error); toast_show(.Error, "The screen % could not be saved: %", what, job.error); } else { log("[capture] Saved %.", job.path); index := viewer_add_capture(job.path); if index >= 0 { viewer_request_full(index); screen_capture.scan_pending.allocator = context.default_allocator; screen_capture.segment_pending.allocator = context.default_allocator; array_add(*screen_capture.scan_pending, index); if !job.video && library_folder_segments(SCREEN_CAPTURE_FOLDER) array_add(*screen_capture.segment_pending, index); } app.restore_window_this_frame = true; if job.video toast_show(.Success, "% x % recorded", job.shot.width, job.shot.height); else toast_show(.Success, "% x % captured and copied to the clipboard", job.shot.width, job.shot.height); } screen_capture_job_free(job); remove job; } // As an import's are, once the models are loaded and free: OCR and labels, then segmentation. if screen_capture.scan_pending.count && !ocr_ui.scan.running && ocr_ui_models_ready(labels_ui.scan_labels) && ocr_ui.engine { ocr_scan_start(screen_capture.scan_pending); array_reset_keeping_memory(*screen_capture.scan_pending); } // Once their catalog entries are in: the scan takes only images, and they say which. if screen_capture.segment_pending.count && !screen_capture.scan_pending.count && !ocr_ui.thread_busy && !ocr_ui.scan.running { catalogued := true; for screen_capture.segment_pending if !viewer.captures[it].catalog && !viewer.captures[it].removed { catalogued = false; break; } if catalogued { segment_scan_start(screen_capture.segment_pending, quiet = true); array_reset_keeping_memory(*screen_capture.segment_pending); } } } // On the shutdown thread: a capture being written is finished, not lost. screen_capture_shutdown :: () { screen_record_shutdown(); // a recording running is stopped and becomes a job, finished below for screen_capture.jobs thread_is_done(*it.thread, -1); } screen_capture_job_free :: (job: *Screen_Capture_Job) { push_allocator(context.default_allocator); screen_shot_free(*job.shot); free(job.date); free(job.path); free(job.error); free(job.recording); free(job.encoder); free(job.source); free(job.audio); free(job); } screen_capture_save_proc :: (thread: *Thread) -> s64 { job := cast(*Screen_Capture_Job) thread.data; context.allocator = context.default_allocator; defer atomic_swap(*job.busy, false); path, error: string; if job.video path, error = screen_record_write(job); else path, error = screen_capture_write(job.shot, job.date); if error job.error = copy_string(error); else job.path = copy_string(path); return 0; } // Writes the capture into the library: its path, or why it could not be (temp). screen_capture_write :: (shot: Screen_Shot, date: string) -> path: string, error: string { image := Image.{pixels = shot.pixels.data, width = shot.width, height = shot.height}; png, _, _, png_ok, png_error := image_preview_png(image, max(shot.width, shot.height)); // at its own size if !png_ok return "", png_error; preview, preview_width, preview_height, preview_ok, preview_error := image_preview_png(image, MCAP_PREVIEW_MAX_SIDE); if !preview_ok return "", preview_error; hash := import_hash(png); doc := New(Capture_Document,, temp); capture_document_init(doc, temp); doc.comment = tprint("Captured by % %.", APP_NAME, VERSION); metadata := capture_add_file(doc, "metadata.json"); import_add(doc, metadata, "AppName", import_text(shot.app_name)); import_add(doc, metadata, "CaptureDate", date); import_add(doc, metadata, "WindowName", import_text(shot.window_title)); info := capture_add_file(doc, "capture.json"); import_add(doc, info, "Region", ifx shot.whole_screen then "screen" else "selection"); import_add_number(doc, info, "Left", xx shot.left); import_add_number(doc, info, "Top", xx shot.top); import_add_number(doc, info, "Width", xx shot.width); import_add_number(doc, info, "Height", xx shot.height); if shot.app_path import_add(doc, info, "AppPath", import_text(shot.app_path)); import_add(doc, info, "ContentHash", import_hash_text(hash)); import_add(doc, info, "Format", "PNG"); name := import_out_name(date, hash); text, text_ok := import_document_text(doc, name); if !text_ok return "", "its Capture Text does not read back cleanly (see the Event Log)"; m: Mcap; m.text = text; m.preview = .{width = cast(u16) preview_width, height = cast(u16) preview_height, png = preview}; m.original_name = tprint("%.png", path_strip_extension(name)); m.original = png; m.rendered_kind = .NONE; serializer, serialized, serialize_error := mcap_serialize(m,, temp); if !serialized return "", serialize_error; path, folder_error := screen_capture_out_path(name); if folder_error return "", folder_error; written, write_error := mcap_write(path, cast([] u8) string.{serializer.count, serializer.data}); if !written return "", write_error; return path, ""; } // Where a capture named name goes in SCREEN_CAPTURE_FOLDER, made if need be: name, or name_2 ... when // another file has it. Temp. screen_capture_out_path :: (name: string) -> path: string, error: string { folder := tprint("%/%", viewer_folder(), SCREEN_CAPTURE_FOLDER); if !make_directory_if_it_does_not_exist(folder, recursive = true) return "", tprint("% could not be made", folder); path := tprint("%/%", folder, name); number := 2; while file_exists(path) { path = tprint("%/%_%.%", folder, path_strip_extension(name), number, MCAP_EXTENSION); number += 1; } return path, ""; } #if OS == .LINUX { Screen_Shot :: struct { pixels: [] u8; width, height: s32; left, top: s32; whole_screen: bool; window_title, app_name, app_path: string; } screen_shot_free :: (shot: *Screen_Shot) { array_free(shot.pixels,, context.default_allocator); shot.* = .{}; } platform_screen_capture_select :: (video := false, immediate := false) -> Screen_Shot, ok: bool, error: string { return .{}, false, "not available on this system yet"; } platform_copy_image_to_clipboard :: (pixels: [] u8, width: s32, height: s32) -> bool { return false; } }