Imagens com DPI alto para densidades de pixel variáveis

Um dos recursos do cenário complexo de dispositivos de hoje é que há uma variedade muito ampla de densidades de pixels de tela disponíveis. Alguns dispositivos têm telas de resolução muito alta, enquanto outros ficam atrás. Os desenvolvedores de aplicativos precisam oferecer suporte a uma variedade de densidades de pixels, o que pode ser bastante desafiador. Na Web para dispositivos móveis, os desafios são compostos por vários fatores:

  • Grande variedade de dispositivos com diferentes formatos.
  • Largura de banda de rede e duração da bateria limitadas.

Em termos de imagens, o objetivo dos desenvolvedores de apps da Web é disponibilizar as imagens de melhor qualidade da maneira mais eficiente possível. Neste artigo, abordaremos algumas técnicas úteis para fazer isso hoje e no futuro próximo.

Evite imagens, se possível

Antes de abrir essa lata de worms, lembre-se de que a Web tem muitas tecnologias poderosas que são, em grande parte, independentes de resolução e DPI. Especificamente, textos, SVG e grande parte do CSS vão "simplesmente funcionar" devido ao recurso de escalonamento automático de pixels da Web (via devicePixelRatio).

Mas nem sempre é possível evitar imagens rasterizadas. Por exemplo, talvez você receba recursos que seriam muito difíceis de replicar em SVG/CSS puro ou esteja lidando com uma fotografia. Embora seja possível converter a imagem em SVG automaticamente, vetorizar fotografias não faz muito sentido, porque as versões ampliadas geralmente não têm uma boa aparência.

Contexto

Um histórico muito curto da densidade de exibição

No início, as telas de computadores tinham uma densidade de pixel de 72 ou 96 dpi (pontos por polegada).

As telas melhoram gradualmente a densidade de pixels, em grande parte devido ao caso de uso para dispositivos móveis, em que os usuários geralmente seguram o smartphone mais perto do rosto, tornando os pixels mais visíveis. Em 2008, os celulares de 150 dpi eram o novo padrão. A tendência de aumento da densidade de exibição continuou, e os novos smartphones de hoje apresentam telas de 300 dpi (marcadas como "Retina" da Apple).

O segredo é, obviamente, uma tela em que os pixels ficam completamente invisíveis. Para o formato de smartphones, a geração atual de telas Retina/HiDPI pode estar parecida com esse ideal. No entanto, novas classes de hardware e wearables, como o Project Glass, provavelmente continuarão a aumentar a densidade de pixels.

Na prática, as imagens de baixa densidade têm a mesma aparência em telas novas e das antigas. No entanto, em comparação com as imagens nítidas que os usuários de alta densidade estão acostumados a ver, as imagens de baixa densidade parecem chocantes e pixels. Confira a seguir uma simulação aproximada de como uma imagem de 1x ficará em uma tela de 2x. Por outro lado, a imagem de 2x é muito boa.

Babuíno 1x
Babuíno 2x
Babuínos! em densidades de pixels diferentes.

Pixels na Web

Quando a Web foi projetada, 99% das telas tinham 96 dpi (ou fingiam ser), e poucas disposições para variação nesse aspecto. Devido a uma grande variação nos tamanhos e densidades de tela, precisávamos de uma maneira padrão para que as imagens tivessem boa aparência em várias densidades e dimensões de tela.

A especificação HTML recentemente resolveu esse problema definindo um pixel de referência que os fabricantes usam para determinar o tamanho de um pixel CSS.

Usando o pixel de referência, um fabricante pode determinar o tamanho do pixel físico do dispositivo em relação ao pixel padrão ou ideal. Essa proporção é chamada de proporção de pixels do dispositivo.

Calcular a proporção de pixels do dispositivo

Suponha que um smartphone tenha uma tela com um tamanho físico de pixel de 180 pixels por polegada (ppi). O cálculo da proporção de pixels do dispositivo requer três etapas:

  1. Compare a distância real em que o dispositivo é mantido com a distância do pixel de referência.

    De acordo com a especificação, sabemos que, com 28 polegadas, o ideal é 96 pixels por polegada. No entanto, como se trata de um smartphone, as pessoas seguram o dispositivo mais perto do rosto do que segurando um laptop. Vamos estimar essa distância como 18 polegadas.

  2. Multiplique a taxa de distância pela densidade padrão (96 ppi) para conseguir a densidade de pixels ideal para a distância especificada.

    idealPixelDensity = (28/18) * 96 = 150 pixels por polegada (aproximadamente)

  3. Considere a proporção entre a densidade física de pixels e a densidade de pixels ideal para chegar à proporção de pixels do dispositivo.

    devicePixelRatio = 180/150 = 1,2

