Implement extRSAuth on SSRS v16.0, SQL Server 2025
The perils of 'minimizing' language in software development
I believe that a large part of the reason that the vast majority of software projects end up over budget and finish well past schedule is that all too often, both developers and product/project managers use minimizing language for things that turn out to not be anywhere near as minimal or as straightforward and simple as minimizing language makes them sound.
When discussing software development, especially when discussing the estimation of time required to complete work items, whenever you speak or hear a statement that contains the phrases, "it's just", "it's only", "it's simple", ''that's just boilerplate", "it's already baked in", "that's just [insert a design pattern while completely omitting the context, data structures and design logic the pattern will be applied to]", or in the Year of our Lord 2025: "ChatGPT will answer that"- run.
Run for the hills and do not return. I'm kidding. But do be very alert to phrases like these because they indicate a potential significant piece of your project that is being glossed over because someone has thought about it abstractly, but not in concrete (code implementation) terms.
This can be developers who are either over-confident and/or feel pressured to give low estimates so that the project schedule does not seem imperiled.
This can be managers who just haven't stepped into the code or discussed the logic enough with the developers to understand the complexity behind a series of words that describe a conceptual software design.
Under-promise and always account for unknowns which will lead to unforeseen roadblocks, detours and changes. If all goes according to plan (and it never does), you over-deliver on your over-estimates. If not, you have may have given yourself enough buffer to still meet the planned schedule and will have successfully accounted for the inevitable unknown.
If you are already on a schedule that is unrealistic and bound to not be met by the deadline, then all you can do is change scope (cut or delay features). If you insist on keeping all the planned features, and have the luxury of time, then you can only increase the time (lengthen the delivery schedule to a future date).
You can certainty try to keep the same calendar date for a release deadline and "just" throw more developers and managers at the project, hoping they can all work round the clock and in parallel to increase productivity, but this never works. Domain knowledge and a cadence of solid productivity and cooperation across teams takes a significant period of time for new hires to learn.
As a quote attributed to Warren Buffet says, "You can't produce a baby in one month by getting nine women pregnant."
And I repeat: Focus on keeping the main thing the main thing
When designing and project planning any significantly complex piece of software, all parties involved (the "stakeholders") must understand that a software project is not fixed- project schedules, planned features, and the human resources to implement the features are going to change.
Many thought that the move from top-heavy waterfall/SDLC-based approaches to Agile would solve this problem. But unfortunately, when Agile refuses to actually be "agile", the waterfall becomes an unnavigable white water rapid stream that is only slightly more conducive to building great things within a certain scheduled space of business time.
And in general, God bless a true Agile craftsmanship approach and all the time-tested statistical process control concepts it is based upon, but our software industry's hyper-reliance on "estimating" and "measuring" things that can unexpectedly and rapidly evolve (and oftentimes measuring the wrong things) does not jive with realistic long-term software planning objectives and almost to a "hyper-time-boxed project"- leads to the worst of all outcomes in the software business: mismanaged (or specifically "missed") expectations. Managment of expectations is everything.
Deliver the most critical parts of a customer's needs first and deliver them as a flawless piece of beautiful software. Working software- especially the end-to-end functioning of your application's most critical workflow- is paramount; everything else should follow from that and never get in its way.
You can iterate, make changes and add features later on.
Focus on outcomes over processes; lest you sink into the bog of minimizing language metastasized into maximally time-consuming (sometimes completely unnecessary) work items. And a project that seems to never get delivered or is (worse, because first impressions are everything...) delivered rife with show-stopping bugs.
You need to eat your own dog food
I recently realized how useless my custom .NET method, "Sonrai.ExtRS.ReferenceDataService.GetGoogleNews(string searchTerm)", really was.
I had referenced Sonrai.ExtRS via Nuget and began trying to use it to improve the display in scrolling news links, in another application I maintain, tickertapes.net.
It was useless! I mean a complete waste of code. It returned potentially useful data related to the searchTerm parameter- data from the GoogleNews API. But that is about it. It literally returned the entire XML response. 🤦♂️
I was parsing all this XML in the tickertapes.net app (why I do not know or care to remember), and so all the work necessary to wrangle the XML response to produce the needed news link collection was on the consumer of the Sonrai.ExtRS Nuget package- not good!
What the Nuget library should do, is all the work. What it should return, to be actually useful, is something structured like a collection of strings, each one representing a single news article, with the news headline being the text for the news article link.
And so I moved back to Sonrai.ExtRS to correct this unfortunate oversight.
public static async Task<List<string>> GetGoogleNewsWithLinks(string search)
{
HttpClient client = new HttpClient();
var content = await client.GetStringAsync(string.Format("https://news.google.com/rss/search?q={0}", search));
var parser = new HtmlParser();
var document = parser.ParseDocument(content);
var newsItems = document.All.Where(m => m.LocalName == "title").ToList();
var linkItems = document.All.Where(x => x.LocalName == "link").ToList();
var newsLinkItems = new List<string>();
for (int i = 0; i < newsItems.Count; i++)
{
newsLinkItems.Add("<a href='" + linkItems[i].NextSibling!.NodeValue + "' target='_blank'>" + newsItems[i].InnerHtml + "</a>");
}
return newsLinkItems;
}
And this provides the Nuget client with something it can actually use, "turnkey"/out-of-the-box.
The moral of the story is one that is as old as business and manufacturing: you must eat your own dog food. If there are problems or areas that need attention it is best that you find this information out before your product is released into the wild and a customer discovers the bug (or in this case, the uselessness), thus sullying your reputation as a business and software provider.
Test, test, test- always unit and/or integration test every user interaction and data movement for every story/path imaginable or support-able.
But there is nothing that replaces simply using your own product the way it is used by real users. And really using it for the purpose it was made. What you discover may help you shore up previously unknown problems and/or inspire you to make something useful that you would never otherwise think of, unless you were thinking from a user's perspective.
Reference: https://www.nuget.org/packages/Sonrai.ExtRS
PS: Imagine if James Newton-King never used NewtonSoft.Json for his own de/serializations? Or if Stack Overflow never used Dapper for the SO app/site? You need to use your creations right at the ground level (as a user/client) to verify that the functionality you designed in the abstract exactly matches how things will play out in concrete reality. Thoughts.. ðŸ’
How to use .NET User Secrets in MSTest classes
When developing unit and integration tests, we don't necessarily want to share the secret keys and values we use as credentials for APIs, databases, etc.
So similarly to how we implement User Secret functionality in Program.cs, we can implement User Secrets and set test class variables that use the secrets via the MSTest classes' constructor (ReferenceDataTests() below):
public static string upsId = "";
public static string upsSecret = "";
private IConfiguration _configuration { get; }
public ReferenceDataTests()
{
// set your API ids and secrets in UserSecrets (right-click project: "Manage User Secrets")
var builder = new ConfigurationBuilder()
.AddUserSecrets<ReferenceDataTests>();
_configuration = builder.Build();
var secretVals = _configuration.GetChildren().ToList();
upsId = secretVals.Where(x => x.Key == "upsId").First().Value!;
upsSecret = secretVals.Where(x => x.Key == "upsSecret").First().Value!;
}
An SSRS IFrame/CORS infinite redirect loop error and a quick and easy solution
If you are trying to render the SSRS ReportViewer control within an <iframe>, you may run into a CORS issue that manifests in a series of 302 (Found) responses and an infinite redirect loop between ReportViewer control (ReportViewer.aspx) and Logon.aspx.
As of SSRS 2022, without an explicit instruction to allow CORS, ReportViewer cannot be rendered within an <iframe> on an origin different than the origin of the report server.
If you are using custom authentication, the solution is easy enough. Just add cookieSameSite="None" and enableCrossAppRedirects="true" to the authentication <forms> tag in the report server's web.config.
<authentication mode="Forms">
<forms loginUrl="logon.aspx" name="sqlAuthCookie" cookieSameSite="None" timeout="60" path="/" enableCrossAppRedirects="true" requireSSL="true">
</forms>
</authentication>
app.UseCors(builder => builder
.WithOrigins("https://localhost", "https://[domain]")
.AllowAnyMethod()
.AllowAnyHeader());
Dichotomies
Apropos of nothing I thought about some interesting dichotomies and wanted to share my perspective. It is interesting how often there are effectively 2 sides to a coin.
Religious Faith/Religious Dogma - Some folks live the "spirit" of the Word and resist structure vs. others, who follow strict adherence to "the letter" of the Word.
Subjective/Objective - You have here opinion and feeling (the supernatural) vs. facts and laws of nature.
Romantic/Classical - similar to the Subjective/Objective dichotomy (see also, "Zen and the Art of Motorcycle Maintenance").
Liberal/Conservative - The ideology of inclusion and change vs. the ideology of exclusion and stasis.
Conglomerate/Individual Co. - Whereas many corporations sought to obtain economies of scale through M&A in 80s, 90s and 00s, General Electric proved- (sold GE Capital, spun off GE Healthcare, GE Aviation and GE Power into 3 individual new companies)- that the conglomerate model doesn't always stand the test of time.
Thin client/Fat client - It makes no sense to be all one or all the other (an overburdened SPA app or an inflexible server-side only app). But we keep moving between one (thin client terminals in the 70s and 80s) and the other (fat client Home PCs in the 90s and 00s). And we've moved back to thin client again with Azure, AWS and GPC and the omnipresence of SaaS. But at the same time, fat-client SPA apps are as popular as ever... I guess we have fat client UIs and thin client APIs.
Imperative/Declarative code - Instructions that read like a book vs. instructions that read like a mathematical proof.
Monolithic app/Microservices - a very heavy Swiss-Army knife vs. a bunch of lightweight kitchen knives.
Software/Hardware - Recipes vs. the raw food itself.
Socialism/Capitalism - The idea that everyone is the same and should be reduced to (or propped up by) exactly such vs. the idea that everyone is incompatibly unique and should be uniquely catered to and provided maximum personal freedoms that can- like assault weapons and ammunition- come at the expense of the greater community.
Urban/Rural - Those from the densely populated cities and suburbs vs. those from the sparsely populated country towns.
Introvert/Extrovert - He or she who expends a great deal of energy socializing and finds solace in silence vs. he or she who is energized by interpersonal social connection and is not comfortable alone for extended periods of time.
Centralization/Decentralization- Putting the heart of a system at the center with dependent, often necessarily-generic/homogenized nodes vs. putting the heart on the nodes themselves, at the expense of (losing) ease of simultaneous node (state) synchronization and a "single source of truth/golden records".
Code Readability vs. Code Golf
Code Golf: https://code-golf.io, https://codegolf.stackexchange.com (have fun, you can learn lots)
ASM, clear for some developers; not most:
Even complex languages can be simplified somewhat by using logical naming conventions to identify what the entities are and what operations are doing.
Clear and consistent naming conventions of all members within the code of an application go a long way for making your code open to collaboration and/or extension by others. In much the same way people prefer different kinds of poetry, we developers prefer certain programming languages or styles (.NET vs. Java vs. NodeJS vs Python vs. C and C++; Message Queues vs. RDBMS; OO vs. FP) more or less than others. We should however, as far as possible, attempt to make our code clearly formatted and named for anyone to easily interpret. We should be as clear and consistent as possible and only comment when explaining the behavior in variable or function names alone won't suffice.
o(char* x, char* y)
{
char* f=
"Old MacDonald had a farm, E-I-E-I-O,\n"
"And on that farm he had a %s%.13s"
"With a %s %s here and a %s %s there,\n"
"Here a %s, there a %s, everywhere a %s %s,\n"
"%.35s!\n";
printf(f,x,f+24,y,y,y,y,y,y,y,y,f);
}
A Timeless Book (by Jerry Fitzpatrick)
TL; DR;
This book gets right to the point of the common aspects required for building great software and is too valuable for any software development professional not to read; and it contains just 172 pages! 😃
Read this book.
Plan Before Implementing
Proper and properly named abstractions to match problem domain and purpose. Thoroughly documented and well-understood behavioral interactions among all accessible components and at minimum a solid high level understanding of all dependencies; know when not to re-invent the wheel and instead utilize a proven 3rd party or open source tool.
Keep it Small
YAGNI ("You ain't gonna need it") is a very real thing. Development teams should focus on working prototypes that can be ironed out for production vs. forever "ideal" implementations that never make it to an actual user's screen.
Write Clearly
Treat a rough draft as a rough draft by encouraging code reviews and frequent minor refactoring to achieve code clarity for the next developers who will inevitably read (and need to understand in order to change or extend) your logic in the future.
Prevent Bugs
Meticulously control scope and member access and understand all ramifications of behavioral code points within the application. Every area of variation/mutation/state change should be covered by tests that are both named and implemented to clearly demonstrate the function/class/behavior being tested.
All tests are important for confidence/assurance in ongoing development but TDD (writing tests before the implementation a la Kent Beck) is not a replacement for good, cohesive design. That being said, I think the TDD backwards approach can help start great design discussion among developers when the design is somewhat unclear (tests, particularly broken tests will expose the innards that most warrant discussing).
Make the Program Robust
Utilize agreed-upon code formatting and pattern implementation standards and non-antagonistic code reviews to ensure these standards are being followed.
Utilize information hiding and useful categorization and encapsulation that extends but does not lend itself to potential client issues.
Do not try/catch everything and log everything unless necessary for compliance or a particuliar initiative being monitored. Make Exceptions very visible (but exit the program gracefully if the exception is not "handleable") and integrate tests to prevent the same (preventable) issues from recurring. Instead use try/finally and let exceptions bubble up to the surface application code.
Exceptions that are allowed to be handled and logged hide problems in the code that need immediate attention.
Use solid CI/CD and automated testing that tests for easily definable application behaviors.
Prevent Excess Coupling
Favor atomic initialization: Initialize everything all at once versus incremental composition.
Discourage unnecessary extensibility points and instead expose what needs to be exposed with as little information sharing as possible.
Strive for immutable members wherever possible.
Cohesive, self-evident abstractions are of utmost importance.
SUMMARY
The concepts so succinctly covered in Timeless Laws of Software Development apply to all types of code (imperative or declarative) and all languages.
While I've read many great software books and all have helped me become a better developer, no text has struck me as so plainly obvious and concise at explaining good software design concepts in such an easy-to-grok manner.
For a worthwhile read, check out Timeless Laws of Software Development by Jerry Fitzpatrick
.NET 5+: the Future of .NET
With .NET 5 (and coming in hot down the pike, .NET 6), developers can use the languages (C#, F#, VB and C++/CLI) and framework (.NET) they are familiar with, to build applications that can run on a Windows, Linux or Mac OS. While .NET Core 1 and 2 lacked several expected features and felt fairly unsupportable, .NET 5 feels more familiar and addresses most issues (this is just the networking improvements made).
There do exist more-than-slightly-subtle differences. For instance ASP.NET Core Runtime 5 projects are structured differently and Global.asax.cs has been replaced with Program.cs and the accompanying Startup.cs which contain the Main entry point and provide spaces for application configuration methods that execute only when your program is initially run (this includes functions to respond to certain events like a unhandled/last chance exception handling, etc.).
public class Startup{public Startup(IConfiguration configuration){Configuration = configuration;}public IConfiguration Configuration { get; }// This method gets called by the runtime. Use this method to add services to the container.public void ConfigureServices(IServiceCollection services){services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true).AddEntityFrameworkStores<ApplicationDbContext>();services.AddControllersWithViews();services.AddRazorPages();services.AddMvc().AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);services.AddLocalization(options => options.ResourcesPath = "Resources");services.Configure<RequestLocalizationOptions>(options =>{options.SetDefaultCulture("en-US");options.AddSupportedCultures("en-US", "es-CO", "fr-FR");........
An example of .NET 5 ASP.NET Startup.cs which replaces most Global.asax.cs functionality; Program.cs handles all else
"Next Big" Software Religiosity and The Go-nowhere Rush
There is far too much religious extremism in information technology these days. And there have always been camps (extreme anti-Microsoft sentiment or its sad corporate counterpart: disdain, fear and suspicion of all things open-source)- but these days it has gotten to the point where sensible, cheap, reliable, proven solutions that everyone on the team understands- are thrown out in favor of chasing the next big thing that some bigshot at some big conference declared was going to be the next, next, next "big thing".
This image does have its merits..
ChartJS for Data Vizualiizations
You can do some really neat and dynamic stuff with ChartJS.
I have used a lot of charting frameworks, and it does not get more flexible or simple than this:
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script>
</head>
<body>
<div>
<canvas id="myChart" style='background-color:darkgray; width:100%; height:100%;'></canvas>
</div>
<script>
var ctx = document.getElementById('myChart').getContext('2d');;
var chart = new Chart(ctx, {
type: 'line',
data: {
labels: ['16_Qtr1', '16_Qtr2', '16_Qtr3', '16_Qtr4', '17_Qtr1', '17_Qtr2', '17_Qtr3', '17_Qtr4', '18_Qtr1', '18_Qtr2', '18_Qtr3', '18_Qtr4', '19_Qtr1', '19_Qtr2', '19_Qtr3', '19_Qtr4', '20_Qtr1', '20_Qtr2', '20_tr3', '20_Qtr4', '21_Qtr1', '21_Qtr2', '21_Qtr3', '21_Qtr4','22_Qtr1', '22_Qtr2', '22_Qtr3', '22_Qtr4', '23_Qtr1', '23_Qtr2', '23_tr3', '23_Qtr4'],
datasets: [{
label: 'Some random quartley demo data..',
backgroundColor: 'black',
borderColor: 'lime',
data: [40.2, 72.88, 47.1, 22, 54.43, 52.18, 17.1, 52, 67.2, 54.88, 64.1, 78, 67.2, 55.88, 58.1, 57, 50.2, 52.88, 57.1, 62, 74.43, 62.18, 67.1, 72, 77.2, 74.88, 74.1, 78, 77.2, 75.88, 78.1, 77, 70.2, 72.88, 77.1, 62, 64.43, 62.18, 67.1, 72, 67.2, 54.88, 44.1, 28, 27.2, 25.88, 38.1, 37, 40.2, 42.88, 44.1, 52, 54.43, 52.18, 67.1, 82, 87.2, 84.88, 84.1, 88, 87.2, 95.88, 108.1, 127]
}]
},
"options": {
"legend": {"position": "bottom"}
}
});
</script>
</body>
</html>
Reference: https://www.chartjs.org/
Over-engineering in Software
This is a great brief guide on how it happens and how to prevent or at least diminish the phenomena that is borne out of a desire to do things exact and right- but ignores that fact that some things never are evaluated and time is.... finite. (in (virtually) every project, we only have so much time to get the bells to ring and the whistles to whistle...
That interface to allow for a potential future Python SDK, putting a small proprietary API on Swagger with loads of documentation and test calls just "because" might not be a road you want to go down yet..
Creating a .NET Standard portable class library of the core functionality with a full suite of tests because you think it may be Nugetized in the future... is not something you should do until the app is already engineered and built well, out the door and humming along swimmingly!
Over-engineering is essentially development of designs and/or writing code that solves problems that do not currently exist.
Some people think design means, “Write down exactly how you’re going to implement an entire project, from here until 2050.” That won’t work, brother. That design is too rigid–it doesn’t allow your requirements to change. And believe me, your requirements are going to change.
Planning is good. I should probably do more planning myself, when it comes to writing code.
But even if you don’t write out detailed plans, you’ll be fine as long as your changes are always small and easily extendable for that unknown future.
-Max
TAKEAWAY:
- "Business requirements never converge. They always diverge."
- "At the beginning, it’s best to stick to the KISS principle over DRY."
- Favor less abstraction for things that are currently 100% exactly known
I am not a big fan of multi-purposing things. But sometimes it is the only way and in this case of a legacy system with an old reporting db, it was the only way for time we had available.
Build the app by the requirements as they currently exist, not by your imagination of how the future may or may not affect the app.
Two principals that have helped many a developer over the years are the acronyms:
- KISS (Keep It Simple)
- YAGNI (You Ain’t Gonna Need It)
Neat UI Effects with jQueryUI and Animate.CSS
It could not be simpler with jQuery UI and other open source options. Here is an easy sample to pick up and run with from jQuery.com:
jQuery UI Effects
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>jQuery UI Effects - Effect demo</title>
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<link rel="stylesheet" href="/resources/demos/style.css">
<style>
.toggler {
width: 500px;
height: 200px;
position: relative;
}
#button {
padding: .5em 1em;
text-decoration: none;
}
#effect {
width: 240px;
height: 170px;
padding: 0.4em;
position: relative;
}
#effect h3 {
margin: 0;
padding: 0.4em;
text-align: center;
}
.ui-effects-transfer {
border: 2px dotted gray;
}
</style>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script>
$(function () {
// run the currently selected effect
function runEffect() {
// get effect type from
var selectedEffect = $("#effectTypes").val();
// Most effect types need no options passed by default
var options = {};
// some effects have required parameters
if (selectedEffect === "scale") {
options = { percent: 50 };
} else if (selectedEffect === "transfer") {
options = { to: "#button", className: "ui-effects-transfer" };
} else if (selectedEffect === "size") {
options = { to: { width: 200, height: 60 } };
}
// Run the effect
$("#effect").effect(selectedEffect, options, 500, callback);
};
// Callback function to bring a hidden box back
function callback() {
setTimeout(function () {
$("#effect").removeAttr("style").hide().fadeIn();
}, 1000);
};
// Set effect from select menu value
$("#button").on("click", function () {
runEffect();
return false;
});
});
</script>
</head>
<body>
<div class="toggler" style="margin-top:200px;">
<div id="effect" class="ui-widget-content ui-corner-all">
<h3 class="ui-widget-header ui-corner-all">Effect</h3>
<p>
Etiam libero neque, luctus a, eleifend nec, semper at, lorem. Sed pede. Nulla lorem metus, adipiscing ut, luctus sed, hendrerit vitae, mi.
</p>
</div>
</div>
<select name="effects" id="effectTypes">
<option value="blind">Blind</option>
<option value="bounce">Bounce</option>
<option value="clip">Clip</option>
<option value="drop">Drop</option>
<option value="explode">Explode</option>
<option value="fade">Fade</option>
<option value="fold">Fold</option>
<option value="highlight">Highlight</option>
<option value="puff">Puff</option>
<option value="pulsate">Pulsate</option>
<option value="scale">Scale</option>
<option value="shake">Shake</option>
<option value="size">Size</option>
<option value="slide">Slide</option>
<option value="transfer">Transfer</option>
</select>
<button id="button" class="ui-state-default ui-corner-all">Run Effect</button>
</body>
</html>
And one I whipped up from Animate.css...:
<html>
<head>
<script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.2/animate.min.css">
<script>
$( document ).ready(function() {
myDiv = document.querySelector('#myDiv')
$("#buttonBounce").on("click", function () {
removeAllClasses();
myDiv.classList.add('animated', 'bounce')
});
$("#buttonSwing").on("click", function () {
removeAllClasses();
myDiv.classList.add('animated', 'swing')
});
$("#buttonWobble").on("click", function () {
removeAllClasses();
myDiv.classList.add('animated', 'wobble')
});
$("#buttonTada").on("click", function () {
removeAllClasses();
myDiv.classList.add('animated', 'tada')
});
$("#buttonBounceOut").on("click", function () {
removeAllClasses();
myDiv.classList.add('animated', 'bounceOutLeft')
});
function removeAllClasses(){
myDiv.classList.remove('animated', 'bounce', 'wobble', 'bounceOutLeft', 'tada', 'swing');
}
});
</script>
</head>
<body style="background:#000000;">
<div id="myDiv" style="margin-top:100px; margin-left:300px; width:250px; background:aqua; font-weight:bold; font-family:Arial; border: 2px solid lime; padding: 10px; border-radius: 25px;">~Something to animate~</h1>
<br/><br/>
<input type="button" id="buttonBounce" class="ui-state-default ui-corner-all" value="Bounce" />
</br><br/>
<input type="button" id="buttonTada" class="ui-state-default ui-corner-all" value="Tada" />
</br><br/>
<input type="button" id="buttonSwing" class="ui-state-default ui-corner-all" value="Swing" />
</br><br/>
<input type="button" id="buttonWobble" class="ui-state-default ui-corner-all" value="Wobble" />
</br><br/>
<input type="button" id="buttonBounceOut" class="ui-state-default ui-corner-all" value="Bounce Out" />
</br><br/>
</body>
</html>
Happy coding!
Codepens:
https://codepen.io/radagast27/pen/pooavXd
https://codepen.io/radagast27/pen/xxxYboM
References:
https://jqueryui.com/effect
https://github.com/daneden/animate.css
ASCII Art with Python
The script is from Paul Bourke. There is very clear commenting on each step. Essentially, an image loaded from the "--file" argument is divided into a grid and each position of the grid is assigned certain ASCII character whose gray scale level most closely matches the average for that area of the image. I simply modified line 146 and 150 to demonstrate on console vs. the outfile.txt default.
Save the following as ascii.py:
#Python code to convert an image to ASCII image.
import sys, random, argparse
import numpy as np
import math
from PIL import Image
# gray scale level values from:
# http://paulbourke.net/dataformats/asciiart/
# 70 levels of gray
gscale1 = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "
# 10 levels of gray
gscale2 = '@%#*+=-:. '
def getAverageL(image):
# get image as numpy array
im = np.array(image)
# get shape
w,h = im.shape
# get average
return np.average(im.reshape(w*h))
def covertImageToAscii(fileName, cols, scale, moreLevels):
# declare globals
global gscale1, gscale2
# open image and convert to grayscale
image = Image.open(fileName).convert('L')
# store dimensions
W, H = image.size[0], image.size[1]
print("input image dims: %d x %d" % (W, H))
# compute width of tile
w = W/cols
# compute tile height based on aspect ratio and scale
h = w/scale
# compute number of rows
rows = int(H/h)
print("cols: %d, rows: %d" % (cols, rows))
print("tile dims: %d x %d" % (w, h))
# check if image size is too small
if cols > W or rows > H:
print("Image too small for specified cols!")
exit(0)
# ascii image is a list of character strings
aimg = []
# generate list of dimensions
for j in range(rows):
y1 = int(j*h)
y2 = int((j+1)*h)
# correct last tile
if j == rows-1:
y2 = H
# append an empty string
aimg.append("")
for i in range(cols):
# crop image to tile
x1 = int(i*w)
x2 = int((i+1)*w)
# correct last tile
if i == cols-1:
x2 = W
# crop image to extract tile
img = image.crop((x1, y1, x2, y2))
# get average luminance
avg = int(getAverageL(img))
# look up ascii char
if moreLevels:
gsval = gscale1[int((avg*69)/255)]
else:
gsval = gscale2[int((avg*9)/255)]
# append ascii char to string
aimg[j] += gsval
# return txt image
return aimg
# main() function
def main():
# create parser
descStr = "This program converts an image into ASCII art."
parser = argparse.ArgumentParser(description=descStr)
# add expected arguments
parser.add_argument('--file', dest='imgFile', required=True)
parser.add_argument('--scale', dest='scale', required=False)
parser.add_argument('--out', dest='outFile', required=False)
parser.add_argument('--cols', dest='cols', required=False)
parser.add_argument('--morelevels',dest='moreLevels',action='store_true')
# parse args
args = parser.parse_args()
imgFile = args.imgFile
# set output file
outFile = 'out.txt'
if args.outFile:
outFile = args.outFile
# set scale default as 0.43 which suits
# a Courier font
scale = 0.43
if args.scale:
scale = float(args.scale)
# set cols
cols = 80
if args.cols:
cols = int(args.cols)
print('generating ASCII art...')
# convert image to ascii txt
aimg = covertImageToAscii(imgFile, cols, scale, args.moreLevels)
# open file
f = open(outFile, 'w')
# write to <console or> file ..f.write(row + '\n')
for row in aimg:
print(row + '\n')
# cleanup
f.close()
print("ASCII art written to console...") # %s" % outFile)
# call main
if __name__ == '__main__':
main()
Using Windows CMD, navigate to the directory where your ascii.py script is located. Then execute the following:
ascii.py --file C:\somedir\logo.jpg --cols 150
Reference: http://paulbourke.net/dataformats/asciiart/