-
-
Notifications
You must be signed in to change notification settings - Fork 104
Adding Tex Inline support for the issue #386 #397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,8 @@ | |
| import java.io.IOException; | ||
| import java.util.List; | ||
| import java.util.Objects; | ||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| /** | ||
| * Implementation of a tex command which takes a string and renders an image corresponding to the | ||
|
|
@@ -34,6 +36,11 @@ | |
| public class TeXCommand extends SlashCommandAdapter { | ||
|
|
||
| private static final String LATEX_OPTION = "latex"; | ||
| // Matches regions between two dollars, like '$foo$'. | ||
| private static final String MATH_REGION = "(\\$[^$]+\\$)"; | ||
| private static final String TEXT_REGION = "([^$]+)"; | ||
| private static final Pattern INLINE_LATEX_REPLACEMENT = | ||
| Pattern.compile(MATH_REGION + "|" + TEXT_REGION); | ||
| private static final String RENDERING_ERROR = "There was an error generating the image"; | ||
| private static final float DEFAULT_IMAGE_SIZE = 40F; | ||
| private static final Color BACKGROUND_COLOR = Color.decode("#36393F"); | ||
|
|
@@ -44,8 +51,7 @@ public class TeXCommand extends SlashCommandAdapter { | |
| * Creates a new Instance. | ||
| */ | ||
| public TeXCommand() { | ||
| super("tex", | ||
| "This command accepts a latex expression and generates an image corresponding to it.", | ||
| super("tex", "Renders LaTeX, also supports inline $-regions like 'see this $frac[x}{2}$'.", | ||
| SlashCommandVisibility.GUILD); | ||
| getData().addOption(OptionType.STRING, LATEX_OPTION, | ||
| "The latex which is rendered as an image", true); | ||
|
|
@@ -56,42 +62,102 @@ public void onSlashCommand(@NotNull final SlashCommandEvent event) { | |
| String latex = Objects.requireNonNull(event.getOption(LATEX_OPTION)).getAsString(); | ||
| String userID = (Objects.requireNonNull(event.getMember()).getId()); | ||
| TeXFormula formula; | ||
|
|
||
| try { | ||
| if (latex.contains("$")) { | ||
| latex = convertInlineLatexToFull(latex); | ||
| } | ||
| formula = new TeXFormula(latex); | ||
| } catch (ParseException e) { | ||
| event.reply("That is an invalid latex: " + e.getMessage()).setEphemeral(true).queue(); | ||
| return; | ||
| } | ||
|
|
||
| event.deferReply().queue(); | ||
| Image image = formula.createBufferedImage(TeXConstants.STYLE_DISPLAY, DEFAULT_IMAGE_SIZE, | ||
| FOREGROUND_COLOR, BACKGROUND_COLOR); | ||
| if (image.getWidth(null) == -1 || image.getHeight(null) == -1) { | ||
| event.getHook().setEphemeral(true).editOriginal(RENDERING_ERROR).queue(); | ||
| logger.warn( | ||
| "Unable to render latex, image does not have an accessible width or height. Formula was {}", | ||
| latex); | ||
| return; | ||
| } | ||
| BufferedImage renderedTextImage = new BufferedImage(image.getWidth(null), | ||
| image.getHeight(null), BufferedImage.TYPE_4BYTE_ABGR); | ||
| renderedTextImage.getGraphics().drawImage(image, 0, 0, null); | ||
| ByteArrayOutputStream renderedTextImageStream = new ByteArrayOutputStream(); | ||
|
|
||
| try { | ||
| ImageIO.write(renderedTextImage, "png", renderedTextImageStream); | ||
| Image image = renderImage(formula); | ||
| sendImage(event, userID, image); | ||
| } catch (IOException e) { | ||
| event.getHook().setEphemeral(true).editOriginal(RENDERING_ERROR).queue(); | ||
| event.getHook().editOriginal(RENDERING_ERROR).queue(); | ||
| logger.warn( | ||
| "Unable to render latex, could not convert the image into an attachable form. Formula was {}", | ||
| latex, e); | ||
| return; | ||
|
|
||
| } catch (IllegalStateException e) { | ||
| event.getHook().editOriginal(RENDERING_ERROR).queue(); | ||
|
|
||
| logger.warn( | ||
| "Unable to render latex, image does not have an accessible width or height. Formula was {}", | ||
| latex, e); | ||
| } | ||
| } | ||
|
|
||
| private @NotNull Image renderImage(@NotNull TeXFormula formula) { | ||
| Image image = formula.createBufferedImage(TeXConstants.STYLE_DISPLAY, DEFAULT_IMAGE_SIZE, | ||
| FOREGROUND_COLOR, BACKGROUND_COLOR); | ||
|
|
||
| if (image.getWidth(null) == -1 || image.getHeight(null) == -1) { | ||
| throw new IllegalStateException("Image has no height or width"); | ||
| } | ||
| return image; | ||
| } | ||
|
|
||
| private void sendImage(@NotNull SlashCommandEvent event, @NotNull String userID, | ||
| @NotNull Image image) throws IOException { | ||
|
|
||
| ByteArrayOutputStream renderedTextImageStream = getRenderedTextImageStream(image); | ||
| event.getHook() | ||
| .editOriginal(renderedTextImageStream.toByteArray(), "tex.png") | ||
| .setActionRow(Button.of(ButtonStyle.DANGER, generateComponentId(userID), "Delete")) | ||
| .queue(); | ||
| } | ||
|
|
||
| @NotNull | ||
| private ByteArrayOutputStream getRenderedTextImageStream(@NotNull Image image) | ||
| throws IOException { | ||
|
|
||
| BufferedImage renderedTextImage = new BufferedImage(image.getWidth(null), | ||
| image.getHeight(null), BufferedImage.TYPE_4BYTE_ABGR); | ||
|
|
||
| renderedTextImage.getGraphics().drawImage(image, 0, 0, null); | ||
| ByteArrayOutputStream renderedTextImageStream = new ByteArrayOutputStream(); | ||
|
|
||
| ImageIO.write(renderedTextImage, "png", renderedTextImageStream); | ||
|
|
||
| return renderedTextImageStream; | ||
| } | ||
|
|
||
| /** | ||
| * Converts inline latex like: {@code hello $\frac{x}{2}$ world} to full latex | ||
| * {@code \text{hello}\frac{x}{2}\text{ world}}. | ||
| */ | ||
| @NotNull | ||
| private String convertInlineLatexToFull(@NotNull String latex) { | ||
| if (isInvalidInlineFormat(latex)) { | ||
| throw new ParseException( | ||
| "The amount of $-symbols must be divisible by two. Did you forget to close an expression? "); | ||
| } | ||
|
|
||
| Matcher matcher = INLINE_LATEX_REPLACEMENT.matcher(latex); | ||
| StringBuilder sb = new StringBuilder(); | ||
|
|
||
| while (matcher.find()) { | ||
| boolean isInsideMathRegion = matcher.group(1) != null; | ||
| if (isInsideMathRegion) { | ||
| sb.append(matcher.group(1).replace("$", "")); | ||
Zabuzard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } else { | ||
| sb.append("\\text{").append(matcher.group(2)).append("}"); | ||
| } | ||
| } | ||
|
|
||
| return sb.toString(); | ||
| } | ||
|
|
||
| private boolean isInvalidInlineFormat(String latex) { | ||
| return latex.chars().filter(ch -> ch == '$').count() % 2 == 1; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We discussed this and agreed that we can ignore it for now. So unless you have a cheap and easy suggestion to adjust the code to support it, we can also go without it for now.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry about that. But, wouldn't
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
No worries, not your fault. It was probably in Discord and not here.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Good idea. Does Javas regex engine support that? @IslamSakrak could you try that out?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, totally correct. We still have a separate GH issue open for adding tests to
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually think that, as soon as this is merged, someone should tackle the unit tests for
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're correct.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wouldn't mind testing this also, I have written some tests already. Also, first time writing tests so might help me get better at it
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you want, feel free to either create a second PR or also do it directly in this, thanks. (Assign yourself to the GH issue then though) |
||
| } | ||
|
|
||
| @Override | ||
| public void onButtonClick(@NotNull final ButtonClickEvent event, | ||
| @NotNull final List<String> args) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.