Como o devicePixelRatio é calculado.
Um diagrama que mostra um pixel angular de referência para ajudar a ilustrar como o devicePixelRatio é calculado.

Agora, quando um navegador precisa saber como redimensionar uma imagem para caber na tela de acordo com a resolução ideal ou padrão, ele se refere à proporção de pixels do dispositivo de 1,2, ou seja, para cada pixel ideal, o dispositivo tem 1,2 pixels físicos. A fórmula para alternar entre pixels ideais (conforme definido pela especificação da Web) e físicos (pontos na tela do dispositivo) é:

physicalPixels = window.devicePixelRatio * idealPixels

Historicamente, os fornecedores de dispositivos costumam arredondar devicePixelRatios (DPRs). O iPhone e o iPad da Apple informam o DPR de 1, e os equivalentes do Retina informam o relatório 2. A especificação CSS (em inglês) recomenda que

a unidade de pixel refere-se ao número inteiro de pixels do dispositivo que melhor se aproxima do pixel de referência.

Um motivo para as proporções de arredondamento serem melhores é que elas podem levar a menos artefatos de subpixel.

No entanto, a realidade do cenário de dispositivos é muito mais variada, e os smartphones Android geralmente têm DPRs de 1,5. O tablet Nexus 7 tem uma DPR de cerca de 1,33, calculada por um cálculo semelhante ao mostrado acima. Espere ver mais dispositivos com DPRs variáveis no futuro. Por isso, nunca presuma que seus clientes terão DPRs de números inteiros.

Visão geral das técnicas de imagem HiDPI

Há muitas técnicas para resolver o problema de mostrar as imagens de melhor qualidade o mais rápido possível, dividindo-se em duas categorias:

  1. Otimizar imagens únicas
  2. Otimizando a seleção entre várias imagens.

Abordagens de imagem única: use uma imagem, mas faça algo inteligente com ela. Essas abordagens têm a desvantagem de inevitavelmente sacrificar o desempenho, já que você fará o download de imagens HiDPI mesmo em dispositivos mais antigos com DPI mais baixo. Confira algumas abordagens para o caso de imagem única:

  • Imagem HiDPI altamente compactada
  • Formato de imagem totalmente incrível
  • Formato de imagem progressivo

Várias abordagens de imagens: use várias imagens, mas escolha algo inteligente para escolher qual carregar. Essas abordagens têm um overhead inerente para o desenvolvedor criar várias versões do mesmo recurso e, em seguida, definir uma estratégia de decisão. As opções são:

  • JavaScript
  • Entrega no lado do servidor
  • Consultas de mídia CSS
  • Recursos integrados do navegador (image-set(), <img srcset>)

Imagem HiDPI altamente compactada

As imagens já correspondem a 60% da largura de banda gasta no download de um site comum. Ao fornecer imagens HiDPI para todos os clientes, vamos aumentar esse número. Quanto ele vai crescer?

Executei alguns testes que geraram fragmentos de imagem de 1x e 2x com qualidade JPEG a 90, 50 e 20. Este é o script de shell que usei (com a implementação do ImageMagick) para gerá-los:

Exemplo 1 de blocos. Exemplo 2 de blocos. Exemplo 3 de blocos.
Exemplos de imagens em diferentes compressões e densidades de pixel.

Com base nessa pequena amostragem não científica, parece que a compactação de imagens grandes proporciona uma boa relação entre qualidade e tamanho. Para mim, imagens 2x muito compactadas realmente ficam melhores do que imagens 1x não compactadas.

Obviamente, veicular imagens 2x e altamente compactadas de baixa qualidade para o dobro de dispositivos é pior do que veicular imagens de mais qualidade. A abordagem acima incorre em penalidades de qualidade da imagem. Se você comparar a qualidade: 90 imagens com qualidade (20 imagens), vai notar uma queda na nitidez e maior granulação. Esses artefatos podem não ser aceitáveis nos casos em que imagens de alta qualidade são essenciais (por exemplo, um aplicativo visualizador de fotos) ou para desenvolvedores de apps que não estão dispostos a comprometer.

A comparação acima foi feita inteiramente com JPEGs compactados. É importante notar que há muitas vantagens e desvantagens entre os formatos de imagem amplamente implementados (JPEG, PNG, GIF), o que nos leva a...

Formato de imagem totalmente incrível

WebP é um formato de imagem bastante interessante que compacta muito bem, mantendo a alta fidelidade da imagem. É claro que ele ainda não foi implementado em todos os lugares.

