I managed to add a blockquote with a css class button to the draftail (code below) and it works ok. The only issue so far is if I try and a link to a word using the standard link button in the editor within that blockquote text, I get a JS error (also below)
I've tried to adapt the stock example from the docs, but I'm guessing I've messed up something.
The code in wagtail_hooks.py
def poquote_entity_decorator(props):
"""
Draft.js ContentState to database HTML.
Converts the poquote entities into a blockquote with class tag.
"""
return DOM.create_element('blockquote', {
'class': 'pullout',
}, props['children'])
class PoQuoteEntityElementHandler(InlineEntityElementHandler):
"""
Database HTML to Draft.js ContentState.
Converts the blockquote tag into a PoQuote entity, with the right
data.
"""
mutability = 'IMMUTABLE'
def get_attribute_data(self, attrs):
return {
'poquote': attrs['class'],
}
#hooks.register('insert_editor_js')
def draftail_editor_js():
js_files = [
'wagtailadmin/js/draftail.js',
'js/draftail.js',
]
js_includes = format_html_join('\n', '<script src="{0}{1}">
</script>',
((settings.STATIC_URL, filename) for filename in js_files)
)
return js_includes
#hooks.register('register_rich_text_features')
def register_poquote_feature(features):
features.default_features.append('poquote')
"""
Registering the `poquote` feature, which uses the `POQUOTE`
Draft.js entity type,
and is stored as HTML with a `<blockquote class='pullout'>` tag.
"""
feature_name = 'poquote'
type_ = 'POQUOTE'
control = {
'type': type_,
'icon': 'icon icon-form',
'description': 'Pull Out Quote',
}
features.register_editor_plugin(
'draftail', feature_name,
draftail_features.EntityFeature(control)
)
features.register_converter_rule('contentstate', feature_name, {
# Note here that the conversion is more complicated than for
blocks and inline styles.
'from_database_format': {'blockquote[class]':
PoQuoteEntityElementHandler(type_)},
'to_database_format': {'entity_decorators': {type_:
poquote_entity_decorator}},
})
and the JS
const React = window.React;
const Modifier = window.DraftJS.Modifier;
const EditorState = window.DraftJS.EditorState;
// Not a real React component – just creates the entities as soon as
it is rendered.
class PoqSource extends React.Component {
componentDidMount() {
const { editorState, entityType, onComplete } = this.props;
const content = editorState.getCurrentContent();
const selection = editorState.getSelection();
const anchorKey = selection.getAnchorKey();
const currentBlock = content.getBlockForKey(anchorKey);
const start = selection.getStartOffset();
const end = selection.getEndOffset();
const selectedText = currentBlock.getText().slice(start,
end);
// Uses the Draft.js API to create a new entity with the right
data.
const contentWithEntity =
content.createEntity(entityType.type, 'IMMUTABLE', {
poq: selectedText,
});
const entityKey = contentWithEntity.getLastCreatedEntityKey();
// We also add some text for the entity to be activated on.
const text = `${selectedText}`;
const newContent = Modifier.replaceText(content, selection,
text, null, entityKey);
const nextState = EditorState.push(editorState, newContent,
'insert-characters');
onComplete(nextState);
}
render() {
return null;
}
}
const Poq = (props) => {
const { entityKey, contentState } = props;
const data = contentState.getEntity(entityKey).getData();
return React.createElement('blockquote', {className: 'pullout'},
props.children);
};
window.draftail.registerPlugin({
type: 'POQUOTE',
source: PoqSource,
decorator: Poq,
});
The error :
vendor.js:1 TypeError: Cannot read property 'startsWith' of undefined
at h (VM7359 draftail.js:1)
at n.value (VM7359 draftail.js:1)
at commitLifeCycles (vendor.js:1)
at e (vendor.js:1)
at x (vendor.js:1)
at w (vendor.js:1)
at batchedUpdates (vendor.js:1)
at Z (vendor.js:1)
at It (vendor.js:1)
l # vendor.js:1
vendor.js:1 Uncaught TypeError: Cannot read property 'startsWith' of
undefined
at h (VM7359 draftail.js:1)
at n.value (VM7359 draftail.js:1)
at commitLifeCycles (vendor.js:1)
at e (vendor.js:1)
at x (vendor.js:1)
at w (vendor.js:1)
at batchedUpdates (vendor.js:1)
at Z (vendor.js:1)
at It (vendor.js:1)
vendor.js:1 TypeError: Cannot read property 'startsWith' of undefined
at h (VM7359 draftail.js:1)
at n.value (VM7359 draftail.js:1)
at commitLifeCycles (vendor.js:1)
at e (vendor.js:1)
at x (vendor.js:1)
at w (vendor.js:1)
at batchedUpdates (vendor.js:1)
at Z (vendor.js:1)
at It (vendor.js:1)
l # vendor.js:1
vendor.js:1 Uncaught TypeError: Cannot read property 'startsWith' of
undefined
at h (VM7359 draftail.js:1)
at n.value (VM7359 draftail.js:1)
at commitLifeCycles (vendor.js:1)
at e (vendor.js:1)
at x (vendor.js:1)
at w (vendor.js:1)
at batchedUpdates (vendor.js:1)
at Z (vendor.js:1)
at It (vendor.js:1)
If all you want is a block as <blockquote class='pullout'>my text</blockquote>, there is a simpler way to do it without writing JavaScript code.
First, start from the example in the official documentation about blocks, using blockquote:
from wagtail.admin.rich_text.converters.html_to_contentstate import BlockElementHandler
#hooks.register('register_rich_text_features')
def register_blockquote_feature(features):
"""
Registering the `blockquote` feature, which uses the `blockquote` Draft.js block type,
and is stored as HTML with a `<blockquote>` tag.
"""
feature_name = 'blockquote'
type_ = 'blockquote'
tag = 'blockquote'
control = {
'type': type_,
'label': '❝',
'description': 'Blockquote',
# Optionally, we can tell Draftail what element to use when displaying those blocks in the editor.
'element': 'blockquote',
}
features.register_editor_plugin(
'draftail', feature_name, draftail_features.BlockFeature(control)
)
features.register_converter_rule('contentstate', feature_name, {
'from_database_format': {tag: BlockElementHandler(type_)},
'to_database_format': {'block_map': {type_: tag}},
})
You can write styles to display those blocks in the editor with the Draftail-block--blockquote class.
Then, you need to adapt the converter rules so this is stored as <blockquote class='pullout'>my text</blockquote> with the pullout class:
features.register_converter_rule('contentstate', feature_name, {
'from_database_format': {'blockquote[class]': BlockElementHandler(type_)},
'to_database_format': {
'block_map': {
type_: {
'element': tag,
'props': {
'class': 'pullout',
},
},
},
},
})
Of course you can use the poquote type and-or feature name if you want to, no need to use blockquote.
Related
I have a plugin for my ckeditor build which should convert pasted content with formulas,
separated by '(' ')', '$$' etc. into math-formulas from ckeditor5-math (https://github.com/isaul32/ckeditor5-math). I changed the AutoMath Plugin so that it supports text with the separators.
I have run into a problem where undoing (ctrl-z) the operation works fine for single-line content, but not for multiline content.
To reproduce the issue, I have built a similar plugin which does not require the math plugin. This plugin converts text enclosed by '&' to bold text.
To reproduce this issue with an editor instance it is required to have the cursor inside a word (not after or before the end of the text, I don't know why that doesn't work, if you know why, help is appreciated^^) and paste it from the clipboard. The content will inside the '&' will be marked bold, however if you undo this operation twice, an model-position-path-incorrect-format error will be thrown.
example to paste:
aa &bb& cc
dd
ee &ff& gg
Undoing the operation twice results in this error:
Uncaught CKEditorError: model-position-path-incorrect-format {"path":[]}
Read more: https://ckeditor.com/docs/ckeditor5/latest/support/error-codes.html#error-model-position-path-incorrect-form
Unfortunately, I haven't found a way to fix this issue, and have not found a similar issue.
I know it has to do with the batches that are operated, and that maybe the position parent has to do something with it, that I should cache the position of the parent. However, I do not know how.
Below my code for an example to reproduce:
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
import Undo from '#ckeditor/ckeditor5-undo/src/undo';
import LiveRange from '#ckeditor/ckeditor5-engine/src/model/liverange';
import LivePosition from '#ckeditor/ckeditor5-engine/src/model/liveposition';
import global from '#ckeditor/ckeditor5-utils/src/dom/global';
export default class Test extends Plugin {
static get requires() {
return [Undo];
}
static get pluginName() {
return 'Test';
}
constructor(editor) {
super(editor);
this._timeoutId = null;
this._positionToInsert = null;
}
init() {
const editor = this.editor;
const modelDocument = editor.model.document;
const view = editor.editing.view;
//change < Clipboard > to < 'ClipboardPipeline' > because in version upgrade from 26 to 27
//the usage of this call changed
this.listenTo(editor.plugins.get('ClipboardPipeline'), 'inputTransformation', (evt, data) => {
const firstRange = modelDocument.selection.getFirstRange();
const leftLivePosition = LivePosition.fromPosition(firstRange.start);
leftLivePosition.stickiness = 'toPrevious';
const rightLivePosition = LivePosition.fromPosition(firstRange.end);
rightLivePosition.stickiness = 'toNext';
modelDocument.once('change:data', () => {
this._boldBetweenPositions(leftLivePosition, rightLivePosition);
leftLivePosition.detach();
rightLivePosition.detach();
}, {priority: 'high'});
});
editor.commands.get('undo').on('execute', () => {
if (this._timeoutId) {
global.window.clearTimeout(this._timeoutId);
this._timeoutId = null;
}
}, {priority: 'high'});
}
_boldBetweenPositions(leftPosition, rightPosition) {
const editor = this.editor;
const equationRange = new LiveRange(leftPosition, rightPosition);
// With timeout user can undo conversation if wants to use plain text
this._timeoutId = global.window.setTimeout(() => {
this._timeoutId = null;
let walker = equationRange.getWalker({ignoreElementEnd: true});
let nodeArray = [];
for (const node of walker) { // remember nodes, because when they are changed model-textproxy-wrong-length error occurs
nodeArray.push(node);
}
editor.model.change(writer => {
for (let node of nodeArray) {
let text = node.item.data;
if (node.item.is('$textProxy') && text !== undefined && text.match(/&/g)) {
let finishedFormulas = this._split(text);
const realRange = writer.createRange(node.previousPosition, node.nextPosition);
writer.remove(realRange);
for (let i = finishedFormulas.length - 1; i >= 0; i--) {
if (i % 2 === 0) {
writer.insertText(finishedFormulas[i], node.previousPosition);
} else {
writer.insertText(finishedFormulas[i], {bold: true}, node.previousPosition);
}
}
}
}
});
}, 100);
}
_split(text) {
let mathFormsAndText = text.split(/(&)/g);
let mathTextArray = [];
for (let i = 0; i < mathFormsAndText.length; i++) {
if (i % 4 === 0) {
mathTextArray.push(mathFormsAndText[i]);
} else if (i % 2 === 0) {
mathTextArray.push(mathFormsAndText[i]);
}
}
return mathTextArray;
}
}
Let me know if I can clarify anything.
The use case is to display server logs in the UI built using Angular9.
I am using ACE editor to display text content received from server upon a http call and the server responds with recent 1000 lines of logs
To verify the content i did console.log() to view the text content in the chrome dev tool.
Console output received from server
While loading the same content to editor I notice special characters
Ace editor content while using in text mode
Attached 2 screenshot to compare
HTML Content
<div ace-editor #codeEditor style="min-height: 550px; width:100%; overflow: auto;"></div>
Typescript
import { Component, ViewChild, ElementRef, Input, SimpleChanges } from '#angular/core';
import * as ace from 'ace-builds';
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/theme-github';
import 'ace-builds/src-noconflict/ext-beautify';
const THEME = 'ace/theme/github';
const LANG = 'ace/mode/text';
#Component({
selector: 'app-text-editor',
templateUrl: './text-editor.component.html',
styleUrls: ['./text-editor.component.css']
})
export class TextEditorComponent {
#ViewChild('codeEditor') codeEditorElmRef: ElementRef;
private codeEditor: ace.Ace.Editor;
#Input() textObject;
#Input() readMode;
data: any;
mode: any;
constructor() { }
ngOnChanges(changes: SimpleChanges) {
for (const properties of Object.keys(changes)) {
if (properties == 'textObject') {
const currentJSONObject = changes[properties];
if (currentJSONObject.currentValue && currentJSONObject.firstChange == false)
this.codeEditor.setValue(currentJSONObject.currentValue, -1);
else
this.data = currentJSONObject.currentValue
}
if (properties == 'readMode') {
const currentReadMode = changes[properties];
if (currentReadMode.firstChange == false)
this.codeEditor.setReadOnly(currentReadMode.currentValue);
else
this.mode = currentReadMode.currentValue
}
}
}
ngAfterViewInit() {
const element = this.codeEditorElmRef.nativeElement;
const editorOptions: Partial<ace.Ace.EditorOptions> = {
highlightActiveLine: true,
displayIndentGuides: true,
highlightSelectedWord: true,
};
this.codeEditor = ace.edit(element, editorOptions);
this.codeEditor.setTheme(THEME);
this.codeEditor.getSession().setMode(LANG);
this.codeEditor.setShowFoldWidgets(true);
this.codeEditor.setHighlightActiveLine(true);
this.codeEditor.setShowPrintMargin(false);
this.codeEditor.setReadOnly(this.readMode);
this.codeEditor.navigateFileEnd();
if (this.data)
this.codeEditor.setValue(this.data, - 1);
if (this.mode)
this.codeEditor.setReadOnly(this.mode);
}
}
This are not characters added by ace, but color control characters sent by the terminal. If you do not want to display colors, and only want the text use the following function
var CSIRegexp;
function getCSIRegexp() {
if (CSIRegexp) return CSIRegexp;
// http://www.inwap.com/pdp10/ansicode.txt
var classes = {
C0 : "\\x00-\\x1F", //
SPACE : "\\x20\\xA0" , // Always and everywhere a blank space
G0 : "\\x21-\\x7E", //
Intermediate : "\\x20-\\x2F", // !"#$%&'()*+,-./
Parameters : "\\x30-\\x3F", // 0123456789:;<=>?
Uppercase : "\\x40-\\x5F", // #ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
Lowercase : "\\x60-\\x7E", // `abcdefghijlkmnopqrstuvwxyz{|}~
Alphabetic : "\\x40-\\x7E", // (all of upper and lower case)
Delete : "\\x7F" , // Always and everywhere ignored
C1 : "\\x80-\\x9F", // 32 additional control characters
G1 : "\\xA1-\\xFE", // 94 additional displayable characters
Special : "\\xA0\\xFF" , // Same as SPACE and DELETE
ESC : "\\x1b" , //
Ignore : "\\x7F\\x00-\\x1F\\x80-\\x9F" // Delete|C0|C1
};
var g = /:ESC:(\[:Parameters:*:Intermediate:?:Alphabetic:?|:Intermediate:+:Parameters:|:Parameters:|:Lowercase:|)|[:Ignore:]/;
var inBrackets = false;
var source = g.source.replace(/(\\.)|([\[\]])|:(\w*):/g, function(_, esc, b, cls) {
if (esc) return esc;
if (b) {
inBrackets = b == "[";
return b;
}
if (!cls) return ":";
var r = classes[cls];
if (!/ESC|Delete|C0|C1/.test(cls)) {
r += classes.Ignore;
}
if (!inBrackets && cls != "ESC")
r = "[" + r + "]";
return r;
});
return CSIRegexp = new RegExp(source, "g");
}
to remove control character before setting data to the editor
this.codeEditor.setValue(this.data.replace(getCSIRegexp(), ""), - 1);
You can test it with the following call:
"\x1b[1mreal text\x1b[10m".replace(getCSIRegexp(), "")
I have an input field which when submitted makes a http call and then plots a graph. When I click on any node of graph, same http call is made and results are appended to the previous results and graph is updated. It is working fine till here. I am using scan operator to update my resultset. Now, what I want is to reset the resultset (ie - return new original response) whenever I am submitting the input form and append to resultset when graph node is clicked. Any ideas on how this can be achieved? Mainly how can I reset this stream on form submit? Or how can I show new data on form submit and updated data on node click
Here linkingDetailsByAccount$ makes the http call and gets the data from the server.
this.linkingDetailsByAccountSubject.next(account);
Same code is called on node click as well as on form submit which then activates my stream.
graph$ = this.linkingDetailsByAccount$.pipe(
pluck('graph'),
scan((linkedDetails, adjacency) => {
const { nodes: linkedNodes = [], edges: linkedEdges = [] } = linkedDetails;
const { nodes: newNodes = [], edges: newEdges = [] } = adjacency;
const updatedNodes = differenceBy(newNodes, linkedNodes, 'id');
const updatedEdges = differenceWith(
newEdges,
linkedEdges,
(newEdge: VisEdge, existingEdge: VisEdge) => newEdge.from === existingEdge.to
);
const allNodes = [...linkedNodes, ...updatedNodes];
const allEdges = [...linkedEdges, ...updatedEdges];
return {
nodes: allNodes,
edges: allEdges
};
}, {} as NodesEdges)
);
Appreciate any inputs on this.
Thanks,
Vatsal
Edit: Updated answer when I received more details from OP.
How I would do it is turn it into a mini Redux like state manager.
So the scan operator should take in functions or event objects.
First you want to store the first initial state from the initial HTTP call you make. You will use this object to reset your state on form submission.
Then create a graphEvents subject.
interface UpdateGraphEvent {
type: 'Update';
account: any;
}
interface ResetGraphEvent {
type: 'Reset';
account: any;
}
type GraphEvent = UpdateGraphEvent | ResetGraphEvent;
this.graphEvents$ = new Subject<GraphEvent>();
Then you can use your new graphEvents$ subject to replace uses of linkingDetailsByAccountSubject.
// When you want to update with new data.
this.graphEvent$.next({type: 'Update', account: account});
// when you want to reset with initial data.
this.graphEvent$.next({type: 'Reset', account: this.initialAccount});
Then use it in your stream.
graph$ = this.graphEvent$.pipe(
pluck('graph'),
scan((linkedDetails, event: GraphEvent) => {
if (event.type === 'Reset') {
return {
nodes: event.account.nodes,
edges: event.account.edges,
}
}
const { nodes: linkedNodes = [], edges: linkedEdges = [] } = linkedDetails;
const { nodes: newNodes = [], edges: newEdges = [] } = event.account;
const updatedNodes = differenceBy(newNodes, linkedNodes, 'id');
const updatedEdges = differenceWith(
newEdges,
linkedEdges,
(newEdge: VisEdge, existingEdge: VisEdge) => newEdge.from === existingEdge.to
);
const allNodes = [...linkedNodes, ...updatedNodes];
const allEdges = [...linkedEdges, ...updatedEdges];
return {
nodes: allNodes,
edges: allEdges
};
}, {} as NodesEdges)
);
The graphEvent$ will be a Subject that emits those events (GraphEvent).
I have a linear scale and I am using it for one of my axis in a chart.
I want to define a custom format only showing positive numbers and following this example I created a customFormat in this way:
function yFormat(formats) {
return function(value) {
var i = formats.length - 1, f = formats[i];
while (!f[1](value)) f = formats[--i];
return f[0](value);
};
}
var customFormat = yFormat([
[d3.format("?????"), function() { return true }],
[d3.format("d"), function(d) { return d >= 0 }]
]);
but I don't know how to define an empty format for Numbers (link to doc)
Does anybody have an idea? Thanks
EDIT
d3.format("") seems to work for time scales but not for linear scale. In a console:
> var emptyTime = d3.time.format("");
undefined
> emptyTime("something");
""
> var emptyInt = d3.format("");
undefined
> emptyInt(5);
"5"
ANSWER
I am just using tickValues on the axis itself. So, something like:
yAxis.tickValues(
d3.range(
0,
d3.max(y.domain()),
shift
)
);
Is there a succinct example of how to upload an image, resize it, store it in a database and then serve the image up using Lift?
I'm sure I could piece it together from the file upload, Java 2D API, Lift Mapper and Response APIs. But is there any example code I can follow to do it the 'correct' or recommended way?
I did this for a Mapper field linked to s3 by creating a new MappedField. I also have a some code to resize, but haven't tested or deployed (so use with caution).
class MappedS3Image[T<:Mapper[T]](owner: T, val path:String, maxWidth: String, maxHeight:String) extends MappedString[T](owner, 36) {
def url:String = MappedS3Image.fullImgPath(path, is)
def setFromUpload(fileHolder: Box[FileParamHolder]) = {
S3Sender.uploadImageToS3(path, fileHolder).map(this.set(_))
}
override def asHtml:Node = <img src={url} style={"max-width:" + maxWidth + ";max-height:"+maxHeight} />
override def _toForm: Box[Elem] = Full(SHtml.fileUpload(fu=>setFromUpload(Full(fu))))
}
import java.awt.Image
import java.awt.image.BufferedImage
import javax.imageio.ImageIO
import java.awt.Graphics2D
import java.awt.AlphaComposite
object ImageResizer {
def resize(is:java.io.InputStream, maxWidth:Int, maxHeight:Int):BufferedImage = {
val originalImage:BufferedImage = ImageIO.read(is)
val height = originalImage.getHeight
val width = originalImage.getWidth
if (width <= maxWidth && height <= maxHeight)
originalImage
else {
var scaledWidth:Int = width
var scaledHeight:Int = height
val ratio:Double = width/height
if (scaledWidth > maxWidth){
scaledWidth = maxWidth
scaledHeight = (scaledWidth.doubleValue/ratio).intValue
}
if (scaledHeight > maxHeight){
scaledHeight = maxHeight
scaledWidth = (scaledHeight.doubleValue*ratio).intValue
}
val scaledBI = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_RGB)
val g = scaledBI.createGraphics
g.setComposite(AlphaComposite.Src)
g.drawImage(originalImage, 0, 0, scaledWidth, scaledHeight, null);
g.dispose
scaledBI
}
}
}
The other answer nicely describes how to resize the image and store a reference to the file on the file system.
If you want to use the lift mapper to store the actual file contents, you have to create your custom model object, and define a binary field on it. Try something like this:
package code {
package model {
import _root_.net.liftweb.mapper._
import _root_.net.liftweb.util._
import _root_.net.liftweb.common._
// singleton object which manipulates storing of Document instances
object Document extends Document with KeyedMetaMapper[Long, Document] {
}
class Document extends KeyedMapper[Long, Document] {
def getSingleton = Document
def primaryKeyField = id
object id extends MappedLongIndex(this)
object name extends MappedString(this, 20) {
override def displayName = "Name"
override def writePermission_? = true
}
object content extends MappedBinary(this) {
override def displayName = "Content"
override def writePermission_? = true
}
}
}
}
Then, in bootstrap class, add this Document at the end:
Schemifier.schemify(true, Schemifier.infoF _, User, Document)
Voila. Using Document save (new Document) stores it into database. A new Document's fields can be set using the set method. Try playing with delete_!, find, findAll methods of the Document singleton to delete or find it in the database. It should be straightforward from this point on.
Finally, to display the image, you can override Lift's dispatching rules (in bootstrap class, Boot.scala). Try playing around with this example which overrides the rules for pdf requests:
def getFile(filename: String): Option[Document] = {
val alldocs = Document.findAll()
alldocs.find(_.name.get == filename)
}
LiftRules.statelessDispatchTable.append {
case Req("file" :: name :: Nil, "pdf", GetRequest) =>
() =>
println("Got request for: " + name + ".pdf")
for {
stream <- tryo(
getFile(name + ".pdf") map {
doc => new java.io.ByteArrayInputStream(doc.content.get)
} getOrElse null
)
if null ne stream
} yield StreamingResponse(stream,
() => stream.close,
stream.available,
List("Content-Type" -> "application/pdf"),
Nil,
200)
}
Based on the accepted answer by Jon Hoffman, I fixed the bugs. His version messes up the aspect ratio (it always becomes 1:1), because the math was off in a few spots. This version resizes big pictures until they fit, and respects the aspect ratio.
def resize(is:java.io.InputStream, maxWidth:Int, maxHeight:Int):BufferedImage = {
require (maxWidth > 0)
require (maxHeight > 0)
val originalImage:BufferedImage = ImageIO.read(is)
var height = originalImage.getHeight
var width = originalImage.getWidth
// Shortcut to save a pointless reprocessing in case the image is small enough already
if (width <= maxWidth && height <= maxHeight)
originalImage
else {
// If the picture was too big, it will either fit by width or height.
// This essentially resizes the dimensions twice, until it fits
if (width > maxWidth){
height = (height.doubleValue() * (maxWidth.doubleValue() / width.doubleValue())).intValue
width = maxWidth
}
if (height > maxHeight){
width = (width.doubleValue() * (maxHeight.doubleValue() / height.doubleValue())).intValue
height = maxHeight
}
val scaledBI = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
val g = scaledBI.createGraphics
g.setComposite(AlphaComposite.Src)
g.drawImage(originalImage, 0, 0, width, height, null);
g.dispose
scaledBI
}
}