diff --git a/components/ItemGrid/GridItem.bs b/components/ItemGrid/GridItem.bs
index 0015a8ea5..fc34ada2f 100644
--- a/components/ItemGrid/GridItem.bs
+++ b/components/ItemGrid/GridItem.bs
@@ -37,7 +37,11 @@ sub itemContentChanged()
' Set Random background colors from pallet
posterBackgrounds = m.global.constants.poster_bg_pallet
- m.backdrop.blendColor = posterBackgrounds[rnd(posterBackgrounds.count()) - 1]
+ if isValidAndNotEmpty(m.top.itemContent.posterBlurhashUrl):
+ m.backdrop.uri = m.top.itemContent.posterBlurhashUrl
+ else
+ m.backdrop.blendColor = posterBackgrounds[rnd(posterBackgrounds.count()) - 1]
+ end if
itemData = m.top.itemContent
diff --git a/components/ItemGrid/GridItemSmall.bs b/components/ItemGrid/GridItemSmall.bs
index 606f04462..c57bd4e97 100644
--- a/components/ItemGrid/GridItemSmall.bs
+++ b/components/ItemGrid/GridItemSmall.bs
@@ -1,5 +1,6 @@
import "pkg:/source/utils/misc.bs"
import "pkg:/source/utils/config.bs"
+import "pkg:/source/utils/fakeBlurhash.bs"
sub init()
m.itemPoster = m.top.findNode("itemPoster")
@@ -23,7 +24,11 @@ sub init()
end sub
sub itemContentChanged()
- m.backdrop.blendColor = "#101010"
+ if isValidAndNotEmpty(m.top.itemContent.posterBlurhashUrl):
+ m.backdrop.uri = m.top.itemContent.posterBlurhashUrl
+ else
+ m.backdrop.blendColor = "#101010"
+ end if
m.title.visible = false
diff --git a/components/data/ChannelData.bs b/components/data/ChannelData.bs
index ff4ec8189..20cc83d92 100644
--- a/components/data/ChannelData.bs
+++ b/components/data/ChannelData.bs
@@ -1,6 +1,7 @@
import "pkg:/source/api/Image.bs"
import "pkg:/source/api/baserequest.bs"
import "pkg:/source/utils/config.bs"
+import "pkg:/source/utils/fakeBlurhash.bs"
sub setFields()
json = m.top.json
@@ -20,5 +21,13 @@ sub setPoster()
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+ if isValidAndNotEmpty(m.top.json.ImageBlurHashes.Primary)
+ blurhash = m.top.json.ImageBlurHashes.Primary[m.top.json.ImageTags.Primary]
+ if get_user_setting("ui.design.renderblurhashes") = "true" and isValidAndNotEmpty(blurhash)
+ timer = CreateObject("roTimeSpan")
+ m.top.posterBlurHashUrl = renderFakeBlurhash(blurhash, imgParams.maxWidth, imgParams.maxHeight)
+ print "Took " + Str(timer.totalMilliseconds()) + " milliseconds to render a blurhash in ChannelData."
+ end if
+ end if
end if
end sub
diff --git a/components/data/CollectionData.bs b/components/data/CollectionData.bs
index 4cce7d183..f6fadd3bc 100644
--- a/components/data/CollectionData.bs
+++ b/components/data/CollectionData.bs
@@ -1,6 +1,8 @@
import "pkg:/source/api/Image.bs"
import "pkg:/source/api/baserequest.bs"
import "pkg:/source/utils/config.bs"
+import "pkg:/source/utils/fakeBlurhash.bs"
+
sub setFields()
json = m.top.json
@@ -24,6 +26,14 @@ sub setPoster()
if m.top.json.ImageTags.Primary <> invalid
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+ if isValidAndNotEmpty(m.top.json.ImageBlurHashes.Primary)
+ blurhash = m.top.json.ImageBlurHashes.Primary[m.top.json.ImageTags.Primary]
+ if get_user_setting("ui.design.renderblurhashes") = "true" and isValidAndNotEmpty(blurhash)
+ timer = CreateObject("roTimeSpan")
+ m.top.posterBlurHashUrl = renderFakeBlurhash(blurhash, imgParams.maxWidth, imgParams.maxHeight)
+ print "Took " + Str(timer.totalMilliseconds()) + " milliseconds to render a blurhash in CollectionData."
+ end if
+ end if
else if m.top.json.BackdropImageTags <> invalid
imgParams = { "maxHeight": 440, "Tag": m.top.json.BackdropImageTags[0] }
m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
diff --git a/components/data/FolderData.bs b/components/data/FolderData.bs
index 7e6da642e..435d79a20 100644
--- a/components/data/FolderData.bs
+++ b/components/data/FolderData.bs
@@ -1,6 +1,7 @@
import "pkg:/source/api/Image.bs"
import "pkg:/source/api/baserequest.bs"
import "pkg:/source/utils/config.bs"
+import "pkg:/source/utils/fakeBlurhash.bs"
sub setFields()
json = m.top.json
@@ -27,6 +28,14 @@ sub setPoster()
else if m.top.json.ImageTags.Primary <> invalid
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+ if isValidAndNotEmpty(m.top.json.ImageBlurHashes.Primary)
+ blurhash = m.top.json.ImageBlurHashes.Primary[m.top.json.ImageTags.Primary]
+ if get_user_setting("ui.design.renderblurhashes") = "true" and isValidAndNotEmpty(blurhash)
+ timer = CreateObject("roTimeSpan")
+ m.top.posterBlurHashUrl = renderFakeBlurhash(blurhash, imgParams.maxWidth, imgParams.maxHeight)
+ print "Took " + Str(timer.totalMilliseconds()) + " milliseconds to render a blurhash in FolderData."
+ end if
+ end if
end if
end sub
diff --git a/components/data/JFContentItem.xml b/components/data/JFContentItem.xml
index 6d4d177e9..73bbc224a 100644
--- a/components/data/JFContentItem.xml
+++ b/components/data/JFContentItem.xml
@@ -4,6 +4,7 @@
+
diff --git a/components/data/MovieData.bs b/components/data/MovieData.bs
index cba66f815..9ac4b36f0 100644
--- a/components/data/MovieData.bs
+++ b/components/data/MovieData.bs
@@ -2,6 +2,7 @@ import "pkg:/source/api/Image.bs"
import "pkg:/source/api/baserequest.bs"
import "pkg:/source/utils/config.bs"
import "pkg:/source/utils/misc.bs"
+import "pkg:/source/utils/fakeBlurhash.bs"
sub setFields()
json = m.top.json
@@ -47,6 +48,14 @@ sub setPoster()
if isValid(m.top.json.ImageTags) and isValid(m.top.json.ImageTags.Primary)
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+ if isValidAndNotEmpty(m.top.json.ImageBlurHashes.Primary)
+ blurhash = m.top.json.ImageBlurHashes.Primary[m.top.json.ImageTags.Primary]
+ if get_user_setting("ui.design.renderblurhashes") = "true" and isValidAndNotEmpty(blurhash)
+ timer = CreateObject("roTimeSpan")
+ m.top.posterBlurHashUrl = renderFakeBlurhash(blurhash, imgParams.maxWidth, imgParams.maxHeight)
+ print "Took " + Str(timer.totalMilliseconds()) + " milliseconds to render a blurhash in MoviesData."
+ end if
+ end if
else if isValid(m.top.json.BackdropImageTags) and isValid(m.top.json.BackdropImageTags[0])
imgParams = { "maxHeight": 440, "Tag": m.top.json.BackdropImageTags[0] }
m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
diff --git a/components/data/MusicAlbumSongListData.bs b/components/data/MusicAlbumSongListData.bs
index 9583cdb4f..bac5faf39 100644
--- a/components/data/MusicAlbumSongListData.bs
+++ b/components/data/MusicAlbumSongListData.bs
@@ -1,6 +1,7 @@
import "pkg:/source/api/Image.bs"
import "pkg:/source/api/baserequest.bs"
import "pkg:/source/utils/config.bs"
+import "pkg:/source/utils/fakeBlurhash.bs"
sub setFields()
json = m.top.json
@@ -18,6 +19,14 @@ sub setPoster()
if m.top.json.ImageTags.Primary <> invalid
imgParams = { "maxHeight": 440, "maxWidth": 295 }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+ if isValidAndNotEmpty(m.top.json.ImageBlurHashes.Primary)
+ blurhash = m.top.json.ImageBlurHashes.Primary[m.top.json.ImageTags.Primary]
+ if get_user_setting("ui.design.renderblurhashes") = "true" and isValidAndNotEmpty(blurhash)
+ timer = CreateObject("roTimeSpan")
+ m.top.posterBlurHashUrl = renderFakeBlurhash(blurhash, imgParams.maxWidth, imgParams.maxHeight)
+ print "Took " + Str(timer.totalMilliseconds()) + " milliseconds to render a blurhash in MusicAlbumSongListData."
+ end if
+ end if
else if m.top.json.BackdropImageTags[0] <> invalid
imgParams = { "maxHeight": 440 }
m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
diff --git a/components/data/MusicArtistData.bs b/components/data/MusicArtistData.bs
index 6f36dc9b5..892bcf84b 100644
--- a/components/data/MusicArtistData.bs
+++ b/components/data/MusicArtistData.bs
@@ -1,6 +1,7 @@
import "pkg:/source/api/Image.bs"
import "pkg:/source/api/baserequest.bs"
import "pkg:/source/utils/config.bs"
+import "pkg:/source/utils/fakeBlurhash.bs"
sub setFields()
json = m.top.json
@@ -21,6 +22,14 @@ sub setPoster()
if m.top.json.ImageTags.Primary <> invalid
imgParams = { "maxHeight": 440, "maxWidth": 440 }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+ if isValidAndNotEmpty(m.top.json.ImageBlurHashes.Primary)
+ blurhash = m.top.json.ImageBlurHashes.Primary[m.top.json.ImageTags.Primary]
+ if get_user_setting("ui.design.renderblurhashes") = "true" and isValidAndNotEmpty(blurhash)
+ timer = CreateObject("roTimeSpan")
+ m.top.posterBlurHashUrl = renderFakeBlurhash(blurhash, imgParams.maxWidth, imgParams.maxHeight)
+ print "Took " + Str(timer.totalMilliseconds()) + " milliseconds to render a blurhash in MusicArtistData."
+ end if
+ end if
else if m.top.json.BackdropImageTags[0] <> invalid
imgParams = { "maxHeight": 440 }
m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
diff --git a/components/data/PersonData.bs b/components/data/PersonData.bs
index 71745b06f..06678fc64 100644
--- a/components/data/PersonData.bs
+++ b/components/data/PersonData.bs
@@ -1,6 +1,7 @@
import "pkg:/source/api/Image.bs"
import "pkg:/source/api/baserequest.bs"
import "pkg:/source/utils/config.bs"
+import "pkg:/source/utils/fakeBlurhash.bs"
sub setFields()
json = m.top.json
@@ -18,6 +19,14 @@ sub setPoster()
if m.top.json.ImageTags.Primary <> invalid
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+ if isValidAndNotEmpty(m.top.json.ImageBlurHashes.Primary)
+ blurhash = m.top.json.ImageBlurHashes.Primary[m.top.json.ImageTags.Primary]
+ if get_user_setting("ui.design.renderblurhashes") = "true" and isValidAndNotEmpty(blurhash)
+ timer = CreateObject("roTimeSpan")
+ m.top.posterBlurHashUrl = renderFakeBlurhash(blurhash, imgParams.maxWidth, imgParams.maxHeight)
+ print "Took " + Str(timer.totalMilliseconds()) + " milliseconds to render a blurhash in PersonData."
+ end if
+ end if
else if m.top.json.BackdropImageTags[0] <> invalid
imgParams = { "maxHeight": 440, "Tag": m.top.json.BackdropImageTags[0] }
m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
diff --git a/components/data/PhotoData.bs b/components/data/PhotoData.bs
index 30443f3df..fe6fa977c 100644
--- a/components/data/PhotoData.bs
+++ b/components/data/PhotoData.bs
@@ -1,6 +1,7 @@
import "pkg:/source/api/Image.bs"
import "pkg:/source/api/baserequest.bs"
import "pkg:/source/utils/config.bs"
+import "pkg:/source/utils/fakeBlurhash.bs"
sub setFields()
json = m.top.json
@@ -20,6 +21,14 @@ sub setPoster()
if m.top.json.ImageTags.Primary <> invalid
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+ if isValidAndNotEmpty(m.top.json.ImageBlurHashes.Primary)
+ blurhash = m.top.json.ImageBlurHashes.Primary[m.top.json.ImageTags.Primary]
+ if get_user_setting("ui.design.renderblurhashes") = "true" and isValidAndNotEmpty(blurhash)
+ timer = CreateObject("roTimeSpan")
+ m.top.posterBlurHashUrl = renderFakeBlurhash(blurhash, imgParams.maxWidth, imgParams.maxHeight)
+ print "Took " + Str(timer.totalMilliseconds()) + " milliseconds to render a blurhash in PhotoData."
+ end if
+ end if
else if m.top.json.BackdropImageTags[0] <> invalid
imgParams = { "maxHeight": 440, "Tag": m.top.json.BackdropImageTags[0] }
m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
diff --git a/components/data/ScheduleProgramData.bs b/components/data/ScheduleProgramData.bs
index c27126208..0b70e4fcb 100644
--- a/components/data/ScheduleProgramData.bs
+++ b/components/data/ScheduleProgramData.bs
@@ -1,6 +1,7 @@
import "pkg:/source/api/Image.bs"
import "pkg:/source/api/baserequest.bs"
import "pkg:/source/utils/config.bs"
+import "pkg:/source/utils/fakeBlurhash.bs"
sub setFields()
json = m.top.json
@@ -42,6 +43,14 @@ sub setPoster()
if m.top.json.ImageTags <> invalid and m.top.json.ImageTags.Thumb <> invalid
imgParams = { "maxHeight": 500, "maxWidth": 500, "Tag": m.top.json.ImageTags.Thumb }
m.top.posterURL = ImageURL(m.top.json.id, "Thumb", imgParams)
+ if isValidAndNotEmpty(m.top.json.ImageBlurHashes.Primary)
+ blurhash = m.top.json.ImageBlurHashes.Primary[m.top.json.ImageTags.Primary]
+ if get_user_setting("ui.design.renderblurhashes") = "true" and isValidAndNotEmpty(blurhash)
+ timer = CreateObject("roTimeSpan")
+ m.top.posterBlurHashUrl = renderFakeBlurhash(blurhash, imgParams.maxWidth, imgParams.maxHeight)
+ print "Took " + Str(timer.totalMilliseconds()) + " milliseconds to render a blurhash in ScheduleProgramData."
+ end if
+ end if
end if
end if
end sub
diff --git a/components/data/SeriesData.bs b/components/data/SeriesData.bs
index f3671f381..74f3a484e 100644
--- a/components/data/SeriesData.bs
+++ b/components/data/SeriesData.bs
@@ -1,6 +1,7 @@
import "pkg:/source/api/Image.bs"
import "pkg:/source/api/baserequest.bs"
import "pkg:/source/utils/config.bs"
+import "pkg:/source/utils/fakeBlurhash.bs"
sub setFields()
json = m.top.json
@@ -38,6 +39,14 @@ sub setPoster()
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+ if isValidAndNotEmpty(m.top.json.ImageBlurHashes.Primary)
+ blurhash = m.top.json.ImageBlurHashes.Primary[m.top.json.ImageTags.Primary]
+ if get_user_setting("ui.design.renderblurhashes") = "true" and isValidAndNotEmpty(blurhash)
+ timer = CreateObject("roTimeSpan")
+ m.top.posterBlurHashUrl = renderFakeBlurhash(blurhash, imgParams.maxWidth, imgParams.maxHeight)
+ print "Took " + Str(timer.totalMilliseconds()) + " milliseconds to render a blurhash in SeriesData."
+ end if
+ end if
else if m.top.json.BackdropImageTags <> invalid
imgParams = { "maxHeight": 440, "Tag": m.top.json.BackdropImageTags[0] }
m.top.posterURL = ImageURL(m.top.json.id, "Backdrop", imgParams)
diff --git a/components/data/TVEpisode.bs b/components/data/TVEpisode.bs
index 6c0542c0c..126d24155 100644
--- a/components/data/TVEpisode.bs
+++ b/components/data/TVEpisode.bs
@@ -1,6 +1,7 @@
import "pkg:/source/api/Image.bs"
import "pkg:/source/api/baserequest.bs"
import "pkg:/source/utils/config.bs"
+import "pkg:/source/utils/fakeBlurhash.bs"
sub setFields()
json = m.top.json
@@ -21,5 +22,13 @@ sub setPoster()
else if m.top.json.ImageTags.Primary <> invalid
imgParams = { "maxHeight": 440, "maxWidth": 295 }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+ if isValidAndNotEmpty(m.top.json.ImageBlurHashes.Primary)
+ blurhash = m.top.json.ImageBlurHashes.Primary[m.top.json.ImageTags.Primary]
+ if get_user_setting("ui.design.renderblurhashes") = "true" and isValidAndNotEmpty(blurhash)
+ timer = CreateObject("roTimeSpan")
+ m.top.posterBlurHashUrl = renderFakeBlurhash(blurhash, imgParams.maxWidth, imgParams.maxHeight)
+ print "Took " + Str(timer.totalMilliseconds()) + " milliseconds to render a blurhash in TVEpisode.bs."
+ end if
+ end if
end if
end sub
diff --git a/components/data/VideoData.bs b/components/data/VideoData.bs
index 1a7654035..3b07822a6 100644
--- a/components/data/VideoData.bs
+++ b/components/data/VideoData.bs
@@ -1,6 +1,7 @@
import "pkg:/source/api/Image.bs"
import "pkg:/source/api/baserequest.bs"
import "pkg:/source/utils/config.bs"
+import "pkg:/source/utils/fakeBlurhash.bs"
sub setFields()
json = m.top.json
@@ -21,5 +22,13 @@ sub setPoster()
else if m.top.json.ImageTags.Primary <> invalid
imgParams = { "maxHeight": 440, "maxWidth": 295, "Tag": m.top.json.ImageTags.Primary }
m.top.posterURL = ImageURL(m.top.json.id, "Primary", imgParams)
+ if isValidAndNotEmpty(m.top.json.ImageBlurHashes.Primary)
+ blurhash = m.top.json.ImageBlurHashes.Primary[m.top.json.ImageTags.Primary]
+ if get_user_setting("ui.design.renderblurhashes") = "true" and isValidAndNotEmpty(blurhash)
+ timer = CreateObject("roTimeSpan")
+ m.top.posterBlurHashUrl = renderFakeBlurhash(blurhash, imgParams.maxWidth, imgParams.maxHeight)
+ print "Took " + Str(timer.totalMilliseconds()) + " milliseconds to render a blurhash in VideoData."
+ end if
+ end if
end if
end sub
diff --git a/settings/settings.json b/settings/settings.json
index 9c33a6b01..1d6da3cf0 100644
--- a/settings/settings.json
+++ b/settings/settings.json
@@ -218,6 +218,13 @@
"type": "integer",
"default": "365"
},
+ {
+ "title": "Render blurhashes",
+ "description": "Use an EXPERIMENTAL algorithm to render image blurhashes. This may slow down page loading and cause the app the crash.",
+ "settingName": "ui.design.renderblurhashes",
+ "type": "bool",
+ "default": "false"
+ },
{
"title": "Show What's New Popup",
"description": "Show What's New popup when Jellyfin is updated to a new version.",
diff --git a/source/utils/fakeBlurhash.bs b/source/utils/fakeBlurhash.bs
new file mode 100644
index 000000000..6b442136e
--- /dev/null
+++ b/source/utils/fakeBlurhash.bs
@@ -0,0 +1,182 @@
+' an implementation of the fake-blurhash decoding algorithm by Austin Crandall
+' https://github.com/sevenrats/fake-blurhash-python
+' based on the implementation in Brightscript by Neil Burrows
+' of Dag Agren's original specification https://github.com/woltapp/blurhash
+
+import "pkg:/source/utils/config.bs"
+import "pkg:/source/utils/librokudev.bs"
+
+' construct a bmp by hand in hex to avoid drawing
+function bitmapImageByteArray(numX as integer, numY as integer, pixels)
+ bmp = "424D4C000000000000001A0000000C000000"
+ bmp = bmp + rdINTtoHEX(numX) + "00" + rdINTtoHEX(numY) + "0001001800"
+ for r = 0 to numY - 1
+ row = ""
+ for c = 0 to numX - 1
+ row = pixels.RemoveTail() + row
+ ' to do: pad rows which are not 4 in length
+ end for
+ bmp = bmp + row
+ end for
+ bmp = bmp + "0000"
+ ba = CreateObject("roByteArray")
+ ba.fromhexstring(bmp)
+ return ba
+end function
+
+function decode83(str as string) as integer
+ digitCharacters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"
+ value = 0
+ for i = 0 to len(str) - 1
+ c = Mid(str, i + 1, 1)
+ digit = Instr(0, digitCharacters, c) - 1
+ value = value * 83 + digit
+ end for
+
+ return value
+
+end function
+
+function isBlurhashValid(blurhash as string) as boolean
+ if blurhash = invalid or len(blurhash) < 6
+ print "The blurhash string must be at least 6 characters"
+ return false
+ end if
+ sizeFlag = decode83(Mid(blurhash, 0, 1))
+ numY = Fix(sizeFlag / 9) + 1
+ numX = (sizeFlag mod 9) + 1
+ if len(blurhash) <> 4 + 2 * numX * numY
+ print "blurhash length mismatch: length is " + Str(len(blurhash)) + " but it should be " + Str(4 + 2 * numX * numY)
+ return false
+ end if
+ return true
+end function
+
+function sRGBToLinear(value as float)
+ v = value / 255
+
+ if v <= 0.04045
+ return v / 12.92
+ else
+ return ((v + 0.055) / 1.055) ^ 2.4
+ end if
+end function
+
+function decodeDC(value as integer)
+ intR = value >> 16
+ intG = (value >> 8) and 255
+ intB = value and 255
+
+ return [sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB)]
+
+end function
+
+function decodeAC(value as float, maximumValue as float)
+ quantR = Fix(value / (19 * 19))
+ quantG = Fix(value / 19) mod 19
+ quantB = value mod 19
+
+ rgb = [
+ signPow((quantR - 9) / 9, 2.0) * maximumValue,
+ signPow((quantG - 9) / 9, 2.0) * maximumValue,
+ signPow((quantB - 9) / 9, 2.0) * maximumValue
+ ]
+ return rgb
+end function
+
+
+function signPow(val as float, exp as float)
+
+ result = Abs(val)
+ for i = 1 to exp step 1
+ result = result * val
+ end for
+
+ return Sgn(val) * val ^ exp
+
+end function
+
+function linearTosRGB(value as float)
+
+ v = value
+
+ if value < 0
+ v = 0
+ else if value > 1
+ v = 1
+ end if
+
+ if v <= 0.0031308
+ return rdINTtoHEX(Cint(v * 12.92 * 255 + 0.5))
+ else
+ return rdINTtoHEX(Cint((1.055 * (v ^ (1 / 2.4)) - 0.055) * 255 + 0.5))
+ end if
+end function
+
+' this function takes a blurhash, dimensions in terms of elements,
+' renders the blurhash using the fake-blurhash decoding algorithm
+' then returns a filesystem uri pointing at the file
+' the images in the fs are named with a hash of the blurhash string
+' so that items which already exist are not rendered twice
+function renderFakeBlurhash(blurhash as string, width as integer, height as integer, punch = 1 as float)
+ ' determine the file name
+ ' create the hasher and the bytestring for the hasher
+ blurhashByteArray = CreateObject("roByteArray")
+ blurhashByteArray.FromAsciiString(blurhash)
+ digest = CreateObject("roEVPDigest")
+ digest.Setup("md5")
+ digest.Update(blurhashByteArray)
+ filename = digest.Final()
+ localFileSystem = CreateObject("roFileSystem")
+ if localFileSystem.Exists("tmp://" + filename + ".bmp")
+ return "tmp://" + filename + ".bmp"
+ end if
+ ' doesn't exist in fs. render it.
+ if isBlurhashValid(blurhash) = false then return invalid
+ sizeFlag = decode83(Mid(blurhash, 1, 1))
+ numY = Fix(sizeFlag / 9) + 1
+ numX = (sizeFlag mod 9) + 1
+ quantisedMaximumValue = decode83(Mid(blurhash, 2, 1))
+ maximumValue = (quantisedMaximumValue + 1) / 166
+ colors = []
+ colorsLength = numX * numY
+ for i = 0 to colorsLength - 1
+ if i = 0
+ value = decode83(Mid(blurhash, 3, 4))
+ colors[i] = decodeDC(value)
+ else
+ value = decode83(Mid(blurhash, 5 + i * 2, 2))
+ colors[i] = decodeAC(value, maximumValue * punch)
+ end if
+ end for
+ pixels = CreateObject("roList")
+ for i = 1 to numX * numY
+ r = 0
+ g = 0
+ b = 0
+ row = cint(i / numX)
+ if i mod numX <> 0
+ column = i mod numX
+ else
+ column = numX
+ end if
+ row_height = height / numY
+ column_width = width / numX
+ x = (column_width / 2) + (column_width * (column - 1))
+ y = (row_height / 2) + (row_height * (row - 1))
+ for j = 0 to numY - 1
+ for n = 0 to numX - 1
+ basis = cos((3.14159265 * x * n) / width) * cos((3.14159265 * y * j) / height)
+ color = colors[n + j * numX]
+ r = r + color[0] * basis
+ g = g + color[1] * basis
+ b = b + color[2] * basis
+ end for
+ end for
+ pixel = linearTosRGB(b) + linearTosRGB(g) + linearTosRGB(r) ' our bitmap format wants bgr
+ pixels.push(pixel)
+ end for
+ ba = bitmapImageByteArray(numX, numY, pixels)
+ ba.WriteFile("tmp://" + filename + ".bmp")
+ return "tmp://" + filename + ".bmp"
+end function
diff --git a/source/utils/librokudev.bs b/source/utils/librokudev.bs
new file mode 100644
index 000000000..5c8e008ea
--- /dev/null
+++ b/source/utils/librokudev.bs
@@ -0,0 +1,51 @@
+'The functions in this file are taken from the librokudev library available
+'at https://github.com/sumitk/librokudev under the terms of its license,
+'available at https://github.com/sumitk/librokudev/blob/master/LICENSE,
+'which, at the time of writing, is as follows:
+
+'Copyright (c) 2010, GandK Labs. All rights reserved.
+
+'Redistribution and use in source and binary forms, with or without
+'modification, are permitted provided that the following conditions are met:
+' * Redistributions of source code must retain the above copyright
+' notice, this list of conditions and the following disclaimer.
+' * Redistributions in binary form must reproduce the above copyright
+' notice, this list of conditions and the following disclaimer in the
+' documentation and/or other materials provided with the distribution.
+' * Neither the GandK Labs name, the libRokuDev name, nor the
+' names of its contributors may be used to endorse or promote products
+' derived from this software without specific prior written permission.
+
+'THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+'ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+'WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+'DISCLAIMED. IN NO EVENT SHALL GANDK LABS BE LIABLE FOR ANY
+'DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+'(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+'LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+'ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+'(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+'SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+function rdRightShift(num as integer, count = 1 as integer) as integer
+ mult = 2 ^ count
+ summand = 1
+ total = 0
+ for i = count to 31
+ if num and summand * mult
+ total = total + summand
+ end if
+ summand = summand * 2
+ end for
+ return total
+end function
+
+function rdINTtoHEX(num as integer) as object
+ ba = CreateObject("roByteArray")
+ ba.setresize(4, false)
+ ba[0] = rdRightShift(num, 24)
+ ba[1] = rdRightShift(num, 16)
+ ba[2] = rdRightShift(num, 8)
+ ba[3] = num ' truncates
+ return ba.toHexString().Right(2)
+end function