From 158e2fcf91deb8eea090653a68a7017a84e7e518 Mon Sep 17 00:00:00 2001 From: Thomas Harrison Date: Sat, 6 Sep 2025 20:30:14 -0400 Subject: [PATCH 1/9] Update args --- src/codepic/cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/codepic/cli.py b/src/codepic/cli.py index 86798cf..86ae82f 100644 --- a/src/codepic/cli.py +++ b/src/codepic/cli.py @@ -28,12 +28,12 @@ @click.option('-a', '--aa_factor', type=float, default=1, help='Antialias factor') @click.option('-s', '--style', type=str, default='one-dark') @click.option('-l', '--lang', type=str) -@click.option('-c', '--clipboard', is_flag=True, help='Copy image to clipboard') +@click.option('-c', '--clipboard', is_flag=True, help='Output image to clipboard') @click.option( '-f', '--image_format', type=click.Choice(['png', 'jpeg', 'bmp', 'gif']), - help='Antialias factor', + help='Image format', ) @click.option( '-o', @@ -48,7 +48,7 @@ ) @click.argument( 'source_file', - # help='Input path of source code or - to read from stdin', + help='Input path of source code or - to read from stdin', type=click.Path( exists=False, dir_okay=False, @@ -62,7 +62,7 @@ def cli( height: str | None, line_numbers: bool, pad: int, - font_name: str | None, + font_name: str, font_size: int, aa_factor: float, image_format: str | None, From 9e7aec0fb639ef982276f5e6481cb6b515124d10 Mon Sep 17 00:00:00 2001 From: Thomas Harrison Date: Sat, 6 Sep 2025 20:32:08 -0400 Subject: [PATCH 2/9] Cleanup image format detection --- src/codepic/cli.py | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/codepic/cli.py b/src/codepic/cli.py index 86ae82f..f14e2c2 100644 --- a/src/codepic/cli.py +++ b/src/codepic/cli.py @@ -18,6 +18,25 @@ from codepic.render import render_code +def detect_format(output, default='png'): + if not output: + return default + + ext = os.path.splitext(output)[1] + + if not ext: + ext = 'png' + + ext = ext.lower() + if ext in ['png', 'jpeg', 'jpg', 'bmp', 'gif']: + if ext == 'jpg': + return 'jpeg' + + return ext + + return default + + @click.command() @click.option('-w', '--width', type=str, help='Fixed width in pixels or percent') @click.option('-h', '--height', type=str, help='Fixed hight in pixels or percent') @@ -72,20 +91,14 @@ def cli( ): code = '' - if font_name is None: - font_name = '' - + # Guess output image format from output file extension, otherwise png if not image_format: - image_format = 'png' - if output: - ext = os.path.splitext(source_file)[1] - if ext: - ext = ext.lower() - if ext in ['png', 'jpeg', 'jpg', 'bmp', 'gif']: - image_format = ext - if image_format == 'jpg': - image_format = 'jpeg' + image_format = detect_format(output) + + # Probably not needed since click forces lower and detect converts to lower + image_format = image_format.lower() + # Only png format can be stored in the clipboard if clipboard and image_format != 'png': exit('Image format must be png to use clipboard') From 224a6e689e0216af1cf8e09f29ebab0f499107c1 Mon Sep 17 00:00:00 2001 From: Thomas Harrison Date: Sat, 6 Sep 2025 20:33:55 -0400 Subject: [PATCH 3/9] Better comments --- src/codepic/cli.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/codepic/cli.py b/src/codepic/cli.py index f14e2c2..40c12ea 100644 --- a/src/codepic/cli.py +++ b/src/codepic/cli.py @@ -18,7 +18,7 @@ from codepic.render import render_code -def detect_format(output, default='png'): +def format_from_extension(output, default='png'): if not output: return default @@ -42,8 +42,8 @@ def detect_format(output, default='png'): @click.option('-h', '--height', type=str, help='Fixed hight in pixels or percent') @click.option('--line_numbers', is_flag=True, help='Show line numbers') @click.option('-p', '--pad', type=int, default=30, help='Padding in pixels') -@click.option('-f', '--font_name', type=str, help='Font size in pt') -@click.option('-s', '--font_size', type=int, default=14, help='Font size in pt') +@click.option('--font_name', type=str, help='Font size in pt', default='') +@click.option('--font_size', type=int, default=14, help='Font size in pt') @click.option('-a', '--aa_factor', type=float, default=1, help='Antialias factor') @click.option('-s', '--style', type=str, default='one-dark') @click.option('-l', '--lang', type=str) @@ -91,9 +91,9 @@ def cli( ): code = '' - # Guess output image format from output file extension, otherwise png + # Use output file extension to detect image format, otherwise png if not image_format: - image_format = detect_format(output) + image_format = format_from_extension(output) # Probably not needed since click forces lower and detect converts to lower image_format = image_format.lower() From 33c32a688e767629f8f7fe28425dd568c6fd59dd Mon Sep 17 00:00:00 2001 From: Thomas Harrison Date: Sat, 6 Sep 2025 20:35:01 -0400 Subject: [PATCH 4/9] Better error --- src/codepic/cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/codepic/cli.py b/src/codepic/cli.py index 40c12ea..99e39b4 100644 --- a/src/codepic/cli.py +++ b/src/codepic/cli.py @@ -100,11 +100,11 @@ def cli( # Only png format can be stored in the clipboard if clipboard and image_format != 'png': - exit('Image format must be png to use clipboard') + raise click.ClickException('Image format must be png to use -c') - write_to_stdout = False - if output == '-': - write_to_stdout = True + # Must have somewhere to output, clipboard or file / stdout + if not output and not clipboard: + raise click.ClickException('No output location was specified, use -o or -c') elif not output: if not clipboard: From f70465584f9121343bfe9d28c29fe4e8a47fd4bd Mon Sep 17 00:00:00 2001 From: Thomas Harrison Date: Sat, 6 Sep 2025 20:35:33 -0400 Subject: [PATCH 5/9] Read in code first --- src/codepic/cli.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/codepic/cli.py b/src/codepic/cli.py index 99e39b4..81ad73c 100644 --- a/src/codepic/cli.py +++ b/src/codepic/cli.py @@ -106,12 +106,19 @@ def cli( if not output and not clipboard: raise click.ClickException('No output location was specified, use -o or -c') - elif not output: - if not clipboard: - if source_file == '-': - write_to_stdout = True - else: - output = os.path.splitext(source_file)[0] + '.' + image_format.lower() + # Write image to stdout, can be used with clipboard output + write_to_stdout = output == '-' + + # Read code from stdin instead of file + read_from_stdin = source_file == '-' + + # Get code before choosing lexer + if read_from_stdin: + code = sys.stdin.read() + + else: + with open(source_file, 'r') as f: + code = f.read() formatter = ImageFormatter( font_name=font_name, From 49d341fe6a240660cd588f4685c5a3ed3e89b2fe Mon Sep 17 00:00:00 2001 From: Thomas Harrison Date: Sat, 6 Sep 2025 20:41:19 -0400 Subject: [PATCH 6/9] Even even better image format detection with print detection --- src/codepic/cli.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/codepic/cli.py b/src/codepic/cli.py index 81ad73c..8f4e60f 100644 --- a/src/codepic/cli.py +++ b/src/codepic/cli.py @@ -19,21 +19,19 @@ def format_from_extension(output, default='png'): - if not output: - return default + if output: + ext = os.path.splitext(output)[1] - ext = os.path.splitext(output)[1] + if ext: + ext = ext.lower() + if ext == 'jpg': + ext = 'jpeg' - if not ext: - ext = 'png' - - ext = ext.lower() - if ext in ['png', 'jpeg', 'jpg', 'bmp', 'gif']: - if ext == 'jpg': - return 'jpeg' - - return ext + if ext in ['png', 'jpeg', 'bmp', 'gif']: + print('Got output image format', ext, 'from output file extension', file=sys.stderr) + return ext + print('No format provided, defaulting to png', file=sys.stderr) return default From 624d47630820290d03f20efcd410afe03a738062 Mon Sep 17 00:00:00 2001 From: Thomas Harrison Date: Sat, 6 Sep 2025 23:19:49 -0400 Subject: [PATCH 7/9] wow that's a lot better --- src/codepic/cli.py | 136 ++++++++++++++-------------------- src/codepic/render.py | 165 +++++++++++++++++++++++++++++++++--------- tests/test_first.py | 10 --- tests/test_render.py | 157 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 344 insertions(+), 124 deletions(-) delete mode 100644 tests/test_first.py create mode 100644 tests/test_render.py diff --git a/src/codepic/cli.py b/src/codepic/cli.py index 8f4e60f..9691ed6 100644 --- a/src/codepic/cli.py +++ b/src/codepic/cli.py @@ -15,7 +15,12 @@ ) from pygments.util import ClassNotFound -from codepic.render import render_code +from codepic.render import render_code, resize_image + + +# Print message to stderr because the image can be written through stdout +def log(msg): + click.echo(msg, err=True) def format_from_extension(output, default='png'): @@ -28,13 +33,49 @@ def format_from_extension(output, default='png'): ext = 'jpeg' if ext in ['png', 'jpeg', 'bmp', 'gif']: - print('Got output image format', ext, 'from output file extension', file=sys.stderr) + log(f'Got output image format {ext} from output file extension') return ext - print('No format provided, defaulting to png', file=sys.stderr) + log('No format provided, defaulting to png') return default +def read_code(source_file): + # Read code from stdin + if source_file == '-': + log('Reading code from stdion') + return sys.stdin.read() + + # TODO maybe remove these as they might be too verbose + log(f'Reading code from file {source_file}') + + # Read code from file + with open(source_file, 'r') as f: + return f.read() + + +def get_lexer(lang, source_file, code): + # Lexer from language name + if lang: + return get_lexer_by_name(lang) + + # Use source file extension + if source_file != '-': + try: + return get_lexer_for_filename(code) + + except ClassNotFound: + log('Could not detect language from source file extension') + pass + + try: + return guess_lexer(code) + + except ClassNotFound: + log('Could not detect language by analyzing code, defaulting to plain text') + return TextLexer() + + @click.command() @click.option('-w', '--width', type=str, help='Fixed width in pixels or percent') @click.option('-h', '--height', type=str, help='Fixed hight in pixels or percent') @@ -65,7 +106,7 @@ def format_from_extension(output, default='png'): ) @click.argument( 'source_file', - help='Input path of source code or - to read from stdin', + # help='Input path of source code or - to read from stdin', type=click.Path( exists=False, dir_okay=False, @@ -87,14 +128,12 @@ def cli( lang: str | None, clipboard: bool, ): - code = '' - # Use output file extension to detect image format, otherwise png if not image_format: image_format = format_from_extension(output) - # Probably not needed since click forces lower and detect converts to lower - image_format = image_format.lower() + else: + log(f'Using image format {image_format}') # Only png format can be stored in the clipboard if clipboard and image_format != 'png': @@ -104,20 +143,13 @@ def cli( if not output and not clipboard: raise click.ClickException('No output location was specified, use -o or -c') - # Write image to stdout, can be used with clipboard output - write_to_stdout = output == '-' - - # Read code from stdin instead of file - read_from_stdin = source_file == '-' - # Get code before choosing lexer - if read_from_stdin: - code = sys.stdin.read() + code = read_code(source_file) - else: - with open(source_file, 'r') as f: - code = f.read() + # Get lexer from lang name or source file extension, defaults to plaintext + lexer = get_lexer(lang, source_file, code) + # Setup image formatting formatter = ImageFormatter( font_name=font_name, font_size=font_size * aa_factor, @@ -127,66 +159,10 @@ def cli( image_format=image_format, ) - lexer = None - - if lang: - lexer = get_lexer_by_name(lang) - - if source_file == '-': - code = sys.stdin.read() - - if not lexer: - try: - lexer = guess_lexer(code) - - except ClassNotFound: - lexer = TextLexer() - - img = render_code(code, lexer, formatter, aa_factor) - - else: - with open(source_file, 'r') as f: - code = f.read() - - if not lexer: - try: - lexer = get_lexer_for_filename(code) - - except ClassNotFound: - try: - lexer = guess_lexer(code) - - except ClassNotFound: - lexer = TextLexer() - - img = render_code(code, lexer, formatter, aa_factor) - - aspect = img.height / img.width - - if height: - if height.endswith('%'): - perc = int(height[:-1]) / 100 - height = int(img.height * perc) - - else: - height = int(height) - - if width: - if width.endswith('%'): - perc = int(width[:-1]) / 100 - width = int(img.width * perc) - - else: - width = int(width) - - if not width and height: - width = int(height / aspect) - - if not height and width: - height = int(width * aspect) + # Render the code + img = render_code(code, lexer, formatter, width, height, aa_factor) - if width and height: - img = img.resize((width, height), resample=Image.Resampling.LANCZOS) + # GARBAGE AFTER HERE buff = io.BytesIO() img.save(buff, format='PNG') @@ -199,8 +175,8 @@ def cli( run(f'xclip -selection clipboard -target image/png < {fp.name}', shell=True) fp.flush() - if write_to_stdout: - sys.stdout.buffer.write(buff) + # if write_to_stdout: + # sys.stdout.buffer.write(buff) elif output and output != '-': with open(output, 'wb') as f: diff --git a/src/codepic/render.py b/src/codepic/render.py index a21b593..c88c954 100644 --- a/src/codepic/render.py +++ b/src/codepic/render.py @@ -1,32 +1,52 @@ import io -import PIL.Image -import PIL.ImageDraw -import PIL.ImageFilter +from PIL import Image, ImageDraw, ImageFilter from pygments import highlight -def add_corners(im, rad): - circle = PIL.Image.new('L', (rad * 2, rad * 2), 0) - draw = PIL.ImageDraw.Draw(circle) - draw.ellipse((0, 0, rad * 2 - 1, rad * 2 - 1), fill=255) - alpha = PIL.Image.new('L', im.size, 255) - w, h = im.size - alpha.paste(circle.crop((0, 0, rad, rad)), (0, 0)) - alpha.paste(circle.crop((0, rad, rad, rad * 2)), (0, h - rad)) - alpha.paste(circle.crop((rad, 0, rad * 2, rad)), (w - rad, 0)) - alpha.paste(circle.crop((rad, rad, rad * 2, rad * 2)), (w - rad, h - rad)) - im.putalpha(alpha) - return im +def add_corners(img: Image.Image, radius: int): + """ + Add rounded corners to an image by generating an alpha mask. + Args: + img (Image): image to modify + radius (int): corner radius in pixels -def makeShadow( - image: PIL.Image.Image, + Returns: + Image: `img` with rounded corners + + """ + width, height = img.size + + # Make circle for corner radius + circle = Image.new('L', (radius * 2, radius * 2), 0) + draw = ImageDraw.Draw(circle) + draw.ellipse((0, 0, radius * 2 - 1, radius * 2 - 1), fill=255) + + # Create alpha mask + alpha = Image.new('L', img.size, 255) + alpha.paste(circle.crop((0, 0, radius, radius)), (0, 0)) + alpha.paste(circle.crop((0, radius, radius, radius * 2)), (0, height - radius)) + alpha.paste(circle.crop((radius, 0, radius * 2, radius)), (width - radius, 0)) + alpha.paste( + circle.crop((radius, radius, radius * 2, radius * 2)), + (width - radius, height - radius), + ) + + # Apply alpha mask + img.putalpha(alpha) + + return img + + +# TODO make this nice +def make_shadow( + image: Image.Image, radius: float, border: int, - offset: tuple[float, float] | list[float], - backgroundColour: tuple[float, float, float, float] | list[float], - shadowColour: tuple[float, float, float, float] | list[float], + offset: tuple[int, int] | list[int], + background_color: tuple[float, ...] | list[float], + shadow_color: tuple[float, ...] | list[float], ): # image: base image to give a drop shadow # radius: gaussian blur radius @@ -36,15 +56,21 @@ def makeShadow( # shadowColour: colour of the drop shadow assert len(offset) == 2 - assert len(backgroundColour) == 4 - assert len(shadowColour) == 4 + assert len(background_color) == 4 + assert len(shadow_color) == 4 + + if isinstance(background_color, list): + background_color = tuple(background_color) + + if isinstance(shadow_color, list): + shadow_color = tuple(shadow_color) # Calculate the size of the shadow's image fullWidth = image.size[0] + abs(offset[0]) + 2 * border fullHeight = image.size[1] + abs(offset[1]) + 2 * border # Create the shadow's image. Match the parent image's mode. - shadow = PIL.Image.new(image.mode, (fullWidth, fullHeight), backgroundColour) + shadow = Image.new(image.mode, (fullWidth, fullHeight), background_color) alpha = image.split()[-1] @@ -53,12 +79,12 @@ def makeShadow( shadowTop = border + max(offset[1], 0) # if <0, push the rest of the image down # Paste in the constant colour shadow.paste( - shadowColour, - [shadowLeft, shadowTop, shadowLeft + image.size[0], shadowTop + image.size[1]], + shadow_color, + (shadowLeft, shadowTop, shadowLeft + image.size[0], shadowTop + image.size[1]), ) # Apply the BLUR filter repeatedly - shadow = shadow.filter(PIL.ImageFilter.GaussianBlur(radius)) + shadow = shadow.filter(ImageFilter.GaussianBlur(radius)) # Paste the original image on top of the shadow imgLeft = border - min(offset[0], 0) # if the shadow offset was <0, push right @@ -68,16 +94,84 @@ def makeShadow( return shadow -def render_code(code: str, lexer, formatter, aa_factor: float = 2): +def resize_image( + img: Image.Image, + width: int | str | None = None, + height: int | str | None = None, + resample: Image.Resampling = Image.Resampling.LANCZOS, +): + """ + Resize an image given width and/or height. + + If only one of `width` or `height` are given, the other will be calculated using + the aspect ration of the starting size. + + If `width` or `height` are a string ending with a percent `%` sign, they will + be interpreted as percentage of the starting size. + + Args: + img (Image): image to resize + width (int | str, optional): resized width in pixels or percentage + height (int | str, optional): resized height in pixels or percentage + resample (Resampling, optional): resampling algorithm to use + + Returns: + Image: resized image + + """ + assert width or height, 'Must provide at least one of width or height' + + aspect = img.height / img.width + + # Convert height to int if provided + if height is not None: + # Calculate percentage + if isinstance(height, str) and height.endswith('%'): + perc = int(height[:-1]) / 100 + height = int(img.height * perc) + height = int(height) + + # Convert width to int if provided + if width is not None: + # Calculate percentage + if isinstance(width, str) and width.endswith('%'): + perc = int(width[:-1]) / 100 + width = int(img.width * perc) + width = int(width) + + # If only height was given, calculate width from the aspect ratio + if width is None: + # Assert to make type checker happy + assert height is not None + width = int(height / aspect) + + # If only width was given, calculate height from the aspect ratio + elif height is None: + height = int(width * aspect) + + # Resize the image, convert width and height to int if they are still strings + img = img.resize((int(width), int(height)), resample=resample) + + return img + + +def render_code( + code: str, + lexer, + formatter, + width: int | str | None = None, + height: int | str | None = None, + aa_factor: float = 2, +): # Create Image i = highlight(code, lexer, formatter) - img = PIL.Image.open(io.BytesIO(i)) + img = Image.open(io.BytesIO(i)) # Rounded Corners img = add_corners(img, int(5 * aa_factor)) # Add drop shadow - img = makeShadow( + img = make_shadow( img, int(10 * aa_factor), int(20 * aa_factor), @@ -86,10 +180,13 @@ def render_code(code: str, lexer, formatter, aa_factor: float = 2): (0, 0, 0, 255), ) - # Anti-aliasing - img = img.resize( - (int(img.width / aa_factor), int(img.height / aa_factor)), - resample=PIL.Image.Resampling.LANCZOS, - ) + # If no width or height were given, just return the image at default size after antialiasing + if width is None: + width = int(img.width / aa_factor) + + if height is None: + height = int(img.height / aa_factor) + + img = resize_image(img, width, height) return img diff --git a/tests/test_first.py b/tests/test_first.py deleted file mode 100644 index f23006f..0000000 --- a/tests/test_first.py +++ /dev/null @@ -1,10 +0,0 @@ -from unittest.mock import patch - -from codepic import cli - - -@patch('codepic.cli.print') -def test_first(mock_print): - cli() - - mock_print.assert_called_once_with('Hello CLI') diff --git a/tests/test_render.py b/tests/test_render.py new file mode 100644 index 0000000..ae09860 --- /dev/null +++ b/tests/test_render.py @@ -0,0 +1,157 @@ +from unittest.mock import Mock, call, patch + +import pytest + +from codepic import render + + +@patch('codepic.render.ImageDraw') +@patch('codepic.render.Image') +def test_add_corners(mock_image, mock_draw): + circle = Mock() + circle.crop.side_effect = ['a', 'b', 'c', 'd'] + alpha = Mock() + mock_image.new.side_effect = [circle, alpha] + + img = Mock() + img.size = (2, 3) + + res = render.add_corners(img, 2) + + assert res == img + img.putalpha.assert_called_once_with(alpha) + + assert mock_image.new.call_count == 2 + mock_image.new.assert_any_call('L', (4, 4), 0) + mock_image.new.assert_any_call('L', (2, 3), 255) + + mock_draw.Draw.assert_called_once_with(circle) + mock_draw.Draw().ellipse.assert_called_once_with((0, 0, 3, 3), fill=255) + + assert circle.crop.call_count == 4 + circle.crop.assert_has_calls( + [ + call((0, 0, 2, 2)), + call((0, 2, 2, 4)), + call((2, 0, 4, 2)), + call((2, 2, 4, 4)), + ] + ) + + assert alpha.paste.call_count == 4 + alpha.paste.assert_has_calls( + [ + call('a', (0, 0)), + call('b', (0, 1)), + call('c', (0, 0)), + call('d', (0, 1)), + ] + ) + + +def test_make_shadow(): + # TODO test this after cleanup + pass + + +def test_resize_image(): + img = Mock() + img.width = 4 + img.height = 8 + + img.resize.return_value = 'resized' + + resample = Mock() + + res = render.resize_image(img, 2, 4, resample=resample) + + assert res == 'resized' + + img.resize.assert_called_once_with((2, 4), resample=resample) + + +def test_resize_image_no_height(): + img = Mock() + img.width = 4 + img.height = 8 + + img.resize.return_value = 'resized' + + resample = Mock() + + res = render.resize_image(img, 2, None, resample=resample) + + assert res == 'resized' + + img.resize.assert_called_once_with((2, 4), resample=resample) + + +def test_resize_image_no_width(): + img = Mock() + img.width = 4 + img.height = 8 + + img.resize.return_value = 'resized' + + resample = Mock() + + res = render.resize_image(img, None, 4, resample=resample) + + assert res == 'resized' + + img.resize.assert_called_once_with((2, 4), resample=resample) + + +def test_resize_image_missing_both(): + img = Mock() + + with pytest.raises( + AssertionError, match='Must provide at least one of width or height' + ): + render.resize_image(img, None, None) + + +def test_resize_image_percentages(): + img = Mock() + img.width = 4 + img.height = 8 + + img.resize.return_value = 'resized' + + resample = Mock() + + res = render.resize_image(img, '50%', '100%', resample=resample) + + assert res == 'resized' + + img.resize.assert_called_once_with((2, 8), resample=resample) + + +@patch('codepic.render.io') +@patch('codepic.render.resize_image') +@patch('codepic.render.make_shadow') +@patch('codepic.render.add_corners') +@patch('codepic.render.Image') +@patch('codepic.render.highlight') +def test_render_code( + mock_highlight, mock_image, mock_corner, mock_shadow, mock_resize, mock_io +): + mock_highlight.return_value = 'highlighted' + mock_io.BytesIO.return_value = 'bytes' + mock_image.open.return_value = 'open' + mock_corner.return_value = 'corners' + mock_shadow.return_value = 'shadow' + mock_resize.return_value = 'resized' + + res = render.render_code('code', 'lexer', 'formatter', 'width', 'height', 3) + + assert res == 'resized' + + mock_highlight.assert_called_once_with('code', 'lexer', 'formatter') + mock_io.BytesIO.assert_called_once_with('highlighted') + mock_image.open.assert_called_once_with('bytes') + mock_corner.assert_called_once_with('open', 15) + mock_shadow.assert_called_once_with( + 'corners', 30, 60, (3, 6), (0, 0, 0, 0), (0, 0, 0, 255) + ) + mock_resize.assert_called_once_with('shadow', 'width', 'height') From 61cbda7028c62c2e94b216f260c4623c38893da7 Mon Sep 17 00:00:00 2001 From: Thomas Harrison Date: Sat, 6 Sep 2025 23:30:36 -0400 Subject: [PATCH 8/9] Update readme --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c615005..42d7587 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Generate an image of code using pygments syntax highlighting. For example: -![](docs/test.png) +![example](docs/test.png) ## Usage @@ -19,14 +19,14 @@ Options: -h, --height TEXT Fixed hight in pixels or percent --line_numbers Show line numbers -p, --pad INTEGER Padding in pixels - -f, --font_name TEXT Font size in pt - -s, --font_size INTEGER Font size in pt + --font_name TEXT Font size in pt + --font_size INTEGER Font size in pt -a, --aa_factor FLOAT Antialias factor -s, --style TEXT -l, --lang TEXT - -c, --clipboard Copy image to clipboard + -c, --clipboard Output image to clipboard -f, --image_format [png|jpeg|bmp|gif] - Antialias factor + Image format -o, --output FILE Output path for image --help Show this message and exit. ``` From 192da45eecd168af12833c603c92351f688f4f87 Mon Sep 17 00:00:00 2001 From: Thomas Harrison Date: Sat, 6 Sep 2025 23:31:25 -0400 Subject: [PATCH 9/9] Only build for PR --- .github/workflows/ci.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f86c44..36c2f68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,14 @@ name: ci on: push: - branch: + branches: + - main + + pull_request: + types: + - opened + - reopened + - synchronize jobs: lint_test: