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