Skip to content

Commit

Permalink
Implement remaining rounded corners features and add optimized anti-a…
Browse files Browse the repository at this point in the history
…liasing
  • Loading branch information
lilith committed Jun 9, 2022
1 parent 990b6de commit 3542b52
Show file tree
Hide file tree
Showing 4 changed files with 371 additions and 52 deletions.
19 changes: 19 additions & 0 deletions imageflow_core/src/graphics/bitmaps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,25 @@ impl<'a,T> BitmapWindowMut<'a, T> {
}
}

impl<'a> BitmapWindowMut<'a, u8> {

pub fn fill_rectangle(&mut self, color: imageflow_helpers::colors::Color32, x: u32, y: u32, x2: u32, y2: u32) -> Result<(), FlowError>{
if y2 == y || x2 == x { return Ok(()); } // Don't fail on zero width rect
if y2 < y || x2 < x || x2 > self.w() || y2 > self.h(){
return Err(nerror!(ErrorKind::InvalidArgument, "Coordinates must be within image dimensions"));
}
if self.info().pixel_layout() != PixelLayout::BGRA {
return Err(nerror!(ErrorKind::InvalidArgument, "Only BGRA supported for rounded corners"));
}
let bgra = color.to_bgra8();
for y in y..y2 {
let mut row_window = self.row_window(y).unwrap();
let row_pixels = row_window.slice_of_pixels_first_row().unwrap();
row_pixels[x as usize..x2 as usize].fill(bgra.clone());
}
Ok(())
}
}