Uma maneira é verificar o suporte para WebP é via JavaScript. Carregue uma imagem de 1 px por meio de data-uri, aguarde os eventos carregados ou de erro disparados e, em seguida, verifique se o tamanho está correto. O Modernizr é fornecido com um script de detecção de recursos (em inglês), que está disponível pelo Modernizr.webp.

Uma maneira melhor de fazer isso é diretamente no CSS usando a função image(). Portanto, se você tiver uma imagem WebP e um substituto de JPEG, poderá criar o seguinte:

#pic {
  background: image("foo.webp", "foo.jpg");
}

Há alguns problemas com essa abordagem. Em primeiro lugar, o image() não foi amplamente implementado. Em segundo lugar, embora a compactação do WebP supere o formato JPEG fora, essa ainda é uma melhoria relativamente incremental: cerca de 30% menor, com base nesta galeria WebP. Assim, o WebP só não é suficiente para resolver o problema do alto DPI.

Formatos de imagem progressivos

Formatos de imagem progressivos, como JPEG 2000, JPEG progressivo, PNG e GIF progressivos, têm o benefício (um pouco discutido) de ver a imagem entrar no lugar antes de ser totalmente carregada. Eles podem gerar uma sobrecarga de tamanho, embora haja evidências conflitantes sobre isso. Jeff Atwood reivindicou que o modo progressivo "aumenta cerca de 20% do tamanho das imagens PNG e 10% do tamanho das imagens JPEG e GIF". No entanto, Stoyan Stefanov afirmou que, para arquivos grandes, o modo progressivo é mais eficiente (na maioria dos casos).

À primeira vista, as imagens progressivas parecem muito promissoras quando usadas para exibir imagens de melhor qualidade o mais rápido possível. A ideia é que o navegador possa interromper o download e decodificar uma imagem quando saber que outros dados não aumentam a qualidade da imagem, ou seja, todas as melhorias de fidelidade são subpixels.

Embora as conexões sejam fáceis de encerrar, reiniciá-las costuma ser cara. Para um site com muitas imagens, a abordagem mais eficiente é manter uma única conexão HTTP ativa, reutilizando-a pelo maior tempo possível. Se a conexão for encerrada prematuramente porque o download de uma imagem foi suficiente, o navegador precisará criar uma nova conexão, o que pode ser muito lento em ambientes de baixa latência.

Uma solução para isso é usar a solicitação HTTP Range, que permite que os navegadores especifiquem um intervalo de bytes a serem buscados. Um navegador inteligente pode fazer uma solicitação HEAD para chegar ao cabeçalho, processá-la, decidir quanto da imagem é realmente necessário e, então, fazer a busca. Infelizmente, o intervalo HTTP é mal suportado em servidores da Web, o que torna essa abordagem impraticável.

Por fim, uma limitação óbvia dessa abordagem é que você não pode escolher qual imagem carregar, apenas as fidelidades variadas da mesma imagem. Como resultado, ele não aborda o caso de uso de "direção da arte".

Usar o JavaScript para decidir qual imagem carregar

A primeira e mais óbvia abordagem para decidir qual imagem carregar é usar o JavaScript no cliente. Essa abordagem permite que você descubra tudo sobre o user agent e faça a coisa certa. Você pode determinar a proporção de pixels do dispositivo com window.devicePixelRatio, conferir a largura e a altura da tela e até mesmo possivelmente identificar a conexão de rede via Navigator.connection ou emitir uma solicitação falsa, como a biblioteca foresight.js. Depois de coletar todas essas informações, você pode decidir qual imagem carregar.

Há aproximadamente um milhão de bibliotecas JavaScript que fazem algo semelhante ao descrito acima, e nenhuma delas é excepcional.

Uma grande desvantagem dessa abordagem é que o uso do JavaScript significa que você atrasará o carregamento da imagem até que o analisador de visualização prévia seja concluído. Isso significa que o download das imagens não será iniciado até que o evento pageload seja disparado. Saiba mais sobre isso no artigo de Jason Grigsby.

Decida qual imagem carregar no servidor

Você pode adiar a decisão para o lado do servidor escrevendo gerenciadores de solicitações personalizados para cada imagem exibida. Esse gerenciador verificaria o suporte à Retina com base no user agent (a única informação transmitida ao servidor). Em seguida, dependendo de a lógica do lado do servidor querer disponibilizar recursos HiDPI, carregue o recurso apropriado (nomeado de acordo com alguma convenção conhecida).

Infelizmente, o user agent não fornece necessariamente informações suficientes para decidir se um dispositivo receberá imagens de alta ou baixa qualidade. Além disso, algo relacionado ao user agent é uma invasão e precisa ser evitada, se possível.

