Compare commits

...

11 Commits

Author SHA1 Message Date
956a979204 Added deprecated notices
Signed-off-by: Marcus Noble <github@marcusnoble.co.uk>
2023-06-02 14:58:26 +01:00
cfd9c7292c Fixed double dash in tweets
Signed-off-by: Marcus Noble <github@marcusnoble.co.uk>
2022-11-05 09:10:37 +00:00
7f14289760 Tweak text sizing slightly
Signed-off-by: Marcus Noble <github@marcusnoble.co.uk>
2022-02-21 16:57:27 +00:00
091f6a455e Added caching 2021-09-04 16:41:15 +01:00
d061860fa2 fix: update release task to restart deployment 2021-07-24 11:20:38 +01:00
afc1cee5d7 feat: added opengraph image 2021-07-24 11:19:44 +01:00
749758fca4 feat: support quoted tweets 2021-07-24 10:52:07 +01:00
607f063abc docs: added example tweets 2021-07-24 10:12:22 +01:00
8be9ddc40a fix: tweaked sizes to better fit svg 2021-07-24 10:12:06 +01:00
f188873c40 fix: only update image if textbox populated 2021-07-24 06:03:59 +01:00
651f3683d2 feat: handle tweets from suspended accounts 2021-07-24 06:02:47 +01:00
9 changed files with 288 additions and 104 deletions

View File

@@ -47,7 +47,7 @@ ci:
.PHONY: release # Release the latest version of the application
release:
@kubectl --namespace tweetsvg set image deployment tweetsvg web=docker.cluster.fun/averagemarcus/tweetsvg:$(SHA)
@kubectl --namespace tweetsvg rollout restart deployment tweetsvg
.PHONY: help # Show this list of commands
help:

View File

@@ -1,3 +1,8 @@
# ⚠️ DEPRECATED
# Twitter has killed off its free API so this no longer works
---
![TweetSVG](logo.png)
Generate an SVG for a given Tweet ID

View File

