Skip to content

Commit 60d2759

Browse files
feat: add dimensions to images (#285)
1 parent 8b19d36 commit 60d2759

18 files changed

Lines changed: 324 additions & 134 deletions

android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,11 +527,11 @@ class EnrichedTextInputView : AppCompatEditText {
527527
parametrizedStyles?.setLinkSpan(start, end, text, url)
528528
}
529529

530-
fun addImage(src: String) {
530+
fun addImage(src: String, width: Float, height: Float) {
531531
val isValid = verifyStyle(EnrichedSpans.IMAGE)
532532
if (!isValid) return
533533

534-
parametrizedStyles?.setImageSpan(src)
534+
parametrizedStyles?.setImageSpan(src, width, height)
535535
layoutManager.invalidateLayout()
536536
}
537537

android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,8 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
255255
view?.addLink(start, end, text, url)
256256
}
257257

258-
override fun addImage(view: EnrichedTextInputView?, src: String) {
259-
view?.addImage(src)
258+
override fun addImage(view: EnrichedTextInputView?, src: String, width: Float, height: Float) {
259+
view?.addImage(src, width, height)
260260
}
261261

262262
override fun startMention(view: EnrichedTextInputView?, indicator: String) {

android/src/main/java/com/swmansion/enriched/spans/EnrichedImageSpan.kt

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
package com.swmansion.enriched.spans
22

33
import android.content.Context
4+
import android.content.res.Resources
45
import android.graphics.Canvas
56
import android.graphics.Paint
67
import android.graphics.drawable.Drawable
78
import android.net.Uri
89
import android.text.style.ImageSpan
910
import androidx.core.graphics.withSave
1011
import com.swmansion.enriched.spans.interfaces.EnrichedInlineSpan
11-
import com.swmansion.enriched.styles.HtmlStyle
1212

1313
class EnrichedImageSpan : ImageSpan, EnrichedInlineSpan {
14-
private var htmlStyle: HtmlStyle? = null
14+
private var width: Int = 0
15+
private var height: Int = 0
1516

16-
constructor(context: Context, uri: Uri, htmlStyle: HtmlStyle, ) : super(context, uri, ALIGN_BASELINE) {
17-
this.htmlStyle = htmlStyle
17+
constructor(context: Context, uri: Uri, width: Int, height: Int) : super(context, uri, ALIGN_BASELINE) {
18+
this.width = width
19+
this.height = height
1820
}
1921

20-
constructor(drawable: Drawable, source: String, htmlStyle: HtmlStyle) : super(drawable, source, ALIGN_BASELINE) {
21-
this.htmlStyle = htmlStyle
22+
constructor(drawable: Drawable, source: String, width: Int, height: Int) : super(drawable, source, ALIGN_BASELINE) {
23+
this.width = width
24+
this.height = height
2225
}
2326

2427
override fun draw(
@@ -35,7 +38,17 @@ class EnrichedImageSpan : ImageSpan, EnrichedInlineSpan {
3538

3639
override fun getDrawable(): Drawable {
3740
val drawable = super.getDrawable()
38-
drawable.setBounds(0, 0, htmlStyle!!.imgWidth, htmlStyle!!.imgHeight)
41+
val scale = Resources.getSystem().displayMetrics.density
42+
43+
drawable.setBounds(0, 0, (width * scale).toInt() , (height * scale).toInt())
3944
return drawable
4045
}
46+
47+
fun getWidth(): Int {
48+
return width
49+
}
50+
51+
fun getHeight(): Int {
52+
return height
53+
}
4154
}

android/src/main/java/com/swmansion/enriched/styles/HtmlStyle.kt

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,6 @@ class HtmlStyle {
4343
var ulBulletSize: Int = 8
4444
var ulBulletColor: Int = Color.BLACK
4545

46-
var imgWidth: Int = 200
47-
var imgHeight: Int = 200
48-
4946
var aColor: Int = Color.BLACK
5047
var aUnderline: Boolean = true
5148

@@ -100,10 +97,6 @@ class HtmlStyle {
10097
ulMarginLeft = parseFloat(ulStyle, "marginLeft").toInt()
10198
ulBulletSize = parseFloat(ulStyle, "bulletSize").toInt()
10299

103-
val imgStyle = style.getMap("img")
104-
imgWidth = parseFloat(imgStyle, "width").toInt()
105-
imgHeight = parseFloat(imgStyle, "height").toInt()
106-
107100
val aStyle = style.getMap("a")
108101
aColor = parseColor(aStyle, "color")
109102
aUnderline = parseIsUnderline(aStyle)
@@ -124,8 +117,8 @@ class HtmlStyle {
124117
private fun parseFloat(map: ReadableMap?, key: String): Float {
125118
val safeMap = ensureValueIsSet(map, key)
126119

127-
val fontSize = safeMap.getDouble(key)
128-
return ceil(PixelUtil.toPixelFromSP(fontSize))
120+
val value = safeMap.getDouble(key)
121+
return ceil(PixelUtil.toPixelFromSP(value))
129122
}
130123

131124
private fun parseColorWithOpacity(map: ReadableMap?, key: String, opacity: Int): Int {

android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -170,26 +170,28 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
170170
}
171171
}
172172

173-
fun setImageSpan(src: String) {
173+
fun setImageSpan(src: String, width: Float, height: Float) {
174174
if (view.selection == null) return
175175

176176
val spannable = view.text as SpannableStringBuilder
177-
var (start, end) = view.selection.getInlineSelection()
178-
val spans = spannable.getSpans(start, end, EnrichedImageSpan::class.java)
177+
val (start, originalEnd) = view.selection.getInlineSelection()
179178

180-
for (s in spans) {
181-
spannable.removeSpan(s)
182-
}
183-
184-
if (start == end) {
179+
if (start == originalEnd) {
185180
spannable.insert(start, "\uFFFC")
186-
end++
181+
} else {
182+
val spans = spannable.getSpans(start, originalEnd, EnrichedImageSpan::class.java)
183+
for (s in spans) {
184+
spannable.removeSpan(s)
185+
}
186+
187+
spannable.replace(start, originalEnd, "\uFFFC")
187188
}
188189

190+
val (imageStart, imageEnd) = spannable.getSafeSpanBoundaries(start, start + 1)
189191
val uri = Uri.fromFile(File(src))
190-
val span = EnrichedImageSpan(view.context, uri, view.htmlStyle)
191-
val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, end)
192-
spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
192+
193+
val span = EnrichedImageSpan(view.context, uri, width.toInt(), height.toInt())
194+
spannable.setSpan(span, imageStart, imageEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
193195
}
194196

195197
fun startMention(indicator: String) {

android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package com.swmansion.enriched.utils;
22

3+
import android.content.res.Resources;
4+
import android.graphics.Bitmap;
5+
import android.graphics.BitmapFactory;
6+
import android.graphics.drawable.BitmapDrawable;
37
import android.graphics.drawable.Drawable;
48
import android.text.Editable;
59
import android.text.Layout;
@@ -9,6 +13,7 @@
913
import android.text.TextUtils;
1014
import android.text.style.AlignmentSpan;
1115
import android.text.style.ParagraphStyle;
16+
import android.util.Log;
1217

1318
import com.swmansion.enriched.spans.EnrichedBlockQuoteSpan;
1419
import com.swmansion.enriched.spans.EnrichedBoldSpan;
@@ -275,7 +280,16 @@ private static void withinParagraph(StringBuilder out, Spanned text, int start,
275280
if (style[j] instanceof EnrichedImageSpan) {
276281
out.append("<img src=\"");
277282
out.append(((EnrichedImageSpan) style[j]).getSource());
278-
out.append("\">");
283+
out.append("\"");
284+
285+
out.append(" width=\"");
286+
out.append(((EnrichedImageSpan) style[j]).getWidth());
287+
out.append("\"");
288+
289+
out.append(" height=\"");
290+
out.append(((EnrichedImageSpan) style[j]).getHeight());
291+
292+
out.append("\"/>");
279293
// Don't output the placeholder character underlying the image.
280294
i = next;
281295
}
@@ -453,7 +467,7 @@ private void handleStartTag(String tag, Attributes attributes) {
453467
} else if (tag.equalsIgnoreCase("h3")) {
454468
startHeading(mSpannableStringBuilder, 3);
455469
} else if (tag.equalsIgnoreCase("img")) {
456-
startImg(mSpannableStringBuilder, attributes, mImageGetter, mStyle);
470+
startImg(mSpannableStringBuilder, attributes, mImageGetter);
457471
} else if (tag.equalsIgnoreCase("code")) {
458472
start(mSpannableStringBuilder, new Code());
459473
} else if (tag.equalsIgnoreCase("mention")) {
@@ -678,20 +692,51 @@ private static void end(Editable text, Class kind, Object repl) {
678692
}
679693
}
680694

681-
private static void startImg(Editable text, Attributes attributes, EnrichedParser.ImageGetter img, HtmlStyle style) {
695+
private static void startImg(Editable text, Attributes attributes, EnrichedParser.ImageGetter img) {
682696
String src = attributes.getValue("", "src");
697+
String width = attributes.getValue("", "width");
698+
String height = attributes.getValue("", "height");
699+
683700
Drawable d = null;
684701
if (img != null) {
685702
d = img.getDrawable(src);
686703
}
687704

705+
if (d == null) {
706+
d = HtmlToSpannedConverter.prepareDrawableForImage(src);
707+
}
708+
688709
if (d == null) {
689710
return;
690711
}
691712

692713
int len = text.length();
693714
text.append("");
694-
text.setSpan(new EnrichedImageSpan(d, src, style), len, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
715+
text.setSpan(new EnrichedImageSpan(d, src, Integer.parseInt(width), Integer.parseInt(height)), len, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
716+
}
717+
718+
private static BitmapDrawable prepareDrawableForImage(String src) {
719+
String cleanPath = src;
720+
if (cleanPath.startsWith("file://")) {
721+
cleanPath = cleanPath.substring(7);
722+
}
723+
724+
BitmapDrawable drawable = null;
725+
726+
try {
727+
Bitmap bitmap = BitmapFactory.decodeFile(cleanPath);
728+
if (bitmap != null) {
729+
drawable = new BitmapDrawable(Resources.getSystem(), bitmap);
730+
// set bounds so it knows how big it is naturally,
731+
// though EnrichedImageSpan will override this with the HTML width/height later.
732+
drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
733+
}
734+
} catch (Exception e) {
735+
// Failed to load file
736+
Log.e("EnrichedParser", "Failed to load image from path: " + cleanPath, e);
737+
}
738+
739+
return drawable;
695740
}
696741

697742
private static void startA(Editable text, Attributes attributes) {

example/src/App.tsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { type MentionItem, MentionPopup } from './components/MentionPopup';
2727
import { useUserMention } from './useUserMention';
2828
import { useChannelMention } from './useChannelMention';
2929
import { HtmlSection } from './components/HtmlSection';
30+
import { ImageModal } from './components/ImageModal';
3031

3132
type StylesState = OnChangeStateEvent;
3233

@@ -75,6 +76,7 @@ export default function App() {
7576
const [isChannelPopupOpen, setIsChannelPopupOpen] = useState(false);
7677
const [isUserPopupOpen, setIsUserPopupOpen] = useState(false);
7778
const [isLinkModalOpen, setIsLinkModalOpen] = useState(false);
79+
const [isImageModalOpen, setIsImageModalOpen] = useState(false);
7880
const [isValueModalOpen, setIsValueModalOpen] = useState(false);
7981
const [currentHtml, setCurrentHtml] = useState('');
8082

@@ -125,6 +127,14 @@ export default function App() {
125127
setIsLinkModalOpen(false);
126128
};
127129

130+
const openImageModal = () => {
131+
setIsImageModalOpen(true);
132+
};
133+
134+
const closeImageModal = () => {
135+
setIsImageModalOpen(false);
136+
};
137+
128138
const openUserMentionPopup = () => {
129139
setIsUserPopupOpen(true);
130140
};
@@ -197,7 +207,7 @@ export default function App() {
197207
closeValueModal();
198208
};
199209

200-
const selectImage = async () => {
210+
const selectImage = async (width: number, height: number) => {
201211
const response = await launchImageLibrary({
202212
mediaType: 'photo',
203213
selectionLimit: 1,
@@ -208,9 +218,11 @@ export default function App() {
208218
? response.assets?.[0]?.originalPath
209219
: response.assets?.[0]?.uri;
210220

211-
if (!imageUri) return;
221+
if (imageUri) {
222+
ref.current?.setImage(imageUri, width, height);
223+
}
212224

213-
ref.current?.setImage(imageUri);
225+
closeImageModal();
214226
};
215227

216228
const handleChangeMention = ({ indicator, text }: OnChangeMentionEvent) => {
@@ -294,7 +306,7 @@ export default function App() {
294306
stylesState={stylesState}
295307
editorRef={ref}
296308
onOpenLinkModal={openLinkModal}
297-
onSelectImage={selectImage}
309+
onSelectImage={openImageModal}
298310
/>
299311
</View>
300312
<View style={styles.buttonStack}>
@@ -318,6 +330,11 @@ export default function App() {
318330
onSubmit={submitLink}
319331
onClose={closeLinkModal}
320332
/>
333+
<ImageModal
334+
isOpen={isImageModalOpen}
335+
onSubmit={selectImage}
336+
onClose={closeImageModal}
337+
/>
321338
<ValueModal
322339
isOpen={isValueModalOpen}
323340
onSubmit={submitSetValue}
@@ -383,10 +400,6 @@ const htmlStyle: HtmlStyle = {
383400
textDecorationLine: 'none',
384401
},
385402
},
386-
img: {
387-
width: 50,
388-
height: 50,
389-
},
390403
ol: {
391404
gapWidth: 16,
392405
marginLeft: 24,

0 commit comments

Comments
 (0)