Usar consultas de mídia CSS

Por serem declarativas, as consultas de mídia CSS permitem indicar sua intenção e permitir que o navegador faça a coisa certa por você. Além do uso mais comum de consultas de mídia (correspondente ao tamanho do dispositivo), também é possível corresponder devicePixelRatio. A consulta de mídia associada é proporção de pixels de dispositivo e tem variantes mínima e máxima associadas, como esperado. Se você quiser carregar imagens com DPI alto e a proporção de pixels do dispositivo exceder um limite, faça o seguinte:

#my-image { background: (low.png); }

@media only screen and (min-device-pixel-ratio: 1.5) {
  #my-image { background: (high.png); }
}

Isso fica um pouco mais complicado com todos os prefixos de fornecedor misturados, especialmente devido às diferenças de posicionamento insanas de prefixos "min" e "max":

@media only screen and (min--moz-device-pixel-ratio: 1.5),
    (-o-min-device-pixel-ratio: 3/2),
    (-webkit-min-device-pixel-ratio: 1.5),
    (min-device-pixel-ratio: 1.5) {

  #my-image {
    background:url(high.png);
  }
}

Com essa abordagem, você recupera os benefícios da análise antecipada, que foram perdidos com a solução JS. Você também ganha a flexibilidade de escolher seus pontos de interrupção responsivos (por exemplo, pode ter imagens de DPI baixo, médio e alto), o que foi perdido com a abordagem do lado do servidor.

Infelizmente, ele ainda é um pouco difícil de administrar e causa um CSS com aparência estranha (ou exige pré-processamento). Além disso, essa abordagem é restrita a propriedades CSS. Portanto, não é possível definir um <img src>, e todas as imagens precisam ter um plano de fundo. Por fim, ao usar estritamente a proporção de pixels do dispositivo, você pode acabar em situações em que seu smartphone de alto DPI faz o download de um grande recurso de imagem de 2x durante uma conexão EDGE. Essa não é a melhor experiência do usuário.

Usar novos recursos do navegador

Tem havido muita discussão recente sobre o suporte à plataforma da Web para o problema de imagens com alto DPI. A Apple recentemente entrou no espaço, trazendo a função CSS image-set() para o WebKit. Por isso, o Safari e o Chrome são compatíveis com ele. Como é uma função CSS, image-set() não resolve o problema das tags <img>. Digite @srcset, que resolve esse problema, mas ainda não tem implementações de referência. A próxima seção é mais aprofundada em image-set e srcset.

Recursos do navegador para suporte a DPI alto

Em última análise, a decisão sobre qual abordagem adotar depende dos seus requisitos específicos. Tenha em mente que todas as abordagens mencionadas acima têm desvantagens. No entanto, quando image-set e srcset tiverem ampla compatibilidade, serão as soluções adequadas para esse problema. Por enquanto, vamos falar sobre algumas práticas recomendadas que podem nos aproximar desse futuro ideal.

Primeiro, qual é a diferença entre eles? Bem, image-set() é uma função CSS, adequada para uso como um valor da propriedade CSS de segundo plano. srcset é um atributo específico para elementos <img>, com sintaxe semelhante. As duas tags permitem especificar declarações de imagem, mas o atributo srcset também permite configurar qual imagem carregar com base no tamanho da janela de visualização.

Práticas recomendadas para conjunto de imagens

A função CSS image-set() está disponível com o prefixo -webkit-image-set(). A sintaxe é bastante simples, usando uma ou mais declarações de imagem separadas por vírgula, que consistem em uma string de URL ou função url() seguida pela resolução associada. Exemplo:

background-image:  -webkit-image-set(
  url(icon1x.jpg) 1x,
  url(icon2x.jpg) 2x
);

Isso informa ao navegador que há duas imagens para escolher. Um deles é otimizado para telas 1x e o outro para telas 2x. O navegador escolhe qual deles carregar, com base em vários fatores, que podem até incluir a velocidade da rede, se o navegador for inteligente o suficiente (no momento não implementado que eu saiba).

Além de carregar a imagem correta, o navegador também a dimensionará da maneira adequada. Em outras palavras, o navegador presume que duas imagens são duas vezes maiores que 1x. Portanto, ele reduz a imagem de 2x por um fator de 2 para que a imagem pareça ter o mesmo tamanho na página.

Em vez de especificar 1x, 1,5x ou Nx, também é possível especificar uma determinada densidade de pixel do dispositivo em dpi.

Isso funciona bem, exceto em navegadores que não são compatíveis com a propriedade image-set, que não mostram nenhuma imagem. Isso é claramente ruim, então você precisa usar um substituto (ou uma série de substitutos) para resolver esse problema:

background-image: url(icon1x.jpg);
background-image: -webkit-image-set(
  url(icon1x.jpg) 1x,
  url(icon2x.jpg) 2x
);
/* This will be useful if image-set gets into the platform, unprefixed.
    Also include other prefixed versions of this */
background-image: image-set(
  url(icon1x.jpg) 1x,
  url(icon2x.jpg) 2x
);

O exemplo acima carregará o recurso apropriado em navegadores compatíveis com image-set. Caso contrário, o recurso usará o de 1x. A ressalva óbvia é que, embora o suporte ao navegador image-set() seja baixo, a maioria dos user agents receberá o recurso de 1x.

Esta demonstração usa o image-set() para carregar a imagem correta, usando o recurso de 1x se essa função CSS não for compatível.

Neste ponto, você pode estar se perguntando por que não apenas o polyfill, ou seja, criar um paliativo JavaScript para image-set(), e chamá-lo de dia. É muito difícil implementar polyfills eficientes para funções CSS. (Para uma explicação detalhada, consulte esta discussão no estilo www).

srcset da imagem

Aqui está um exemplo de srcset:

<img alt="my awesome image"
  src="banner.jpeg"
  srcset="banner-HD.jpeg 2x, banner-phone.jpeg 640w, banner-phone-HD.jpeg 640w 2x">

Como você pode notar, além das declarações x fornecidas por image-set, o elemento srcset também usa valores w e h que correspondem ao tamanho da janela de visualização, tentando exibir a versão mais relevante. O conteúdo acima veicularia banner-phone.jpeg para dispositivos com largura da janela de visualização abaixo de 640 px, banner-phone-HD.jpeg para dispositivos de tela pequena com alto DPI, banner-HD.jpeg para dispositivos de alto DPI com telas maiores que 640 px e banner.jpeg para todo o restante.

Usar image-set para elementos de imagem

Como o atributo srcset em elementos img não é implementado na maioria dos navegadores, pode ser tentador substituir esses elementos por <div>s com planos de fundo e usar a abordagem de conjunto de imagens. Isso vai funcionar, com ressalvas. A desvantagem disso é que a tag <img> tem um valor semântico de longo prazo. Na prática, isso é importante principalmente para rastreadores da Web e motivos de acessibilidade.

Se você acabar usando -webkit-image-set, talvez fique tentado a usar a propriedade CSS em segundo plano. A desvantagem dessa abordagem é que você precisa especificar o tamanho da imagem, o que é desconhecido se você estiver usando uma imagem que não seja de 1x. Em vez de fazer isso, você pode usar a propriedade CSS de conteúdo da seguinte maneira:

<div id="my-content-image"
  style="content: -webkit-image-set(
    url(icon1x.jpg) 1x,
    url(icon2x.jpg) 2x);">
</div>

Isso vai dimensionar a imagem automaticamente com base no devicePixelRatio. Confira este exemplo da técnica acima em ação, com um outro substituto para url() para navegadores que não oferecem suporte a image-set.

srcset de polyfilling

Um recurso útil do srcset é que ele vem com um substituto natural. Quando o atributo srcset não é implementado, todos os navegadores sabem que precisam processar o atributo src. Além disso, como é apenas um atributo HTML, é possível criar polyfills com JavaScript.

Esse polyfill vem com testes de unidade para garantir que ele esteja o mais próximo possível da especificação. Além disso, existem verificações que impedem o polyfill de executar qualquer código se o srcset for implementado de forma nativa.

Confira uma demonstração do polyfill em ação.

Conclusão

Não existe uma fórmula mágica para resolver o problema de imagens com DPI alto.

A solução mais fácil é evitar totalmente imagens, optando por SVG e CSS. No entanto, isso nem sempre é realista, especialmente se você tiver imagens de alta qualidade no seu site.

As abordagens em JS, CSS e o uso do lado do servidor têm pontos fortes e pontos fracos. No entanto, a abordagem mais promissora é aproveitar os novos recursos do navegador. Embora o suporte dos navegadores a image-set e srcset ainda esteja incompleto, há substitutos razoáveis para o uso no momento.

Resumindo, minhas recomendações são:

  • Para imagens de plano de fundo, use image-set com os substitutos apropriados para navegadores que não são compatíveis com ela.
  • Para imagens de conteúdo, use um polyfill srcset ou substitua usando image-set (confira acima).
  • Para situações em que você quer sacrificar a qualidade da imagem, considere o uso de imagens 2x compactadas.