@@ -3,7 +3,7 @@ package main
import emoji "github.com/tmdvs/Go-Emoji-Utils"
var charWidths = map[string]float64{
" ": 4.9,
" ": 6,
"0": 14.333328247070312,
"1": 10.583328247070312,
"2": 13.433334350585938,
@@ -74,9 +74,10 @@ var charWidths = map[string]float64{
"%": 18.416671752929688,
"^": 9.683334350585938,
"*": 9.683334350585938,
"(": 7.883331298828125,
")": 7.883331298828125,
"(": 7.8,
")": 7.8,
"-": 10.300003051757812,
"—": 10.300003051757812,
"_": 9.73333740234375,
"=": 14.333328247070312,
"+": 14.333328247070312,
@@ -89,10 +90,11 @@ var charWidths = map[string]float64{
"'": 5.26666259765625,
"\"": 9.5,
",": 6.26666259765625,
".": 6.26666259765625,
".": 5,
"/": 7.76666259765625,
"?": 11.649993896484375,
"`": 11.833328247070312,
"”": 9,
}
func getCharWidth(char string) float64 {

2
go.mod
View File

@@ -11,6 +11,8 @@ require (
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 // indirect
github.com/grokify/html-strip-tags-go v0.0.1
github.com/joho/godotenv v1.3.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/rivo/uniseg v0.2.0
github.com/tmdvs/Go-Emoji-Utils v1.1.0
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
)

4
go.sum
View File

@@ -14,6 +14,10 @@ github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q
github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/tmdvs/Go-Emoji-Utils v1.1.0 h1:gtPix7HZPrd49+MNDcuRLvv4xVNxCE5wgjqyuvmbyYg=
github.com/tmdvs/Go-Emoji-Utils v1.1.0/go.mod h1:J82i2WeGn+Kz+T3s5v9+i/OJlvevIVfGZ6qXgqiNWBc=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=

View File

@@ -13,9 +13,10 @@
<meta property="og:url" content="https://tweet.cluster.fun">
<meta property="og:description" content="Generate an SVG for a given Tweet ID">
<meta property="og:type" content="website">
<meta property="og:image" content="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSItNTAwIC0xMDAgMTUxMiA3MTIiPgogIDxnPgogICAgPHBhdGggZD0iTTYxIDE0MGMtOCAwLTE0LTYtMTQtMTRWNjFjMC04IDYtMTQgMTQtMTRoNjVhMTQgMTQgMCAxMTAgMjhINzV2NTFjMCA4LTYgMTQtMTQgMTR6IiBmaWxsPSIjZWY3MWE4Ii8+CiAgICA8cGF0aCBkPSJNNDUxIDQ2NWgtNjVhMTQgMTQgMCAxMTAtMjhoNTF2LTUxYTE0IDE0IDAgMTEyOCAwdjY1YzAgOC02IDE0LTE0IDE0eiIgZmlsbD0iI2VmNzFhOCIvPgogICAgPHBhdGggZD0iTTQ5OCA1MTJIMTRjLTggMC0xNC02LTE0LTE0VjE0QzAgNiA2IDAgMTQgMGg0ODRjOCAwIDE0IDYgMTQgMTR2NDg0YzAgOC02IDE0LTE0IDE0ek0yOCA0ODRoNDU2VjI4SDI4djQ1NnoiIGZpbGw9IiNlZjcxYTgiLz4KICA8L2c+CiAgPHBhdGggZD0iTTM2OSAyNjZjOC0yMSAxMi00NCAxMi02NXYtMWMwLTQgMi03IDUtOSAxMC05IDI2LTMwIDI2LTMwbC00MC02Yy0yIDAtMTItMTEtMTQtMTJhNjggNjggMCAwMC02Ni0xNGMtMjIgNy0zOSAyNS00NCA0Ny0yIDgtMyAxNy0yIDI1djJhMyAzIDAgMDEtMiAxIDE3OCAxNzggMCAwMS0xMjItNjVjLTItMi01LTItNiAwYTY4IDY4IDAgMDAxMSA4MmMtNS0xLTExLTMtMTUtNi0zLTEtNiAxLTYgMyAwIDI4IDE2IDUyIDQwIDYzYTcwIDcwIDAgMDEtMTMtMWMtMy0xLTUgMi00IDQgOCAyNCAyOSA0MiA1MyA0Ni0yMCAxNC00NCAyMS02OSAyMWgtOGMtMyAwLTUgMi01IDQtMSAzIDAgNSAyIDYgMjkgMTcgNjEgMjUgOTQgMjVhMTc2IDE3NiAwIDAwMTM4LTYxIiBmaWxsPSIjNzFjYWQxIi8+CiAgPHBhdGggZD0iTTE5NiA0MDFjLTM2IDAtNzAtMTAtMTAxLTI4LTctNC0xMS0xMy05LTIxIDItOSAxMC0xNSAxOS0xNWg4YzEyIDAgMjQtMiAzNS01YTgyIDgyIDAgMDEtMzMtNDRjLTEtMy0xLTggMS0xMWE4MiA4MiAwIDAxLTI0LTU5YzAtNSAzLTEwIDctMTRhODIgODIgMCAwMTUtNzIgMTggMTggMCAwMTI5LTIgMTY0IDE2NCAwIDAwOTkgNThsMi0xNWE4MCA4MCAwIDAxNzktNjIgODIgODIgMCAwMTU3IDIzbDcgNyAzOCA2YTE0IDE0IDAgMDE5IDIzYy0yIDItMTcgMjEtMjkgMzEgMCAyMy00IDQ3LTEzIDcwYTE0IDE0IDAgMTEtMjYtMTBjNy0yMCAxMS00MCAxMS02MHYtMWMwLTggNC0xNSAxMC0yMGw5LTktMTUtMmMtNi0xLTExLTUtMjEtMTRsLTEtMmE1MyA1MyAwIDAwLTUzLTExYy0xNyA2LTMwIDIwLTM0IDM3LTIgNy0yIDE0LTIgMjBhMTcgMTcgMCAwMS0xOCAxOSAxOTEgMTkxIDAgMDEtMTIwLTU3IDU0IDU0IDAgMDAxNSA1MCAxNCAxNCAwIDAxLTE0IDI0YzQgMTQgMTUgMjYgMjkgMzNhMTQgMTQgMCAwMS0yIDI2YzggMTIgMjEgMjAgMzUgMjJhMTQgMTQgMCAwMTUgMjZjLTE1IDEwLTMyIDE3LTUwIDIxYTE3NCAxNzQgMCAwMDEyOS02YzIxLTEwIDM5LTIzIDU0LTQxYTE0IDE0IDAgMTEyMSAxOSAxOTAgMTkwIDAgMDEtMTQ4IDY2em00OS0yMTF6bTEyNy0yMXptLTI2MS0yMXptMjYyLTh6IiBmaWxsPSIjZWY3MWE4Ii8+Cjwvc3ZnPgo=">
<meta name="twitter:card" content="summary" />
<meta property="og:image" content="https://opengraph.cluster.fun/opengraph/?siteTitle=&title=&tags=&image=https%3A%2F%2Fcdn.githubraw.com%2FAverageMarcus%2Ftweetsvg%2Fmaster%2Flogo.png&twitter=Marcus_Noble_&github=AverageMarcus%2Ftweetsvg&website=https%3A%2F%2Ftweet.cluster.fun&bgColor=%23ffffff&fgColor=%23263943">
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:creator" content="@Marcus_Noble_" />
<meta name="twitter:image" content="https://opengraph.cluster.fun/opengraph/?siteTitle=&title=&tags=&image=https%3A%2F%2Fcdn.githubraw.com%2FAverageMarcus%2Ftweetsvg%2Fmaster%2Flogo.png&twitter=Marcus_Noble_&github=AverageMarcus%2Ftweetsvg&website=https%3A%2F%2Ftweet.cluster.fun&bgColor=%23ffffff&fgColor=%23263943">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
@@ -34,6 +35,24 @@
textarea {
height: 200px;
}
#examples {
align-items: start;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
#examples figure {
margin: 2px;
margin-bottom: 50px;
}
figcaption {
text-align: center;
}
.announcement {
font-size: 3em;
font-weight: bold;
}
</style>
</head>
<body>
@@ -47,6 +66,10 @@
Generate an SVG for a given Tweet ID
</blockquote>
<p class="announcement">
⚠️ DEPRECATED - Twitter has killed off its free API so this no longer works
</p>
<p>
Enter the URL or ID of a tweet to have an SVG generated for it, no JavaScript required!
</p>
@@ -77,6 +100,44 @@
</form>
</div>
<div>
<h3>Example Tweets</h3>
<div id="examples">
<figure>
<img src="/1285788484280545280" />
<figcaption>Text with Emoji</figcaption>
</figure>
<figure>
<img src="/1417089885647761409" />
<figcaption>Tweet with 1 image</figcaption>
</figure>
<figure>
<img src="/1400358030869372928" />
<figcaption>Tweet with 2 images</figcaption>
</figure>
<figure>
<img src="/1401103668267556868" />
<figcaption>Tweet with 3 images</figcaption>
</figure>
<figure>
<img src="/1396802436052836360" />
<figcaption>Tweet with 4 images</figcaption>
</figure>
<figure>
<img src="/1370521139684970496" />
<figcaption>Tweet with quoted Tweet</figcaption>
</figure>
<figure>
<img src="/1281166762394791937" />
<figcaption>Tweet with link</figcaption>
</figure>
<figure>
<img src="/1396979104944201741" />
<figcaption>Reply Tweet</figcaption>
</figure>
</div>
</div>
<div>
Source code available on <a href="https://github.com/AverageMarcus/tweetsvg" target="_blank" rel="noopener noreferrer">GitHub</a>, <a href="https://gitlab.com/AverageMarcus/tweetsvg" target="_blank" rel="noopener noreferrer">GitLab</a>, <a href="https://bitbucket.org/AverageMarcus/tweetsvg/" target="_blank" rel="noopener noreferrer">Bitbucket</a> & <a href="https://git.cluster.fun/AverageMarcus/tweetsvg" target="_blank" rel="noopener noreferrer">my own Gitea server</a>.
</div>
@@ -106,9 +167,11 @@
}
document.getElementById('tweetURL').addEventListener('change', function(e) {
if (e.target.value.trim() != "") {
let parts = e.target.value.split("/");
let tweetID = parts[parts.length-1];
loadTweet(tweetID);
}
});
document.getElementById('imageTagExample').addEventListener('click', function(e) {

180
main.go
View File

@@ -7,6 +7,7 @@ import (
"fmt"
"html/template"
"log"
"math"
"net/http"
"os"
"regexp"
@@ -17,9 +18,12 @@ import (
"github.com/ChimeraCoder/anaconda"
strip "github.com/grokify/html-strip-tags-go"
"github.com/joho/godotenv"
"github.com/patrickmn/go-cache"
"github.com/rivo/uniseg"
emoji "github.com/tmdvs/Go-Emoji-Utils"
)
//go:embed index.html tweet.svg.tmpl
//go:embed index.html tweet.svg.tmpl suspendedTweet.svg
var content embed.FS
@@ -33,6 +37,8 @@ var (
accessTokenSecret string
consumerKey string
consumerSecret string
ch *cache.Cache
)
func init() {
@@ -43,6 +49,8 @@ func init() {
accessTokenSecret = os.Getenv("ACCESS_TOKEN_SECRET")
consumerKey = os.Getenv("CONSUMER_KEY")
consumerSecret = os.Getenv("CONSUMER_SECRET")
ch = cache.New(24*time.Hour, 48*time.Hour)
}
func main() {
@@ -75,26 +83,58 @@ func getTweet(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(400)
return
}
result, found := ch.Get(id)
if !found {
fmt.Println("No cached tweet found, generating new...")
tweet, err := api.GetTweet(i, nil)
if err != nil {
switch err := err.(type) {
case *anaconda.ApiError:
switch err.Decoded.Errors[0].Code {
case 63:
fmt.Printf("Generating suspended tweet image for %s\n", id)
suspendedTweet(w)
return
}
}
fmt.Println(err)
w.WriteHeader(404)
return
}
re := regexp.MustCompile(`[\x{1F300}-\x{1F6FF}]`)
emojis := re.FindAllString(tweet.FullText, -1)
processTweet(&tweet)
emojiCount := 0
for _, emoji := range emojis {
emojiCount += len([]byte(emoji)) - 1
result = renderTemplate(tweet, false)
ch.Set(id, result, cache.DefaultExpiration)
}
tweet.FullText = tweet.FullText[tweet.DisplayTextRange[0] : tweet.DisplayTextRange[1]+emojiCount]
w.Header().Set("Content-type", "image/svg+xml")
_, err = w.Write(result.([]byte))
if err != nil {
fmt.Println(err)
w.WriteHeader(500)
return
}
}
func processTweet(tweet *anaconda.Tweet) {
gr := uniseg.NewGraphemes(tweet.FullText)
count := 0
displayText := ""
for gr.Next() {
if count >= tweet.DisplayTextRange[0] && count < tweet.DisplayTextRange[1] {
displayText += gr.Str()
}
count += 1
}
tweet.FullText = displayText
for _, user := range tweet.Entities.User_mentions {
tweet.FullText = strings.ReplaceAll(tweet.FullText, "@"+user.Screen_name, fmt.Sprintf("<a rel=\"noopener\" target=\"_blank\" href=\"https://twitter.com/%s/\">@%s</a>", user.Screen_name, user.Screen_name))
}
for _, url := range tweet.Entities.Urls {
tweet.FullText = strings.ReplaceAll(tweet.FullText, url.Url, fmt.Sprintf("<a rel=\"noopener\" target=\"_blank\" href=\"https://twitter.com/%s/\">%s</a>", url.Expanded_url, url.Display_url))
tweet.FullText = strings.ReplaceAll(tweet.FullText, url.Url, fmt.Sprintf("<a rel=\"noopener\" target=\"_blank\" href=\"%s\">%s</a>", url.Expanded_url, url.Display_url))
}
for _, hashtag := range tweet.Entities.Hashtags {
tweet.FullText = strings.ReplaceAll(tweet.FullText, "#"+hashtag.Text, fmt.Sprintf("<a rel=\"noopener\" target=\"_blank\" href=\"https://twitter.com/hashtag/%s\">#%s</a>", hashtag.Text, hashtag.Text))
@@ -102,6 +142,13 @@ func getTweet(w http.ResponseWriter, r *http.Request) {
tweet.FullText = strings.ReplaceAll(tweet.FullText, "\n", "<br />")
if tweet.QuotedStatus != nil {
processTweet(tweet.QuotedStatus)
}
}
func renderTemplate(tweet anaconda.Tweet, isQuoted bool) []byte {
templateFuncs := template.FuncMap{
"base64": func(url string) string {
res, err := http.Get(url)
@@ -124,15 +171,60 @@ func getTweet(w http.ResponseWriter, r *http.Request) {
return template.HTML(in)
},
"calculateHeight": func(tweet anaconda.Tweet) string {
height := 64.0 /* Avatar */ + 20 /* footer */ + 46 /* test margin */ + 32 /* margin */
return fmt.Sprintf("%dpx", calculateHeight(tweet))
},
"renderTweet": func(tweet anaconda.Tweet) template.HTML {
return template.HTML(string(renderTemplate(tweet, true)))
},
"tweetWidth": func() string {
if isQuoted {
return "450px"
}
return "499px"
},
"className": func() string {
if isQuoted {
return "subtweet"
}
return "tweetsvg"
},
}
t := template.Must(
template.New("tweet.svg.tmpl").
Funcs(templateFuncs).
ParseFS(content, "tweet.svg.tmpl"))
var buf bytes.Buffer
t.Execute(&buf, tweet)
return buf.Bytes()
}
func calculateHeight(tweet anaconda.Tweet) int64 {
height := 64.0 /* Avatar */ + 20 /* footer */ + 46 /* text margin */ + 22 /* margin */
lineWidth := 0.0
tweetText := strings.ReplaceAll(tweet.FullText, "<br /><br />", " \n ")
lineHeight := 28.0
tweetText := strings.ReplaceAll(tweet.FullText, "<br />", " \n")
tweetText = strip.StripTags(tweetText)
words := strings.Split(tweetText, " ")
tweetText = strings.ReplaceAll(tweetText, "--", "——")
words := regexp.MustCompile(`[ |-]`).Split(tweetText, -1)
for _, word := range words {
word = strings.ReplaceAll(word, "——", "--")
if len(emoji.FindAll(word)) > 0 {
lineHeight = 32.0
}
if strings.HasPrefix(word, "\n") {
height += lineHeight
lineWidth = 0
word = strings.TrimPrefix(word, "\n")
}
if strings.Contains(word, "\n") {
height += 28
height += lineHeight
lineHeight = 28.0
lineWidth = 0
continue
}
@@ -143,52 +235,62 @@ func getTweet(w http.ResponseWriter, r *http.Request) {
wordWidth += getCharWidth(char)
}
if lineWidth+wordWidth > 443 {
height += 28
if wordWidth > 435 {
height += (lineHeight * (math.Ceil(wordWidth/435) + 1))
lineHeight = 28.0
lineWidth = 0
} else if lineWidth+getCharWidth(" ")+wordWidth > 435 {
height += lineHeight
lineHeight = 28.0
lineWidth = wordWidth
} else {
lineWidth += wordWidth
}
}
if lineWidth > 0 {
height += 28
height += lineHeight
}
if tweet.InReplyToScreenName != "" {
height += 34
height += 42
}
height += float64(strings.Count(tweet.FullText, "<br /><br />") * 28)
if len(tweet.ExtendedEntities.Media) >= 1 {
ratio := float64(tweet.ExtendedEntities.Media[0].Sizes.Small.W) / 464
if len(tweet.ExtendedEntities.Media) == 2 {
height += ((float64(tweet.ExtendedEntities.Media[0].Sizes.Small.H) / ratio) + 5) / 2
} else {
height += (float64(tweet.ExtendedEntities.Media[0].Sizes.Small.H) / ratio) + 5
}
for i, img := range tweet.ExtendedEntities.Media {
ratio := float64(img.Sizes.Small.W) / 468
tweet.ExtendedEntities.Media[i].Sizes.Small.W = 468
tweet.ExtendedEntities.Media[i].Sizes.Small.H = int((float64(img.Sizes.Small.H) / ratio) + 5.0)
}
if len(tweet.ExtendedEntities.Media) > 1 {
for i := range tweet.ExtendedEntities.Media {
tweet.ExtendedEntities.Media[i].Sizes.Small.W = 225
for i, img := range tweet.ExtendedEntities.Media {
tweet.ExtendedEntities.Media[i].Sizes.Small.W = (img.Sizes.Small.W / 2) - 20
tweet.ExtendedEntities.Media[i].Sizes.Small.H = (img.Sizes.Small.H / 2) - 20
}
}
return fmt.Sprintf("%dpx", int64(height))
},
switch len(tweet.ExtendedEntities.Media) {
case 1:
height += float64(tweet.ExtendedEntities.Media[0].Sizes.Small.H)
case 2:
height += math.Max(float64(tweet.ExtendedEntities.Media[0].Sizes.Small.H), float64(tweet.ExtendedEntities.Media[1].Sizes.Small.H)) + 5
case 3:
height += math.Max(float64(tweet.ExtendedEntities.Media[0].Sizes.Small.H), float64(tweet.ExtendedEntities.Media[1].Sizes.Small.H)) + 5
height += float64(tweet.ExtendedEntities.Media[2].Sizes.Small.H) + 35
case 4:
height += math.Max(float64(tweet.ExtendedEntities.Media[0].Sizes.Small.H), float64(tweet.ExtendedEntities.Media[1].Sizes.Small.H)) + 10
height += math.Max(float64(tweet.ExtendedEntities.Media[2].Sizes.Small.H), float64(tweet.ExtendedEntities.Media[3].Sizes.Small.H)) + 10
height += 7
}
t := template.Must(
template.New("tweet.svg.tmpl").
Funcs(templateFuncs).
ParseFS(content, "tweet.svg.tmpl"))
if tweet.QuotedStatus != nil {
height += float64(calculateHeight(*tweet.QuotedStatus)) + 9
}
return int64(height)
}
func suspendedTweet(w http.ResponseWriter) {
w.Header().Set("Content-type", "image/svg+xml")
err = t.Execute(w, tweet)
if err != nil {
fmt.Println(err)
w.WriteHeader(500)
return
}
tweet, _ := content.ReadFile("suspendedTweet.svg")
w.Write(tweet)
}

1
suspendedTweet.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="566" height="46" viewBox="0 0 566 46"><rect width="100%" height="100%" fill="rgba(0, 0, 0, 0)"/><path vector-effect="non-scaling-stroke" stroke="#f7f9f9" stroke-width="20" stroke-linejoin="round" fill="#f7f9f9" d="M-50-50V50H50V-50z" transform="matrix(5.46 0 0 .26 283 23.17)"/><g><text font-family="'Open Sans', sans-serif" font-size="15" style="white-space:pre" stroke-width="0" fill="#536471" transform="translate(221.5 21.91)"><tspan x="-202.5" y="5.34" style="white-space:pre" font-size="17">This Tweet is from a suspended account.</tspan></text></g></svg>

After

Width:  |  Height:  |  Size: 652 B

View File

@@ -1,44 +1,49 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="499px" height="{{ calculateHeight . }}">
<foreignObject x="0" y="0" width="499px" height="100%" fill="#eade52">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{ tweetWidth }}" height="{{ calculateHeight . }}">
<foreignObject x="0" y="0" width="{{ tweetWidth }}" height="100%" fill="#eade52">
<style>
.tweetsvg{clear:none;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;}
.tweetsvg.text{font-size: 23px;}
a.tweetsvg{color: rgb(27, 149, 224); text-decoration:none;}
.tweetsvg a { color: #1da1f2; }
blockquote.tweetsvg{margin:1px; background-color:#fefefe; border-radius:2%; border-style:solid; border-width:.1em; border-color:#ddd; padding:1em; font-family:sans; width:29rem}
.avatar-tweetsvg{float:left; width:4rem; height:4rem; border-radius:50%;margin-right:.5rem;;margin-bottom:.5rem;border-style: solid; border-width:.1em; border-color:#ddd;}
h1.tweetsvg{margin:0;font-size:15px;text-decoration:none;color:#000;}
h2.tweetsvg{margin:0;font-size:15px;font-weight:normal;text-decoration:none;color:rgb(101, 119, 134);}
p.tweetsvg{font-size:1rem; clear:both;}
hr.tweetsvg{color:#ddd;}
.media-tweetsvg{border-radius:2%; max-width:100%;border-radius: 2%; border-style: solid; border-width: .1em; border-color: #ddd;}
time.tweetsvg{font-size:15px;margin:0;margin-left: 2px;padding-bottom:1rem;color:rgb(101, 119, 134);text-decoration:none;}
.tweetsvg.reply{font-size:15px;color:rgb(110, 118, 125);}
.tweetsvg.footer{display:block;}
.{{ className }}{clear:none;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;}
.{{ className }}.text{font-size: 23px;}
a.{{ className }}{color: rgb(27, 149, 224); text-decoration:none;}
.{{ className }} a { color: #1da1f2; }
blockquote.{{ className }}{margin:1px; background-color:#fefefe; border-radius:2%; border-style:solid; border-width:.1em; border-color:#ddd; padding:1em; font-family:sans; width:29rem}
blockquote.subtweet{width:26rem; padding:0.8em;}
.avatar-{{ className }}{float:left; width:4rem; height:4rem; border-radius:50%;margin-right:.5rem;;margin-bottom:.5rem;border-style: solid; border-width:.1em; border-color:#ddd;}
h1.{{ className }}{margin:0;font-size:15px;text-decoration:none;color:#000;}
h2.{{ className }}{margin:0;font-size:15px;font-weight:normal;text-decoration:none;color:rgb(101, 119, 134);}
p.{{ className }}{font-size:1rem; clear:both;}
hr.{{ className }}{color:#ddd;}
.media-{{ className }}{border-radius:2%; max-width:100%;border-radius: 2%; border-style: solid; border-width: .1em; border-color: #ddd;}
time.{{ className }}{font-size:15px;margin:0;margin-left: 2px;padding-bottom:1rem;color:rgb(101, 119, 134);text-decoration:none;}
.{{ className }}.reply{font-size:15px;color:rgb(110, 118, 125);}
.{{ className }}.footer{display:block;}
</style>
<blockquote class="tweetsvg" xmlns="http://www.w3.org/1999/xhtml">
<a rel="noopener" target="_blank" class="tweetsvg" href="https://twitter.com/{{ .User.ScreenName }}/"><img class="avatar-tweetsvg" alt="" src="data:image/jpeg;base64,{{ base64 .User.ProfileImageUrlHttps }}" /></a>
<blockquote class="{{ className }}" xmlns="http://www.w3.org/1999/xhtml">
<a rel="noopener" target="_blank" class="{{ className }}" href="https://twitter.com/{{ .User.ScreenName }}/"><img class="avatar-{{ className }}" alt="" src="data:image/jpeg;base64,{{ base64 .User.ProfileImageUrlHttps }}" /></a>
<a rel="noopener" target="_blank" class="tweetsvg" href="https://twitter.com/{{ .User.ScreenName }}/"><h1 class="tweetsvg">{{ .User.Name }}</h1></a>
<a rel="noopener" target="_blank" class="{{ className }}" href="https://twitter.com/{{ .User.ScreenName }}/"><h1 class="{{ className }}">{{ .User.Name }}</h1></a>
<a rel="noopener" target="_blank" class="tweetsvg" href="https://twitter.com/{{ .User.ScreenName }}/"><h2 class="tweetsvg">@{{ .User.ScreenName }}</h2></a>
<a rel="noopener" target="_blank" class="{{ className }}" href="https://twitter.com/{{ .User.ScreenName }}/"><h2 class="{{ className }}">@{{ .User.ScreenName }}</h2></a>
{{ if .InReplyToScreenName }}
<p class="tweetsvg reply">Replying to <a rel="noopener" target="_blank" href="https://twitter.com/{{ .InReplyToScreenName }}/">@{{ .InReplyToScreenName }}</a></p>
<p class="{{ className }} reply">Replying to <a rel="noopener" target="_blank" href="https://twitter.com/{{ .InReplyToScreenName }}/">@{{ .InReplyToScreenName }}</a></p>
{{ end }}
<p class="tweetsvg text">{{ html .FullText }}</p>
<p class="{{ className }} text">{{ html .FullText }}</p>
{{ if .QuotedStatus }}
{{ renderTweet .QuotedStatus }}
{{ end }}
{{ if .ExtendedEntities }}
{{ range .ExtendedEntities.Media }}
<a rel="noopener" target="_blank" href="{{ .Media_url_https }}"><img class="media-tweetsvg" width="{{ .Sizes.Small.W }}" src="data:image/jpeg;base64,{{ base64 .Media_url }}" alt="{{ .ExtAltText }}"/></a>
<a rel="noopener" target="_blank" href="{{ .Media_url_https }}"><img class="media-{{ className }}" width="{{ .Sizes.Small.W }}" src="data:image/jpeg;base64,{{ base64 .Media_url }}" alt="{{ .ExtAltText }}"/></a>
{{ end }}
{{ end }}
<a rel="noopener" target="_blank" class="tweetsvg footer" href="https://twitter.com/{{ .User.ScreenName }}/status/{{ .Id }}">
<time class="tweetsvg" datetime="{{ isoDate .CreatedAt }}">{{ humanDate .CreatedAt }}</time>
<a rel="noopener" target="_blank" class="{{ className }} footer" href="https://twitter.com/{{ .User.ScreenName }}/status/{{ .Id }}">
<time class="{{ className }}" datetime="{{ isoDate .CreatedAt }}">{{ humanDate .CreatedAt }}</time>
</a>
</blockquote>
</foreignObject>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB