244 lines
11 KiB
Plaintext
244 lines
11 KiB
Plaintext
// 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;
|
|
}
|
|
}
|