impl Bitmap{
pub fn get_window_u8(&mut self) -> Option<BitmapWindowMut<u8>>{
Expand Down
328 changes: 284 additions & 44 deletions imageflow_core/src/graphics/rounded_corners.rs
Original file line number Diff line number Diff line change
@@ -1,78 +1,318 @@
use imageflow_types::{Color, RoundCornersMode};
use crate::graphics::prelude::*;

fn get_radius(radius: RoundCornersMode, w: u32, h: u32) -> RoundCornersRadius{
let smallest_dimension = w.min(h) as f32;
match radius{
RoundCornersMode::Percentage(p) =>
RoundCornersRadius::All(
smallest_dimension *
p.min(100f32).max(0f32) / 200f32),
RoundCornersMode::Pixels(p) =>
RoundCornersRadius::All(p.max(0f32).min(smallest_dimension / 2f32)),
RoundCornersMode::Circle =>
RoundCornersRadius::Circle,
RoundCornersMode::PercentageCustom { top_left, top_right, bottom_right, bottom_left } =>
RoundCornersRadius::Custom([
smallest_dimension *
top_left.min(100f32).max(0f32) / 200f32,
smallest_dimension *
top_right.min(100f32).max(0f32) / 200f32,
smallest_dimension *
bottom_left.min(100f32).max(0f32) / 200f32,
smallest_dimension *
bottom_right.min(100f32).max(0f32) / 200f32
]),
RoundCornersMode::PixelsCustom { top_left, top_right, bottom_right, bottom_left } =>
RoundCornersRadius::Custom([
top_left.max(0f32).min(smallest_dimension / 2f32),
top_right.max(0f32).min(smallest_dimension / 2f32),
bottom_left.max(0f32).min(smallest_dimension / 2f32),
bottom_right.max(0f32).min(smallest_dimension / 2f32)
])
}
}
#[derive(Copy, Clone, PartialEq, Debug)]
enum RoundCornersRadius{
All(f32),
Circle,
Custom([f32;4])
}

fn plan_quadrants(radii: RoundCornersRadius, w: u32, h: u32) -> Result<[QuadrantInfo;4], FlowError>{
// Simplify Circle scenario
if radii == RoundCornersRadius::Circle{
let smallest_dimension = w.min(h) as f32;
let offset_x = ((w as i64 - (h as i64)).max(0) / 2) as u32;
let offset_y = ((h as i64 - (w as i64)).max(0) / 2) as u32;
let mut quadrants = plan_quadrants(
RoundCornersRadius::All(smallest_dimension / 2f32), w.min(h), w.min(h))
.map_err(|e| e.at(here!()))?;
for q in quadrants.iter_mut(){
q.x = q.x + offset_x;
q.y = q.y + offset_y;
q.image_width = w;
q.image_height = h;
q.center_x = q.center_x + offset_x as f32;
q.center_y = q.center_y + offset_y as f32;
}
return Ok(quadrants);
}
// Expand 'all' into corners
if let RoundCornersRadius::All(v) = radii{
return plan_quadrants(RoundCornersRadius::Custom([v,v,v,v]), w, h).map_err(|e| e.at(here!()));
}
// Ok, deal with radius pixels
if let RoundCornersRadius::Custom([top_left, top_right, bottom_left, bottom_right]) = radii{
// Integer division so we don't overlap quadrants when dimensions are odd numbers
let right_half_width = w / 2;
let bottom_half_height = h / 2;
let left_half_width = w - right_half_width;
let top_half_height = h - bottom_half_height;


fn get_radius_pixels(radius: RoundCornersMode, w: u32, h: u32) -> Result<f32, FlowError>{
match radius{
RoundCornersMode::Percentage(p) => Ok(w.min(h) as f32 * p / 200f32),
RoundCornersMode::Pixels(p) => Ok(p),
RoundCornersMode::Circle => Err(unimpl!("RoundCornersMode::Circle is not implemented")),
RoundCornersMode::PercentageCustom {.. } => Err(unimpl!("RoundCornersMode::PercentageCustom is not implemented")),
RoundCornersMode::PixelsCustom {.. } => Err(unimpl!("RoundCornersMode::PixelsCustom is not implemented"))
Ok([QuadrantInfo{
which: Quadrant::TopLeft,
x: 0,
y: 0,
width: left_half_width,
height: top_half_height,
image_width: w,
image_height: h,
radius: top_left,
center_x: top_left,
center_y: top_left,
is_top: true,
is_left: true,
},
QuadrantInfo{
which: Quadrant::TopRight,
x: left_half_width,
y: 0,
width: right_half_width,
height: top_half_height,
image_width: w,
image_height: h,
radius: top_right,
center_x: w as f32 - top_right,
center_y: top_right,
is_top: true,
is_left: false
},
QuadrantInfo{
which: Quadrant::BottomLeft,
x: 0,
y: top_half_height,
width: left_half_width,
height: bottom_half_height,
image_width: w,
image_height: h,
radius: bottom_left,
center_x: bottom_left,
center_y: h as f32 - bottom_left,
is_top: false,
is_left: true,
},
QuadrantInfo{
which: Quadrant::BottomRight,
x: left_half_width,
y: top_half_height,
width: right_half_width,
height: bottom_half_height,
image_width: w,
image_height: h,
radius: bottom_right,
center_x: w as f32 - bottom_right,
center_y: h as f32 - bottom_right,
is_top: false,
is_left: false,
}
])
}else {
Err(unimpl!("Enum not handled, must be new"))
}
}
#[derive(Copy, Clone, PartialEq, Debug)]
enum Quadrant{
TopLeft,
TopRight,
BottomRight,
BottomLeft
}
#[derive(Copy, Clone, PartialEq, Debug)]
struct QuadrantInfo{
which: Quadrant,
x: u32,
y: u32,
width: u32,
height: u32,
image_width: u32,
image_height: u32,
radius: f32,
center_x: f32,
center_y: f32,
is_top: bool,
is_left: bool,
}

impl QuadrantInfo{
fn bottom(&self) -> u32{
self.y + self.height
}
fn right(&self) -> u32{
self.x + self.width
}
}


//
// fn plan_quadrant(center_x: f32, center_y: f32,
// radius: f32,
// canvas_width: u32,
// canvas_height: u32) -> Result<Vec<QuadrantDrawOp>, FlowError>{
//
// let mut orders = Vec::with_capacity(radius.ceil() * 8);
// let r2f = radius * radius;
//
//
//
// for y in (0..=radius_ceil).rev(){
// let yf = y as f32 - 0.5;
// clear_widths.push(radius_ceil - f32::sqrt(r2f - yf * yf).round() as usize);
// }
// }

pub unsafe fn flow_bitmap_bgra_clear_around_rounded_corners(
b: &mut BitmapWindowMut<u8>,
radius_mode: RoundCornersMode,
color: imageflow_types::Color
round_corners_mode: RoundCornersMode,
color: Color
) -> Result<(), FlowError> {
if b.info().pixel_layout() != PixelLayout::BGRA {
return Err(nerror!(ErrorKind::InvalidArgument));
return Err(nerror!(ErrorKind::InvalidArgument, "Only BGRA supported for rounded corners"));
}

let radius = get_radius_pixels(radius_mode, b.w(), b.h())?;
let radius_ceil = radius.ceil() as usize;
let colorcontext = ColorContext::new(WorkingFloatspace::LinearRGB,0f32);
let matte32 = color.to_color_32().map_err(|e| FlowError::from(e).at(here!()))?;
let matte = matte32.to_bgra8();

let rf = radius as f32;
let r2f = rf * rf;
let alpha_to_float = (1.0f32) / 255.0f32;

let mut clear_widths = Vec::with_capacity(radius_ceil);
for y in (0..=radius_ceil).rev(){
let yf = y as f32 - 0.5;
clear_widths.push(radius_ceil - f32::sqrt(r2f - yf * yf).round() as usize);
}
let matte_a = matte.a as f32 * alpha_to_float;
let matte_b = colorcontext.srgb_to_floatspace(matte.b);
let matte_g = colorcontext.srgb_to_floatspace(matte.g);
let matte_r = colorcontext.srgb_to_floatspace(matte.r);

let bgcolor = color.to_color_32().unwrap().to_bgra8();
let w = b.w();
let h = b.h();

let radius_usize = radius_ceil;
let width = b.w() as usize;
let height = b.h() as usize;
//If you created a circle with the surface area of a 1x1 square, this would be its radius
//Useful for calculating pixel intensities while being correct on average regardless of angle
let volumetric_offset = 0.56419f32;

//eprintln!("color {},{},{},{:?}", bgcolor.r, bgcolor.g, bgcolor.b, bgcolor.a);
let radius_set = get_radius(round_corners_mode, b.w(), b.h());
let quadrants = plan_quadrants(radius_set,b.w(), b.h())
.map_err(|e| e.at(here!()))?;

for y in 0..height{
if y <= radius_usize || y >= height - radius_usize {
let mut row = b.row_window(y as u32).unwrap();
for quadrant in quadrants{
if quadrant.y > 0 && quadrant.which == Quadrant::TopLeft{
//Clear top rows, must be a circle
b.fill_rectangle(matte32, 0, 0, w, quadrant.y)
.map_err(|e| e.at(here!()))?;
}
if h > quadrant.bottom() && quadrant.which == Quadrant::BottomLeft{
//Clear bottom rows, must be a circle
b.fill_rectangle(matte32, 0, quadrant.bottom(), w, h)
.map_err(|e| e.at(here!()))?;
}
let radius_ceil = quadrant.radius.ceil() as usize;
let start_y = if quadrant.is_top { quadrant.y as usize } else { quadrant.bottom() as usize - radius_ceil};
let end_y = if quadrant.is_top { quadrant.y as usize + radius_ceil } else { quadrant.bottom() as usize };
let start_x = if quadrant.is_left { quadrant.x as usize} else { quadrant.right() as usize - radius_ceil};
let end_x = if quadrant.is_left { quadrant.x as usize + radius_ceil } else { quadrant.right() as usize };

let (clear_x_from, clear_x_to) = if quadrant.is_left { (0, quadrant.x) } else { (quadrant.right(), w)};

//Clear the edges for rows where the quadrant isn't rendering an arc
if clear_x_from != clear_x_to{
for y in (quadrant.y..start_y as u32).chain(end_y as u32..quadrant.bottom()){
b.fill_rectangle(matte32, clear_x_from, y, clear_x_to, y+1)
.map_err(|e| e.at(here!()))?;
}
}

// Calculate radii
// Pixels within the radius of solid are never touched
// Pixels within the radius of influence may be aliased
// Pixels outside the radius of influence are replaced with the matte
let radius_of_influence = quadrant.radius + (1f32 - volumetric_offset);
let radius_of_solid = quadrant.radius - volumetric_offset;
let radius_aliasing_width = radius_of_influence - radius_of_solid;


let radius_of_influence_squared = radius_of_influence * radius_of_influence;
let radius_of_solid_squared= radius_of_solid * radius_of_solid;

for y in start_y..end_y{
let mut row_window = b.row_window(y as u32).unwrap();
let row_pixels = row_window.slice_of_pixels_first_row().unwrap();
let yf = y as f32 + 0.5;
let y_dist_from_center = (quadrant.center_y - yf).abs();
let y_dist_squared = y_dist_from_center * y_dist_from_center;

let row_width = row.w();
let slice = row.slice_of_pixels_first_row().unwrap();
let x_dist_from_center_solid = f32::sqrt((radius_of_solid_squared - y_dist_squared).max(0f32));
let x_dist_from_center_influenced = f32::sqrt((radius_of_influence_squared - y_dist_squared).max(0f32));

let pixels_from_bottom = height - y - 1;
let edge_solid_x1 = (quadrant.center_x - x_dist_from_center_solid).ceil().max(0f32) as usize;
let edge_solid_x2 = (quadrant.center_x + x_dist_from_center_solid).floor().min(w as f32) as usize;

let nearest_line_index = y.min(pixels_from_bottom);
let edge_influence_x1 = (quadrant.center_x - x_dist_from_center_influenced).floor().max(0f32) as usize;
let edge_influence_x2 = (quadrant.center_x + x_dist_from_center_influenced).ceil().min(w as f32) as usize;

let mut clear_width = if nearest_line_index < clear_widths.len() {
clear_widths[nearest_line_index]
//Clear what we don't need to alias
if quadrant.is_left {
row_pixels[0..edge_influence_x1].fill(matte.clone());
} else {
0
row_pixels[edge_influence_x2..w as usize].fill(matte.clone());
};

//eprintln!("row width {}, slice width {}, bitmap width {}", row_width, slice.len(), width);
if slice.len() != width { panic!("Width mismatch bug"); }
let (alias_from, alias_to) = if quadrant.is_left{
(edge_influence_x1,edge_solid_x1)
}else{
(edge_solid_x2, edge_influence_x2)
};

clear_width = clear_width.min(width);
for x in alias_from..alias_to{
let xf = x as f32 + 0.5;
let diff_x = quadrant.center_x - xf;
let distance = (diff_x * diff_x + y_dist_squared).sqrt();

if clear_width > 0 {
//eprintln!("clear {}", clear_width);
slice[0..clear_width].fill(bgcolor.clone());
slice[width-clear_width..width].fill(bgcolor.clone());
}
if distance > radius_of_influence{
row_pixels[x] = matte.clone();
} else if distance > radius_of_solid{
//Intensity should be 0..1, where 1 is full matte color and 0 is full image color
let intensity = (distance - radius_of_solid) / (radius_aliasing_width);

}
}
let pixel = row_pixels[x].clone();
let pixel_a = pixel.a;
let pixel_a_f32 = pixel_a as i32 as f32 * alpha_to_float * (1f32 - intensity);

let matte_a = (1.0f32 - pixel_a_f32) * matte_a;
let final_a: f32 = matte_a + pixel_a_f32;
row_pixels[x] = rgb::alt::BGRA8 {
b: colorcontext.floatspace_to_srgb(
(colorcontext.srgb_to_floatspace(pixel.b) * pixel_a_f32 + matte_b * matte_a) / final_a),
g: colorcontext.floatspace_to_srgb(
(colorcontext.srgb_to_floatspace(pixel.g) * pixel_a_f32 + matte_g * matte_a) / final_a),
r: colorcontext.floatspace_to_srgb(
(colorcontext.srgb_to_floatspace(pixel.r) * pixel_a_f32 + matte_r * matte_a) / final_a),
a: uchar_clamp_ff(255f32 * final_a)
};

}

}
}

}
Ok(())
}
Loading

2 comments on commit 3542b52

@github-actions
Copy link

@github-actions github-actions bot commented on 3542b52 Jun 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@check-spelling-bot Report

🔴 Please review

Unrecognized words (63)
aarch
appleclang
autodeleted
bdd
Bikeshed
bools
BORLANDC
brackes
Conditionhandler
cppcoreguidelines
CPPUNWIND
CUDACC
cumn
defed
deoptimize
deoptimized
destructable
disttribution
erfinv
GCCs
generatortracker
GLIBCXX
gtest
hicpp
ibmxl
IGenerator
impls
iqr
ISingleton
libstd
LOGWRITE
lom
los
milleniumbug
monostate
NOINTERNAL
NOLINTNEXTLINE
nsamples
nttp
ORBIS
overallocating
pcg
Prefs
ptrdiff
Regehr
rotr
RTSS
RTX
skipq
SPDX
sqcb
sstr
stddev
STRINGIZE
TStorage
unregister
unscoped
URng
VARGS
Wgnu
Wnon
XLC
xorshifted
Previously acknowledged words that are now absent Bools lfirst llast prefs reimplementation rfirst rlast uft UNSUPPRESS
Available dictionaries could cover words not in the dictionary

cspell:fullstack/fullstack.txt (181) covers 11 of them
cspell:aws/aws.txt (1485) covers 10 of them
cspell:rust/rust.txt (112) covers 9 of them
cspell:npm/npm.txt (671) covers 8 of them
cspell:java/java.txt (33524) covers 8 of them
cspell:golang/go.txt (7745) covers 6 of them
cspell:scala/scala.txt (2752) covers 4 of them
cspell:lua/lua.txt (391) covers 4 of them
cspell:django/django.txt (2342) covers 4 of them
cspell:ruby/ruby.txt (354) covers 2 of them
cspell:python/python.txt (364) covers 2 of them

Consider adding them using:

      with:
        extra_dictionaries:
          cspell:fullstack/fullstack.txt
          cspell:aws/aws.txt
          cspell:rust/rust.txt
          cspell:npm/npm.txt
          cspell:java/java.txt
          cspell:golang/go.txt
          cspell:scala/scala.txt
          cspell:lua/lua.txt
          cspell:django/django.txt
          cspell:ruby/ruby.txt
          cspell:python/python.txt

To stop checking additional dictionaries, add:

      with:
        check_extra_dictionaries: ''
To accept these unrecognized words as correct (and remove the previously acknowledged and now absent words), run the following commands

... in a clone of the [email protected]:imazen/imageflow.git repository
on the main branch:

update_files() {
perl -e '
my @expect_files=qw('".github/actions/spell-check/expect/21a881426bac4ce7da7479525c41638edb10dab3.txt
.github/actions/spell-check/expect/4fd706fd879e80192d1ecef241faca522e84be19.txt
.github/actions/spell-check/expect/expect.txt"');
@ARGV=@expect_files;
my @stale=qw('"$patch_remove"');
my $re=join "|", @stale;
my $suffix=".".time();
my $previous="";
sub maybe_unlink { unlink($_[0]) if $_[0]; }
while (<>) {
if ($ARGV ne $old_argv) { maybe_unlink($previous); $previous="$ARGV$suffix"; rename($ARGV, $previous); open(ARGV_OUT, ">$ARGV"); select(ARGV_OUT); $old_argv = $ARGV; }
next if /^(?:$re)(?:(?:\r|\n)*$| .*)/; print;
}; maybe_unlink($previous);'
perl -e '
my $new_expect_file=".github/actions/spell-check/expect/3542b52d7572526b298d2853ee924d18b9a5cae2.txt";
use File::Path qw(make_path);
use File::Basename qw(dirname);
make_path (dirname($new_expect_file));
open FILE, q{<}, $new_expect_file; chomp(my @words = <FILE>); close FILE;
my @add=qw('"$patch_add"');
my %items; @items{@words} = @words x (1); @items{@add} = @add x (1);
@words = sort {lc($a)."-".$a cmp lc($b)."-".$b} keys %items;
open FILE, q{>}, $new_expect_file; for my $word (@words) { print FILE "$word\n" if $word =~ /\w/; };
close FILE;
system("git", "add", $new_expect_file);
'
}

comment_json=$(mktemp)
curl -L -s -S \
  -H "Content-Type: application/json" \
  "https://api.github.com/repos/imazen/imageflow/comments/75747127" > "$comment_json"
comment_body=$(mktemp)
jq -r ".body // empty" "$comment_json" > $comment_body
rm $comment_json

patch_remove=$(perl -ne 'next unless s{^</summary>(.*)</details>$}{$1}; print' < "$comment_body")
  

patch_add=$(perl -e '$/=undef; $_=<>; print "$1" if m{Unrecognized words[^<]*</summary>\n*```\n*([^<]*)```\n*</details>$}m;' < "$comment_body")
  
update_files
rm $comment_body
git add -u

@github-actions
Copy link

@github-actions github-actions bot commented on 3542b52 Jun 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@check-spelling-bot Report

🔴 Please review

Unrecognized words (63)
aarch
appleclang
autodeleted
bdd
Bikeshed
bools
BORLANDC
brackes
Conditionhandler
cppcoreguidelines
CPPUNWIND
CUDACC
cumn
defed
deoptimize
deoptimized
destructable
disttribution
erfinv
GCCs
generatortracker
GLIBCXX
gtest
hicpp
ibmxl
IGenerator
impls
iqr
ISingleton
libstd
LOGWRITE
lom
los
milleniumbug
monostate
NOINTERNAL
NOLINTNEXTLINE
nsamples
nttp
ORBIS
overallocating
pcg
Prefs
ptrdiff
Regehr
rotr
RTSS
RTX
skipq
SPDX
sqcb
sstr
stddev
STRINGIZE
TStorage
unregister
unscoped
URng
VARGS
Wgnu
Wnon
XLC
xorshifted
Previously acknowledged words that are now absent Bools lfirst llast prefs reimplementation rfirst rlast uft UNSUPPRESS
Available dictionaries could cover words not in the dictionary

cspell:fullstack/fullstack.txt (181) covers 11 of them
cspell:aws/aws.txt (1485) covers 10 of them
cspell:rust/rust.txt (112) covers 9 of them
cspell:npm/npm.txt (671) covers 8 of them
cspell:java/java.txt (33524) covers 8 of them
cspell:golang/go.txt (7745) covers 6 of them
cspell:scala/scala.txt (2752) covers 4 of them
cspell:lua/lua.txt (391) covers 4 of them
cspell:django/django.txt (2342) covers 4 of them
cspell:ruby/ruby.txt (354) covers 2 of them
cspell:python/python.txt (364) covers 2 of them

Consider adding them using:

      with:
        extra_dictionaries:
          cspell:fullstack/fullstack.txt
          cspell:aws/aws.txt
          cspell:rust/rust.txt
          cspell:npm/npm.txt
          cspell:java/java.txt
          cspell:golang/go.txt
          cspell:scala/scala.txt
          cspell:lua/lua.txt
          cspell:django/django.txt
          cspell:ruby/ruby.txt
          cspell:python/python.txt

To stop checking additional dictionaries, add:

      with:
        check_extra_dictionaries: ''
To accept these unrecognized words as correct (and remove the previously acknowledged and now absent words), run the following commands

... in a clone of the [email protected]:imazen/imageflow.git repository
on the refs/tags/v1.7.1-rc65 branch:

update_files() {
perl -e '
my @expect_files=qw('".github/actions/spell-check/expect/21a881426bac4ce7da7479525c41638edb10dab3.txt
.github/actions/spell-check/expect/4fd706fd879e80192d1ecef241faca522e84be19.txt
.github/actions/spell-check/expect/expect.txt"');
@ARGV=@expect_files;
my @stale=qw('"$patch_remove"');
my $re=join "|", @stale;
my $suffix=".".time();
my $previous="";
sub maybe_unlink { unlink($_[0]) if $_[0]; }
while (<>) {
if ($ARGV ne $old_argv) { maybe_unlink($previous); $previous="$ARGV$suffix"; rename($ARGV, $previous); open(ARGV_OUT, ">$ARGV"); select(ARGV_OUT); $old_argv = $ARGV; }
next if /^(?:$re)(?:(?:\r|\n)*$| .*)/; print;
}; maybe_unlink($previous);'
perl -e '
my $new_expect_file=".github/actions/spell-check/expect/3542b52d7572526b298d2853ee924d18b9a5cae2.txt";
use File::Path qw(make_path);
use File::Basename qw(dirname);
make_path (dirname($new_expect_file));
open FILE, q{<}, $new_expect_file; chomp(my @words = <FILE>); close FILE;
my @add=qw('"$patch_add"');
my %items; @items{@words} = @words x (1); @items{@add} = @add x (1);
@words = sort {lc($a)."-".$a cmp lc($b)."-".$b} keys %items;
open FILE, q{>}, $new_expect_file; for my $word (@words) { print FILE "$word\n" if $word =~ /\w/; };
close FILE;
system("git", "add", $new_expect_file);
'
}

comment_json=$(mktemp)
curl -L -s -S \
  -H "Content-Type: application/json" \
  "https://api.github.com/repos/imazen/imageflow/comments/75747149" > "$comment_json"
comment_body=$(mktemp)
jq -r ".body // empty" "$comment_json" > $comment_body
rm $comment_json

patch_remove=$(perl -ne 'next unless s{^</summary>(.*)</details>$}{$1}; print' < "$comment_body")
  

patch_add=$(perl -e '$/=undef; $_=<>; print "$1" if m{Unrecognized words[^<]*</summary>\n*```\n*([^<]*)```\n*</details>$}m;' < "$comment_body")
  
update_files
rm $comment_body
git add -u

Please sign in to comment.